Skip to content

feat(docs): server-render the blog (no rebuild needed for new posts) - #119

Merged
leoisadev1 merged 1 commit into
mainfrom
posthog-code/notra-blog-ssr
Jun 28, 2026
Merged

leoisadev1 merged 1 commit into
mainfrom
posthog-code/notra-blog-ssr

Conversation

@leoisadev1

Copy link
Copy Markdown
Member

Summary

Switches the blog from static prerendering to on-demand SSR so newly published Notra posts appear on the live site within ~a minute, with no rebuild.

How

  • Prerender exclusion (vite.config.ts prerender.filter): /blog, /blog/$slug, /og/blog/* are no longer baked at build; the rest of the site stays static.
  • Deploy the full Build Output (vercel.json -> .vercel/output): non-prerendered routes are served by the Nitro SSR function (__server); prerendered docs/home are still served as static files via the BOA filesystem handle.
  • Runtime fetch (src/lib/notra-runtime.ts): server functions fetch published posts from Notra at request time. The key stays server-side; @usenotra/sdk + the Markdown renderer are dynamically imported so they never reach the client bundle (verified: SDK is bundled under __server.func/_libs only). Responses are edge-cached (s-maxage=60, stale-while-revalidate=600).
  • Blog routes + the OG image route load from the runtime fetch; $slug head reads loaderData.
  • The build-time snapshot remains the source for sitemap/rss/feed (refreshed on rebuild).

Verified

  • typecheck OK, 30 tests OK, lint clean
  • Vercel-preset build: /blog excluded from static, docs still static, __server present, no API key or SDK in client output
  • Default node-server build (CI) OK
  • NOTRA_API_KEY set for Preview so the preview deploy validates the live runtime fetch

Created with PostHog Code

…a rebuild

The blog now renders on-demand instead of being baked at build time:

- vite.config excludes /blog, /blog/$slug, and /og/blog/* from prerendering
  (prerender.filter); everything else stays prerendered to static HTML.
- vercel.json deploys the full Build Output (functions + static) so non-prerendered
  routes hit the Nitro SSR function while prerendered docs/home are still served
  from static files via the build-output filesystem handle.
- src/lib/notra-runtime.ts fetches published posts from Notra at request time via
  server functions. The API key stays server-side; @usenotra/sdk and the Markdown
  renderer are dynamically imported so they never reach the client bundle. Edge
  caching (s-maxage + stale-while-revalidate) means Notra is hit at most once per
  cache window.
- The blog routes and the OG image route load from the runtime fetch; the $slug
  head reads loaderData.

The build-time snapshot stays the source for sitemap/rss/feed (refreshed on
rebuild). New posts now appear on the live blog within the cache window.

Generated-By: PostHog Code
Task-Id: b674afbd-54f5-4303-ac9f-edf84b4622af
@vercel

vercel Bot commented Jun 28, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
email-sdk-fumadocs Ready Ready Preview, Comment Jun 28, 2026 4:47pm

Request Review

@leoisadev1
leoisadev1 merged commit 6b203ca into main Jun 28, 2026
3 of 4 checks passed
@leoisadev1
leoisadev1 deleted the posthog-code/notra-blog-ssr branch June 28, 2026 16:53
@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR moves the blog to runtime SSR backed by Notra. The main changes are:

  • New server functions for fetching and mapping Notra blog posts.
  • Blog index, blog detail, and blog OG routes now load data at request time.
  • Blog and blog OG paths are excluded from prerendering.
  • Vercel output is changed to deploy the full Build Output directory.

Confidence Score: 4/5

The runtime blog path can fail closed when Notra config or Notra itself is unavailable, and the deployment output setting should be confirmed before merging.

  • Missing runtime key makes existing blog content disappear.
  • Notra request failures can turn public blog and OG routes into server errors.
  • Single-post rendering now scales with the full post collection.
  • The Vercel output change is central to making the SSR routes reachable.

apps/fumadocs/src/lib/notra-runtime.ts and vercel.json

Important Files Changed

Filename Overview
apps/fumadocs/src/lib/notra-runtime.ts Adds runtime Notra fetching for blog list and detail data, with availability and scaling risks on the request path.
apps/fumadocs/src/routes/blog/$slug.tsx Moves blog detail loading and metadata to the runtime server function and adds edge caching.
apps/fumadocs/src/routes/blog/index.tsx Moves blog index loading to the runtime server function and adds edge caching.
apps/fumadocs/src/routes/og/blog/$.ts Moves blog OG image lookup to the runtime server function.
apps/fumadocs/vite.config.ts Excludes blog and blog OG routes from prerendering and removes build-time blog page enumeration.
vercel.json Changes deployment output from the static subdirectory to the full Build Output directory.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Blog or OG request] --> B[SSR route]
  B --> C[notra-runtime server function]
  C --> D{Runtime key present}
  D -- No --> E[Empty data]
  E --> F[Empty blog or 404]
  D -- Yes --> G[Fetch Notra pages]
  G --> H{Fetch succeeds}
  H -- No --> I[Unhandled error]
  I --> J[500 response]
  H -- Yes --> K[Map posts and render]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[Blog or OG request] --> B[SSR route]
  B --> C[notra-runtime server function]
  C --> D{Runtime key present}
  D -- No --> E[Empty data]
  E --> F[Empty blog or 404]
  D -- Yes --> G[Fetch Notra pages]
  G --> H{Fetch succeeds}
  H -- No --> I[Unhandled error]
  I --> J[500 response]
  H -- Yes --> K[Map posts and render]
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(docs): server-render the blog so ne..." | Re-trigger Greptile

// the client bundle; the API key stays server-side.
async function fetchBlogData(): Promise<{ posts: BlogPost[]; bodies: Record<string, string> }> {
const apiKey = process.env.NOTRA_API_KEY?.trim();
if (!apiKey) return { posts: [], bodies: {} };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing Key Empties Blog

When NOTRA_API_KEY is absent in the SSR runtime, this branch returns an empty dataset. Since the blog and OG routes are no longer prerendered, existing posts then disappear from /blog, /blog/$slug returns 404, and OG images return 404 instead of serving the generated snapshot that the old build-time path preserved.

Fix in Claude Code

Comment on lines +29 to +35
for (;;) {
const response = await notra.content.listPosts({
status: "published",
sort: "desc",
limit: 100,
page,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Notra Failure Returns 500

This request-time Notra call is not caught. A transient Notra outage, auth failure, timeout, or SDK error will throw through the server function and make /blog, /blog/$slug, and /og/blog/*.svg fail at request time, while the old build-time fetch path caught failures and kept serving the last generated content.

Fix in Claude Code

Comment on lines +29 to +34
for (;;) {
const response = await notra.content.listPosts({
status: "published",
sort: "desc",
limit: 100,
page,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Future Posts Become Visible

The new list route asks Notra only for status: "published" and then returns every mapped post, without the old getPublishedBlogPosts() date check. If Notra contains a published post with a future publishedAt, /blog can show it before its scheduled date, even though the previous index route filtered future-dated posts out.

Fix in Claude Code

Comment on lines +66 to +67
const { posts, bodies } = await fetchBlogData();
const post = posts.find((item) => item.slug === slug);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Slug Lookup Fetches Everything

Each single-post request calls fetchBlogData(), which paginates through all published posts before doing posts.find(...). A cache miss for several slugs or OG images can multiply external Notra calls by the full page count and make single-post rendering slow or rate-limited as the blog grows.

Fix in Claude Code

Comment thread vercel.json
@@ -1,5 +1,5 @@
{
"outputDirectory": "apps/fumadocs/.vercel/output/static",
"outputDirectory": "apps/fumadocs/.vercel/output",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Output Root May Bypass Functions

This changes outputDirectory from the static assets folder to the Build Output root. If Vercel treats this setting as the static output directory instead of recognizing the Build Output API structure, the generated functions under .vercel/output can be ignored and the newly non-prerendered /blog and /og/blog/* routes will 404 or expose raw output paths.

Fix in Claude Code

This branch was successfully deployed

1 active deployment
Preview — bba14e18 Deployed Jun 28, 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