diff --git a/www/reports/migration-summary.md b/www/reports/migration-summary.md index 98dd24c..79fb324 100644 --- a/www/reports/migration-summary.md +++ b/www/reports/migration-summary.md @@ -2008,3 +2008,126 @@ analytics, tracking, form/backend, or future-phase changes. No file is staged. `merge-plan.md` remains untracked and unchanged. Its verified SHA-256 is `015db80cdbaf7d68799265d2070db760155c342b432ce6795f17ebc2218c641c`. + +## Phase 6A — Library Architecture Decision (2026-10-09 UTC) + +**Starting checkpoint:** `34d5394b3ce765028a27b9cddd9952de3f09bfe7`. +This is a research and decision milestone. It changes no Library implementation, +content, dependency, route, redirect, publication rule, or deployment file. + +### Scope and Method + +Inspected the catchall route, all three Library index components, +`BlogIndexLayout`, `BlogCard`, `BlogPostLayout`, content/author schemas, all blog +and author records, publication policy/manifest, structured-data author +resolution, SEO/publication tests, route-audit tooling, Library page records, +README, migration instructions, accepted reports, and the latest offline route +reconciliation. + +Production `robots.txt` was checked before 29 bounded sequential GETs from +**2026-10-09 18:01:51–18:02:17 UTC**. Requests covered Library/author roots, +first-page aliases, representative/last/invalid pages in all three languages, +trailing-slash examples, `/search`, and one search query. No redirects were +followed, forms submitted, authentication used, or production writes made. One +headless Chromium GET verified client-rendered search results. + +Responses variously reported HubSpot `HIT`, `MISS`, or `REVALIDATED` and mostly +October 3–4 prerenders. They establish consistent current public behavior, not +origin freshness. Historical October 7 observations remain intact and support +the intervening English page 3–5/author page 3–5 statuses without repeat calls. + +### Current Inventory and Confirmed Behavior + +- **65 blog records:** 46 English, 12 Spanish, seven Arabic. +- All 65 are non-draft and generated. Two English syndicated records are + accessible `noindex` external-canonical exceptions, leaving **63 sitemap- + eligible posts**. +- Every post has title, description, date, author, category, featured image, + canonical, and translation key. Tags are empty on all 65; no post declares + `updatedDate` or image-alt frontmatter. +- Every post uses author slug `rula-diab`. There is one author record per + language, all sharing explicit translation key `rula-diab`. +- Current indexes sort date descending only; stable manifest order implicitly + breaks equal dates by route. They render every locale post in one list. +- English alone has an inert search input. Spanish/Arabic have no search control. + No local pagination or author routes exist. +- The three Library Markdown bodies contain bypassed historical listing + snapshots, creating a stale competing representation even though they do not + render. +- Production uses ten posts per page: English 55 posts/six pages, Spanish 12/two, + Arabic seven/one. Author archives have the same counts because all posts are + currently attributed to Rula Diab. +- All six `/page/1` Library/author aliases return duplicate 200 content. English + trailing-slash examples return 301 to no-slash roots. First invalid page + boundaries return 404. +- Production search is a JavaScript-rendered GET `/search` flow. A browser query + for `autism` returned 66 mixed-language blog/listing results, ten per offset + page. Raw HTML contains no results and production supplies no useful search + canonical/robots/title. + +### Architecture Recommendation + +Generate one small metadata-only JSON search index per language from a new +shared Library catalog based on the existing publication manifest. Use a +dependency-free TypeScript client with Unicode-aware English/Spanish/Arabic +normalization, weighted deterministic matching, accessible result state, and no +external query service. Current measured aggregate metadata is 42,090 bytes raw, +12,693 gzip, or 10,709 Brotli; a raw-body prototype is 474,623/135,796 gzip and +is not recommended without a verified need. + +Use the same catalog for ten-item static Library and author pagination, explicit +date-descending/route-ascending sorting, author validation, and generated route +descriptors integrated with the shared publication policy. Page 1 remains each +root; `/page/1` becomes a locale-specific permanent redirect. Invalid or empty +pages are not generated. Numbered pages never gain hreflang from matching page +numbers. Author roots may use explicit author translation keys after localized +author content is source-reconciled and reviewed. + +The complete proposal, alternatives, current evidence, URL disposition matrix, +route-count model, multilingual/RTL/SEO design, accessibility behavior, Phase 6B +checkpoints, test matrix, and decision register are in +[`phase-6a-library-architecture.md`](phase-6a-library-architecture.md). + +### Route Implications and Sequencing + +With the current corpus, the proposed `/search`, five numbered Library pages, +three author roots, and five numbered author pages would add 14 HTML routes: +**96 → 110**. Under the recommended policy, sitemap URLs would be **92 → 105**. +Validated author-root equivalence would add 12 hreflang links: **174 → 186**. +Redirect aliases add no HTML routes. + +The current corpus can generate only five nonempty English pages. Production +page 6 and author page 6 contain real distinct content. Phase 6C's nine missing +English posts must therefore precede final pagination acceptance; no empty page, +generic-home redirect, or fabricated record is recommended. If all nine are +ordinary indexable records, the combined upper-bound projection is 121 HTML +routes and 116 sitemap URLs; exact policy and translation relationships must be +recomputed from the reconciled records. + +### Validation + +| Check | Result | +| --- | --- | +| Normal `npm run build` | Passed; Astro check 0 errors/warnings/hints; 96 HTML routes | +| `npm run audit:routes -- --check` | Passed offline against `route-reconciliation-2026-10-09-offline-17-20-21-008Z.json`; no network/writes | +| Indexing-enabled evidence build | Passed; 96 routes, 92 sitemap URLs, 174 hreflang links across 53 pages | +| Final normal build | Passed; restored staging output | +| Final staging safeguards | 96 routes; global `noindex,nofollow`; empty sitemap; no hreflang | + +### Decisions Awaiting Approval + +1. Metadata-only dependency-free search rather than full-body/Pagefind/client + library search. +2. Ten posts per page and permanent page-1 alias redirects. +3. Preserved noindex `/search` compatibility for `term`, `q`, legacy `type`, and + `offset`, while embedded Library search remains locale-scoped. +4. Indexable, self-canonical, sitemap-listed numbered/archive pages. +5. Continued listing/search visibility for the two accessible noindex syndicated + pages, preserving current Astro behavior. +6. Explicit author-root hreflang, optional localized author `displayName`, and + source-supported author bio reconciliation with human language review. +7. Phase 6C content sequencing before English page-6 acceptance. + +No functional source file, dependency, route, redirect rule, content record, +Nix/nginx/deployment configuration, publication policy, or generated application +behavior was changed in Phase 6A. diff --git a/www/reports/phase-6a-library-architecture.md b/www/reports/phase-6a-library-architecture.md new file mode 100644 index 0000000..20dbac3 --- /dev/null +++ b/www/reports/phase-6a-library-architecture.md @@ -0,0 +1,550 @@ +# Phase 6A Library Architecture Proposal + +**Decision date:** 2026-10-09 UTC +**Starting checkpoint:** `34d5394b3ce765028a27b9cddd9952de3f09bfe7` +**Status:** proposal only; implementation is not authorized + +## A. Executive Decision Summary + +### Recommended architecture + +1. **Search:** generate one compact JSON document index per language from the + existing publication manifest and blog collection. Load it on demand and use + a small dependency-free TypeScript client for normalization, ranking, result + rendering, URL state, and accessibility. Search title, description, headings, + category, author display data, and future tags. Do not index full article + bodies in the first release. +2. **Library pagination:** render ten articles per static page, matching the + verified production page size. Keep each locale's existing Library root as + page 1, generate only nonempty `/page/{n}` pages for `n >= 2`, and permanently + redirect `/page/1` aliases to the corresponding root. +3. **Author archives:** derive localized author pages and their pagination from + author records plus the same eligible-post catalog. Generate no manually + maintained post lists. Author roots use explicit author `translationKey` + relationships; numbered author pages do not become translations merely + because their numbers match. +4. **Routing and policy:** add archive/search descriptors to the shared route and + publication model rather than creating a second eligibility system. The + existing `[...slug].astro` manifest-driven route architecture should dispatch + content and generated archive kinds from one validated static-path inventory. +5. **Progressive enhancement:** server-rendered paginated Library and author + pages remain complete navigation without JavaScript. Search is an explicit + enhancement; without JavaScript the search control is not presented as + functional and visitors retain normal archive navigation. + +This is preferable at the present scale because the three measured metadata +indexes total **42,090 bytes uncompressed, 12,693 bytes gzip, or 10,709 bytes +Brotli** for 65 records. English alone is 26,391/8,036/6,652 bytes. A custom +metadata index keeps queries local, adds no package or service, excludes drafts +at build time, and can implement Spanish/Arabic normalization explicitly. A +full-body prototype would be 474,623 bytes uncompressed and 135,796 bytes gzip; +that additional payload is not justified by a verified visitor requirement. + +### Important tradeoffs + +- Metadata search will not find a term appearing only in article body copy. +- Dependency-free ranking will be deterministic and understandable but less + sophisticated than fuzzy/stemmed third-party search. +- Production currently has 55 English articles and six ten-item pages; Astro has + 46 English records and can create only five nonempty pages. The nine missing + Phase 6C articles must be reconciled before English page 6 and author page 6 + can be accepted. The architecture must not create an empty page or redirect a + distinct archive page merely to satisfy a route count. +- The current localized author records are structurally usable but their names + and English-only short descriptions do not reproduce the current Spanish and + Arabic archive headings/biographies. Author content needs source-supported + reconciliation and human language review before those archives publish. + +### Approval gates + +Approval is required for the metadata-only search scope, the `/search` +compatibility behavior, inclusion of accessible noindex syndicated records in +listings/search, pagination/archive indexing and sitemap treatment, author +record changes, and sequencing Phase 6C before final English pagination. + +## B. Current Architecture Inventory + +### Rendering and routing + +- `src/pages/[...slug].astro` asks `getPublicationManifest()` for every eligible + page/post route and dispatches the three `slug: library` records to separate + English, Spanish, and Arabic components. There is no generated archive kind. +- `LibraryEnglish.astro`, `LibrarySpanish.astro`, and `LibraryArabic.astro` each + independently filter the manifest by language, sort by date descending, render + every eligible post, and repeat a five-item “popular” slice. English alone + renders an inert ``; no script, form, filtering, state, or + announcements exist. +- `BlogIndexLayout.astro` supplies shared hero/intro/sidebar layout and delegates + list contents. `BlogCard.astro` creates localized article URLs and dates, but + hardcodes Rula Diab's English byline/avatar and does not link to an author + archive. +- `BlogPostLayout.astro` likewise hardcodes the visible English byline. Its + BlogPosting JSON-LD independently resolves an exact locale/slug author and + already fails for missing or ambiguous records. +- The three Library Markdown bodies contain old extracted listing snapshots. + `[...slug].astro` bypasses these bodies, so they do not render, but they are a + competing stale representation. Phase 6B should replace them with maintenance + notes only after the generated Library implementation is accepted. + +### Publication and translation policy + +- `publication.ts` combines page and blog collections and delegates route, + canonical, eligibility, sitemap, collision, and translation validation to + `publication-policy.ts`. +- Drafts are not generated. Published noindex and external-canonical records are + generated but excluded from the sitemap and hreflang graph. Staging globally + emits `noindex,nofollow`, an empty sitemap, and no hreflang. +- Translation equivalence is explicit and collection-scoped through + `translationKey`; it is never guessed from slugs. +- Authors are intentionally outside the current publication manifest. Author + records declare locale and translation keys, but no author routes exist. + +### Current content inventory + +The historical 65-post count remains accurate, but it was independently +recalculated from the current files and frontmatter: + +| Language | Records | Generated/non-draft | Noindex + external canonical | Sitemap-eligible | Raw Markdown/MDX body bytes | +| --- | ---: | ---: | ---: | ---: | ---: | +| English | 46 | 46 | 2 | 44 | 276,099 | +| Spanish | 12 | 12 | 0 | 12 | 105,272 | +| Arabic | 7 | 7 | 0 | 7 | 41,832 | +| **Total** | **65** | **65** | **2** | **63** | **423,203** | + +There are no drafts. The two exceptions are +`community-highlight-meet-rula-diab` and `new-aia-scottsdale-office`; both are +accessible local English routes with verified `noindex` and external canonicals. +Current Library components use `eligible`, not `sitemapEligible`, so all 65 +appear in lists. + +Every record has title, description, date, author, category, featured image, +canonical, and translation key. All use category `Library`, all have an empty +tags array, none has `updatedDate`, and none supplies image `alt` frontmatter. +The content model can support tags and modification dates later without changing +the archive algorithm. Search should not pretend empty tags add useful ranking. + +Equal-date sets exist in both English and Spanish. Current ordering is date +descending with an implicit stable route order inherited from the manifest. +The new catalog should make this explicit: **date descending, then normalized +route/slug ascending**. + +### Author inventory and relationships + +| Language | Author records | Author slug | Referencing posts | +| --- | ---: | --- | ---: | +| English | 1 | `rula-diab` | 46 | +| Spanish | 1 | `rula-diab` | 12 | +| Arabic | 1 | `rula-diab` | 7 | + +The three records share `translationKey: rula-diab`, name `Rula Diab`, one +avatar, and the same short English description. The schema has `name`, `slug`, +optional `description`, optional `avatar`, `lang`, and `translationKey`. It is +sufficient to identify and relate archives, but not to reproduce production's +localized credentialed headings and biographies without content reconciliation. +An optional locale-specific `displayName` is justified for archive presentation +while preserving `name` as the person's name in BlogPosting schema. No multiple- +author field should be added until a real multi-author record requires it. + +## C. Production Behavior and URL Evidence + +### Method and freshness + +`robots.txt` was checked first. It allows the requested public paths. Twenty-nine +bounded sequential GETs were made from **2026-10-09 18:01:51 through 18:02:17 +UTC** with no-cache request headers, no redirect following, no authentication, +and no form submission. Requests covered representative roots, first-page +aliases, last pages, first invalid boundaries, author equivalents, and search. +One headless Chromium GET then allowed the existing search JavaScript to render +one result page; it did not submit a form or write to production. + +HubSpot responses variously reported `HIT`, `MISS`, or `REVALIDATED` and mostly +identified October 3–4 prerenders. `Cache-Control: no-store, no-cache, +must-revalidate` did not establish origin freshness. Findings are current public +behavior from consistent responses, with potentially cached content. The older +2026-10-07 route report remains historical evidence for URLs not repeated. + +### Observed behavior + +| Family | Fresh result | Visible results | Navigation / metadata | +| --- | --- | ---: | --- | +| `/library` | 200, `lang=en-us` | 10 | Pages 1–6; page 6 has 5; page 7 is 404 | +| `/es/library` | 200, `lang=es` | 10 | Pages 1–2; page 2 has 2; page 3 is 404 | +| `/ar/library` | 200, `lang=ar dir=rtl` | 7 | One page; page 2 is 404 | +| English author root | 200 | 10 | Pages 1–6; page 6 has 5; page 7 is 404 | +| Spanish author root | 200 | 10 | Pages 1–2; page 2 has 2; page 3 is 404 | +| Arabic author root | 200, RTL | 7 | One page; page 2 is 404 | + +All six `/page/1` candidates return 200 and show the same ordered article links, +title, and counts as their root rather than redirecting. English trailing slashes +on `/library/` and `/library/author/rula-diab/` return 301 to their no-slash +forms. The inspected archive/listing pages expose no canonical or robots meta; +their pagination/author URLs are absent from the production sitemap. + +The author roots display substantial localized bios and credentialed headings: +English `Rula Diab, Clinical Director, M.Ed, BCBA, LBA`, a Spanish localized +equivalent, and an Arabic localized equivalent. These are not represented by +the current local author records and must not be invented from this report. + +### Production search + +Library search forms use GET `/search` with `term` plus repeated `type=BLOG_POST` +and `type=LISTING_PAGE`. The search page's own form uses `q` and also includes +`SITE_PAGE`. Raw `/search` and `/search?term=autism` HTML contains no results; +the existing HubSpot JavaScript loads them after page load. + +A browser-level GET of +`/search?term=autism&type=BLOG_POST&type=LISTING_PAGE` displayed **66 mixed- +language results**, ten per result page, and result offsets `0`, `10`, … `60`. +The first page included English and Spanish Library roots/posts, confirming that +production search is global rather than scoped to the Library page's language. +The search page has no useful document title (raw parsing resolves an SVG title), +canonical, or robots directive. With JavaScript unavailable, production shows +no results. + +## D. URL Disposition Matrix + +`Current` below means the October 9 check; `historical` means the accepted +October 7 route evidence. Proposed archive pages are self-canonical, indexable, +and sitemap-eligible under the shared policy. `/search` is noindex and excluded. + +| Production URL or pattern | Purpose / language | Evidence and existing behavior | Proposed Astro behavior and preservation | Redirect / canonical / indexing | Phase 6B validation | +| --- | --- | --- | --- | --- | --- | +| `/library` | Library page 1, EN | Current 200, 10 results, pages 1–6 | Preserve static page 1 | Self-canonical; index; sitemap; existing EN/ES/AR root hreflang | Generated HTML, canonical, first 10 sorted posts | +| `/es/library` | Library page 1, ES | Current 200, 10 results, pages 1–2 | Preserve static page 1 | Self-canonical; index; sitemap; existing root hreflang | Locale cards/date/labels and two-page navigation | +| `/ar/library` | Library page 1, AR | Current 200, 7 results, RTL | Preserve static one-page archive | Self-canonical; index; sitemap; existing root hreflang | `lang=ar dir=rtl`; no nonexistent page 2 | +| `/library/page/1` | First-page alias, EN | Current 200 duplicate | Preserve incoming URL through **301 to `/library`** | Redirect only; canonical destination `/library`; no HTML/sitemap | Redirect map and no duplicate HTML | +| `/es/library/page/1` | First-page alias, ES | Current 200 duplicate | 301 to `/es/library` | Redirect only | Redirect test | +| `/ar/library/page/1` | First-page alias, AR | Current 200 duplicate | 301 to `/ar/library` | Redirect only | Redirect test | +| `/library/page/2`–`/5` | Library pages, EN | Current 200; `/2` historical/current, `/3`–`/5` historical 200 | Generate nonempty ten-item pages from current corpus | Self-canonical; index; sitemap; no hreflang inferred | Route/card boundaries, prev/next, direct access | +| `/library/page/6` | Library last page, EN | Current 200 with 5 production posts | Generate only after Phase 6C supplies the missing records; do not create empty/redirected substitute | Self-canonical; index; sitemap once real | Phase 6C prerequisite; assert 5 real cards | +| `/library/page/7+` | Invalid future boundary, EN | Current `/7` 404 | Generate only if eligible count later requires it; otherwise 404 | No canonical/sitemap while absent | Beyond-last route absent | +| `/es/library/page/2` | Library last page, ES | Current 200 with 2 | Generate static page | Self-canonical; index; sitemap; no page-number hreflang | Exactly 2 cards today | +| `/es/library/page/3+` | Invalid boundary, ES | Current `/3` 404 | No route until count requires it | 404 while absent | Beyond-last route absent | +| `/ar/library/page/2+` | Invalid boundary, AR | Current `/2` 404 | No route until count requires it | 404 while absent | Beyond-last route absent | +| `/library/author/rula-diab` | Author root, EN | Current 200, localized bio, pages 1–6 | Generate from EN author record and posts after author content reconciliation | Self-canonical; index; sitemap; explicit author-root hreflang only | Author resolution, bio, links, first 10 posts | +| `/es/library/author/rula-diab` | Author root, ES | Current 200, Spanish bio, pages 1–2 | Generate from ES author record; no invented copy | Self-canonical; index; sitemap; author-root hreflang | Human-reviewed source content and route test | +| `/ar/library/author/rula-diab` | Author root, AR | Current 200, Arabic bio, RTL | Generate from AR author record; no invented copy | Self-canonical; index; sitemap; author-root hreflang | RTL and human-reviewed source content | +| `/library/author/rula-diab/page/1` | Author first alias, EN | Current 200 duplicate | 301 to author root | Redirect only | Redirect test | +| `/es/library/author/rula-diab/page/1` | Author first alias, ES | Current 200 duplicate | 301 to ES author root | Redirect only | Redirect test | +| `/ar/library/author/rula-diab/page/1` | Author first alias, AR | Current 200 duplicate | 301 to AR author root | Redirect only | Redirect test | +| `/library/author/rula-diab/page/2`–`/5` | Author pages, EN | Current/historical 200 | Generate from current matching posts | Self-canonical; index; sitemap; no numbered hreflang | Same deterministic chunks as Library today | +| `/library/author/rula-diab/page/6` | Author last page, EN | Current 200 with 5 | Generate after Phase 6C; no empty substitute | Self-canonical; index; sitemap once real | Phase 6C prerequisite | +| `/library/author/rula-diab/page/7+` | Invalid boundary, EN | Current `/7` 404 | 404 until count requires route | Absent from sitemap | Beyond-last route absent | +| `/es/library/author/rula-diab/page/2` | Author last page, ES | Current 200 with 2 | Generate static page | Self-canonical; index; sitemap | Exactly 2 matching posts | +| `/es/library/author/rula-diab/page/3+` | Invalid boundary, ES | Current `/3` 404 | No route until required | 404 | Boundary test | +| `/ar/library/author/rula-diab/page/2+` | Invalid boundary, AR | Current `/2` 404 | No route until required | 404 | Boundary test | +| `/search` | Search shell, global | Current 200; client-rendered; malformed/absent SEO metadata | Preserve one static compatibility route using generated locale indexes and locale filter | Self-canonical path; **noindex,follow**; no sitemap/hreflang | Direct route, empty state, JS/no-JS behavior | +| `/search?term={query}` and repeated `type` | Legacy search state | Current form behavior; mixed-language results | Read `term`; safely ignore legacy `type`; preserve URL and client-render compatible results | Canonical `/search`; noindex; query escaped | Direct-load and XSS/Unicode tests | +| `/search?q={query}` | Search-page form state | Current search page uses `q` | Accept as alias input; normalize to one in-page state without network forwarding | Canonical `/search`; noindex | `q` and `term` equivalence test | +| `/search?...&offset={10n}` | Search pagination state | Browser verified offsets 0–60 | Honor nonnegative multiples of 10 on compatibility route; invalid values normalize to 0 | Canonical `/search`; noindex | Direct page, last/invalid offset tests | +| `/{locale?}/library/` and author trailing slash variants | Normalization | English examples current 301; other variants not individually requested | Host-level 301 to no-slash normalized route | Redirect only | Redirect-map and deployment acceptance test | + +## E. Search Alternatives and Comparison + +| Approach | Quality / languages | Payload and build | Accessibility / privacy | Maintenance | Decision | +| --- | --- | --- | --- | --- | --- | +| Generated metadata JSON + custom TypeScript | Explicit field weighting; deterministic Unicode normalization; Spanish accent and Arabic mark handling tested locally; no stemming/fuzzy by default | Current all-language index 42,090 raw / 12,693 gzip; one small client module; ordinary Astro static endpoints | Full control of semantic form/status/focus; queries stay in browser except legacy `/search` URLs; no third party | Small project-owned helper and tests; transparent failure modes | **Recommended** | +| MiniSearch-style client index | Built-in field boost, prefix, fuzzy matching, suggestions | Adds dependency and serialized index/client code; exact project payload not measured because no package was installed | Local/private and static; UI still project-owned | Better relevance features, but tokenizer/fuzzy behavior needs locale tuning and dependency upkeep | Defer until measured search-quality failures justify it | +| Pagefind-style post-build search | Indexes rendered pages after Astro, separates indexes by document language, supports static hosting | Adds post-build executable/runtime bundles and tends toward full-page text; changes build/deployment pipeline | Local/private; UI can be accessible but requires integration; no server | Strong general-site search, but broader than this Library and duplicates manifest eligibility/filter work | Reject for current 65-record Library; reconsider for verified site-wide full-text search | +| External hosted search/API | Potentially strongest ranking/analytics | Network/service dependency and operational credentials | Visitor terms leave the site; logging, consent, retention, failure, and cost concerns | Highest operational burden | Reject under static/data-minimization requirements | + +The proposed index stores route, locale, title, description, headings, category, +future nonempty tags, author display name/slug, and publication date. It excludes +article body HTML, unpublished content, likes/views, and sensitive or runtime +data. Featured images need not be duplicated; result cards can use the existing +article metadata or omit thumbnails in the compact search view. + +Normalization should use Unicode NFKC, locale-aware lowercasing, collapsed +whitespace, and Unicode punctuation separators. A secondary comparison key may +fold Latin combining marks so `evaluacion` finds `evaluación`, and remove Arabic +tashkeel/tatweel so marked and unmarked forms match, while preserving original +display text. Do not apply English stemming to Spanish or Arabic. Rank exact +title phrase, title tokens, heading matches, description, category/tags, and +author in that order; break equal scores by date descending then route ascending. +Prefix matching should require a minimum token length. Empty queries render the +normal archive; no-result queries announce zero and offer clear/reset. + +## F. Pagination and Author Architecture + +### Shared catalog + +Add a pure `library-catalog` module that accepts the existing publication +manifest and author records and returns: + +- eligible posts grouped by locale; +- explicit deterministic ordering; +- ten-item page slices and nonempty page descriptors; +- exact locale/slug author resolution with duplicate/missing validation; +- author archive descriptors derived from post relationships; +- compact locale search records. + +It must not read extracted Library Markdown listing bodies or maintain manual +post arrays. Current accessible noindex/external-canonical posts remain listable +because that is established behavior, while drafts remain absent. An author +archive should require a valid locale author record and at least one eligible +post, including at least one sitemap-eligible post before the archive itself can +be indexable. + +### Static route generation + +Extend the manifest-driven static-path inventory with validated generated route +descriptors (`library-page`, `author-index`, `author-page`, `search`) and run +their canonical, collision, robots, and sitemap state through reusable Phase 3A +policy functions. Do not hand-author content records for numbered pages. The +catchall renderer can dispatch descriptor kinds to shared Library/archive +components while preserving content routes and collision detection. + +Page 1 is always the unnumbered root. Next/previous and numbered links are real +anchors with localized accessible labels, `aria-current="page"` on the current +page, and no links to nonexistent pages. Invalid numbers, zero, negative values, +nonintegers, unknown author slugs, authors with no eligible posts, and pages past +the end produce no static route and therefore 404. + +Search temporarily replaces the visible page list with all-language-appropriate +matching results and hides the archive pager; clear/reset restores the original +server-rendered page. Adding, removing, drafting, or changing an article updates +search and page chunks in one build without manual index edits. + +### Author presentation + +Resolve the author once by exact locale and slug. Reuse the same resolution in +BlogCard, BlogPostLayout, BlogPosting schema, and author archives. Add an optional +locale-specific `displayName` only to preserve the published credentialed archive +heading; retain `name` for the Person entity. The existing description/avatar can +drive the bio after source reconciliation. Build must fail on missing or duplicate +locale/slug records. Do not treat authors as team members or infer employment. + +### Route-count model + +With the current 46/12/7 corpus and ten items per page: + +| Addition | HTML routes | Sitemap URLs | Hreflang links | +| --- | ---: | ---: | ---: | +| `/search` | +1 | 0 | 0 | +| Library pages 2+ (4 EN, 1 ES) | +5 | +5 | 0 | +| Three author roots | +3 | +3 | +12 if explicit author equivalence is approved | +| Author pages 2+ (4 EN, 1 ES) | +5 | +5 | 0 | +| **Projected current total** | **96 + 14 = 110** | **92 + 13 = 105** | **174 + 12 = 186** | + +This current-corpus model intentionally lacks the two distinct English page-6 +routes. If all nine Phase 6C English posts are ordinary eligible/indexable +records, those posts add nine HTML/sitemap URLs and produce one more Library and +one more author page. The combined projection becomes **121 HTML routes** and +**up to 116 sitemap URLs**. Exact counts must be recalculated from final policy; +new translation relationships could separately affect hreflang. + +## G. Multilingual and SEO Design + +- Each JSON index contains one locale only. Embedded Library search never mixes + languages. The `/search` compatibility page may search all locales but must + expose an explicit locale filter and label result language. +- Components receive localized labels/messages; none are generated by machine + translation. Missing copy blocks implementation for that locale rather than + falling back silently to English. +- Arabic uses the existing `lang=ar dir=rtl` layout. Pagination order, arrows, + search icon placement, result counts, focus order, and mixed Latin/Arabic text + require RTL browser testing. Use `Intl.DateTimeFormat`/`Intl.NumberFormat` with + the page locale; keep machine-readable dates stable. +- Existing Library roots retain their explicit three-language translation set. + Numbered pages do not receive hreflang because page N contains different + records across locales. +- Author roots may participate only through the author collection's explicit, + validated `translationKey`, with reciprocal published archives and English as + `x-default`. Numbered author pages receive no hreflang. +- `/search` has no translation graph and is noindex. JSON endpoints and redirect + sources never enter the sitemap. +- Generated archive pages self-canonicalize to normalized local URLs and use the + staging/production robots switch. Page 2+ and author archives are recommended + as indexable and sitemap-eligible to remain consistent with Phase 3A's “all + eligible self-canonical local routes” rule. This intentionally improves on + production's missing canonicals and sitemap omission and requires approval. +- LanguageSwitcher uses only the validated graph. It must not construct page-N, + author, or search equivalents from route shape. + +## H. Accessibility and Progressive Enhancement + +- Render complete cards and archive pagination on the server. +- Hide/reveal the enhanced search form only after its script initializes; include + a `