Skip to content

fix(docs): render raw markdown from page data, not request-time fs read - #46

Merged
nibzard merged 1 commit into
mainfrom
fix/docs-page-runtime-fs-500
May 29, 2026
Merged

fix(docs): render raw markdown from page data, not request-time fs read#46
nibzard merged 1 commit into
mainfrom
fix/docs-page-runtime-fs-500

Conversation

@nibzard

@nibzard nibzard commented May 29, 2026

Copy link
Copy Markdown
Contributor

Problem

Sharing a freshly deployed doc page (e.g. /changelog/changelog-027) on LinkedIn showed 500: Internal Server Error instead of a preview. The LinkedIn Post Inspector confirmed the page itself returned an error body (title literally 500: Internal Server Error, no OG tags found).

Root cause

Every doc page is SSG with revalidate: 1h, so the page component re-runs on the serverless function whenever it renders outside the prerender cache — ISR revalidation, cold start, cache miss, or RSC payload regeneration.

The component read raw markdown for the <LLMShare> copy button via:

const fileContent = await fs.readFile(page.data._file.absolutePath, 'utf-8');

But content/docs source files are not bundled into the function sandbox, and the build-time absolute path doesn't resolve at runtime. So the read throws ENOENT → unhandled → 500.

Why it looked intermittent: the build-time prerendered HTML is cached at the edge (returns 200 to most of us), but the first origin hit at a cold edge POP — exactly what LinkedIn's US crawler did on a brand-new URL — got the 500, which LinkedIn then cached as the preview.

Reproduced via the RSC render path (curl -H 'RSC: 1'), which intermittently returned the 500 error page while the cached HTML stayed 200.

Fix

Use page.data.content — the raw markdown fumadocs already bundles into the function — instead of a request-time filesystem read. This matches how lib/get-llm-text.ts sources content for the working /llms.mdx route. No filesystem access at request time, no 500.

Verification

  • tsc --noEmit and Biome pass
  • Full bun run build succeeds; all 147 changelog pages prerender
  • Confirmed /llms.mdx/changelog/changelog-027 (which uses page.data.content) returns 200 at runtime in production, proving that path is reliable in the function sandbox

Follow-up after merge/deploy

Re-run the affected URLs through the LinkedIn Post Inspector to clear LinkedIn's cached 500 preview.

Every doc page is SSG with revalidate: 1h, so the page component re-runs on
the serverless function whenever it renders outside the prerender cache (ISR
revalidation, cold start, cache miss, RSC payload regeneration). The component
read the raw markdown via fs.readFile(page.data._file.absolutePath) for the
LLMShare copy button, but the content/docs source files are not present in the
function sandbox and the baked-in build-time path does not resolve there, so
the read threw and returned a 500.

This surfaced as intermittent 500s on freshly deployed pages: the build-time
prerendered HTML is cached at the edge (200), but the first origin hit at a
cold POP — e.g. LinkedIn's crawler scraping a new changelog URL — got the 500
error page and cached it as the link preview.

Use page.data.content (the raw markdown fumadocs bundles into the function),
matching how lib/get-llm-text.ts already sources content for the /llms.mdx
route. No filesystem access at request time, no 500.
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 29, 2026 5:50pm

Request Review

@nibzard
nibzard merged commit 10d7097 into main May 29, 2026
2 of 3 checks passed
@nibzard
nibzard deleted the fix/docs-page-runtime-fs-500 branch May 29, 2026 17:48

@ghost ghost left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review Summary

This PR fixes a critical 500 error that occurred when doc pages were accessed outside the prerender cache (cold starts, ISR revalidation, RSC payload regeneration). The fix is correct, minimal, and well-tested.

What Changed

  • Removed: import fs from 'node:fs/promises' (unused after fix)
  • Removed: const fileContent = await fs.readFile(page.data._file.absolutePath, 'utf-8');
  • Changed: Now uses page.data.content (raw markdown bundled by fumadocs) instead of filesystem read
  • Added: Clear comment explaining why this approach is necessary

Root Cause Analysis (Verified)

The original code attempted to read source files from disk at request time. This fails in serverless function sandboxes because content/docs/ source files are not bundled into the function. The build-time absolute path doesn't resolve at runtime → ENOENT → unhandled exception → 500 error.

Why This Fix Is Correct

  1. page.data.content is already available: Fumadocs bundles raw markdown into the page data during build, making it available at runtime without filesystem access.
  2. Precedent in codebase: lib/get-llm-text.ts already uses page.data.content successfully for the /llms.mdx route, which works reliably in production.
  3. Semantically identical: Both approaches extract raw markdown via matter() and filter import statements the same way.
  4. No data loss: The content is identical; only the source changes from disk to bundled data.

Verification

Biome linting: All 191 files pass (no formatting/style issues)
TypeScript: No type errors
Full build: All 452 pages prerender successfully (including 147 changelog pages)
Link validation: 0 errors
No orphaned imports: fs import removed, no other references to _file.absolutePath in codebase

Risk Assessment

Very low risk:

  • Single, surgical change to one file
  • Uses an existing, proven pattern from lib/get-llm-text.ts
  • No changes to component interfaces or data structures
  • All quality gates pass
  • The fix directly addresses the root cause without side effects

Follow-up (Already Noted in PR)

The PR description correctly notes that LinkedIn's cached 500 preview should be cleared by re-running affected URLs through the LinkedIn Post Inspector after deployment.

Recommendation: APPROVE

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