feat(docs): server-render the blog (no rebuild needed for new posts) - #119
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR moves the blog to runtime SSR backed by Notra. The main changes are:
Confidence Score: 4/5The runtime blog path can fail closed when Notra config or Notra itself is unavailable, and the deployment output setting should be confirmed before merging.
apps/fumadocs/src/lib/notra-runtime.ts and vercel.json Important Files Changed
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]
%%{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]
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: {} }; |
There was a problem hiding this comment.
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.
| for (;;) { | ||
| const response = await notra.content.listPosts({ | ||
| status: "published", | ||
| sort: "desc", | ||
| limit: 100, | ||
| page, | ||
| }); |
There was a problem hiding this comment.
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.
| for (;;) { | ||
| const response = await notra.content.listPosts({ | ||
| status: "published", | ||
| sort: "desc", | ||
| limit: 100, | ||
| page, |
There was a problem hiding this comment.
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.
| const { posts, bodies } = await fetchBlogData(); | ||
| const post = posts.find((item) => item.slug === slug); |
There was a problem hiding this comment.
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.
| @@ -1,5 +1,5 @@ | |||
| { | |||
| "outputDirectory": "apps/fumadocs/.vercel/output/static", | |||
| "outputDirectory": "apps/fumadocs/.vercel/output", | |||
There was a problem hiding this comment.
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.
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
vite.config.tsprerender.filter):/blog,/blog/$slug,/og/blog/*are no longer baked at build; the rest of the site stays static.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 BOAfilesystemhandle.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/_libsonly). Responses are edge-cached (s-maxage=60, stale-while-revalidate=600).$slugheadreadsloaderData.Verified
/blogexcluded from static, docs still static,__serverpresent, no API key or SDK in client outputNOTRA_API_KEYset for Preview so the preview deploy validates the live runtime fetchCreated with PostHog Code