diff --git a/.gitignore b/.gitignore index 2eef085..96d16bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .hlx/* coverage/* +drafts/* logs/* node_modules/* diff --git a/blocks/post-index/post-index.css b/blocks/post-index/post-index.css new file mode 100644 index 0000000..9a04211 --- /dev/null +++ b/blocks/post-index/post-index.css @@ -0,0 +1,101 @@ +/* tva */ + +/* EDS section wrapper handles horizontal padding */ +.post-index { + max-width: var(--measure); + margin-inline: auto; + padding-inline: 0; +} + +.post-index .post-entry { + margin-block-end: var(--section-spacing); +} + +.post-index .post-entry + .post-entry { + border-top: 1px solid var(--color-border-subtle); + padding-block-start: var(--section-spacing); +} + +.post-index .post-type { + display: block; + font-family: var(--font-heading); + font-size: var(--body-font-size-xs); + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1; +} + +.post-index h2 { + font-size: var(--heading-font-size-m); + font-weight: 600; + color: var(--color-heading); + line-height: var(--line-height-heading); + margin-block: 0; +} + +/* Title link styles — intentionally ordered before tag link rules. + The tag link selectors (.post-tags a:hover) reach higher specificity via + pseudo-classes, so title link base styles appear first in source order. */ +.post-index h2 a { + color: inherit; + text-decoration: none; +} + +.post-index h2 a:hover { + text-decoration: underline; +} + +.post-index h2 a:focus-visible { + outline: 2px solid var(--color-heading); + outline-offset: 2px; +} + +.post-index .post-description { + font-size: var(--body-font-size-s); + color: var(--color-text); + line-height: var(--line-height-body); +} + +.post-index .post-meta { + font-size: var(--body-font-size-xs); + color: var(--color-text-muted); +} + +.post-index .post-tags { + display: inline; + list-style: none; + margin: 0; + padding: 0; +} + +.post-index .post-tags li { + display: inline; +} + +.post-index .post-tags li + li::before { + content: " \B7 "; + color: var(--color-text-muted); +} + +.post-index .post-meta time + .post-tags::before { + content: " \B7 "; + color: var(--color-text-muted); +} + +/* Tag link styles have higher specificity than h2 a due to .post-tags class. + Intentional: tag links are scoped to metadata, title links to headings. */ +/* stylelint-disable-next-line no-descending-specificity */ +.post-index .post-tags a { + color: var(--color-link); + text-decoration: none; +} + +.post-index .post-tags a:hover { + text-decoration: underline; +} + +.post-index .post-tags a:focus-visible { + outline: 2px solid var(--color-heading); + outline-offset: 2px; +} diff --git a/blocks/post-index/post-index.js b/blocks/post-index/post-index.js new file mode 100644 index 0000000..b0f2c1a --- /dev/null +++ b/blocks/post-index/post-index.js @@ -0,0 +1,206 @@ +// tva + +/** + * Post Index block — fetches /query-index.json and renders a reverse-chronological + * list of post entries on the home page. + * + * Design spec: docs/design-decisions/DDD-004-home-post-index.md + */ + +const TYPE_LABELS = { + 'build-log': 'Build Log', + pattern: 'Pattern', + 'tool-report': 'Tool Report', + til: 'TIL', +}; + +const DATE_FORMATTER = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', +}); + +/** + * Parse a date value from query-index.json. + * EDS may return Unix timestamps (seconds) or ISO strings. + * Returns a millisecond timestamp for use with Date(). + * @param {string|number} value + * @returns {number} + */ +function parseDate(value) { + if (!value) return 0; + const num = Number(value); + if (!Number.isNaN(num) && num > 0 && num < 1e10) { + // EDS returns Unix timestamps in seconds. 1e10 distinguishes seconds (10 digits) + // from milliseconds (13 digits) to prevent double-multiplication. + return num * 1000; + } + return new Date(value).getTime() || 0; +} + +/** + * Format a millisecond timestamp as YYYY-MM-DD for datetime attributes. + * @param {number} ms + * @returns {string} + */ +function toIsoDate(ms) { + const d = new Date(ms); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * Build a single post entry
element. + * @param {Object} entry - A single entry from query-index.json .data array + * @param {number} position - 1-based position in sorted list (for aria-labelledby IDs) + * @returns {HTMLElement|null} article element, or null if entry should be skipped + */ +function buildEntry(entry, position) { + const { + path, title, type, description, date, tags, + } = entry; + + // Required field — skip entry entirely if missing + if (!title) return null; + + // Security: validate path before use in href + if (!path || !path.startsWith('/')) return null; + + const titleId = `post-${position}-title`; + const typeLabel = TYPE_LABELS[type] || null; + + const article = document.createElement('article'); + article.className = 'post-entry'; + article.setAttribute('aria-labelledby', titleId); + + // Type badge (visible, aria-hidden — heading sr-only prefix carries the type for AT) + if (typeLabel) { + const badge = document.createElement('span'); + badge.className = 'post-type'; + badge.setAttribute('aria-hidden', 'true'); + badge.textContent = typeLabel; + article.append(badge); + } + + // Title heading + const h2 = document.createElement('h2'); + h2.id = titleId; + + if (typeLabel) { + // sr-only prefix gives screen reader heading nav the type context. + // Trailing ': ' (colon-space) ensures AT separates type from title without + // relying on whitespace text nodes. + const srPrefix = document.createElement('span'); + srPrefix.className = 'sr-only'; + srPrefix.textContent = `${typeLabel}: `; + h2.append(srPrefix); + } + + const titleLink = document.createElement('a'); + titleLink.href = path; + titleLink.textContent = title; + h2.append(titleLink); + article.append(h2); + + // Description + if (description) { + const p = document.createElement('p'); + p.className = 'post-description'; + p.textContent = description; + article.append(p); + } + + // Metadata footer (date + tags) + const footer = document.createElement('footer'); + footer.className = 'post-meta'; + + const dateMs = parseDate(date); + if (dateMs) { + const time = document.createElement('time'); + time.setAttribute('datetime', toIsoDate(dateMs)); + time.textContent = DATE_FORMATTER.format(new Date(dateMs)); + footer.append(time); + } + + if (tags) { + const slugs = tags + .split(',') + .map((t) => t.trim()) + .filter((t) => /^[a-z0-9-]+$/.test(t)); // validate slug before use in href + + if (slugs.length > 0) { + // Intentionally no aria-label on tag list -- single list per article makes it unambiguous + const ul = document.createElement('ul'); + ul.className = 'post-tags'; + + slugs.forEach((slug) => { + const li = document.createElement('li'); + const a = document.createElement('a'); + a.href = `/tags/${slug}`; + a.textContent = slug; + li.append(a); + ul.append(li); + }); + + footer.append(ul); + } + } + + // Only append footer if it has content + if (footer.children.length > 0) { + article.append(footer); + } + + return article; +} + +/** + * Loads and decorates the post-index block. + * Fetches /query-index.json, sorts by date descending, renders entries. + * @param {Element} block The block element + */ +export default async function decorate(block) { + // sr-only h1 must appear first in DOM order + const h1 = document.createElement('h1'); + h1.className = 'sr-only'; + h1.textContent = 'Posts'; + + block.textContent = ''; + block.append(h1); + + let entries; + try { + const response = await fetch('/query-index.json'); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const json = await response.json(); + entries = json.data; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Post index: failed to load query-index.json', error); + return; + } + + if (!Array.isArray(entries) || entries.length === 0) { + // eslint-disable-next-line no-console + console.warn('Post index: query-index.json returned no entries'); + return; + } + + // Sort by date descending; entries without dates fall to end + entries.sort((a, b) => { + const da = parseDate(a.date); + const db = parseDate(b.date); + return db - da; + }); + + entries.forEach((entry, i) => { + const article = buildEntry(entry, i + 1); + if (article) { + block.append(article); + } + }); +} diff --git a/docs/content-model.md b/docs/content-model.md index f0da702..4510bf2 100644 --- a/docs/content-model.md +++ b/docs/content-model.md @@ -60,3 +60,26 @@ Tags only. Flat namespace. Lowercase, hyphenated. ## Structured Data JSON-LD on every post page using `TechArticle` with `proficiencyLevel: Expert`. + +## Query Index + +Blog post metadata is exposed to the frontend via the EDS content index. The `helix-query.yaml` file at the project root controls indexing. + +**Endpoint:** `/query-index.json` + +**Include path:** `/blog/**` (only blog posts are indexed) + +**Indexed columns:** + +| Column | Source | Notes | +|---|---|---| +| `path` | Automatic | Page URL path. Included by EDS automatically. | +| `title` | `og:title` meta tag | Page title. | +| `description` | `description` meta tag | 1-2 sentence summary. | +| `date` | `date` meta tag | Publication date. Used for sort order. | +| `type` | `type` meta tag | Post type enum. | +| `tags` | `tags` meta tag | Comma-separated tag slugs. | + +Adding a new metadata field to posts requires a corresponding property entry in `helix-query.yaml` to make it available in the index. + +Changes to `helix-query.yaml` take effect after EDS reindexes content (automatic on next content preview/publish). diff --git a/docs/design-decisions/DDD-004-home-post-index.md b/docs/design-decisions/DDD-004-home-post-index.md index ef1b6e2..366261a 100644 --- a/docs/design-decisions/DDD-004-home-post-index.md +++ b/docs/design-decisions/DDD-004-home-post-index.md @@ -1,6 +1,6 @@ # DDD-004: Home Page Post Index -Status: **Proposal** +Status: **Implemented** ## Context @@ -546,4 +546,13 @@ The `description` field shown in each index entry can come from two sources: (a) ### Reviewer Notes -_Human writes here during review._ +**Implemented** on 2026-03-13, branch `nefario/ddd-004-home-post-index`. + +**Open Question resolutions:** +- OQ1 (entry spacing): Using `--section-spacing` (48px) as the initial value. Can be tuned with real content. +- OQ2 (tag casing): Tags displayed as lowercase slugs, matching the content model canonical form. +- OQ3 (tag links before DDD-007): Tags rendered as links from day one. 404s are acceptable -- the 404 page has a "Go home" link. +- OQ4 (helix-query.yaml): Configured with standard EDS property selectors. `path` automatic. +- OQ5 (.sr-only placement): Defined in `styles/styles.css` (eager) rather than `lazy-styles.css`, per accessibility review. +- OQ6 (auto-block vs authored): Auto-block via `buildPostIndexBlock()` in `scripts.js`, detecting `pathname === '/'`. +- OQ7 (description provenance): Uses `description` meta tag as authored by the writer. Not auto-extracted. diff --git a/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index.md b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index.md new file mode 100644 index 0000000..c722a44 --- /dev/null +++ b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index.md @@ -0,0 +1,139 @@ +--- +task: "Implement DDD-004: Build post-index block for the home page" +date: 2026-03-13 +source-issue: 20 +slug: implement-ddd-004-home-post-index +mode: execution +task-count: 4 +gate-count: 0 +compaction-events: 2 +--- + +# Nefario Report: Implement DDD-004 Home Post Index + +## Summary + +Built the `post-index` EDS block that fetches `/query-index.json` and renders a reverse-chronological post list on the home page. Created `helix-query.yaml` for content indexing, `blocks/post-index/post-index.css` and `post-index.js` for the block, auto-block wiring in `scripts.js`, `.sr-only` utility class in `styles.css`, and documentation updates to `content-model.md` and DDD-004 status. + +## Original Prompt + +Implement DDD-004: Build post-index block for the home page (#20). Create helix-query.yaml, post-index block (CSS + JS), auto-block wiring in scripts.js, .sr-only utility, and documentation updates per the design contract in docs/design-decisions/DDD-004-home-post-index.md. + +## Key Design Decisions + +### parseDate boundary guard (num < 1e10) + +The original implementation used `num > 0` to detect Unix timestamps, which would incorrectly double-multiply millisecond timestamps (producing dates in year ~57,000). Code review caught this — added `num < 1e10` to distinguish 10-digit second timestamps from 13-digit millisecond timestamps. Excel serial number handling was intentionally omitted (indistinguishable from Unix timestamps per margo ADVISE). + +### .sr-only in styles.css (eager) not lazy-styles.css + +Frontend-minion recommended lazy-styles.css. Accessibility-minion recommended styles.css. Resolution: styles.css wins — zero rendering cost, prevents FOUC if any future eager-loaded block needs it, eliminates timing dependency. Implementation uses modern `clip-path: inset(50%)` instead of deprecated `clip: rect(0,0,0,0)` per stylelint. + +### Path validation for open redirect prevention + +Security advisory: `entry.path.startsWith('/')` guard added to prevent open redirect if query-index.json is poisoned with external URLs. Low-probability risk for a static site, but zero-cost defensive measure. + +### Tag links render as 404s until DDD-007 + +Tags are rendered as links (`/tags/{slug}`) from day one even though tag pages don't exist yet. Accepted trade-off: the 404 page has a "Go home" link, and retroactively adding links after DDD-007 would require a code change. + +## Phases + +| Phase | Outcome | +|---|---| +| 1. Meta-plan | 5 specialists identified (frontend-minion, ux-strategy-minion, ux-design-minion, accessibility-minion, software-docs-minion) | +| 2. Specialist planning | All aligned; 1 conflict (.sr-only placement). Comprehensive DOM structure, CSS selectors, and edge cases documented. | +| 3. Synthesis | 4 tasks, 0 gates. Tasks 1+2+4 parallelizable per margo ADVISE. | +| 3.5. Architecture review | 6 reviewers: 2 APPROVE (ux-strategy, accessibility), 4 ADVISE (security, test, margo, lucy), 0 BLOCK | +| 4. Execution | 4 tasks in 2 batches. All lint clean. | +| 5. Code review | 3 reviewers: 1 APPROVE (margo), 2 ADVISE (code-review-minion, lucy). 1 BLOCK finding auto-fixed (parseDate boundary). | +| 6. Tests | Lint passes (ESLint + Stylelint). No unit test framework. Test content in drafts/ for manual verification. | +| 8. Documentation | Skipped — all docs handled in Task 4 (content-model.md + DDD-004 status). | + +## Agent Contributions + +### Planning (Phase 2) + +| Agent | Contribution | +|---|---| +| frontend-minion | Complete DOM structure, CSS selectors, auto-block pattern, date parsing, tag handling | +| ux-strategy-minion | Validated scan-friendly layout, progressive disclosure, empty state handling | +| ux-design-minion | Verified DDD-004 visual spec completeness, token usage, spacing rationale | +| accessibility-minion | ARIA patterns (aria-labelledby, aria-hidden, sr-only), .sr-only placement, AccName 1.2 compliance | +| software-docs-minion | Content-model.md structure, DDD-004 status update format, OQ resolution documentation | + +### Review (Phase 3.5) + +| Agent | Verdict | Key Finding | +|---|---|---| +| security-minion | ADVISE | Path validation guard for open redirect prevention | +| test-minion | ADVISE | Mock query-index.json needed for local testing; edge-case test post | +| margo | ADVISE | Tasks 1+2 can parallelize; drop Excel serial number claim | +| lucy | ADVISE | Add drafts/ to .gitignore; CSS padding comment; DDD-004 status note | +| ux-strategy-minion | APPROVE | Journey coherence validated | +| accessibility-minion | APPROVE | ARIA patterns correct | + +### Code Review (Phase 5) + +| Agent | Verdict | Key Finding | +|---|---|---| +| code-review-minion | ADVISE | parseDate boundary guard (BLOCK → auto-fixed); pathname comment; empty-array console.warn | +| lucy | ADVISE | clip-path vs clip deviation noted (improvement over spec); boilerplate margin cleanup opportunity | +| margo | APPROVE | No over-engineering; proportional complexity | + +## Execution + +### Task 1: Create helix-query.yaml + .sr-only utility class +- **Agent**: frontend-minion (sonnet) +- **Files**: `helix-query.yaml` (new, +25 lines), `styles/styles.css` (modified, +13 lines) +- **Note**: .sr-only uses `clip-path: inset(50%)` (modern) instead of `clip: rect(0,0,0,0)` (deprecated) per stylelint + +### Task 2: Create post-index block (CSS + JS) +- **Agent**: frontend-minion (sonnet) +- **Files**: `blocks/post-index/post-index.css` (new, +101 lines), `blocks/post-index/post-index.js` (new, +206 lines) +- **Note**: All 17 DDD-004 CSS selectors implemented; no innerHTML; path validation; tag slug validation + +### Task 3: Wire auto-block in scripts.js + test content +- **Agent**: frontend-minion (sonnet) +- **Files**: `scripts/scripts.js` (modified, +14 lines), `drafts/` (4 HTML files + query-index.json mock) +- **Note**: `drafts/` added to .gitignore; mock includes edge-case entry with missing fields + +### Task 4: Update content model docs + DDD-004 status +- **Agent**: software-docs-minion (sonnet) +- **Files**: `docs/content-model.md` (modified, +23 lines), `docs/design-decisions/DDD-004-home-post-index.md` (modified) +- **Note**: All 7 open questions resolved in DDD-004 reviewer notes + +## Verification + +Code review passed (1 BLOCK finding auto-fixed: parseDate boundary guard). Lint passes clean (ESLint + Stylelint). No unit test infrastructure. Documentation handled in execution. + +## Test Plan + +- [ ] Start dev server: `npx -y @adobe/aem-cli up --no-open --forward-browser-logs --html-folder drafts` +- [ ] Verify `http://localhost:3000/` renders post-index block with 3 test entries in reverse-chronological order +- [ ] Verify edge-case entry (missing fields) degrades gracefully +- [ ] Verify type badges display correctly (Build Log, Tool Report, TIL) +- [ ] Verify tag links point to `/tags/{slug}` +- [ ] Verify focus rings on title and tag links (keyboard navigation) +- [ ] Verify screen reader announces type prefix via sr-only span +- [ ] Check preview URL after push: `https://nefario-ddd-004-home-post-index--grounded--benpeter.aem.page/` + +## Session Resources + +
+Skills Invoked + +- `/nefario` — orchestration + +
+ +
+Compaction + +2 compaction events (Phase 3 and Phase 3.5 checkpoints). + +
+ +## Working Files + +Companion directory: `docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/` diff --git a/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase1-metaplan-prompt.md b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase1-metaplan-prompt.md new file mode 100644 index 0000000..c1719a2 --- /dev/null +++ b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase1-metaplan-prompt.md @@ -0,0 +1,58 @@ +MODE: META-PLAN + +You are creating a meta-plan — a plan for who should help plan. + +## Task + + +**Outcome**: The home page displays posts as a reverse-chronological list, implementing the design contract defined in `docs/design-decisions/DDD-004-home-post-index.md`. Visitors landing on the site see an index of all published posts with type badge, title, description, date, and tags per entry — the primary navigation surface for the blog. + +**Success criteria**: +- `blocks/post-index/post-index.js` and `blocks/post-index/post-index.css` exist and follow EDS block conventions +- `helix-query.yaml` exists at project root with columns: `path`, `title`, `description`, `date`, `type`, `tags` and include path `/blog/**` +- Home page renders post entries matching the decorated DOM structure in DDD-004 (article > type badge + h2 + description + footer with time and tag list) +- All design tokens map to existing CSS custom properties per DDD-004's Token Usage table — no new tokens +- Type badges render as uppercase text-only labels via CSS `text-transform` with identical treatment for all four types +- Tag slugs validated against `/^[a-z0-9-]+$/` before use in href attributes +- DOM built via `createElement()`/`textContent`/`setAttribute` — no `innerHTML` +- Visually hidden `

` appears first in `
` DOM order +- Focus indicators match DDD-002/DDD-003 pattern: `outline: 2px solid var(--color-heading); outline-offset: 2px` on `:focus-visible` +- `.sr-only` utility class defined (or verified existing) in project CSS +- Empty state: no articles rendered when query index returns zero results +- `npm run lint` passes +- Local dev server at `localhost:3000` renders the post index correctly + +**Scope**: +- In: `blocks/post-index/` (JS + CSS), `helix-query.yaml`, home page wiring (auto-block or authored — resolve DDD-004 Open Question 6), `.sr-only` class if not already defined +- Out: Tag index pages (DDD-007), blog post detail pages (DDD-005), dark mode toggle, pagination, RSS, search + +**Constraints**: +- All implementation decisions must conform to `docs/design-decisions/DDD-004-home-post-index.md` +- Resolve DDD-004 Open Questions during implementation (entry spacing, tag casing, description provenance, auto-block vs authored) + + +## Working Directory +/Users/ben/github/benpeter/mostly-hallucinations + +## External Skill Discovery +Before analyzing the task, scan for project-local skills. If skills are +discovered, include an "External Skill Integration" section in your meta-plan +(see your Core Knowledge for the output format). + +## Instructions +1. Read relevant files to understand the codebase context +2. Discover external skills: + a. Scan .claude/skills/ and .skills/ in the working directory for SKILL.md files + b. Read frontmatter (name, description) for each discovered skill + c. For skills whose description matches the task domain, classify as + ORCHESTRATION or LEAF (see External Skill Integration in your Core Knowledge) + d. Check the project's CLAUDE.md for explicit skill preferences + e. Include discovered skills in your meta-plan output +3. Analyze the task against your delegation table +4. Identify which specialists should be CONSULTED FOR PLANNING + (not execution — planning). These are agents whose domain + expertise is needed to create a good plan. +5. For each specialist, write a specific planning question that + draws on their unique expertise. +6. Return the meta-plan in the structured format. +7. Write your complete meta-plan to `/var/folders/3k/bfjvvz9s6dvdn_hvlhvr8lc00000gn/T//nefario-scratch-d3ZWda/implement-ddd-004-home-post-index/phase1-metaplan.md` diff --git a/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase1-metaplan.md b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase1-metaplan.md new file mode 100644 index 0000000..8a2722f --- /dev/null +++ b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase1-metaplan.md @@ -0,0 +1,80 @@ +# Phase 1: Meta-Plan — Implement DDD-004 Home Post Index + +## Task Summary + +Implement the home page post index block as specified in `docs/design-decisions/DDD-004-home-post-index.md`. This involves creating a new EDS block (`blocks/post-index/`), a `helix-query.yaml` configuration file, wiring the block into the home page (auto-block vs authored), and adding the `.sr-only` utility class. The design contract is fully defined -- this is pure implementation work. + +## Scope + +**In scope**: +- `blocks/post-index/post-index.js` -- fetch query-index.json, sort, build DOM per DDD-004 +- `blocks/post-index/post-index.css` -- all styling per DDD-004 CSS Approach section +- `helix-query.yaml` at project root -- columns: path, title, description, date, type, tags; include: /blog/** +- Home page wiring -- auto-block injection in `scripts.js` (DDD-004 OQ6 recommends auto-block) +- `.sr-only` utility class in `styles/lazy-styles.css` (confirmed not yet defined anywhere) +- Resolution of DDD-004 Open Questions 1, 5, 6 during implementation + +**Out of scope**: Tag index pages (DDD-007), blog post detail (DDD-005), dark mode toggle, pagination, RSS, search + +## Codebase Context + +Key observations from reading the codebase: +- **No `.sr-only` class exists** -- `styles/lazy-styles.css` is empty; no definition found anywhere +- **No `helix-query.yaml` exists** -- must be created +- **`buildAutoBlocks()` in `scripts.js`** currently only handles hero blocks and fragments. Post-index auto-blocking needs to be added here +- **Existing block patterns**: Header block shows the DOM construction pattern (createElement, textContent, setAttribute). Footer is simpler (fragment-based). Post-index follows the header pattern since it builds DOM from JSON data +- **`styles/styles.css` section layout**: `main > .section > div` gets `max-width: var(--layout-max)` and responsive padding. The post-index block needs `max-width: var(--measure)` within this container +- **Current branch**: `nefario/ddd-004-home-post-index` (already exists from the design phase) + +## Planning Consultations + +### Consultation 1: EDS Block Implementation Strategy +- **Agent**: frontend-minion +- **Planning question**: Given the codebase patterns in `scripts/scripts.js` (buildAutoBlocks, loadEager/loadLazy phases) and existing blocks (header.js, footer.js), what is the optimal implementation approach for the post-index block? Specifically: (1) Should the auto-block injection in `buildAutoBlocks()` check `window.location.pathname === '/'` or use a different home page detection method? (2) How should the block handle the fetch of `/query-index.json` -- inline in `decorate()` or via a utility? (3) The `styles/styles.css` applies `main > .section > div { max-width: var(--layout-max) }` -- will the block's `max-width: var(--measure)` work correctly within this container without layout conflicts? (4) What is the correct `helix-query.yaml` syntax for EDS query index configuration? +- **Context to provide**: `scripts/scripts.js`, `blocks/header/header.js`, `styles/styles.css`, DDD-004 HTML Structure and CSS Approach sections, EDS documentation at aem.live +- **Why this agent**: Frontend-minion has EDS block development expertise and can identify implementation pitfalls in the auto-blocking pattern, section layout interaction, and query index fetch + +### Consultation 2: Accessibility Implementation Review +- **Agent**: accessibility-minion +- **Planning question**: DDD-004 specifies a detailed accessibility pattern: sr-only h1, aria-hidden badges, sr-only prefixes in h2, aria-labelledby on articles, tag validation regex. Review the proposed DOM structure and interaction patterns for correctness. Specifically: (1) Is the `aria-labelledby` pointing to the `

` id correct when the h2 contains both an sr-only span and an anchor? Will screen readers announce the full computed text or just the anchor text? (2) Does the `.sr-only` class definition in DDD-004 match current best practices? (3) Are there any keyboard navigation issues with the tab order (title link, then tag links per entry)? (4) Does the empty state (sr-only h1 only, no articles) create any accessibility issues? +- **Context to provide**: DDD-004 HTML Structure section, the `.sr-only` class definition, existing focus ring patterns from DDD-002/DDD-003 +- **Why this agent**: The accessibility pattern in DDD-004 is sophisticated (dual badge/sr-only, aria-labelledby, tag list semantics). Getting this wrong creates WCAG failures that are hard to catch in code review + +## Cross-Cutting Checklist + +- **Testing** (test-minion): Not needed for planning. The implementation is a single EDS block with well-defined success criteria. Test strategy is straightforward: lint passes, dev server renders correctly. Phase 6 handles test execution post-implementation. +- **Security** (security-minion): Not needed for planning. The security concern (tag slug validation) is already addressed in DDD-004 with the regex `/^[a-z0-9-]+$/`. No auth, no user input, no secrets. The implementing agent applies the validation as specified. +- **Usability -- Strategy** (ux-strategy-minion): ALWAYS include. The home page IS the primary navigation surface. UX strategy should validate that the implementation plan preserves the user journey coherence defined in DDD-004. Planning question: The home page is the only navigation surface for the blog (no sidebar, no search, no categories). Does the implementation plan adequately address the "first visit" experience? Should the empty state (no posts yet) include any affordance, or is the DDD-004 decision (render nothing) correct for V1? +- **Usability -- Design** (ux-design-minion): Not needed for planning. DDD-004 fully specifies all visual design decisions (typography, spacing, interactions). This is implementation of a resolved design, not design exploration. +- **Documentation** (software-docs-minion): ALWAYS include. Planning question: What documentation updates are needed alongside this implementation? The `helix-query.yaml` is a new configuration artifact. Should `docs/site-structure.md` or `docs/content-model.md` be updated to reference it? Are there any EDS-specific documentation conventions for query index configuration? +- **Observability** (observability-minion): Not needed for planning. This is a client-side block with no backend services, APIs, or background processes. No logging/metrics/tracing needed. + +## Anticipated Approval Gates + +1. **`helix-query.yaml` configuration** (OPTIONAL gate): This is the EDS query index configuration that determines what data the block can access. It's easy to change later (additive YAML file), but getting the column names or include paths wrong would cause the block to render nothing. Low blast radius (only post-index depends on it) but judgment call on column naming. Could be gated if the frontend-minion raises ambiguity about exact EDS YAML syntax. + +2. **Auto-block injection approach** (NO gate): The auto-blocking decision is well-defined in DDD-004 OQ6, easy to reverse (change a few lines in scripts.js), and has low blast radius. + +3. **Overall implementation** (NO gate): The DDD-004 design contract is comprehensive enough that the implementation is largely mechanical. Post-execution review (Phase 5) catches quality issues. + +Given the thorough DDD-004 spec, this plan likely needs 0-1 gates. The design decisions are already made. + +## Rationale + +This is an **implementation task with a comprehensive design contract**. DDD-004 specifies the exact DOM structure, CSS selectors, token usage, accessibility patterns, and interaction behaviors. The planning consultations focus on: + +1. **frontend-minion**: EDS-specific implementation mechanics (auto-blocking, query index fetch, section layout interaction) that DDD-004 doesn't specify at the code level +2. **accessibility-minion**: Validating that the sophisticated a11y pattern actually works as intended in real browser/screen reader behavior +3. **ux-strategy-minion** (cross-cutting): Journey coherence check on the primary navigation surface +4. **software-docs-minion** (cross-cutting): Documentation impact of new configuration artifacts + +Agents NOT consulted for planning: +- **security-minion**: Tag validation regex is already specified; no new attack surface +- **test-minion**: Test strategy is straightforward (lint + visual verification); Phase 6 handles it +- **ux-design-minion**: Design is fully resolved in DDD-004 +- **observability-minion**: No runtime services +- **data-minion**: No database; query-index.json is an EDS-generated static file + +## External Skill Integration + +No external skills detected in project. Scanned `.claude/skills/` and `.skills/` -- no SKILL.md files found. diff --git a/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase2-accessibility-minion-prompt.md b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase2-accessibility-minion-prompt.md new file mode 100644 index 0000000..1f25692 --- /dev/null +++ b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase2-accessibility-minion-prompt.md @@ -0,0 +1,50 @@ +You are contributing to the PLANNING phase of a multi-agent project. +You are NOT executing yet — you are providing your domain expertise +to help build a comprehensive plan. + +## Project Task + +Implement the home page post index block as specified in `docs/design-decisions/DDD-004-home-post-index.md`. This involves creating a new EDS block (`blocks/post-index/`) with a sophisticated accessibility pattern including sr-only elements, aria-labelledby, and semantic HTML. + +## Your Planning Question + +DDD-004 specifies a detailed accessibility pattern: sr-only h1, aria-hidden badges, sr-only prefixes in h2, aria-labelledby on articles, tag validation regex. Review the proposed DOM structure and interaction patterns for correctness. Specifically: + +1. Is the `aria-labelledby` pointing to the `

` id correct when the h2 contains both an sr-only span and an anchor? Will screen readers announce the full computed text or just the anchor text? +2. Does the `.sr-only` class definition in DDD-004 match current best practices? +3. Are there any keyboard navigation issues with the tab order (title link, then tag links per entry)? +4. Does the empty state (sr-only h1 only, no articles) create any accessibility issues? + +## Context + +Read these files for codebase context: +- `docs/design-decisions/DDD-004-home-post-index.md` -- the full design contract with HTML structure and accessibility patterns +- `styles/tokens.css` -- design tokens including focus ring patterns +- `blocks/header/header.js` and `blocks/header/header.css` -- existing focus ring implementation +- `blocks/footer/footer.js` and `blocks/footer/footer.css` -- existing link accessibility patterns + +## Instructions +1. Read relevant files to understand the current state +2. Apply your domain expertise to the planning question +3. Identify risks, dependencies, and requirements from your perspective +4. If you believe additional specialists should be involved that + aren't already part of the planning, say so and explain why +5. Return your contribution in this format: + +## Domain Plan Contribution: accessibility-minion + +### Recommendations + + +### Proposed Tasks + +For each task: what to do, deliverables, dependencies + +### Risks and Concerns + + +### Additional Agents Needed + +(or "None" if the current team is sufficient) + +6. Write your complete contribution to `/var/folders/3k/bfjvvz9s6dvdn_hvlhvr8lc00000gn/T//nefario-scratch-d3ZWda/implement-ddd-004-home-post-index/phase2-accessibility-minion.md` diff --git a/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase2-accessibility-minion.md b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase2-accessibility-minion.md new file mode 100644 index 0000000..4cdf4f8 --- /dev/null +++ b/docs/history/nefario-reports/2026-03-13-155422-implement-ddd-004-home-post-index/phase2-accessibility-minion.md @@ -0,0 +1,204 @@ +# Domain Plan Contribution: accessibility-minion + +## Recommendations + +### 1. `aria-labelledby` pointing to `

` with mixed content (sr-only span + anchor) + +**Verdict: Correct, but with a nuance worth documenting.** + +When `aria-labelledby` references an element, the accessible name computation (AccName 1.2) uses the **text content of the referenced element and all its descendants**, recursively. For `

`, the computed accessible name will be the concatenation of all descendant text nodes: + +``` +"Build Log: " + "Building a Design System for EDS" += "Build Log: Building a Design System for EDS" +``` + +This is correct. The `` element inside the `

` does not change the text computation -- the algorithm walks through it and collects the text content. The sr-only `` text is included because it is part of the DOM (`.sr-only` only visually hides; it does not remove from the accessibility tree like `aria-hidden` or `display: none` would). + +**Screen reader behavior**: When navigating by landmarks, screen readers will announce the `
` as "Build Log: Building a Design System for EDS, article". When navigating by headings, screen readers will announce "heading level 2, Build Log: Building a Design System for EDS". Both are the desired behavior. + +**One edge case to verify in testing**: Ensure the trailing space in `Build Log: ` produces a space between the prefix and the link text. In practice, the whitespace between the closing `` tag and the `` tag, combined with the space after the colon, should produce correct spacing. However, if the DOM is constructed programmatically without whitespace text nodes between elements, screen readers may concatenate without a space. The `textContent` of the sr-only span should end with `: ` (colon-space) to guarantee separation regardless of whitespace normalization. + +**Recommendation**: When constructing the DOM in JavaScript, set the sr-only span's `textContent` to `'Build Log: '` (with trailing space) as specified. This is sufficient. Do NOT rely on whitespace text nodes between elements. + +### 2. `.sr-only` class definition review + +The proposed definition in DDD-004: + +```css +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +``` + +**Verdict: This matches current best practices.** This is the standard sr-only / visually-hidden pattern used by Bootstrap 5, Tailwind, and recommended by WebAIM and the A11Y Project. Specific assessment: + +- `position: absolute` -- removes from normal flow without hiding from AT. Correct. +- `width: 1px; height: 1px` -- maintains the element in the accessibility tree (unlike `display: none` or `visibility: hidden`). Correct. +- `margin: -1px` -- prevents the 1px element from causing scrollbar or layout shifts. Correct. +- `overflow: hidden` -- clips any content that exceeds the 1x1 box. Correct. +- `clip: rect(0, 0, 0, 0)` -- legacy clip (deprecated but still supported everywhere). The modern equivalent is `clip-path: inset(50%)`, but `clip: rect(0,0,0,0)` has broader support and is not removed from any browser yet. Acceptable. +- `white-space: nowrap` -- prevents the hidden content from wrapping and potentially affecting layout measurements. Correct. +- `border: 0` -- removes any default borders. Correct. + +**One improvement to consider (non-blocking)**: Some implementations also add `clip-path: inset(50%)` as a progressive enhancement alongside the legacy `clip` property. This is optional -- the current definition works in all browsers. Not worth blocking implementation. + +**Placement**: DDD-004 recommends placing `.sr-only` in `styles/lazy-styles.css` (Open Question 5). This is acceptable because the post-index block is lazy-loaded. However, `.sr-only` is a utility class likely needed by future blocks and potentially by eager-loaded content. I recommend defining it in `styles/styles.css` instead, as it is a zero-cost addition (no rendering impact, tiny CSS footprint) and avoids a potential FOUC issue if any eager-loaded block later needs `.sr-only`. This is a minor preference, not a blocker. + +### 3. Keyboard navigation and tab order + +**Verdict: The proposed tab order is correct and follows WCAG 2.4.3 Focus Order (A).** + +The tab order per the spec is: +1. Title link (entry 1) +2. Tag link 1 (entry 1) +3. Tag link 2 (entry 1) +4. Tag link N (entry 1) +5. Title link (entry 2) +6. ... and so on + +This follows DOM order, which matches the visual reading order (top-to-bottom, left-to-right within the metadata line). No `tabindex` manipulation is needed or desired. + +**Considerations:** + +- **Tab stop count**: With 10 posts averaging 3 tags each, that is approximately 40 tab stops on the page (plus header/footer links). This is manageable. WCAG does not set a maximum, and the structure is predictable. Users can navigate by headings (10 stops) or landmarks (10 articles) for faster traversal. + +- **Focus indicators**: The spec uses `outline: 2px solid var(--color-heading); outline-offset: 2px` on both title links and tag links, matching the header and footer patterns. This satisfies WCAG 2.4.13 Focus Appearance (AA) requirements: + - The focus indicator is at least 2px thick (meets minimum area requirement). + - `--color-heading` (#3F5232) on `--color-background` (#F6F4EE) achieves 7.75:1 contrast in light mode -- well above the 3:1 minimum for focus indicators. + - In dark mode, `--color-heading` (#F6F4EE) on `--color-background` (#3A3A33) achieves 10.42:1. Passes. + +- **WCAG 2.4.11 Focus Not Obscured (A)**: Not a concern here -- the page has no sticky overlays, popups, or fixed-position elements that could obscure focused items. The header has `position: relative` (not sticky/fixed), so it will not cover focused elements. + +- **Tag links before DDD-007**: Tags link to `/tags/{slug}` which will 404 until DDD-007 ships. When a keyboard user tabs to a tag link and activates it, they get a 404 page. This is not an accessibility violation (the link is functional and navigable), but it is a usability issue. Ensure the 404 page (`404.html`) is accessible and provides a way back. This is already in the project structure. + +### 4. Empty state accessibility + +**Verdict: The empty state (sr-only h1 only) does not create accessibility issues, but there is one consideration.** + +When the query index returns zero results, the block renders: + +```html +

Posts

+``` + +This is a valid, minimal page. The heading hierarchy is correct (h1 exists). Screen readers will announce the heading when navigating by headings, which is correct behavior. + +**Consideration**: The `
` element will contain only a visually hidden h1 with no visible content. Sighted users see an empty page with header and footer -- this is intentional per the spec ("No 'coming soon' message"). For screen reader users, `
` will announce as the main landmark with a single heading "Posts" and nothing else. This accurately represents the page state. + +**No additional ARIA or messaging is needed.** An `aria-live` region announcing "no posts" would be unnecessary -- this is a static page state, not a dynamic update. The absence of content is self-evident. + +**One edge case**: If the fetch fails (network error, malformed JSON), the block should still render the sr-only h1. The user should not encounter an error-less blank main landmark. The implementation should handle fetch failures gracefully -- render the h1 regardless of fetch outcome. + +## Proposed Tasks + +### Task 1: Define `.sr-only` utility class + +**What**: Add the `.sr-only` CSS class definition to `styles/styles.css` (not `lazy-styles.css`). + +**Deliverables**: Updated `styles/styles.css` with the `.sr-only` class as specified in DDD-004. + +**Dependencies**: None. Can be done first or in parallel with block implementation. + +**Rationale for styles.css over lazy-styles.css**: The class is a global utility with near-zero rendering cost. Placing it in the eager stylesheet prevents FOUC if future eager-loaded blocks need it, and avoids a timing dependency where the sr-only content briefly flashes before lazy CSS loads. + +### Task 2: Verify accessible name computation in implementation + +**What**: After the block is built, verify with browser DevTools (Chrome Accessibility tab or Firefox Accessibility Inspector) that: +- Each `
` element's computed accessible name matches `"{Type}: {Title}"` (e.g., "Build Log: Building a Design System for EDS"). +- Each `

` element's computed accessible name matches the same pattern. +- The visible `.post-type` badge (with `aria-hidden="true"`) does NOT appear in the accessibility tree. + +**Deliverables**: Verification notes or screenshot confirming accessible names are computed correctly. + +**Dependencies**: Block implementation must be complete. Requires a running dev server with test content. + +### Task 3: Keyboard navigation testing + +**What**: After block implementation, test keyboard tab order through the full page (header -> post entries -> footer). Verify: +- Tab order follows: title link -> tag links (per entry) -> next entry's title link. +- Focus indicators are visible on all interactive elements. +- No keyboard traps exist. +- Focus indicators meet WCAG 2.4.13 contrast requirements (verify with DevTools contrast picker). + +**Deliverables**: Pass/fail verification notes. + +**Dependencies**: Block implementation complete, dev server running with test content. + +### Task 4: Screen reader spot-check + +**What**: After block implementation, test with at least one screen reader (VoiceOver + Safari on macOS is available in this environment). Verify: +- Heading navigation reads: "heading level 2, Build Log: Building a Design System for EDS" (not double-announcing the type). +- Landmark navigation reads: "Build Log: Building a Design System for EDS, article". +- The tag list announces correctly: "list, N items" with each tag link readable. +- The `