Skip to content

docs(seo): version-explicit metadata, canonical consolidation, and descriptive link text - #8239

Merged
sneub merged 5 commits into
mainfrom
prisma/versioned-docs-metadata-canonicals
Sep 11, 2026
Merged

sneub merged 5 commits into
mainfrom
prisma/versioned-docs-metadata-canonicals

Conversation

@prisma-robot

@prisma-robot prisma-robot Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

SEO audit remediation, work package 4 of 8 (audit 2.3 duplicate titles/descriptions/H1s, 3.3 generic anchor text, 3.4 stale bookmark), all in apps/docs.

Versioned docs pages (orm/v6, orm/v7, cli/v7, guides/v7, /v7) shared their <title>, meta description, and H1 with their twin in every other version tree, including the unversioned Prisma 8 tree. Both v6 and v7 stay indexable; the version is now explicit in the metadata, and a canonical is used only where two versioned URLs are the same document.

What it delivers

A. Version-explicit titles, descriptions, and H1 marker

  • src/lib/version-metadata.ts (new): getPageVersion(pageUrl) → { version, label } or null for latest/unversioned, built on the existing pathname helpers in version.ts. Labels: /orm/v6/* → "Prisma ORM v6", /orm/v7/* → "Prisma ORM v7", /cli/v7/* → "Prisma CLI v7", /guides/v7/* and /v7/* → "Prisma 7". Plus withVersionTitle / withVersionDescription, which skip pages whose text already names the version. Named getPageVersion/getPageVersionLabel rather than getVersionLabel because version.ts already exports a getVersionLabel(version).
  • generateMetadata folds the label in: title → Supported databases (Prisma ORM v6), description → … Applies to Prisma ORM v6.. OG and Twitter read the same variables, so they stay in sync. The %s | Prisma Documentation template is untouched.
  • The H1 area renders an Eclipse Badge with the label next to (never inside) <DocsTitle>. Note: badge: frontmatter is only rendered in the sidebar today (sidebar-badge-provider.tsx), not on the page, so there was no existing page badge to sit beside — the version badge reuses that component and its rounded-full! pattern.
  • The .md / llms.mdx rendition's title line carries the same label (get-llm-text.ts), so agents see the version too.
  • structured-data.tsx: the TechArticle headline/description use the same labelled values, and mainEntityOfPage.@id now points at the canonical instead of always at the page's own URL.

B. Canonical consolidation

New optional canonical frontmatter field (source.config.ts), resolved by src/lib/canonical.ts (page.data.canonical ?? self, /docs base path applied, absolute URLs passed through). scripts/find-version-twin-duplicates.ts (new, re-runnable, --check mode) compares every v6 page against its v7 twin with frontmatter stripped and /orm/v6/ normalised to /orm/v7/. 12 of 133 pairs are materially identical (5 byte-identical, 7 differing only by version paths); each v6 page now canonicals to its v7 twin. No noindex was added.

v6 page canonical
orm/v6/more/troubleshooting/bundler-issues /orm/v7/more/troubleshooting/bundler-issues
orm/v6/more/troubleshooting/check-constraints /orm/v7/more/troubleshooting/check-constraints
orm/v6/more/troubleshooting/graphql-autocompletion /orm/v7/more/troubleshooting/graphql-autocompletion
orm/v6/more/troubleshooting/nuxt /orm/v7/more/troubleshooting/nuxt
orm/v6/more/troubleshooting/raw-sql-comparisons /orm/v7/more/troubleshooting/raw-sql-comparisons
orm/v6/more/comparisons/prisma-and-mongoose /orm/v7/more/comparisons/prisma-and-mongoose
orm/v6/more/dev-environment/environment-variables /orm/v7/more/dev-environment/environment-variables
orm/v6/prisma-client/client-extensions/type-utilities /orm/v7/prisma-client/client-extensions/type-utilities
orm/v6/prisma-client/debugging-and-troubleshooting/debugging /orm/v7/prisma-client/debugging-and-troubleshooting/debugging
orm/v6/prisma-client/setup-and-configuration/database-polyfills /orm/v7/prisma-client/setup-and-configuration/database-polyfills
orm/v6/prisma-schema/data-model/multi-schema /orm/v7/prisma-schema/data-model/multi-schema
orm/v6/reference/supported-databases /orm/v7/reference/supported-databases

The two audit "Duplicate content" hits, /orm/v7/more/troubleshooting/graphql-autocompletion and /orm/v7/prisma-schema/data-model/multi-schema, duplicate their v6 twins (not an unversioned page: no v6 or v7 page is identical to its Prisma 8 counterpart — verified by the same comparison run against the unversioned tree). Both are covered by the v6 → v7 canonicals above, so the v7 URL is the one that consolidates.

C. Cross-section duplicates — metadata rewritten from each page's own H1 and intro:

Page Old New
guides/integrations/embed-studio title Prisma MCP Server / desc Manage Prisma Postgres databases using LLMs with the Prisma Model-Context-Protocol (MCP) Server Embed Prisma Studio in a Next.js app / Embed Prisma Studio in a Next.js application with @prisma/studio-core so you can browse and edit your database from inside your app.
postgres/database/query-insights Query Insights with Prisma Postgres / Inspect slow queries, connect Prisma calls to SQL, and apply focused fixes with Prisma Postgres. Query Insights for a Prisma Postgres database / How Query Insights surfaces slow queries for a Prisma Postgres database in the Prisma Console, and where the full documentation lives.
orm/v6/reference/errors/index Errors / Prisma Client, Migrate, Introspection error message reference Prisma ORM error troubleshooting index / Where to find help for common Prisma ORM errors: connection pool, migration, shadow database, baselining, and MongoDB replica set issues.
orm/v7/reference/errors/index same as above same as above

Decisions: /query-insights (the dedicated section, 183 lines) stays primary and is unchanged; the Prisma Postgres page is a short overview that already links to it, so it was differentiated rather than canonicalised. The four ai/tools/* pages and their orm/v6/more/ai-tools/* copies differ in body (checked), so they got no canonical — Part A's "(Prisma ORM v6)" label differentiates the v6 copies. The error-reference pages keep their metadata; only the errors/ hub pages (the non-primary of each same-tree pair) were rewritten.

D. Stale bookmark + a gap in the link linter

orm/v7/.../opentelemetry-tracing.mdx linked #step-1-install-up-to-date-prisma-orm-dependencies; the heading is now "Step 1. Install Prisma ORM dependencies". Fixed (the v6 copy still has the old heading and was correct already). pnpm lint:links did not catch it: next-validate-link resolves fragments only on links that carry a path, so same-page bookmarks pass silently. scripts/lint-links.ts now checks #fragment-only links and href="#..." against the page's toc ids, headings (including ones nested in MDX components), and id="…" attributes. Across all 688 pages it reported exactly that one link and no false positives. Fence tracking moved to scripts/lib/markdown-regions.ts and now honours CommonMark (a closing fence carries no info string) — the old "count every fence line" approach desynchronised on nested examples and hid the tail of long files such as prisma-client-reference.mdx.

E. Generic anchor text — scripts/rewrite-generic-link-text.ts (new, re-runnable, --check mode) rewrote every anchor whose text was exactly one of the 14 generic phrases: 157 links on 154 lines in 65 files. It names the destination from the target page's frontmatter title, or the target heading when the link has a fragment; external destinations come from a curated name table and are never guessed (a destination it cannot name is reported and left alone — none remain). Hrefs are unchanged; code blocks and headings are skipped. Output was reviewed line by line and four sentences were hand-adjusted afterwards (create-db and prisma platform references in the v6/v7 CLI reference). Re-running the script is now a no-op.

Folder Links Lines Files
orm/v6 87 85 36
orm/v7 56 55 17
guides 9 9 7
accelerate 2 2 2
ai 2 2 2
console 1 1 1
total 157 154 65

F. Housekeeping

  • .claude/CLAUDE.md: the "Version Fallback" paragraph (and the content/docs.v6/ structure bullets and the stale src/app/docs/[version]/… route path) now describe reality — versions are content folders, there is no fallback, and the metadata/canonical helpers are named.
  • Deleted src/components/version-banner.tsx (nothing imported it).
  • src/lib/content-metadata.test.ts (new): fails if two pages in the same version tree share metaTitle + metaDescription, and if any page is missing either. Twins across trees stay allowed because Part A differentiates them at render time.

How it was tested

  • pnpm check (includes the new tests): green — 29 tests in apps/docs, 31 in the repo.
  • pnpm types:check: docs, blog, eclipse, @prisma/eclipse, @prisma-docs/ui green. site#types:check fails in this sandbox with DOCS_ORIGIN is required in production — reproduced on unmodified main, and apps/site is untouched here. @prisma-docs/ui also needs pnpm --filter @prisma/eclipse build first in a fresh clone.
  • pnpm lint:links: 0 errors, now including same-page fragments. Verified the new check actually fails by reintroducing the stale bookmark.
  • pnpm --filter docs run lint:spellcheck: 827 files, 0 issues.
  • apps/docs unit tests cover the helpers directly: 16 for version-metadata, 4 for canonical, 2 content invariants.
  • No next dev / next build for apps/docs or apps/blog (they OOM in the sandbox, per the task), so the badge placement and rendered <head> were verified by reading the source, not in a browser.

Assumptions and choices

  • The description suffix is a sentence (Applies to Prisma ORM v6.) rather than a parenthetical, so it reads naturally when Google truncates it.
  • "Materially identical" for a canonical means byte-identical after frontmatter removal and /orm/v6/ → /orm/v7/ normalisation. Every one of the 12 pairs was diffed; all differences were version paths only. Nothing that differs in substance was consolidated.
  • The docs test script now runs node --import tsx --test instead of bare node --test: without the loader, a test cannot import a module that uses extensionless relative imports (version-metadata.ts → ./version). tsx is already a devDependency and the gate is unchanged in what it asserts.
  • The canonical frontmatter value is a docs-relative path; absolute URLs also work.

Follow-ups not done here (out of scope)

  • The sitemap still lists v6 URLs that now declare a different canonical (src/app/(docs)/sitemap.ts); it should probably drop or deprioritise pages with a foreign canonical.
  • scripts/find-version-twin-duplicates.ts --check is not wired into a gate. It would catch a v6 page that drifts away from its twin while keeping a canonical, but content edits would then fail the build, which deserves its own decision.
  • pnpm check (oxfmt) reformats two unrelated apps/blog files on a clean checkout of main; reverted here to keep this diff scoped.
  • Several metaTitles are still generic across sections (for example Errors on both error-reference pages, one per tree); they are unique within a tree, so the new test passes, but they are weak titles.

builder (Program) added 2 commits September 11, 2026 08:30
…e fragment lint

Versioned docs pages (orm/v6, orm/v7, cli/v7, guides/v7, /v7) shared their
title, meta description, and H1 with their twin in every other version tree.
generateMetadata now folds the version into the title and description, the H1
area carries a version badge, and the markdown rendition's title line matches.

Adds an optional `canonical` frontmatter field and points the twelve ORM v6
pages whose body is identical to their v7 twin at that twin.

lint:links now checks same-page `#fragment` links, which next-validate-link
skips; fixes the one stale bookmark it found (audit 3.4).
…tale docs notes

Rewrites 157 non-descriptive anchors ("here", "learn more", "docs", ...) in
apps/docs content so the link text names its destination, via a re-runnable
script that takes the name from the target page's frontmatter title or heading.

Differentiates four same-tree metadata duplicates (embed-studio, the Prisma
Postgres query-insights overview, and the v6/v7 error troubleshooting indexes)
and adds a test that fails when two pages in one version tree share
metaTitle + metaDescription.

Structured data now carries the same version label and canonical as the page
metadata; CLAUDE.md describes how versions actually work; the unused
version-banner component is gone.
@vercel

vercel Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
blog Ready Ready Preview Sep 11, 2026 10:50am UTC
docs Ready Ready Preview Sep 11, 2026 10:50am UTC
eclipse Ready Ready Preview Sep 11, 2026 10:50am UTC
site Ready Ready Preview Sep 11, 2026 10:50am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7cc7dc93-5d26-4aa5-9a4d-342695f0dac6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

🍈 Lychee Link Check Report

2454 links: ✅ 864 OK | 🚫 0 errors | 🔀 91 redirects | 👻 1590 excluded

✅ All links are working!


Full Statistics Table
Status Count
✅ Successful 864
🔀 Redirected 91
👻 Excluded 1590
🚫 Errors 0
⛔ Unsupported 0
⏳ Timeouts 0
❓ Unknown 0

…dropped noun

- structured-data: mainEntityOfPage.@id prefixed the base URL onto every
  resolved canonical, so an absolute canonical: frontmatter value (which
  resolveCanonicalUrl passes through by design) would render as
  https://www.prisma.iohttps://... . Join only relative canonicals; covered
  by two tests on the new absoluteCanonicalUrl helper.
- .claude/CLAUDE.md: the versioned getting-started tree lives under
  content/docs/(index)/v7/, not content/docs/v7/.
- shared-extensions (v6, v7): the anchor rewrite absorbed the word "page"
  and left "visit the Shared packages & examples."; drop the article.
@prisma-robot

prisma-robot Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed f8ba56fc672f0bdd3e9596c58281a42eb15e7a96: full diff, the code around it, and the gates run locally (pnpm --filter docs test 29/29, lint:links 0 errors incl. the new same-page fragment check, types:check for docs, cspell 0 issues, find-version-twin-duplicates --check and rewrite-generic-link-text --check both clean, oxfmt/oxlint clean in apps/docs). The 12 canonicals match what the comparison script reports. The Vercel preview is login-gated, so rendered <head> was checked from source, not in a browser.

Three findings, fixed and pushed in cf6ffa1259b6779869da2a33924f5daace5759de:

  1. structured-data.tsx — JSON-LD @id breaks on an absolute canonical. mainEntityOfPage.@id was ${baseUrl}${canonical}, but resolveCanonicalUrl passes absolute canonical: values through by design (documented in this PR and covered by its tests), so such a page would emit https://www.prisma.iohttps://www.prisma.io/docs/.... None of the 12 current canonicals are absolute, so nothing is broken today, but the feature advertises the form. Added absoluteCanonicalUrl(canonical, baseUrl) in canonical.ts (relative → joined, absolute → untouched), used it there, and added two tests (31 now pass).
  2. .claude/CLAUDE.md — wrong path. The new "Content Structure" bullet named content/docs/v7/ for the versioned getting-started tree; that folder does not exist, the pages live in content/docs/(index)/v7/ and are served at /v7. Corrected.
  3. Anchor rewrite dropped a noun. In orm/v6 and orm/v7 prisma-client/client-extensions/shared-extensions/index.mdx the script turned "visit this page." into "visit the Shared packages & examples." Dropped the article so it reads "visit Shared packages & examples." Re-running the rewrite script is still a no-op.

I will re-review the new head from scratch shortly.

— reviewer

mentionsVersion only recognised the full label ('Prisma ORM v7') or the
bare 'v7' token, so titles that name the version without the v still got
the label appended: 'What is Prisma 7?' rendered as 'What is Prisma 7?
(Prisma ORM v7)' and 'What is Prisma ORM? (Prisma 6 overview)' as '...
(Prisma 6 overview) (Prisma ORM v6)', the stutter the helper's own doc
says it avoids. It now also matches 'Prisma [ORM|CLI] N' (N followed by a
non-digit, so 'Prisma 6.16' counts and 'Prisma 60' does not). Tests
added for both directions.
@prisma-robot

prisma-robot Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Re-reviewed cf6ffa1259b6779869da2a33924f5daace5759de from scratch (fresh clone, full diff including my round-1 fixes, gates run locally: pnpm --filter docs test 31/31, types:check for docs, lint:links exit 0 with the same-page fragment check, lint:agent-ready 0 failures, cspell 0 issues, find-version-twin-duplicates --check and rewrite-generic-link-text --check both no-ops, oxfmt/oxlint clean in apps/docs; the 12 canonicals match the comparison script; Badge spreads its props so data-markdown-ignore reaches the DOM).

One finding, fixed and pushed in d613787b3b9d89d1946393048a4c0a597a54fbe0:

version-metadata.ts — the anti-stutter guard misses "Prisma 7". mentionsVersion only recognised the full label (Prisma ORM v7) or the bare v7 token, so a title that names the version without the v still got the label appended. In this content that produces What is Prisma 7? (Prisma ORM v7) (orm/v7/index.mdx metaTitle, and its title: Prisma 7 in the .md rendition) and What is Prisma ORM? (Prisma 6 overview) (Prisma ORM v6) (orm/v6/overview/introduction/what-is-prisma.mdx) — exactly the stutter the helper's docstring says it avoids. It now also matches Prisma [ORM|CLI] N with N followed by a non-digit, so Prisma 6.16 counts as naming the 6 line and Prisma 60 does not. Tests added for both directions (32 now pass). Nothing else in the versioned trees is affected; to-v7.mdx's description ("…upgrade to Prisma ORM 7") on the v6 page still gets "Applies to Prisma ORM v6.", which is correct.

Everything else read clean. I will re-review the new head from scratch shortly.

— reviewer

@prisma-robot

prisma-robot Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Review clean at d613787

Third cold read (fresh clone, branch 0 behind main, full source and content diff including the two previous fix commits, gates run locally): pnpm --filter docs test 32/32, types:check for docs, lint:links exit 0 with the same-page fragment check, lint:agent-ready 0 failures, cspell 0 issues in 827 files, find-version-twin-duplicates --check and rewrite-generic-link-text --check both no-ops, oxfmt/oxlint clean in apps/docs. No human comments in the thread. Nothing left to fix.

What the PR does (all in apps/docs): versioned pages (orm/v6, orm/v7, cli/v7, guides/v7, /v7) get a version label folded into <title>, meta description, JSON-LD headline, the .md rendition title, and a Badge beside the H1, so twins across version trees stop sharing identical metadata; a new optional canonical frontmatter field points 12 v6 pages that are byte-identical to their v7 twin at the v7 URL (verified by the new comparison script); four cross-section duplicate title/description pairs are rewritten; one stale same-page bookmark is fixed and lint:links now catches that class; 157 generic anchors (here, learn more, …) are rewritten to name their destination with hrefs unchanged; .claude/CLAUDE.md now describes the real content layout; an unused version-banner.tsx is deleted; a content test enforces unique metaTitle + metaDescription within a tree.

Risk: low. Every change is confined to the docs app and its content; the helpers are unit-tested including the edge cases found in rounds 1–2; the anchor rewrites change only link text and every href still resolves; the canonicals are reversible frontmatter on pages whose bodies are provably identical. The one thing not verified in a browser (the Vercel preview is login-gated) is the badge placement next to the H1 — it is a flex-wrap wrapper around DocsTitle and a Badge with the sidebar's existing rounded-full! pattern, so the downside is cosmetic.

Proposed follow-up (awaiting a human's approval, not blocking this PR): a builder task to drop pages with a foreign canonical from src/app/(docs)/sitemap.ts, which the PR body names as out of scope — the 12 v6 URLs are still listed there while their <link rel=canonical> points at v7.

— reviewer

@prisma-robot

prisma-robot Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Review clean at 782cf66

Cold read of the new head after the merge of main (fresh clone, branch 0 behind main). The merge commit is a clean merge: the PR's diff against its main parent is byte-for-byte the same 98 files / +1361 −206 as before, and what main brought in (#8237: unversioned ORM/CLI content and cspell.json) touches no file this PR touches. I re-read the full source and content diff, including the fixes from rounds 1–3, then ran the gates on the merged head: pnpm --filter docs test 32/32, types:check for docs (after @prisma/eclipse build), lint:links 0 errors with the same-page fragment check, find-version-twin-duplicates --check (12 identical, 0 missing, 0 stale), rewrite-generic-link-text --check no-op, lint:agent-ready 0 failures, cspell 0 issues in 827 files, oxfmt clean, oxlint only the pre-existing unused-import warnings. I also planted a generic [here](…) link and a dead #fragment on a page and confirmed both new checks fail on it (then restored the file). The new content from main passes the new same-tree unique metaTitle + metaDescription test as well.

What the PR does (all in apps/docs): versioned pages (orm/v6, orm/v7, cli/v7, guides/v7, /v7) get a version label folded into <title>, meta description, JSON-LD headline, the .md rendition title, and a Badge beside the H1; an optional canonical frontmatter field points 12 v6 pages that are identical to their v7 twin at the v7 URL; four cross-section duplicate title/description pairs are rewritten; one stale same-page bookmark is fixed and lint:links now catches that class; 157 generic anchors are rewritten to name their destination with hrefs unchanged; .claude/CLAUDE.md describes the real content layout; the unused version-banner.tsx is deleted; a content test enforces unique metaTitle + metaDescription within a tree.

Risk: low. Everything is confined to the docs app and its content, the helpers are unit-tested including the edge cases found in earlier rounds, every rewritten anchor still resolves, and the canonicals are reversible frontmatter on provably identical pages. Not verified in a browser (the Vercel preview is login-gated): the badge placement beside the H1, which is cosmetic at worst. One cosmetic note, not a fix: two v7 comparison pages now link "[Type safety Overview]" because that is the v7 page's own frontmatter title (its metaTitle is "Type safety"); that title predates this PR.

The sitemap follow-up (drop or deprioritise pages with a foreign canonical in src/app/(docs)/sitemap.ts) was already proposed as a builder task in the previous review round and awaits a human's decision; I have not proposed it again.

— reviewer

@sneub
sneub merged commit 813d11c into main Sep 11, 2026
17 checks passed
@sneub
sneub deleted the prisma/versioned-docs-metadata-canonicals branch September 11, 2026 11:10

This branch was successfully deployed

4 active deployments
Preview – docs — 782cf665 Deployed Sep 11, 2026 by vercel[bot]
Preview – blog — 782cf665 Deployed Sep 11, 2026 by vercel[bot]
Preview – site — 782cf665 Deployed Sep 11, 2026 by vercel[bot]
Preview – eclipse — 782cf665 Deployed Sep 11, 2026 by vercel[bot]
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.

1 participant