Discoverability & SPA Metadata Pass¶
Date: 2026-07-19
Branch: feature/discoverability-metadata (worktree, off develop)
Status: Design approved
Problem¶
Two related issues:
-
Stale PWA manifest.
packages/web/frontend/public/manifest.jsonstill declares"name": "PhonoLex v3.0.0"and carries a thin, outdated description with none of the modern PWA fields. The app is at v6.4.0. -
Discoverability gaps. PhonoLex is a client-rendered React SPA (routes
/,/privacy,/terms; the six tools are tabs inside/). A crawler or LLM bot that does not execute JavaScript sees an empty<div id="root">at/. Only the three hand-written static landing pages (public/landing/*.html) carry crawlable body content. The sitemap has stalelastmoddates, and there is nollms.txtfor AI assistants.
The manifest staleness is a symptom of a deeper problem: the app version is duplicated as a
hardcoded literal in static files. The single source of truth is package.json (read by
src/config/product.ts), but index.html's JSON-LD hardcodes "softwareVersion": "6.4.0"
and manifest.json hardcoded "3.0.0". Manual copying is what let the manifest drift three
major versions behind. The fix is to stop duplicating the version in static files, not merely
to bump the number.
Goal¶
Maximize what human visitors, search crawlers, social-card unfurlers, and LLM/AI bots can learn from the site without executing JavaScript, and eliminate the version-drift class of bug. Scope is static metadata + a no-JS fallback only — no build-time prerendering and no per-word SEO pages (those are separately spec'able follow-ups).
Non-Goals (this pass)¶
- Build-time prerendering / SSG of the SPA routes (evaluated, deferred).
- Per-word indexable pages for the lexicon (large; its own workstream).
- Adding
react-helmetor any head-management library. - New maskable icon assets (noted as a future nice-to-have).
Components¶
1. public/manifest.json — full rewrite¶
Root-cause the drift by removing the version from the app name (installed PWAs should not carry a version in their name — that is what went stale).
name:"PhonoLex — Phonological Analysis Toolkit"short_name:"PhonoLex"description: rewritten to match the real product (six tools, 125K+ words, free REST API).- Add:
id: "/",lang: "en",dir: "ltr",scope: "/",categories: ["education", "productivity", "medical"],display_override: ["standalone"], and ascreenshotsentry reusingog-image.png(1200×630,form_factor: "wide"). icons: changepurposefrom"any maskable"to"any". The current logos are not padded with a maskable safe zone, so declaring them maskable causes Android to crop them. A dedicated padded maskable icon is a future nice-to-have, not shipped here.- Keep
theme_color#1E5A5Aandbackground_color#FAFAF8(matchindex.html).
The manifest carries no version string after this change, so it cannot drift again.
2. packages/web/frontend/index.html¶
<noscript>fallback. Add a<noscript>block inside<body>with a one-paragraph description of PhonoLex, a short list of the six tools, and links to the three landing pages and/docs/. A no-JS crawler then receives real content instead of a blank div. Styled minimally inline so it renders acceptably if ever shown to a human with JS disabled.- Version injection (root-cause). Replace the hardcoded
"softwareVersion": "6.4.0"in the JSON-LD with a placeholder"__APP_VERSION__", and add a small VitetransformIndexHtmlplugin invite.config.tsthat replaces__APP_VERSION__with the version read frompackage.jsonat build time. After this, the JSON-LD version trackspackage.jsonautomatically and never needs a manual bump.
3. public/sitemap.xml¶
- Refresh all
lastmoddates from2026-03-10to the current date. - Confirm all indexable URLs are present:
/,/docs/, the three/landing/*.htmlpages,/privacy,/terms. - Remains hand-maintained (only 7 URLs). The
/docs/sitemap is generated separately by MkDocs and referenced fromrobots.txt.
4. public/robots.txt¶
- Keep AI-crawler-friendly: no
Disallowfor GPTBot / ClaudeBot / PerplexityBot / etc. Being open to automated tools is an explicit goal. - Minor tidy only; the existing
Sitemap:lines stay.
5. public/llms.txt — NEW¶
Add an llms.txt at the site root following the emerging convention
(https://llmstxt.org/): a Markdown brief that gives an AI assistant a fast, accurate summary
of the site. Contents:
- H1 title + one-line summary of PhonoLex.
- A short prose paragraph on what it is and who it is for (SLPs, linguists, researchers, educators).
- A
## Toolssection listing the six tools with one line each. - A
## APIsection noting the free public REST API and linking/docs/. - A
## Key pagessection linking the landing pages,/docs/,/privacy,/terms.
6. Per-route meta for /privacy & /terms¶
Add a small dependency-free usePageMeta hook (src/hooks/usePageMeta.ts) that, in a
useEffect, sets document.title and the <link rel="canonical"> href for the current route,
restoring the defaults on unmount. Call it from PrivacyPolicy.tsx and TermsOfService.tsx.
- No library, no build-pipeline change.
- Benefits JS-rendering crawlers (e.g. Googlebot) and gives correct browser-tab titles and history entries per route.
- Honest limitation: crawlers that do not execute JS still see
index.html's default meta for these routes (they are low priority, 0.3 in the sitemap). Accepted for this pass.
Testing / Verification¶
manifest.json,sitemap.xml,llms.txt,robots.txt— validate as well-formed (JSON parse, XML parse). Manually eyeball against the Web App Manifest spec fields.index.html— confirm the JSON-LD still parses as valid JSON after placeholder replacement by running a production build (npm run build) and inspectingdist/index.htmlfor the injected version string equal topackage.json.usePageMeta— frontend type-check + existing test suite must pass (npm run build,npm test). Add a focused unit test asserting the hook sets and restoresdocument.title.- Run the exact CI checks locally before pushing (frontend type-check + lint + build; the workers/data suites are untouched by this pass).
Rollout¶
- Single PR from
feature/discoverability-metadata→develop(staging). - Verify on staging (
develop.phonolex.pages.dev): manifest installs cleanly,/llms.txt,/sitemap.xml,/robots.txtserve,dist/index.htmlcarries the injected version, and the CSP in_headersis not violated (JSON-LD is a data block, unaffected). - Promote to
mainafter review.
Follow-ups (not this pass)¶
- Build-time prerender / SSG so
/,/privacy,/termsship real HTML for non-JS crawlers. - Per-word indexable pages for the lexicon (48K canonical → 125K) with sharded sitemaps.
- Dedicated padded maskable PWA icon.