mirror of
https://github.com/docker/docs.git
synced 2026-11-04 10:01:00 +00:00
Revamp the guides section: taxonomy, single-page structure, and new landing/filter UX (#25338)
## What A full revamp of the `/guides` section — taxonomy, structure, and UX. Preview: https://deploy-preview-25338--docsdocker.netlify.app/guides/ ### Taxonomy & structure - Replaces the sprawling 24-tag set with a focused 9-tag taxonomy aligned to use case / SDLC: **Languages & frameworks, AI, Testing, CI/CD, Security, Databases, Deployment, Admin, Hands-on labs**. - Removes the languages taxonomy entirely (`data/languages.yaml` and all `languages:` front matter). - Collapses all multi-page guide series into single pages, with aliases so old sub-page URLs continue to resolve. - Deletes redirect-only shim pages and flattens directory-wrapped single pages. ### Landing page (`/guides/`) - A clean index: hero, then category sections listing each guide as a plain **title / summary** row. - **Browse rail** (sticky, left): click a topic to filter the page to just that topic's guides; "All guides" resets. A count + Clear bar appears while filtering. - **Text filter**: instant client-side search over title, summary, and tags. Topic filter and text search are mutually exclusive and share the same flat-list view. - No more "Featured guides" section. ### Guide content pages - Removes the left sidebar for the guides section (mobile drawer kept). - Centered reading column (`92ch`) with the TOC taking the remaining width. - Summary rendered as a lead paragraph under the h1. - TOC scrollbar auto-hides until hover; breadcrumbs render at a consistent size inside and outside `prose`. ## Notes - Deep full-text search across guide bodies is still handled by the existing site-wide Pagefind (⌘K); the landing filter is the fast metadata-narrowing surface. - `series.html` is retained — it's still used by `get-started` pages. ## Validation - Site builds clean locally (Hugo dev server, no errors / broken refs). - All collapsed sub-page URLs redirect via aliases. ## Follow-ups - Prune guides that we no longer need/want - Tighten the guides that span too many concepts (e.g. language guides; remove cicd/deploy/sscs) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ef2165d47c
commit
f02e2a5633
302 files changed
+23207
-24782
No files matched your search
@@ -1,5 +1,5 @@
|
||||
<aside
|
||||
class="sticky top-16 h-[calc(100vh-64px)] min-w-52 space-y-4 overflow-y-auto py-4 w-full"
|
||||
class="scrollbar-hover sticky top-16 h-[calc(100vh-64px)] min-w-52 space-y-4 overflow-y-auto py-4 w-full"
|
||||
>
|
||||
{{ partial "github-links.html" . }}
|
||||
<div id="TableOfContents" class="overflow-x-auto">
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{{ partial "breadcrumbs.html" . }}
|
||||
<h1 data-pagefind-weight="10">{{ .Title | safeHTML }}</h1>
|
||||
{{- if and (eq .Type "guides") .Params.summary }}
|
||||
<p class="mt-0 text-lg text-gray-500 dark:text-gray-400">
|
||||
{{ .Params.summary }}
|
||||
</p>
|
||||
{{- end }}
|
||||
<div class="hidden lg:block">
|
||||
{{ partialCached "md-dropdown.html" "-" "-" }}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{{- range . -}}
|
||||
{{- $lang := index hugo.Data.languages . -}}
|
||||
{{- with $lang }}
|
||||
<span class="chip">
|
||||
<img
|
||||
class="py-1"
|
||||
height="18"
|
||||
width="18"
|
||||
title="{{ .title }}"
|
||||
src="{{ .icon }}"
|
||||
/>
|
||||
<span>{{ .title }}</span>
|
||||
</span>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -1,9 +0,0 @@
|
||||
{{- range . -}}
|
||||
{{- $tag := index hugo.Data.tags . -}}
|
||||
{{- with $tag }}
|
||||
<span class="chip">
|
||||
<span class="icon-svg icon-sm pb-0.5">{{ partialCached "icon" "tag" "tag" }}</span>
|
||||
{{ .title }}
|
||||
</span>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -1,58 +0,0 @@
|
||||
{{/* - Multi-page guide: render a progress bar - */}}
|
||||
<div class="my-4 flex flex-col items-start text-sm">
|
||||
{{ $totalPages := len .CurrentSection.Pages }}
|
||||
{{/* - initialize the page store
|
||||
|
||||
$stepper_seen controls the color of the item in the stepper
|
||||
|
||||
green: "completed" - index is lower than current page
|
||||
blue: current page
|
||||
gray: next in section
|
||||
for the section page, they're all gray
|
||||
|
||||
default to true if kind = section
|
||||
(make all entries gray)
|
||||
|
||||
-
|
||||
*/}}
|
||||
{{ page.Store.Set "stepper_seen" .IsSection }}
|
||||
{{/* - Loop over the pages in this guide - */}}
|
||||
{{ range $i, $e := .CurrentSection.Pages }}
|
||||
{{ $isLast := eq (add $i 1) $totalPages }}
|
||||
<div class="flex">
|
||||
<div class="flex flex-col items-center">
|
||||
{{/* - Render the page's index digit (1,2,3,4 etc) - */}}
|
||||
<a
|
||||
href="{{ .Permalink }}"
|
||||
class="{{ if (page.Store.Get "stepper_seen") }}
|
||||
bg-gray-400 dark:bg-gray-500
|
||||
{{ else if (eq . page) }}
|
||||
{{ .Store.Set "stepper_seen" true }} bg-blue-400 dark:bg-blue-800
|
||||
{{ else }}
|
||||
bg-green-400 dark:bg-green-dark-400
|
||||
{{ end }} flex h-8 w-8 items-center justify-center rounded-full text-white no-underline"
|
||||
>
|
||||
{{ add $i 1 }}
|
||||
</a>
|
||||
{{/* - Render the vertical border - */}}
|
||||
{{ if not $isLast }}
|
||||
<div
|
||||
class="{{ if (page.Store.Get "stepper_seen") }}
|
||||
border-gray-400 dark:border-gray-400
|
||||
{{ else if (eq . page) }}
|
||||
border-blue-400 dark:border-blue-400
|
||||
{{ else }}
|
||||
border-green-400 dark:border-green-400
|
||||
{{ end }} h-6 border-l-2"
|
||||
></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{/* - Render the page's title - */}}
|
||||
<div class="ml-2 flex h-8 items-center self-start">
|
||||
<a href="{{ .Permalink }}">
|
||||
{{ .LinkTitle }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -1,39 +0,0 @@
|
||||
{{- $guide := cond (eq .CurrentSection .FirstSection) . .CurrentSection -}}
|
||||
<div class="flex flex-col gap-4 px-4 pt-2">
|
||||
<div class="font-semibold">
|
||||
<a href="{{ $guide.Permalink }}">{{ $guide.Title }}</a>
|
||||
</div>
|
||||
<div>{{ $guide.Summary }}</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{- with $guide.Params.languages }}
|
||||
{{ partial "guide-languages.html" . }}
|
||||
{{- end }}
|
||||
{{- with $guide.Params.tags }}
|
||||
{{ partial "guide-tags.html" . }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- with $guide.Params.time }}
|
||||
<div class="flex gap-2 text-gray-400 dark:text-gray-400">
|
||||
<span class="icon-svg"
|
||||
>{{ partialCached "icon" "clock" "clock" }}</span
|
||||
>
|
||||
<span>{{ . }}</span>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- if gt (len $guide.Pages) 0 }}
|
||||
{{- partial "guides-stepper.html" . }}
|
||||
{{- end }}
|
||||
{{- with $guide.Params.resource_links }}
|
||||
<div class="space-y-2">
|
||||
<p>Resources:</p>
|
||||
<ul class="ml-4 space-y-2">
|
||||
{{- range . }}
|
||||
<li><a href="{{ .url }}" class="link">{{ .title }}</a></li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{- end }}
|
||||
<a href="/guides/" class="link mt-2">« Back to all guides</a>
|
||||
</div>
|
||||
+2
-6
@@ -30,7 +30,7 @@
|
||||
}
|
||||
}
|
||||
})"
|
||||
class="bg-background-toc dark:bg-background-toc fixed top-0 z-40 hidden h-screen w-full flex-none overflow-x-hidden overflow-y-auto md:sticky md:top-16 md:z-auto md:block md:h-[calc(100vh-64px)] md:w-[320px]"
|
||||
class="bg-background-toc dark:bg-background-toc fixed top-0 z-40 hidden h-screen w-full flex-none overflow-x-hidden overflow-y-auto{{ if ne .Type "guides" }} md:sticky md:top-16 md:z-auto md:block md:h-[calc(100vh-64px)] md:w-[320px]{{ end }}"
|
||||
:class="{ 'hidden': ! $store.showSidebar }"
|
||||
>
|
||||
<!-- Gray backdrop on small screens -->
|
||||
@@ -53,11 +53,7 @@
|
||||
<!-- Actual Sidebar Content -->
|
||||
{{ block "left" . }}
|
||||
{{ partial "sidebar/mainnav.html" . }}
|
||||
{{ if eq .Type "guides" }}
|
||||
{{ partial "sidebar/guides.html" . }}
|
||||
{{ else }}
|
||||
{{ partial "sidebar/sections.html" . }}
|
||||
{{ end }}
|
||||
{{ partial "sidebar/sections.html" . }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+207
-262
@@ -1,273 +1,218 @@
|
||||
{{ define "left" }}
|
||||
{{- partial "sidebar/mainnav.html" . }}
|
||||
<div class="navbar-font flex flex-col gap-2 p-4 text-sm">
|
||||
<p>Filter guides by tag or programming language.</p>
|
||||
<p></p>
|
||||
<div
|
||||
class="space-y-4"
|
||||
x-data="{
|
||||
filters: { tags: [], languages: [] },
|
||||
toggleFilter(grp, tag) {
|
||||
this.filters[grp].includes(tag)
|
||||
? this.filters[grp] = this.filters[grp].filter(el => el != tag)
|
||||
: this.filters[grp].push(tag);
|
||||
|
||||
// Update URL
|
||||
const url = new URL(window.location.href);
|
||||
if (this.filters[grp].length > 0) {
|
||||
url.searchParams.set(grp, this.filters[grp].join('~'));
|
||||
} else {
|
||||
url.searchParams.delete(grp);
|
||||
}
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
this.$dispatch('guide-filter', { filters: this.filters });
|
||||
},
|
||||
|
||||
init() {
|
||||
const url = new URL(window.location.href);
|
||||
const tags = url.searchParams.get('tags')
|
||||
const langs = url.searchParams.get('languages')
|
||||
if (tags != null) {
|
||||
this.filters['tags'] = tags.split('~');
|
||||
}
|
||||
if (langs != null) {
|
||||
this.filters['languages'] = langs.split('~');
|
||||
}
|
||||
}
|
||||
}"
|
||||
@guide-filter.window="filters = $event.detail.filters;"
|
||||
>
|
||||
<div class="pl-2"><strong>Tags</strong></div>
|
||||
<fieldset class="flex flex-col gap-2">
|
||||
{{- range $name, $data := hugo.Data.tags }}
|
||||
{{- if where $.Pages "Params.tags" "intersect" (slice $name) }}
|
||||
{{- $id := anchorize (fmt.Printf "tag-%s" $name) }}
|
||||
<div class="flex gap-2 pl-2">
|
||||
<input
|
||||
value="{{ $name }}"
|
||||
type="checkbox"
|
||||
id="{{ $id }}"
|
||||
@change="toggleFilter('tags', '{{ $name }}')"
|
||||
:checked="filters['tags'].includes('{{ $name }}')"
|
||||
/>
|
||||
<label class="select-none" for="{{ $id }}"
|
||||
>{{ $data.title }}</label
|
||||
>
|
||||
</div>
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
</fieldset>
|
||||
<div class="pl-2"><strong>Languages</strong></div>
|
||||
<fieldset class="flex flex-wrap gap-2 pl-2">
|
||||
{{- range $name, $data := hugo.Data.languages }}
|
||||
{{- if where $.Pages "Params.languages" "intersect" (slice $name) }}
|
||||
{{- $id := anchorize (fmt.Printf "lang-%s" $name) }}
|
||||
<button
|
||||
class="border-divider-light dark:border-divider-dark flex gap-1 rounded-full border bg-white px-2 py-1 dark:bg-gray-800"
|
||||
:class="{ 'ring-3-2 ring-3-blue-light-400 dark:ring-3-blue-dark-400': filters['languages'].includes('{{ $name }}') }"
|
||||
@click="toggleFilter('languages', '{{ $name }}')"
|
||||
>
|
||||
<img
|
||||
height="18"
|
||||
width="18"
|
||||
title="{{ $data.title }}"
|
||||
src="{{ $data.icon }}"
|
||||
/>
|
||||
<span>{{ $data.title }}</span>
|
||||
</button>
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<div class="flex w-full gap-8">
|
||||
<article class="prose dark:prose-invert max-w-none min-w-0">
|
||||
{{- partial "breadcrumbs.html" . }}
|
||||
<h1 data-pagefind-weight="10" class="scroll-mt-36">{{ .Title }}</h1>
|
||||
{{ .Content }}
|
||||
<div
|
||||
class="not-prose min-h-screen"
|
||||
x-data="{
|
||||
filters: { tags: [], languages: [] },
|
||||
hidden: [],
|
||||
total: {{ len .Pages }},
|
||||
{{- $tagOrder := slice "languages" "ai" "testing" "cicd" "security" "databases" "deployment" "admin" "labs" }}
|
||||
|
||||
noFilters() {
|
||||
return Object.values(this.filters).every(arr=> Array.isArray(arr) && arr.length === 0);
|
||||
},
|
||||
|
||||
isVisible(el) {
|
||||
return !this.hidden.includes(el.id);
|
||||
},
|
||||
|
||||
updateVisible() {
|
||||
const hiddenSet = new Set();
|
||||
// show if no filters have been selected
|
||||
if (this.noFilters()) {
|
||||
this.hidden = [];
|
||||
return;
|
||||
};
|
||||
const guideContainer = this.$refs['guide-container'];
|
||||
const guides = guideContainer.children
|
||||
|
||||
// Loop over the filters object
|
||||
for (const [key, value] of Object.entries(this.filters)) {
|
||||
if (!value || value.length === 0) continue;
|
||||
|
||||
for (const g of guides) {
|
||||
// Get the dataset for the current guide (e.g., languages or tags)
|
||||
const terms = JSON.parse(g.dataset[key]);
|
||||
|
||||
// Check if any of the filter values exist in the terms
|
||||
const containsSome = terms.some(term => this.filters[key].includes(term));
|
||||
|
||||
// If none of the terms match the filter, mark the guide as hidden
|
||||
if (!containsSome) {
|
||||
hiddenSet.add(g.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.hidden = Array.from(hiddenSet)
|
||||
},
|
||||
|
||||
init() {
|
||||
const url = new URL(window.location.href);
|
||||
const tags = url.searchParams.get('tags')
|
||||
const langs = url.searchParams.get('languages')
|
||||
if (tags != null) {
|
||||
this.filters['tags'] = tags.split('~');
|
||||
}
|
||||
if (langs != null) {
|
||||
this.filters['languages'] = langs.split('~');
|
||||
}
|
||||
this.updateVisible();
|
||||
}
|
||||
}"
|
||||
x-cloak
|
||||
@guide-filter.window="filters = $event.detail.filters; updateVisible(); $nextTick(() => { window.scrollTo({ top: 0 }) })"
|
||||
>
|
||||
<div x-show="noFilters()">
|
||||
<h2>Featured guides</h2>
|
||||
<div
|
||||
class="not-prose grid grid-cols-1 gap-8 py-4 lg:grid-cols-2 xl:grid-cols-3"
|
||||
>
|
||||
{{- $featured := where .Pages "Params.featured" true }}
|
||||
{{- with $featured }}
|
||||
{{- range . }}
|
||||
<div class="flex h-full flex-col">
|
||||
<a class="hover:underline" href="{{ .Permalink }}">
|
||||
{{- $img := resources.Get (.Params.image | default "/images/thumbnail.webp") }}
|
||||
<img
|
||||
class="h-64 w-full rounded-sm object-cover shadow"
|
||||
src="{{ $img.Permalink }}"
|
||||
/>
|
||||
<p class="my-4 text-xl leading-snug">{{ .Title }}</p>
|
||||
</a>
|
||||
<p class="flex-grow text-sm">{{ .Summary }}</p>
|
||||
<div class="mt-4">
|
||||
{{ template "guide-metadata" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
<hr class="text-divider-light dark:text-divider-dark" />
|
||||
</div>
|
||||
<h2 x-show="noFilters()" id="all-guides" class="scroll-mt-36">
|
||||
All guides
|
||||
</h2>
|
||||
<div x-show="!noFilters()" class="flex flex-col gap-2 pb-8">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-svg icon-sm mb-1"
|
||||
>{{ partialCached "icon" "funnel" "funnel" }}</span
|
||||
>
|
||||
<p>
|
||||
Filtered results: showing
|
||||
<span x-text="total - hidden.length"></span> out of
|
||||
<span x-text="total"></span> guides.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pl-4">
|
||||
{{- range $name, $data := hugo.Data.tags }}
|
||||
<div x-show="filters.tags.includes('{{ $name }}')">
|
||||
{{ template "termchip" $data.title }}
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- range $name, $data := hugo.Data.languages }}
|
||||
<div
|
||||
x-show="filters.languages.includes('{{ $name }}')"
|
||||
class="border-divider-light dark:border-divider-dark inline-flex items-center gap-1 rounded-full border bg-gray-100 px-2 text-sm text-gray-200 select-none dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<img
|
||||
class="py-1"
|
||||
height="18"
|
||||
width="18"
|
||||
title="{{ $data.title }}"
|
||||
src="{{ $data.icon }}"
|
||||
/>
|
||||
<span>{{ $data.title }}</span>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
<div x-ref="guide-container">
|
||||
{{- range .Pages }}
|
||||
<div
|
||||
id="guide-{{ math.Counter }}"
|
||||
data-languages="{{ jsonify (.Params.languages | default slice) }}"
|
||||
data-tags="{{ jsonify (.Params.tags | default slice) }}"
|
||||
x-show="isVisible($el);"
|
||||
class="border-divider-light dark:border-divider-dark flex flex-col justify-between border-b p-4 hover:bg-gray-100 hover:dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex flex-col justify-between xl:flex-row">
|
||||
<a
|
||||
href="{{ .Permalink }}"
|
||||
class="mb-2 truncate text-lg hover:underline xl:mb-0"
|
||||
>{{ .Title }}</a
|
||||
>
|
||||
{{ template "guide-metadata" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{- define "guide-metadata" }}
|
||||
<div
|
||||
class="flex items-center justify-between gap-8 text-sm text-gray-400 dark:text-gray-300"
|
||||
class="not-prose mx-auto flex min-h-[calc(100dvh-7rem)] max-w-7xl flex-col"
|
||||
x-data="{
|
||||
query: '',
|
||||
tagFilter: '',
|
||||
words() { return this.query.trim().toLowerCase().split(/\s+/).filter(Boolean) },
|
||||
textFiltering() { return this.query.trim().length > 0 },
|
||||
filtering() { return this.textFiltering() || this.tagFilter !== '' },
|
||||
matchText(text) { return this.words().every(w => text.includes(w)) },
|
||||
rowVisible(el) { return !this.textFiltering() || this.matchText(el.dataset.search) },
|
||||
sectionVisible(el) {
|
||||
if (this.tagFilter) return el.dataset.tag === this.tagFilter;
|
||||
if (this.textFiltering()) return Array.from(el.querySelectorAll('[data-guide]')).some(r => this.matchText(r.dataset.search));
|
||||
return true;
|
||||
},
|
||||
resultCount() {
|
||||
if (this.tagFilter) return document.querySelectorAll('section[data-tag=' + this.tagFilter + '] [data-guide]').length;
|
||||
return Array.from(document.querySelectorAll('[data-guide]')).filter(r => this.matchText(r.dataset.search)).length;
|
||||
},
|
||||
selectTag(tag) { this.tagFilter = this.tagFilter === tag ? '' : tag; this.query = ''; window.scrollTo({ top: 0, behavior: 'smooth' }); },
|
||||
clear() { this.query = ''; this.tagFilter = ''; }
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{- with .Params.languages }}
|
||||
{{ partial "guide-languages.html" . }}
|
||||
{{- end }}
|
||||
{{- with .Params.tags }}
|
||||
{{ partial "guide-tags.html" . }}
|
||||
{{- partial "breadcrumbs.html" . }}
|
||||
|
||||
<div class="mt-4">
|
||||
<h1
|
||||
data-pagefind-weight="10"
|
||||
class="scroll-mt-36 text-5xl font-bold tracking-tight"
|
||||
>
|
||||
Guides
|
||||
</h1>
|
||||
{{- with .Description }}
|
||||
<p class="mt-3 text-lg text-gray-500 dark:text-gray-400">{{ . }}</p>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- with .Params.time }}
|
||||
<div class="flex flex-shrink gap-2 whitespace-nowrap">
|
||||
<span class="icon-svg"
|
||||
>{{ partialCached "icon" "clock" "clock" }}</span
|
||||
>
|
||||
<span>{{ . }}</span>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
{{- define "termchip" }}
|
||||
<span class="chip">
|
||||
<span class="icon-svg icon-sm pb-0.5">
|
||||
{{ partialCached "icon" "tag" "tag" }}
|
||||
</span>
|
||||
{{ . }}
|
||||
</span>
|
||||
{{- end }}
|
||||
<!-- Text filter -->
|
||||
<div class="relative mt-8 mb-10 max-w-xl">
|
||||
<span
|
||||
class="icon-svg icon-sm pointer-events-none absolute top-1/2 left-3.5 -translate-y-1/2 text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
{{ partialCached "icon" "magnifying-glass" "magnifying-glass" }}
|
||||
</span>
|
||||
<input
|
||||
type="search"
|
||||
x-model="query"
|
||||
@input="tagFilter = ''"
|
||||
@keydown.escape="clear()"
|
||||
aria-label="Filter guides"
|
||||
placeholder="Filter guides by name, topic, or tag…"
|
||||
class="w-full rounded-xl border border-gray-300 bg-white py-2.5 pr-10 pl-11 text-sm text-gray-900 shadow-sm transition placeholder:text-gray-400 focus:border-blue-400 focus:ring-2 focus:ring-blue-400/40 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500"
|
||||
/>
|
||||
<button
|
||||
x-show="query"
|
||||
x-cloak
|
||||
@click="clear()"
|
||||
aria-label="Clear filter"
|
||||
class="icon-svg icon-sm absolute top-1/2 right-2.5 -translate-y-1/2 rounded-md p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
>
|
||||
{{ partialCached "icon" "x-mark" "x-mark" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 gap-16 xl:gap-24">
|
||||
<!-- Sticky topic filter -->
|
||||
<nav class="sticky top-20 hidden w-52 flex-none flex-col self-start text-gray-500 xl:flex dark:text-gray-400">
|
||||
<p class="mb-3 text-xs font-semibold tracking-widest text-gray-400 uppercase dark:text-gray-500">
|
||||
Browse
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="clear()"
|
||||
class="flex items-center justify-between py-1.5 text-left text-sm transition-colors"
|
||||
:class="!filtering() ? 'text-black dark:text-white font-medium' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'"
|
||||
>
|
||||
<span class="flex items-center gap-2.5">
|
||||
<span>All guides</span>
|
||||
</span>
|
||||
<span class="text-xs tabular-nums text-gray-300 dark:text-gray-600">{{ len .RegularPagesRecursive }}</span>
|
||||
</button>
|
||||
{{- range $i, $tag := $tagOrder }}
|
||||
{{- $tagData := index hugo.Data.tags $tag }}
|
||||
{{- $pages := where $.RegularPagesRecursive "Params.tags" "intersect" (slice $tag) }}
|
||||
{{- if $pages }}
|
||||
<button
|
||||
type="button"
|
||||
@click="selectTag('{{ $tag }}')"
|
||||
class="flex items-center justify-between py-1.5 text-left text-sm transition-colors"
|
||||
:class="tagFilter === '{{ $tag }}' ? 'text-black dark:text-white font-medium' : 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'"
|
||||
>
|
||||
<span class="flex items-center gap-2.5">
|
||||
<span>{{ $tagData.title }}</span>
|
||||
</span>
|
||||
<span class="text-xs tabular-nums text-gray-300 dark:text-gray-600">{{ len $pages }}</span>
|
||||
</button>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</nav>
|
||||
|
||||
<!-- Guide list -->
|
||||
<div
|
||||
class="min-w-0 flex-1"
|
||||
:class="{ 'guides-flat': filtering() }"
|
||||
>
|
||||
<!-- Featured: curated highlights, shown only in the default view -->
|
||||
{{- $featured := where $.RegularPagesRecursive "Params.featured" true }}
|
||||
{{- with $featured }}
|
||||
<div x-show="!filtering()" class="mb-12">
|
||||
<p class="mb-4 flex items-center gap-1.5 text-xs font-semibold tracking-widest text-gray-400 uppercase dark:text-gray-500">
|
||||
<span class="icon-svg icon-sm text-blue-500">{{ partialCached "icon" "star" "star" }}</span>
|
||||
Featured
|
||||
</p>
|
||||
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{{- range .ByWeight }}
|
||||
<a
|
||||
href="{{ .Permalink }}"
|
||||
class="group flex flex-col gap-1.5 rounded-lg border border-gray-200 p-4 transition-colors hover:border-gray-300 dark:border-gray-800 dark:hover:border-gray-700"
|
||||
>
|
||||
<span class="font-medium leading-snug text-gray-900 transition-colors group-hover:text-blue-600 dark:text-gray-100 dark:group-hover:text-blue-400">{{ .Title }}</span>
|
||||
<span class="line-clamp-2 text-sm leading-relaxed text-gray-500 dark:text-gray-400">{{ .Summary }}</span>
|
||||
</a>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
<!-- Filtering meta bar -->
|
||||
<div
|
||||
x-show="filtering()"
|
||||
x-cloak
|
||||
class="mb-8 flex items-center justify-between border-b border-gray-200 pb-4 dark:border-gray-800"
|
||||
>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-semibold text-gray-900 dark:text-gray-100" x-text="resultCount()"></span>
|
||||
of {{ len .RegularPagesRecursive }} guides
|
||||
</p>
|
||||
<button
|
||||
@click="clear()"
|
||||
class="text-sm font-medium text-blue-600 transition hover:underline dark:text-blue-400"
|
||||
>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{- range $i, $tag := $tagOrder }}
|
||||
{{- $tagData := index hugo.Data.tags $tag }}
|
||||
{{- $pages := where $.RegularPagesRecursive "Params.tags" "intersect" (slice $tag) }}
|
||||
{{- if $pages }}
|
||||
<section
|
||||
id="{{ $tag }}"
|
||||
data-tag="{{ $tag }}"
|
||||
x-show="sectionVisible($el)"
|
||||
class="scroll-mt-16 border-t border-gray-200 py-12 first:border-t-0 first:pt-0 dark:border-gray-800"
|
||||
>
|
||||
<div class="mb-6" x-show="!filtering()">
|
||||
<p class="mb-1 text-xs font-semibold tracking-widest text-gray-400 uppercase dark:text-gray-500">
|
||||
{{- printf "%02d" (add $i 1) -}}
|
||||
</p>
|
||||
<h2 class="text-2xl font-bold">{{ $tagData.title }}</h2>
|
||||
{{- with $tagData.description }}
|
||||
<p class="mt-1 text-gray-500 dark:text-gray-400">{{ . }}</p>
|
||||
{{- end }}
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{{- range $pages }}
|
||||
{{- $tagTitles := slice }}
|
||||
{{- range .Params.tags }}{{ $tagTitles = $tagTitles | append (index hugo.Data.tags .).title }}{{ end }}
|
||||
{{- $search := lower (printf "%s %s %s %s" .Title .Summary (delimit .Params.tags " ") (delimit $tagTitles " ")) }}
|
||||
{{- $search = $search | replaceRE "\\s+" " " }}
|
||||
<div
|
||||
data-guide
|
||||
data-search="{{ $search }}"
|
||||
x-show="rowVisible($el)"
|
||||
class="grid grid-cols-[5fr_6fr] items-start gap-6 py-4"
|
||||
>
|
||||
<a
|
||||
href="{{ .Permalink }}"
|
||||
class="font-medium leading-snug text-gray-900 hover:underline dark:text-gray-100"
|
||||
>{{ .Title }}</a>
|
||||
<span class="text-sm leading-relaxed text-gray-500 dark:text-gray-400">{{ .Summary }}</span>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
<!-- No matches -->
|
||||
<div
|
||||
x-show="filtering() && resultCount() === 0"
|
||||
x-cloak
|
||||
class="flex flex-col items-center justify-center gap-3 py-24 text-center"
|
||||
>
|
||||
<span class="icon-svg icon-lg text-gray-300 dark:text-gray-600">
|
||||
{{ partialCached "icon" "magnifying-glass" "magnifying-glass" }}
|
||||
</span>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
No guides match “<span class="font-medium text-gray-700 dark:text-gray-200" x-text="query"></span>”.
|
||||
</p>
|
||||
<button
|
||||
@click="clear()"
|
||||
class="text-sm font-medium text-blue-600 transition hover:underline dark:text-blue-400"
|
||||
>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,14 @@
|
||||
{{ define "left" }}
|
||||
{{- partial "sidebar/mainnav.html" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<div class="mx-auto flex w-full max-w-7xl justify-center gap-12">
|
||||
<article class="prose dark:prose-invert min-w-0 max-w-[92ch]">
|
||||
{{ partial "content-default.html" . }}
|
||||
</article>
|
||||
<div class="-mt-8 hidden w-72 flex-none lg:block">
|
||||
{{ partial "aside.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1 +0,0 @@
|
||||
{{- template "alias.html" (dict "Permalink" .Params.target) -}}
|
||||
Reference in new issue
Block a user