Skip to content

feat(docs): honor description and keywords frontmatter in docs <head>#834

Merged
LadyBluenotes merged 2 commits intoTanStack:mainfrom
AlemTuzlak:docs/frontmatter-description-keywords
May 6, 2026
Merged

feat(docs): honor description and keywords frontmatter in docs <head>#834
LadyBluenotes merged 2 commits intoTanStack:mainfrom
AlemTuzlak:docs/frontmatter-description-keywords

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented Apr 17, 2026

Summary

Right now, doc page <meta name="description"> is always auto-generated from the first paragraph of the file — any description in frontmatter is ignored — and <meta name="keywords"> is never emitted for doc pages. This means library authors can't author SEO copy alongside their docs.

This PR wires the existing frontmatter fields into the docs renderer:

  • extractFrontMatter preserves the user-supplied description when present (exposed as userDescription) and falls back to the existing auto-generated excerpt otherwise. The return type stays compatible (data.description is still a non-empty string).
  • fetchDocs / fetchDocsPage prefer the user description and expose a normalized keywords top-level field. Array input is joined with ", "; strings pass through trimmed; empty/missing values become undefined.
  • The two docs catch-all routes ($libraryId/$version/docs/$ and the framework variant) pass keywords through to seo(), which already supports it.

Library docs can then author:

---
title: Streaming
description: "Stream AI responses in real time with TanStack AI — async iterable chunks, strategies, and partial JSON for responsive chat UIs."
keywords:
  - tanstack ai
  - streaming
  - async iterable
---

Pages that omit these fields keep today's exact behavior.

Motivation: TanStack/ai#464 adds description and keywords to every hand-authored doc in the TanStack AI repo. That PR is a no-op until this one lands; together they give TanStack AI docs proper SEO.

Test plan

  • Pull a doc that already sets a description in frontmatter and confirm <meta name="description"> uses it (not the auto-excerpt)
  • Pull a doc that sets a keywords array and confirm <meta name="keywords"> shows the comma-joined list
  • Pull a doc with neither field and confirm behavior is unchanged (auto-excerpt description, no keywords tag)
  • Spot-check OG/Twitter tags still render correctly since they share the same description
  • Spot-check a framework-scoped doc page (e.g. Query React) since that route is also touched

Summary by CodeRabbit

  • New Features
    • Documentation pages now include SEO keywords metadata for improved search engine visibility.
    • Documentation frontmatter can provide a custom description that is used when present.
    • Automatic extraction and normalization of keywords from documentation metadata.

The docs renderer previously auto-generated a <meta name="description">
from the first paragraph of every doc and ignored any description in
frontmatter. It also did not emit a <meta name="keywords"> tag at all.

- extractFrontMatter now preserves the user-supplied description when
  present (exposed as userDescription) and falls back to the existing
  auto-generated excerpt otherwise.
- fetchDocs / fetchDocsPage prefer the user description and expose a
  normalized keywords field (arrays of strings are joined with ", ";
  empty values are dropped).
- The two docs catch-all routes (\$libraryId/\$version/docs/\$ and the
  framework variant) pass keywords through to seo().

Individual library docs repos can now author SEO copy in frontmatter:

  ---
  title: ...
  description: "..."
  keywords:
    - ...
  ---

Pages that omit these fields keep today's behavior unchanged.
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

👷 Deploy request for tanstack pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit a35e425

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21a14ef3-0857-4394-8ea4-2de6a5f1f762

📥 Commits

Reviewing files that changed from the base of the PR and between fc2ec40 and a35e425.

📒 Files selected for processing (4)
  • src/routes/$libraryId/$version.docs.$.tsx
  • src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
  • src/utils/docs.functions.ts
  • src/utils/documents.server.ts
✅ Files skipped from review due to trivial changes (1)
  • src/routes/$libraryId/$version.docs.$.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
  • src/utils/docs.functions.ts

📝 Walkthrough

Walkthrough

The PR exposes document frontmatter keywords and user-provided descriptions through the docs utilities, surfaces keywords in doc loader payloads, and adds those keywords to the head SEO metadata for both generic and framework-specific docs routes. No exported signatures were removed.

Changes

Documentation SEO Pipeline

Layer / File(s) Summary
Frontmatter parsing
src/utils/documents.server.ts
extractFrontMatter now parses result.data.description into userDescription (non-empty trimmed string or undefined) and sets data.description to prefer that user-provided description over a generated excerpt.
Keyword extraction helper
src/utils/docs.functions.ts
Added `extractFrontMatterKeywords(value: unknown): string
Docs fetch payload
src/utils/docs.functions.ts
fetchDocs now prefers frontMatter.userDescription for description, extracts keywords via the new helper, and includes keywords in its returned payload. fetchDocsPage propagates doc.keywords into its return shape.
Route metadata wiring
src/routes/$libraryId/$version.docs.$.tsx, src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
Both route head()/SEO generation calls now include a keywords field sourced from loaderData?.keywords, alongside existing title/description/og image/noindex values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 In the meadow of markdown I hop and seek,
I pull tiny keywords from frontmatter creek,
I stitch them to headers, a tidy little seam,
Docs hum with meaning, and metadata gleams 🌸📚

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: honoring description and keywords frontmatter fields in the docs page metadata. It is concise, specific, and clearly reflects the PR's primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/utils/docs.functions.ts (2)

230-232: LGTM on the description/keywords wiring.

Preferring frontMatter.userDescription over the markdown-stripped excerpt correctly honors author-provided copy, and extractFrontMatterKeywords gracefully handles arrays, strings, and missing values.

One optional thought: you may want to de-duplicate keywords (case-insensitive) before joining, since authored lists can drift. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/docs.functions.ts` around lines 230 - 232, The keywords can include
duplicates with differing case; update extractFrontMatterKeywords (or the place
where keywords are joined into the final string) to normalize (e.g.,
toLowerCase) and deduplicate keywords case-insensitively before
returning/joining them so authored lists don’t produce repeated entries; ensure
this preserves original trimming and handles arrays/strings/missing values and
keep description logic (frontMatter.userDescription ??
removeMarkdown(frontMatter.excerpt ?? '')) unchanged.

267-283: Non-string array entries are silently dropped — consider logging.

extractFrontMatterKeywords filters out non-string array items (e.g. numbers or nested arrays in YAML). That's safe, but a silent drop can make misauthored frontmatter hard to debug. Consider a console.warn when value is an array but some entries are filtered out, or when the input type is unexpected (not array/string/undefined).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/docs.functions.ts` around lines 267 - 283, The function
extractFrontMatterKeywords currently drops non-string array entries and unknown
input types silently; update it to warn when that happens by detecting when
value is an array and some items are filtered out (i.e., original length !==
normalized.length) and calling console.warn (or the project logger) with a clear
message including the offending original value and which entries were dropped,
and also emit a warning when value is neither string nor array (and not
undefined) to surface unexpected frontmatter types; keep the existing return
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/documents.server.ts`:
- Around line 424-443: userDescription currently checks trimmed length but
returns the original untrimmed result.data.description; update the logic around
userDescription and the returned data.description so any validated description
is trimmed before being stored/returned. Specifically, in the block that
computes userDescription (referencing result.data.description and
userDescription) set userDescription to result.data.description.trim() when
non-empty, and ensure the returned data.description uses this trimmed
userDescription (falling back to createExcerpt(result.content) as before) so no
leading/trailing whitespace leaks into meta tags.

---

Nitpick comments:
In `@src/utils/docs.functions.ts`:
- Around line 230-232: The keywords can include duplicates with differing case;
update extractFrontMatterKeywords (or the place where keywords are joined into
the final string) to normalize (e.g., toLowerCase) and deduplicate keywords
case-insensitively before returning/joining them so authored lists don’t produce
repeated entries; ensure this preserves original trimming and handles
arrays/strings/missing values and keep description logic
(frontMatter.userDescription ?? removeMarkdown(frontMatter.excerpt ?? ''))
unchanged.
- Around line 267-283: The function extractFrontMatterKeywords currently drops
non-string array entries and unknown input types silently; update it to warn
when that happens by detecting when value is an array and some items are
filtered out (i.e., original length !== normalized.length) and calling
console.warn (or the project logger) with a clear message including the
offending original value and which entries were dropped, and also emit a warning
when value is neither string nor array (and not undefined) to surface unexpected
frontmatter types; keep the existing return behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34e86ee4-4f7f-40cb-a3b1-ef9d76f7ad3a

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0fdb0 and fc2ec40.

📒 Files selected for processing (4)
  • src/routes/$libraryId/$version.docs.$.tsx
  • src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
  • src/utils/docs.functions.ts
  • src/utils/documents.server.ts

Comment on lines +424 to 443
const userDescription =
typeof result.data.description === 'string' &&
result.data.description.trim().length > 0
? result.data.description
: undefined

return {
...result,
data: {
...result.data,
description: createExcerpt(result.content),
description: userDescription ?? createExcerpt(result.content),
redirect_from: redirectFrom,
redirectFrom,
} as { [key: string]: any } & {
description: string
redirect_from?: Array<string>
redirectFrom?: Array<string>
},
userDescription,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider trimming userDescription before returning.

userDescription is validated via .trim().length > 0 but the original (untrimmed) result.data.description is returned. If authors accidentally include leading/trailing whitespace or newlines (common with YAML block scalars like description: >), that whitespace will leak into the rendered <meta name="description"> and OG/Twitter tags downstream.

Proposed fix
   const userDescription =
     typeof result.data.description === 'string' &&
     result.data.description.trim().length > 0
-      ? result.data.description
+      ? result.data.description.trim()
       : undefined
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const userDescription =
typeof result.data.description === 'string' &&
result.data.description.trim().length > 0
? result.data.description
: undefined
return {
...result,
data: {
...result.data,
description: createExcerpt(result.content),
description: userDescription ?? createExcerpt(result.content),
redirect_from: redirectFrom,
redirectFrom,
} as { [key: string]: any } & {
description: string
redirect_from?: Array<string>
redirectFrom?: Array<string>
},
userDescription,
}
const userDescription =
typeof result.data.description === 'string' &&
result.data.description.trim().length > 0
? result.data.description.trim()
: undefined
return {
...result,
data: {
...result.data,
description: userDescription ?? createExcerpt(result.content),
redirect_from: redirectFrom,
redirectFrom,
} as { [key: string]: any } & {
description: string
redirect_from?: Array<string>
redirectFrom?: Array<string>
},
userDescription,
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/documents.server.ts` around lines 424 - 443, userDescription
currently checks trimmed length but returns the original untrimmed
result.data.description; update the logic around userDescription and the
returned data.description so any validated description is trimmed before being
stored/returned. Specifically, in the block that computes userDescription
(referencing result.data.description and userDescription) set userDescription to
result.data.description.trim() when non-empty, and ensure the returned
data.description uses this trimmed userDescription (falling back to
createExcerpt(result.content) as before) so no leading/trailing whitespace leaks
into meta tags.

…ription-keywords

# Conflicts:
#	src/routes/$libraryId/$version.docs.$.tsx
#	src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
#	src/utils/docs.functions.ts
@LadyBluenotes LadyBluenotes merged commit fcade96 into TanStack:main May 6, 2026
4 checks passed
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