← Back

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-head block (eyebrow, heading, lead paragraph)
  • .quote.quote--inline.js-rail-note figure — starts display: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.
  • .columns kanban 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 .rail on 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.json at 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" and autocomplete="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 input event
  • Renders .search-drop overlay below the search form
  • Each result: title (top), section badge + content snippet (meta row below)
  • Closes on blur (150 ms delay to allow click) or Escape
  • mousedown on results uses preventDefault() 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-page layout

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-body child → uses innerHTML (rail widget path)
  • No .widget-body → targets .note-text and .note-cite directly (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 .columns
  • layouts/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):

  1. 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);
    })();
    
  2. Added explicit data-theme CSS selectors to ADMIN_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 :root rule (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

FileChange
layouts/index.htmlFull rewrite — .wrap.has-rail grid, inline note figure, kanban board
layouts/partials/ed-rail.htmlRemoved note widget section entirely
layouts/_default/taxonomy.html.Kind branch — term → postlist+rail, else → chips
layouts/_default/search.htmlRewritten to use /api/search API
layouts/partials/ed-masthead.htmlAdded id="searchinput" + autocomplete="off"
assets/css/editorial.cssBreakpoint 1000→768px; search dropdown styles; .lab-head styles; .quote--inline; removed .board/.hero rules
assets/js/sidebar-hydrate.jssetupNotes() show/hide logic; setNote() dual-path; inline search IIFE appended
server.pyimport_search_index(), fts_query(), /api/search endpoint; admin dark/light theme script + CSS
scripts/init_db.pyAdded pages + pages_fts FTS5 tables to schema
content/features.mdNew — features documentation page
docs/patch-notes-2026-06-10-session2.mdThis 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.