Skip to content

Discoverability & SPA Metadata Pass

Date: 2026-07-19 Branch: feature/discoverability-metadata (worktree, off develop) Status: Design approved

Problem

Two related issues:

  1. Stale PWA manifest. packages/web/frontend/public/manifest.json still 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.

  2. 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 stale lastmod dates, and there is no llms.txt for 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-helmet or 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 a screenshots entry reusing og-image.png (1200×630, form_factor: "wide").
  • icons: change purpose from "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 #1E5A5A and background_color #FAFAF8 (match index.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 Vite transformIndexHtml plugin in vite.config.ts that replaces __APP_VERSION__ with the version read from package.json at build time. After this, the JSON-LD version tracks package.json automatically and never needs a manual bump.

3. public/sitemap.xml

  • Refresh all lastmod dates from 2026-03-10 to the current date.
  • Confirm all indexable URLs are present: /, /docs/, the three /landing/*.html pages, /privacy, /terms.
  • Remains hand-maintained (only 7 URLs). The /docs/ sitemap is generated separately by MkDocs and referenced from robots.txt.

4. public/robots.txt

  • Keep AI-crawler-friendly: no Disallow for 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 ## Tools section listing the six tools with one line each.
  • A ## API section noting the free public REST API and linking /docs/.
  • A ## Key pages section 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 inspecting dist/index.html for the injected version string equal to package.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 restores document.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-metadatadevelop (staging).
  • Verify on staging (develop.phonolex.pages.dev): manifest installs cleanly, /llms.txt, /sitemap.xml, /robots.txt serve, dist/index.html carries the injected version, and the CSP in _headers is not violated (JSON-LD is a data block, unaffected).
  • Promote to main after review.

Follow-ups (not this pass)

  • Build-time prerender / SSG so /, /privacy, /terms ship real HTML for non-JS crawlers.
  • Per-word indexable pages for the lexicon (48K canonical → 125K) with sharded sitemaps.
  • Dedicated padded maskable PWA icon.