Files
docker-docs/layouts/guides/landing.html
T
David KarlssonandClaude Sonnet 4.6 f02e2a5633 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>
2026-07-01 10:47:10 +02:00

219 lines
9.8 KiB
HTML

{{ define "left" }}
{{- partial "sidebar/mainnav.html" . }}
{{ end }}
{{ define "main" }}
{{- $tagOrder := slice "languages" "ai" "testing" "cicd" "security" "databases" "deployment" "admin" "labs" }}
<div
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 = ''; }
}"
>
{{- 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>
<!-- 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 }}