-
-
Notifications
You must be signed in to change notification settings - Fork 26
feat(docs): server-render the blog (no rebuild needed for new posts) #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { createServerFn } from "@tanstack/react-start"; | ||
|
|
||
| import type { BlogPost } from "./blog-types"; | ||
| import type { NotraPostInput } from "../../scripts/notra-content"; | ||
|
|
||
| export type BlogPostDetail = { | ||
| post: BlogPost; | ||
| html: string; | ||
| }; | ||
|
|
||
| // Server-only: fetch published posts from Notra at request time and map them to | ||
| // the blog shape. The blog routes are not prerendered (see vite.config.ts), so | ||
| // these run inside the Vercel SSR function and pick up new posts without a | ||
| // rebuild. Dynamic imports keep @usenotra/sdk and the Markdown renderer out of | ||
| // 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: {} }; | ||
|
|
||
| const [{ Notra }, { mapNotraPost }] = await Promise.all([ | ||
| import("@usenotra/sdk"), | ||
| import("../../scripts/notra-content"), | ||
| ]); | ||
|
|
||
| const notra = new Notra({ bearerAuth: apiKey }); | ||
| const raw: NotraPostInput[] = []; | ||
| let page = 1; | ||
|
|
||
| for (;;) { | ||
| const response = await notra.content.listPosts({ | ||
| status: "published", | ||
| sort: "desc", | ||
| limit: 100, | ||
| page, | ||
|
Comment on lines
+29
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new list route asks Notra only for |
||
| }); | ||
|
Comment on lines
+29
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
| raw.push(...(response.posts as NotraPostInput[])); | ||
|
|
||
| const nextPage = response.pagination?.nextPage; | ||
| if (!nextPage) break; | ||
| page = nextPage; | ||
| } | ||
|
|
||
| const seenSlugs = new Set<string>(); | ||
| const posts: BlogPost[] = []; | ||
| const bodies: Record<string, string> = {}; | ||
|
|
||
| for (const item of raw) { | ||
| const mapped = mapNotraPost(item, seenSlugs); | ||
| if (!mapped) continue; | ||
|
|
||
| posts.push(mapped.post); | ||
| bodies[mapped.post.slug] = mapped.html; | ||
| } | ||
|
|
||
| posts.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)); | ||
| return { posts, bodies }; | ||
| } | ||
|
|
||
| export const getBlogPostsServerFn = createServerFn({ method: "GET" }).handler(async () => { | ||
| return (await fetchBlogData()).posts; | ||
| }); | ||
|
|
||
| export const getBlogPostServerFn = createServerFn({ method: "GET" }) | ||
| .inputValidator((slug: string) => slug) | ||
| .handler(async ({ data: slug }): Promise<BlogPostDetail | null> => { | ||
| const { posts, bodies } = await fetchBlogData(); | ||
| const post = posts.find((item) => item.slug === slug); | ||
|
Comment on lines
+66
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Each single-post request calls |
||
| if (!post) return null; | ||
|
|
||
| return { post, html: bodies[post.slug] ?? "" }; | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "outputDirectory": "apps/fumadocs/.vercel/output/static", | ||
| "outputDirectory": "apps/fumadocs/.vercel/output", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This changes |
||
| "rewrites": [ | ||
| { | ||
| "source": "/", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
NOTRA_API_KEYis 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/$slugreturns 404, and OG images return 404 instead of serving the generated snapshot that the old build-time path preserved.