diff --git a/docs/superpowers/follow-ups/2026-06-02-blog-search-featured-match.md b/docs/superpowers/follow-ups/2026-06-02-blog-search-featured-match.md new file mode 100644 index 0000000..dc375ed --- /dev/null +++ b/docs/superpowers/follow-ups/2026-06-02-blog-search-featured-match.md @@ -0,0 +1,34 @@ +# Follow-up: featured article should respect search match + +**Date filed:** 2026-06-02 +**Source:** Manual test of `feat/blog-search` before merge +**Relates to:** `docs/superpowers/specs/2026-06-01-blog-search-design.md` (§ "What should happen to the featured article when a query is active?") + +## What's wrong + +When a query is typed into the blog search input, the featured article currently hides regardless of whether its content matches the query. In manual testing this felt wrong — if the featured article matches the search term, it should remain visible like any other matching row. + +## What the original design said + +During brainstorming, three options were presented for featured-article behavior under filtering. The user chose "Hide while filtering" with the reasoning "simpler mental model: query active = filter mode." After actually using the feature, the user now wants option 2 from that brainstorm: **treat the featured article as a filterable row** — visible iff it matches the query. + +## What needs to change + +In `src/components/molecules/BlogSearch.astro`, the `applyFilter` function has this branch: + +```ts +} else if (isFeatured) { + // Featured hides whenever there's a query, regardless of match. + visible = false; +} +``` + +Replace it so the featured row goes through the same `tokens.every(t => haystack.includes(t))` check as every other row. Drop the `isFeatured` special case entirely — it can fall through to the general match branch. + +Then sanity-check the empty-state count: `visibleCount` currently skips the featured (`if (visible && !isFeatured) visibleCount += 1`). Once featured participates in the filter, it should count toward the visible total — otherwise a query that *only* matches the featured will trigger the "no entries match" message even though featured is showing. Drop the `!isFeatured` guard there too. + +Visually the featured block is much bigger than a row, so when it's the sole match the layout will look unusual but not broken. If that turns out to feel wrong in practice, the second-order fix would be to render the featured-as-result in the same compact PostRow style as the archive. Don't pre-emptively build that — wait until the simple behavior change is in and see if it's actually needed. + +## Update the spec when you fix this + +Update `docs/superpowers/specs/2026-06-01-blog-search-design.md`'s relevant section so the spec reflects the new behavior. The current text claims "Featured hides while filtering" — that line is now wrong. diff --git a/docs/superpowers/plans/2026-06-01-blog-search.md b/docs/superpowers/plans/2026-06-01-blog-search.md new file mode 100644 index 0000000..1f0cf6e --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-blog-search.md @@ -0,0 +1,601 @@ +# Blog Search Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a light, in-page keyword/topic filter to `/blog` that narrows the year-grouped archive as the reader types. + +**Architecture:** Tiny client-side filter implemented as `data-search` attributes on each row + featured block. A small inline vanilla-JS script in a new `BlogSearch.astro` component tokenizes the query, toggles `.is-hidden` on rows whose `data-search` doesn't contain every token, hides empty year heads, and toggles a no-results message. No JSON index, no fetch, no React island, no new dependencies. + +**Tech Stack:** Astro 5 content collections, Astro components (`.astro`), scoped CSS, vanilla JS. + +**Source spec:** `docs/superpowers/specs/2026-06-01-blog-search-design.md` + +**Verification note:** This repo has no automated test harness, and the spec deliberately doesn't add one. Each task ends with a manual verification step using the Astro dev server (`npm run dev` → http://localhost:4321/blog) and/or grep/curl against the rendered HTML. Confirm the verification passes before committing. + +--- + +## File map + +| File | Action | Responsibility | +|---|---|---| +| `src/components/molecules/PostRow.astro` | modify | Accept `searchText` prop, emit `data-search` on root anchor | +| `src/components/molecules/YearHead.astro` | modify | Emit `data-year-head` on root | +| `src/pages/blog/index.astro` | modify | Build `searchText` per post; wire to `PostRow` + featured `
`; render `` and the empty-state `

`; add `.is-hidden` rule | +| `src/components/molecules/BlogSearch.astro` | create | Input, styles, and the filter script (including keyboard shortcuts) | + +--- + +## Task 1: `PostRow` accepts and emits `data-search` + +**Files:** +- Modify: `src/components/molecules/PostRow.astro` + +- [ ] **Step 1: Add the `searchText` prop and emit it on the root anchor** + +Edit `src/components/molecules/PostRow.astro` — change the frontmatter and the `` tag: + +```astro +--- +interface Props { + date: string; + title: string; + href: string; + deck?: string; + tag?: string; + searchText: string; +} + +const { date, title, href, deck, tag, searchText } = Astro.props; +--- + + + {date} + + {title} + {deck && {deck}} + + {tag && {tag}} + +``` + +Leave the ` +``` + +- [ ] **Step 2: Mount the component and add the empty-state element + `.is-hidden` rule in the blog index** + +In `src/pages/blog/index.astro`: + +(a) Inside the `` body, place `` between the closing `` of `.masthead` and the `{featured && (...)}` block. The resulting structure should look like: + +```astro +

+ {/* ...unchanged... */} +
+ + + + { + featured && ( +
+ {/* ...unchanged... */} +
+ ) + } +``` + +(b) After the closing `` of `.archive`, add the empty-state paragraph: + +```astro + +``` + +(c) Inside the existing ``): + +```css + .is-hidden { display: none !important; } + + .empty { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.08em; + color: var(--fg-on-dark-3); + margin: clamp(28px, 5vh, 52px) 0 0; + font-feature-settings: "onum" 1; + } + :global(body:not(.dark)) .empty { color: var(--fg-3); } +``` + +(The `!important` on `.is-hidden` defends against any future row-level `display` rule overriding it.) + +- [ ] **Step 3: Verify the build and visual placement** + +Run: `npm run build 2>&1 | tail -10` +Expected: Build succeeds. + +Run: `grep -c 'class="bsearch"' dist/blog/index.html` +Expected: `1`. + +Run: `grep -c 'data-empty' dist/blog/index.html` +Expected: `2` (the `

` and the inner ``). + +Run: `npm run dev` (background), open http://localhost:4321/blog, confirm the search input renders between the masthead and the featured article and looks at home with the rest of the page. Typing has no effect yet — that's expected. Then stop the dev server. + +- [ ] **Step 4: Commit** + +```bash +git add src/components/molecules/BlogSearch.astro src/pages/blog/index.astro +git commit -m "feat(blog): render search input, empty-state, hidden utility" +``` + +--- + +## Task 5: Add the filter script to `BlogSearch` + +**Files:** +- Modify: `src/components/molecules/BlogSearch.astro` + +- [ ] **Step 1: Append the ` +``` + +A few notes for the engineer: + +- The script runs once at module-evaluation time after the DOM is parsed (Astro hoists ` diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 14329fc..290243e 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -43,7 +43,7 @@ function buildSearchText(post: (typeof posts)[number]): string { return parts .join(" ") .toLowerCase() - .replace(/[#*_>`\[\]()]/g, " ") + .replace(/[<>#*_`\[\]()]/g, " ") .replace(/\s+/g, " ") .trim(); } @@ -129,7 +129,7 @@ for (const post of posts) {

@@ -265,7 +265,9 @@ for (const post of posts) { .feature .img { order: -1; aspect-ratio: 16 / 9; } } - .is-hidden { display: none !important; } + /* :global so the runtime-toggled class reaches PostRow and YearHead roots, + which carry their own component scope hashes — not this page's. */ + :global(.is-hidden) { display: none !important; } .empty { font-family: var(--font-mono);