# 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 `