Skip to content

fix(seo): emit a single H1 on generated API reference pages - #94

Open
coderdan wants to merge 1 commit into
mainfrom
seo/api-reference-single-h1
Open

fix(seo): emit a single H1 on generated API reference pages#94
coderdan wants to merge 1 commit into
mainfrom
seo/api-reference-single-h1

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

What

One-line change in the TypeDoc options: hidePageTitle: falsetrue.

Why

TypeDoc's reflection.template.js writes # <symbol> as the first line of each generated page's body:

if (!this.options.getValue('hidePageTitle') && !includeReadme) {
    md.push(heading(1, this.partials.pageTitle()));
}

The docs route then renders page.data.title through Fumadocs' <DocsTitle> as an <h1> above it. Both resolve to the same symbol name, so every generated page shipped two identical H1s.

That is all 23 API reference pages, and it accounts for every multiple-H1 finding in the Ahrefs site audit — the counts match exactly.

Verification

Can't run the full generator here (its prebuild step shells out to bun install against the published packages), so I ran TypeDoc against a minimal throwaway project with the same option block, once each way. The complete diff between the two outputs:

5,6d4
< # Documentation
<

Only the H1 line and its trailing blank. ## member headings are untouched, so page anchors survive — including the two deep links the Drizzle guide makes into its API reference (#createencryptionoperators, #encryptedindexes).

Nothing changes visually: the removed heading and the surviving one carry the same text.

Side effect

getLLMText prepends # ${page.data.title} to the processed body, so the .mdx mirrors and llms.txt carried the same duplicate. They now have one H1 too.

Note for the next reader

This is hidePageTitle, not the hidePageHeader immediately above it in the same options block. The name suggests otherwise, but hidePageHeader gates the breadcrumb/navigation block — the H1 is gated on hidePageTitle.

Not in scope

Page size. Four API reference pages are over Googlebot's 2 MB crawl limit (eql.v3 is 14.6 MB and takes ~19s to transfer). That needs breaking the modules into per-symbol pages, which changes URLs and the sidebar — a separate PR.

https://claude.ai/code/session_01Rvx5yeC4Vy2svyohuVvxWj

TypeDoc's reflection template writes `# <symbol>` as the first line of
each generated page's body, and the docs route renders `page.data.title`
through Fumadocs' `<DocsTitle>` as an <h1> above it. Both resolve to the
same string, so all 23 API reference pages shipped two identical H1s —
every multiple-H1 warning in the site audit, and nothing else.

Set `hidePageTitle: true` so TypeDoc omits its copy. Verified against a
minimal TypeDoc project run with the option both ways: the only
difference in the output is the removal of the `# <name>` line and its
trailing blank. Member headings stay at `##`, so page anchors — and the
two `#createencryptionoperators`/`#encryptedindexes` deep links in the
Drizzle guide — are unaffected.

Worth flagging for the next reader: this is `hidePageTitle`, not the
`hidePageHeader` sitting directly above it. The heading in
`reflection.template.js` is gated on the former; `hidePageHeader`
controls the breadcrumb/navigation block, which is a different thing.

The raw-markdown mirrors improve too. `getLLMText` prepends
`# ${page.data.title}` to the processed body, so those files carried the
same duplicate; they now have one H1 as well.

Claude-Session: https://claude.ai/code/session_01Rvx5yeC4Vy2svyohuVvxWj
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
public-docs Ready Ready Preview Aug 18, 2026 12:28pm

Request Review

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