Skip to content

fix(app): one title and description per page - #12442

Merged
MarkusNeusinger merged 6 commits into
mainfrom
feature/head-metadata-dedup
Oct 29, 2026
Merged

MarkusNeusinger merged 6 commits into
mainfrom
feature/head-metadata-dedup

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • One title and description per page. The SPA shell's default <title>, description, and Open Graph and Twitter tags stayed in <head> next to the ones every page renders since refactor(app): replace react-helmet-async with React 19 native metadata #12424, so each route had two of each and the shell's description came first. The six defaults are now marked data-shell-default, and watchShellDefaults() (a MutationObserver on <head>, started in app/src/main.tsx) detaches a default only while a page renders a tag of the same kind and re-attaches it when none is left. /debug gets its own title.
  • How it was built. Best-of-2 run with Google Jules (Gemini Flash) on feature/head-metadata-dedup: variant A deleted the shell defaults (fix(app): drop the shell's per-route head defaults #12439, closed: clients without JavaScript lost title and description, and /debug had none), variant B removed them once at boot (fix(app): strip the shell's head defaults when the app boots #12440, merged into the feature branch). The Gemini Flash pre-PR check then found that B left the tab without a title while a lazy route chunk loads, on the error boundary, and on a loading or failed spec page, and that its tests accepted zero descriptions; Claude replaced the one-time removal with the fallback-keeping sync and tightened the tests.

Plan

Refs #9629 (the head-duplication half of audit Medium#6).

Test plan

  • yarn lint, yarn fm:check, yarn type-check, yarn test (110 files, 1192 tests), yarn build
  • Built against the production API and loaded in headless Chromium: exactly one <title>, one description and one og:title on /, /plots, /plots?lib=matplotlib, /about, /legal, /debug, /scatter-basic and a 404; /debug and the 404 keep the shell description as their fallback; a client-side navigation from / to /about leaves one title
  • Built HTML still carries the shell defaults for clients without JavaScript
  • Gemini 3.8 Flash pre-PR check: its title-fallback and test findings fixed; its claim that only SpecPage renders a description was checked and is wrong (eleven pages do)

MarkusNeusinger and others added 5 commits October 29, 2026 11:21
Fixes duplicate metadata elements (title, description, and OG/Twitter
properties) that are present in the shell `index.html` by default and
redundantly appended when React routes boot up and hydrate. We now mark
the 6 shell tags with `data-shell-default` and aggressively strip them
immediately before creating the React root.
Additionally, `DebugPage` now consistently sets its own explicit title.

Tests added for both the DOM utility and integration into the specific
pages.

Verification steps run successfully:
```
yarn lint
yarn fm:check
yarn type-check
yarn test src/pages src/utils
yarn build
```

Refs #9629

---
*PR created automatically by Jules for task
[16201668932719198013](https://jules.google.com/task/16201668932719198013)
started by @MarkusNeusinger*

---------

Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gemini Flash pre-PR check: removing the shell's title and description before
the first render left the tab without a title while a lazy route chunk loads,
on the route error boundary, and on a spec page that is loading or failed.
watchShellDefaults() now detaches a shell default only while a page renders a
tag of the same kind and re-attaches it when none is left (MutationObserver on
<head>). The page tests run that sync and require exactly one title and one
description instead of "at most one".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 29, 2026 10:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Browser-runtime confirmation is still needed because the fix depends on React 19 metadata hoisting and MutationObserver ordering during lazy, error, loading, and client-side route transitions, while the local servers were unavailable.

No open issues 🎉

What changed in this PR

This pull request deduplicates SPA page metadata while preserving shell defaults for non-JavaScript, loading, and error states. It also adds titles for all /debug states and expands metadata synchronization tests.

Changes:

  • Marks six shell metadata tags as fallback defaults.
  • Adds MutationObserver-based synchronization for route metadata.
  • Adds /debug titles across all render states.
  • Adds unit, integration, and changelog coverage.
File Summary
changelog.d/​head-metadata-dedup.md Documents the metadata deduplication and fallback behavior.
app/​src/​utils/​shellMetadata.ts Implements shell metadata detection, detachment, restoration, and observation.
app/​src/​utils/​shellMetadata.test.ts Tests synchronization, restoration, idempotency, and observer behavior.
app/​src/​pages/​headMetadata.test.tsx Tests page-level metadata deduplication; coverage should also include all supported Open Graph and Twitter tags and shell-only fallback behavior. Nit.
app/​src/​pages/​DebugPage.tsx Adds titles across authenticated, loading, error, and success states.
app/​src/​main.tsx Starts shell metadata observation before rendering the React application.
app/​index.html Marks six shell metadata tags as defaults; og:url and og:image remain unmarked and can duplicate SpecPage metadata. Moderate.

This pull request requires changes.

Review effort: Lite

@codecov

codecov Bot commented Oct 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@MarkusNeusinger
MarkusNeusinger merged commit d03055f into main Oct 29, 2026
17 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feature/head-metadata-dedup branch October 29, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants