Patch notes — homepage redesign, search, rail fixes (2026-06-10 session 2)
Branch: dev. Continued from session 1 (admin consolidation phases 1–5).
All changes in working directory — not yet committed.
1 — Homepage layout redesign
Problem: The old homepage used a .board grid (hero + .board > .rail) separate from
the inner-page .wrap.has-rail grid. The hero/board structure left dead space when the
note widget was hidden, and the kanban board could not fill the gap dynamically.
Fix: Replaced the entire homepage layout with .wrap.has-rail (same 2-column grid as
all other pages).
Left column (rail-main):
.lab-headblock (eyebrow, heading, lead paragraph).quote.quote--inline.js-rail-notefigure — startsdisplay:none, shown only when notes are enabled in admin. Positioned directly above the kanban board so the board floats up naturally when no note is active..columnskanban board (Featured · Latest · Docs · Lab Notes)
Right column: {{ partial "ed-rail.html" . }} — same sidebar used on all other pages.
CSS changes (editorial.css):
- Removed
.board,.board > :first-child,.board > .rail,.hero,.hero-about - Added
.lab-head,.lab-title,.lab-lead - Added
.quote--inline { transform: rotate(-.6deg); margin: 0 0 28px; max-width: 480px; } @media (max-width: 1080px)now only collapses.columns(previously also collapsed.railon the homepage board)
Files: layouts/index.html, assets/css/editorial.css
2 — Full-text search (FTS5)
Architecture: Hugo outputs /public/index.json (Blowfish’s built-in JSON output).
At server startup, import_search_index() reads this file and imports all non-taxonomy
pages into SQLite FTS5. Search queries are served from /api/search.
Schema (added to scripts/init_db.py):
CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
url TEXT NOT NULL,
section TEXT NOT NULL DEFAULT '',
summary TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL DEFAULT ''
);
CREATE VIRTUAL TABLE IF NOT EXISTS pages_fts USING fts5(
title, content, content='pages', content_rowid='id'
);
import_search_index() (server.py):
- Reads
/public/index.jsonat startup (once) - Skips taxonomy/meta pages (types:
tags,categories,authors,series,taxonomy,page; also skips permalink/and/search) - Upserts all remaining entries into
pages+ triggers FTS5 index rebuild
fts_query(query) (server.py):
- Tokenises query to
[A-Za-z0-9ÆØÅæøå]+words (max 8 terms) - Returns FTS5 prefix-query string (
term1* term2* …)
/api/search endpoint:
GET /api/search?q=<query>- Returns
{"query": "...", "results": [{title, url, section, snippet}]} - Uses
snippet(pages_fts, 1, '', '', '…', 20)for content excerpts - Max 15 results, ordered by FTS5 relevance rank
- Gracefully returns empty results on
sqlite3.OperationalError
Search input (layouts/partials/ed-masthead.html):
- Added
id="searchinput"andautocomplete="off"to the search<input>
Inline dropdown (assets/js/sidebar-hydrate.js):
- Appended as a self-contained IIFE after the main hydration code
- 180 ms debounce on
inputevent - Renders
.search-dropoverlay below the search form - Each result: title (top), section badge + content snippet (meta row below)
- Closes on
blur(150 ms delay to allow click) orEscape mousedownon results usespreventDefault()to prevent blur before navigation
Search page (layouts/_default/search.html):
- Rewritten to use
fetch("/api/search?q=…")on page load (reads?q=from URL) - Debounced live updates as user types in the on-page search field
- Results rendered in the same
.search-pagelayout
Search dropdown CSS (added to editorial.css):
.search-drop — absolute positioned overlay, z-index 200, shadow
.sdrop-row — flex column, each result link
.sdrop-title — bold result title
.sdrop-meta — flex row: section badge + snippet
.sdrop-section — uppercase badge chip
.sdrop-snip — muted truncated content excerpt
.sdrop-empty — "No results" message
Files: server.py, scripts/init_db.py, layouts/partials/ed-masthead.html,
assets/js/sidebar-hydrate.js, layouts/_default/search.html, assets/css/editorial.css
3 — Note widget (hide/show + homepage only)
Problem 1 — note showed when no notes enabled.
The .js-rail-note element was always visible on load; JS only populated text.
Fix: Both the homepage <figure> and the rail <section> now start with
style="display:none". setupNotes() in sidebar-hydrate.js sets
noteWidget.style.display = "" to reveal or "none" to keep hidden, based on whether
any notes with enabled=1 exist in the API response.
Problem 2 — note was duplicated (rail + homepage figure).
The rail had {{ if not .IsHome }} guard and the homepage had an inline figure, but both
were .js-rail-note targets. setNote() needed to handle both structures.
setNote() fix:
- Checks for
.widget-bodychild → usesinnerHTML(rail widget path) - No
.widget-body→ targets.note-textand.note-citedirectly (homepage figure path)
Problem 3 — note in wrong column.
Note was being rendered inside ed-rail.html (right column) even on the homepage where
the quote should appear in the left column above the kanban board.
Fix:
layouts/index.html—<figure class="quote quote--inline js-rail-note">placed in left column above.columnslayouts/partials/ed-rail.html— the note<section>block removed entirely (no more{{ if not .IsHome }}guard; rail never renders the note widget on any page)- Note is now front page only
Files: layouts/index.html, layouts/partials/ed-rail.html,
assets/js/sidebar-hydrate.js
4 — Sidebar visibility (CSS breakpoint fix)
Problem: @media (max-width: 1000px) collapsed the 2-column .wrap.has-rail grid to
single-column, pushing the sidebar below the main content. Typical laptop viewports
(1024–1280 px) fell at or near this boundary, making the sidebar invisible without
scrolling far down.
Fix: Lowered the collapse breakpoint from 1000px to 768px.
/* before */
@media (max-width: 1000px) { .wrap.has-rail { grid-template-columns: 1fr; } … }
@media (max-width: 620px) { .has-rail .rail { grid-template-columns: 1fr; } }
/* after */
@media (max-width: 768px) { .wrap.has-rail { grid-template-columns: 1fr; } … }
@media (max-width: 480px) { .has-rail .rail { grid-template-columns: 1fr; } }
At 769 px+, the sidebar renders as a proper 300 px right column. At ≤768 px (tablet/phone), the grid collapses to single column.
Files: assets/css/editorial.css
5 — taxonomy.html (Hugo 0.161.1 compatibility)
Problem: Hugo 0.161.1 routes taxonomy term pages (e.g. /categories/documentation/)
through _default/taxonomy.html rather than _default/term.html. This caused the
Documentation and Homelab pages to render as chip-tag overviews (no sidebar, no postlist).
Local Hugo 0.154.5 used term.html correctly — the divergence was version-specific.
Fix: taxonomy.html now checks .Kind and branches:
{{- if eq .Kind "term" }}
<!-- has-rail postlist layout (same as term.html) -->
{{- else }}
<!-- chips overview layout (categories/tags index) -->
{{- end }}
Both /categories/ (kind taxonomy) and /categories/documentation/ (kind term) are
handled correctly by a single template. term.html remains in place and continues to work
on Hugo versions that route to it.
Files: layouts/_default/taxonomy.html
6 — Admin dark/light mode
Problem: The admin area used @media(prefers-color-scheme:dark) only — it did not read
the user’s manual light/dark preference stored in localStorage as skui-theme.
Fix (two parts):
Added the theme-reading inline script to
chrome_head()(runs before first paint, identical to the public site’s pattern):(function(){ var t = localStorage.getItem("skui-theme"); if (t === "light" || t === "dark") document.documentElement.setAttribute("data-theme", t); })();Added explicit
data-themeCSS selectors toADMIN_CSS:html[data-theme=dark] { --bg:#1a2330; --surface:#222d3a; … } html[data-theme=light] { --bg:#f3eee2; --surface:#fbf8f0; … }These have higher specificity (0-1-1) than the media-query
:rootrule (0-1-0), so a manual theme preference always wins over the OS preference.
Admin now follows the same light/dark state as the public site immediately on page load, with no flash.
Files: server.py
7 — Features page
Created /content/features.md — a standalone Hugo page at /features/ documenting:
- Site & theme capabilities
- Admin area (table of all sections)
- Right rail widgets
- Custom and Blowfish shortcodes (full list)
- Page templates and their URL patterns
- Infrastructure (Hugo, Python server, SQLite, Docker, Gitea Actions)
Not in the site navigation (personal reference, not public-facing).
Files: content/features.md
File change summary
| File | Change |
|---|---|
layouts/index.html | Full rewrite — .wrap.has-rail grid, inline note figure, kanban board |
layouts/partials/ed-rail.html | Removed note widget section entirely |
layouts/_default/taxonomy.html | .Kind branch — term → postlist+rail, else → chips |
layouts/_default/search.html | Rewritten to use /api/search API |
layouts/partials/ed-masthead.html | Added id="searchinput" + autocomplete="off" |
assets/css/editorial.css | Breakpoint 1000→768px; search dropdown styles; .lab-head styles; .quote--inline; removed .board/.hero rules |
assets/js/sidebar-hydrate.js | setupNotes() show/hide logic; setNote() dual-path; inline search IIFE appended |
server.py | import_search_index(), fts_query(), /api/search endpoint; admin dark/light theme script + CSS |
scripts/init_db.py | Added pages + pages_fts FTS5 tables to schema |
content/features.md | New — features documentation page |
docs/patch-notes-2026-06-10-session2.md | This file |
Known state
- All changes are in the working directory — not yet committed (user preference).
- Container rebuild required to see any change:
docker compose -f compose.dev.yml up -d --build - Local Hugo (0.154.5) is too old to build the site; all local Hugo commands are for template debugging only. The Docker build (0.161.1) is the authoritative build.