Skip to content

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

Open
AlemTuzlak wants to merge 1 commit intoTanStack:mainfrom
AlemTuzlak:docs/frontmatter-description-keywords
Open

feat(docs): honor description and keywords frontmatter in docs <head>#834
AlemTuzlak wants to merge 1 commit 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
  • Support for user-provided custom descriptions in documentation frontmatter
  • Automatic keyword extraction and normalization 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 fc2ec40

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

The changes add SEO keyword support to the documentation system by extracting keywords from document frontmatter, processing them through utility functions, exposing them in route loaders, and including them in the head metadata generation for both library docs and framework-specific docs.

Changes

Cohort / File(s) Summary
Route SEO Metadata
src/routes/$libraryId/$version.docs.$.tsx, src/routes/$libraryId/$version.docs.framework.$framework.$.tsx
Updated head metadata generation to include a keywords field sourced from loaderData?.keywords for enhanced SEO output in both generic and framework-specific documentation routes.
Documentation Data Functions
src/utils/docs.functions.ts
Introduced extractFrontMatterKeywords() helper to parse and normalize keywords from frontmatter data (supports arrays or single strings). Updated fetchDocs and fetchDocsPage to extract keywords and include them in returned payloads. Also improved description derivation to prefer user-provided descriptions when available.
Document Processing
src/utils/documents.server.ts
Modified extractFrontMatter to prefer user-provided description from frontmatter over auto-generated excerpts, and added userDescription field to return shape to track the user-provided value separately.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Keywords now bloom in metadata fields,
Through frontmatter gardens where data yields,
From extract to routes, the pipeline flows,
SEO perfection—see how the doc garden grows! 📚✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(docs): honor description and keywords frontmatter in docs ' directly and clearly summarizes the main changes: enabling frontmatter description and keywords to be used in the document head metadata for docs pages.

✏️ 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.

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