feat(seo): validate translation relationships and generate hreflang

This commit is contained in:
DeCentN2Madness committed 2026-10-08 13:53:31 -07:00
1 parent f28bf9ad06
commit 32112edbde
8 files changed
+686 -23

No files matched your search

+204
View File
@@ -443,3 +443,207 @@ files retain their historical facts.
No new human publication-policy decision is needed for Phase 3A. Human gates
for portal access, forms/backends, analytics/consent, landing-page ownership,
and conflicting service facts remain deferred. Phase 3B was not started.
## Phase 3B: Explicit Translation Relationships and Hreflang — 2026-10-07 UTC
This section records new work after the accepted Phase 3A structural review.
Earlier audit findings and implementation history above remain historical.
Starting branch: `faithful-astro-migration`; HEAD: `1e1aafa`. The only commit
after Phase 3A (`cd097d0`) added the conversation transcript. The Phase 1 and
Phase 2 checkpoints remain `60c534d` and `ea2f1ea`. This work is not committed.
### Inspection and Policy
The existing manifest owns every generated content route and already supplies
eligibility, normalized local URL, canonical, noindex, external-canonical and
sitemap eligibility. Inspection found 32 page records, 65 blog records and
three author records, all declaring `translationKey`. No hreflang was emitted.
The language switcher was the only equivalent-route guesser: it removed a
language prefix and tried the same URL suffix in all languages. The Header's
locale-home fallback is navigation, not a translation relationship, and remains
unchanged.
`createPublicationManifest` in `src/utils/publication-policy.ts` now groups
explicit keys within each collection. It attaches one validated `translations`
array to every eligible member of a multilingual set. Each reference contains
the declared language and the target's actual manifest route and absolute URL.
It never derives an equivalent from a slug, filename or URL suffix.
Participation reuses `sitemapEligible`: a generated/published entry that is
indexable and self-canonical under the production policy. At least two eligible
languages are required. All members, including drafts, are checked for duplicate
language declarations before filtering; two entries for the same collection,
key and language fail validation. The graph validator rejects dangling targets,
wrong collection/key/language/URL, ineligible endpoints, duplicate languages,
missing self references and nonreciprocity. All members share the same complete,
ordered set, so reciprocity is generated rather than manually maintained.
**Grouping versus missing references:** `translationKey` is an optional grouping
label, not a pointer or a list of required locales. A singleton remains valid
and emits no alternates; it does not assert that a Spanish/Arabic record exists.
An absent referenced graph node fails policy validation; an absent referenced
HTML file fails rendered-output validation. A typo that creates a new singleton
or deletion of the last counterpart cannot be distinguished from intentionally
standalone content by this schema. No new required-target field or guessed
language requirement was introduced.
The existing optional `translationKey` schema now rejects empty and
whitespace-padded values in pages, blog and authors. There are no new frontmatter
fields and **no content/frontmatter edits**. Placeholders must remain drafts;
the only marked content placeholder is `/ar`, already `draft: true`.
### Current Explicit Sets
The existing declarations produce **24 sets / 53 participating routes**:
nine page sets (19 routes) and 15 blog sets (34 routes). English contributes
24 members, Spanish 21, Arabic eight. There are 19 two-language sets and five
three-language sets. The following inventory lists actual local routes, not
inferred URLs. A dash means there is no eligible declared counterpart.
| Collection / key | English | Spanish | Arabic |
| --- | --- | --- | --- |
| pages / index | `/` | `/es` | — |
| pages / library | `/library` | `/es/library` | `/ar/library` |
| pages / aba-therapy | `/aba-therapy` | `/es/aba-therapy` | — |
| pages / aba-therapy-intake-process | `/aba-therapy-intake-process` | `/es/aba-therapy-intake-process` | — |
| pages / autism-evaluations | `/autism-evaluations` | `/es/autism-evaluations` | — |
| pages / client-consultation | `/client-consultation` | `/es/client-consultation` | — |
| pages / contact | `/contact` | `/es/contact` | — |
| pages / learner-social-club | `/learner-social-club` | `/es/learner-social-club` | — |
| pages / services | `/services` | `/es/services` | — |
| blog / aba-school-readiness-arizona | `/library/aba-school-readiness-arizona` | `/es/library/aba-school-readiness-arizona` | — |
| blog / aba-school-readiness-guide | `/library/aba-school-readiness-guide` | — | `/ar/library/aba-school-readiness-guide` |
| blog / autism-evaluation-diagnosis-arizona-parent-guide | `/library/autism-evaluation-diagnosis-arizona-parent-guide` | `/es/library/autism-evaluation-diagnosis-arizona-parent-guide` | — |
| blog / autism-evaluation-what-to-expect | `/library/autism-evaluation-what-to-expect` | `/es/library/autism-evaluation-what-to-expect` | — |
| blog / autism-family-self-care-tips | `/library/autism-family-self-care-tips` | `/es/library/autism-family-self-care-tips` | — |
| blog / autism-self-advocacy-skills-aba | `/library/autism-self-advocacy-skills-aba` | `/es/library/autism-self-advocacy-skills-aba` | `/ar/library/autism-self-advocacy-skills-aba` |
| blog / behavior-management-functions-guide | `/library/behavior-management-functions-guide` | — | `/ar/library/behavior-management-functions-guide` |
| blog / emotional-regulation-aba | `/library/emotional-regulation-aba` | `/es/library/emotional-regulation-aba` | — |
| blog / enhancing-generalization-skills | `/library/enhancing-generalization-skills` | `/es/library/enhancing-generalization-skills` | — |
| blog / executive-functioning-skills-autism | `/library/executive-functioning-skills-autism` | `/es/library/executive-functioning-skills-autism` | `/ar/library/executive-functioning-skills-autism` |
| blog / first-then-cards-autism-transitions | `/library/first-then-cards-autism-transitions` | `/es/library/first-then-cards-autism-transitions` | `/ar/library/first-then-cards-autism-transitions` |
| blog / parents-guide-to-autism-and-aba | `/library/parents-guide-to-autism-and-aba` | `/es/library/parents-guide-to-autism-and-aba` | — |
| blog / positive-reinforcement-techniques | `/library/positive-reinforcement-techniques` | `/es/library/positive-reinforcement-techniques` | `/ar/library/positive-reinforcement-techniques` |
| blog / proactive-reactive-aba-strategies-guide | `/library/proactive-reactive-aba-strategies-guide` | — | `/ar/library/proactive-reactive-aba-strategies-guide` |
| blog / social-pragmatic-communication-autism | `/library/social-pragmatic-communication-autism` | `/es/library/social-pragmatic-communication-autism` | — |
Excluded: `/ar` is draft and uses `home`, not the published home set's `index`
key; its key was not changed. `/schedule-consultation`, `/employee-portal`,
`/library/community-highlight-meet-rula-diab`, and
`/library/new-aia-scottsdale-office` remain noindex; the latter two also retain
external canonicals. None can be a source or target. Three `authors:rula-diab`
records share a key but have no generated author routes and do not enter the
graph. The other 39 indexable routes have no eligible translated counterpart.
Live-only Spanish privacy, author/archive/search routes and missing languages
are not manufactured. There are no additional current URL-matched published
pairs lacking explicit keys; different-slug and unkeyed same-slug fixtures test
that future content will not be guessed.
### SEO, Navigation and Default Language
`Seo.astro` calls the shared `hreflangLinksFor` helper. An indexing-enabled build
emits absolute alternate URLs, including the page itself, on each set member.
English is the project's established default; `x-default` points to that set's
eligible English member, never an unrelated homepage. A set without eligible
English omits `x-default` (covered with Spanish/Arabic fixtures and excluded
English fixtures). All 24 current sets have an eligible English member.
This follows [Google's localized-page guidance](https://developers.google.com/search/docs/specialty/international/localized-versions)
on fully qualified, reciprocal, self-referencing alternates and an appropriate
default. Staging emits no hreflang; its global `noindex,nofollow` and empty
sitemap remain intact. The graph still supplies local language navigation.
`LanguageSwitcher.astro` uses those same manifest equivalents and the entry's
declared language for its active label. It constructs no counterpart URLs.
Labels, SVG and CSS are unchanged. Without a translation set, it retains the
current-language label but emits no empty menu, focusable trigger or listbox
promise. Existing translated sets keep self and equivalent links. Header and
route/template dispatch code are unchanged.
### Fresh Production Observations
Six read-only GETs on **2026-10-07 23:00:32–23:00:38 UTC**, at least one second
apart, checked robots first and then these five pages. Robots permitted the
paths; all six responses were HTTP 200, with no redirects followed. No forms,
APIs, broad crawl or production writes were used; no production HTML is added
to the repository. These checks supplement, rather than rewrite, earlier audit
evidence.
| Observed page | Check time UTC | Declared production alternates |
| --- | --- | --- |
| `/` | 23:00:33 | en `/`, es `/es/`, x-default `/` |
| `/library` | 23:00:35 | en-us `/library`, es `/es/library`, ar `/ar/library` |
| `/library/autism-self-advocacy-skills-aba` | 23:00:36 | en-us, es, ar, x-default English |
| `/es/library/autism-self-advocacy-skills-aba` | 23:00:37 | en-us, es, ar, x-default English |
| `/ar/library/autism-self-advocacy-skills-aba` | 23:00:38 | en-us, es, ar, x-default English |
The article's three observations reference the same three full article paths
listed in the set inventory. The homepage and article use English defaults.
Production's Library index has no x-default; the local policy adds its real
English index as the default, consistent with the approved rule. Library
production labels English `en-us`; local output uses the existing content
language `en`, without inventing regional variants. Relationships are based on
explicit existing content declarations, supported by these targeted checks;
this is not a new full content reconciliation or linguistic-fluency approval.
### Validation and Structural Review
`npm run test:publication`: **33 passed**, including the original 19 Phase 3A
tests and 14 translation tests. Coverage includes different-slug equivalence,
unrelated matching slugs, collection boundaries, all publication exclusions,
malformed/duplicate keys, dangling graph/generated references, reciprocity,
self references, default eligibility and identical switcher/SEO membership.
Isolated real-Astro builds exercise staging, indexing and draft home/Library
indexes without changing the real content or deploying anything.
The requested validation sequence passed (offline refreshes keep source
fingerprints current without making discovery requests):
```sh
cd www
npm run test:publication
npm run build
PUBLIC_ALLOW_INDEXING=true npm run build
npm run audit:routes -- --offline
npm run audit:routes -- --check
npm run audit:links
npm run build
npm run audit:routes -- --offline
npm run audit:routes -- --check
git diff --check
```
All three regular build invocations checked 71 files with zero errors, warnings
or hints, and generated 96 HTML routes. The link audit found zero broken links.
At 23:02:40 UTC, the indexing artifact had 92 sitemap entries, 92 indexable
pages and four noindex exceptions, with 174 hreflang links on 53 routes across
24 sets. Canonicals matched the pre-change artifact on every retained route.
At 23:03:49 UTC, the final staging artifact had 96 `noindex,nofollow` pages,
zero sitemap entries and zero hreflang links. Every language menu matched the
graph. Route set, titles, canonical URLs, language/direction and main content
matched the pre-change artifact; the main-content comparison excludes script
nodes and normalizes whitespace and existing random contact-obfuscation IDs.
The final durable route evidence is
[`route-reconciliation-2026-10-07-offline-23-03-24-581Z.json`](route-reconciliation-2026-10-07-offline-23-03-24-581Z.json).
It preserves the original 70 production request records and observation times.
The discovery inventory remains 129 routes, production sitemap inventory 102,
local generated 96, local/sitemap overlap 92 and live-sitemap local omissions
10. Those production counts are historical discovery evidence, not the local
92-entry production-policy sitemap. The intermediate indexing reconciliation
is retained only in temporary storage; both modes are reproducible above.
The diff is confined to the shared publication policy, translation-key schema
validation, switcher, SEO component, existing publication tests, README, this
report and the final dated route reconciliation. No content/frontmatter, route
templates, styles, dependencies, environment files or deployment settings were
changed. No Phase 3A blocker or accounting defect was found. The original
`merge-plan.md` remains untracked with SHA-256
`015db80cdbaf7d68799265d2070db760155c342b432ce6795f17ebc2218c641c`.
No new human publication-policy decision is required. Existing decisions about
employee access, campaign ownership, forms, analytics/consent and conflicting
eligibility claims remain deferred. Substantive future translations require
human language review. Phase 4A, article/FAQ schema, content reconciliation,
visual fidelity and deployment work were not started. Stop for review before
any Phase 3B checkpoint commit.