Today’s goal
A visitor has the navigation, and nothing else. They cannot search, and every tab title looks alike.
- A distinct title and a description for every page
- The sitemap and feed Hugo has generated all along
- A Search page that filters as the reader types
And a page that still lists every destination when JavaScript does not run.
By the end of today you can
- Give each page a distinct title and description, with a site-wide fallback
- Recognise the sitemap and feed as XML, and say what each is for
- Build a search page from your own content, filtered in the browser
- Test by keyboard and without JavaScript, and say what a score cannot show
Start from Chapter 14 (branch chapter-14), and work on a branch.
A better head
In layouts/baseof.html, replace the title line with:
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}
{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
<meta name="description"
content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
<link rel="canonical" href="{{ .Permalink }}">if .IsHome: home uses the site name alone (titleis one line in the file)with .Description ... else: the page’s own description, or the fallback- Canonical: which address counts as the original
Let readers discover the feed
After the description and canonical lines:
{{ with .OutputFormats.Get "rss" }}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}"
href="{{ .Permalink }}" title="{{ $.Site.Title }}">
{{ end }}- Home and section pages have a feed; an article does not, so
withskips it - Inside
with, the dot is the output format;$is the context the template started with: the page
layouts/search/page.html
{{ define "main" }}
<article>
<h1>{{ .Title }}</h1>
{{ partial "page-meta.html" . }}
{{ .Content }}
<form class="search-form" role="search">
<label for="search-query">Search titles and descriptions</label>
<input type="search" id="search-query" name="q" autocomplete="off">
</form>
<p id="search-status" role="status"></p>Read the layout
- A regular page like Resources, so
page.html .Site.RegularPages: every individual page, not section landing pagesif ne .RelPermalink $.RelPermalink: leave out the Search page itself;$because the dot is now the listed pagerole="search"marks the region;role="status"marks text whose changes should be announced- A
search.jsscript tag follows, withdefer: it runs after the page exists
Build and open /search/: six pages, all linked. Typing does nothing yet.
The filtering script
The heart of static/js/search.js (full script in the chapter):
function filter() {
var query = input.value.trim().toLowerCase();
var shown = 0;
items.forEach(function (item) {
var match = query === "" || item.textContent.toLowerCase().includes(query);
item.hidden = !match;
if (match) { shown += 1; }
});
}Read it as six decisions
| Part | What it does |
|---|---|
Four document lookups | Find the form, box, list, and status by their names |
if (!form ...) guard | Stop quietly if one is missing |
filter() | Hide items that do not contain the query |
item.hidden = !match | Use the standard hidden attribute |
The submit listener | Stop Enter from reloading the page |
filter() at the end | Fill in the status once on load |
Add Search to the navigation
In layouts/baseof.html, after Resources:
<a href="{{ "search/" | relURL }}">Search</a>- Six items now: check a phone width; the navigation should wrap
- Hugo’s menu system could produce these links, but six explicit links are easier to read
- Use menus when navigation differs between languages: Chapter 16
Diagnose from the symptom
The status paragraph is empty, and setting it is the script’s last step, so the script never got there.
- Inspect the list element and read its
id - Read the four names at the top of
search.js - Find the one that differs
Chapter 5’s selector mistake again: one name, two files. Restore id="search-index".
A broken script still leaves a complete, usable index: the list is real HTML.
Test like a reader
- Keyboard: Tab reaches the skip link, the navigation, the box, then only the visible results, with a clear focus outline
- No JavaScript: disable it and reload: the full list, and a box that does nothing
- Small widths and zoom: the form stays in the column, nothing is cut off
role="status" is intended to be announced. How screen readers do it varies, and it is not yet tested: do not claim more.
Automated tools, not scores
Run Lighthouse or axe DevTools on /search/ and on an article.
- Read the individual findings: missing alt text, skipped headings, low contrast
- Several could become Chapter 14 rules
- Ignore the overall number
A tool cannot tell whether descriptions are accurate, results are useful, or an article is true. Chapter 13’s unsupported sentence would score perfectly.
Give About and Resources descriptions
Add to each page’s front matter, in your own accurate words:
description: "Why I keep this notebook, and what you can expect to find in it."description: "References that support the work recorded in this notebook."Both pages now have their own head description, and become searchable by it. Add the Search template and script to AGENTS.md.
Propose it as a pull request
git switch -c find-and-search
git add hugo.toml layouts/baseof.html layouts/search/page.html
git add content/search/index.md static/js/search.js static/css/site.css
git add content/about/index.md content/resources/index.md AGENTS.md
git commit -m "Add a search page, document titles, descriptions, and feed discovery"
git push -u origin find-and-searchLet the three Chapter 14 checks pass, read Files changed yourself, merge, and verify the live site.
When to replace this search
- The Search page’s HTML grows with the site
- It only sees titles and descriptions
Somewhere between tens and a few hundred pages, or when readers expect full-text results, move to a generated index or a search service.
Change the approach when you can describe the reader’s unmet need, not at a page count.
When something goes wrong
| What you see | What to check |
|---|---|
| The home tab repeats the name | The if .IsHome branch replaced the old line |
| Every page shows the site description | The fallback: add page descriptions |
.Site.Title empty in the feed block | Use $.Site.Title |
| Typing does nothing, no console error | The four names in search.js |
| A page is missing from Search | Is it a draft? |
Completion check
- Each page has a distinct title; home does not repeat the name
- Every page has a head description, with a fallback
- I can say what
/sitemap.xmland/index.xmlare for - I can explain why
$is needed insidewithandrange - Search filters as I type and reports how many matched
- I diagnosed the silent failure from the empty status line
- The page lists every destination without JavaScript
- I can name something no audit could establish