From a1611bf44919a1bcb452b4ff630d32b4b914fc4e Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Mon, 4 May 2026 10:39:42 -0400 Subject: [PATCH] www: render blog posts on server (#45461) Render blog posts on server so they are available in initial HTML response. ## Summary by CodeRabbit * **New Features** * New collapsible sections for blog content * Server-side MDX compilation for blog posts * Improved TOC extraction producing both structured JSON and markdown * **Refactor** * Blog rendering converted to a server-rendered flow with unified MDX components * Tag handling normalized for related-post matching * **Bug Fixes** * Consistent image/self-closing tag normalization and corrected TOC indentation * Errors are now surfaced instead of being swallowed --- apps/www/app/blog/[slug]/page.tsx | 27 +- apps/www/components/Blog/BlogCollapsible.tsx | 35 ++ apps/www/components/Blog/BlogPostRenderer.tsx | 289 ++++++++++++++- .../Blog/BlogPostRendererClient.tsx | 333 ------------------ .../www/components/LaunchWeek/7/BlogLinks.tsx | 4 +- apps/www/lib/mdx/addSelfClosingTags.ts | 16 + apps/www/lib/mdx/compileBlogMdx.ts | 22 ++ apps/www/lib/mdx/extractToc.ts | 75 ++++ apps/www/lib/mdx/mdxComponents.tsx | 110 +++--- apps/www/lib/mdx/mdxOptionsBlog.ts | 81 +++++ apps/www/lib/mdx/mdxSerialize.ts | 3 +- apps/www/package.json | 1 + apps/www/types/post.ts | 6 +- pnpm-lock.yaml | 11 +- 14 files changed, 584 insertions(+), 429 deletions(-) create mode 100644 apps/www/components/Blog/BlogCollapsible.tsx delete mode 100644 apps/www/components/Blog/BlogPostRendererClient.tsx create mode 100644 apps/www/lib/mdx/addSelfClosingTags.ts create mode 100644 apps/www/lib/mdx/compileBlogMdx.ts create mode 100644 apps/www/lib/mdx/extractToc.ts create mode 100644 apps/www/lib/mdx/mdxOptionsBlog.ts diff --git a/apps/www/app/blog/[slug]/page.tsx b/apps/www/app/blog/[slug]/page.tsx index 4af56dd149f..62d7ceb9ec3 100644 --- a/apps/www/app/blog/[slug]/page.tsx +++ b/apps/www/app/blog/[slug]/page.tsx @@ -105,14 +105,17 @@ export default async function BlogPostPage({ params }: { params: Promise const { isEnabled: isDraft } = await draftMode() const matter = (await import('gray-matter')).default - const { mdxSerialize } = await import('lib/mdx/mdxSerialize') try { const postContent = await getPostdata(slug, '_blog') const parsedContent = matter(postContent) as unknown as MatterReturn const content = parsedContent.content const tocDepth = (parsedContent.data as any)?.toc_depth ?? 3 - const mdxSource = await mdxSerialize(content, { tocDepth }) + const { preprocessMdxWithCodeTabs } = await import('~/components/CodeTabs') + const { addSelfClosingTags } = await import('lib/mdx/addSelfClosingTags') + const { extractToc } = await import('lib/mdx/extractToc') + const preprocessed = await preprocessMdxWithCodeTabs(addSelfClosingTags(content)) + const tocResult = await extractToc(preprocessed, tocDepth) const { generateReadingTime } = await import('lib/helpers') const blogPost = { ...parsedContent.data, @@ -129,12 +132,15 @@ export default async function BlogPostPage({ params }: { params: Promise const currentIndex = allPosts.findIndex((post) => post.slug === slug) const nextPost = currentIndex === allPosts.length - 1 ? null : allPosts[currentIndex + 1] const prevPost = currentIndex === 0 ? null : allPosts[currentIndex - 1] - const tocResult = (mdxSource as any).scope?.toc || { content: '' } - const processedContent = tocResult.content + const frontmatterTags = (parsedContent.data as { tags?: Array }) + ?.tags + const tagNames = Array.isArray(frontmatterTags) + ? frontmatterTags.map((t) => (typeof t === 'string' ? t : t?.name)).filter(Boolean) + : undefined const relatedPosts = getSortedPosts({ directory: '_blog', limit: 3, - tags: (mdxSource as { scope: { tags?: string[] } }).scope.tags, + tags: tagNames, currentPostSlug: slug, }) as unknown as (BlogData & PostReturnType)[] @@ -145,11 +151,8 @@ export default async function BlogPostPage({ params }: { params: Promise blog: { ...(blogPost as any), slug, - content: mdxSource, - toc: { - ...tocResult, - content: processedContent, - }, + content: preprocessed, + toc: tocResult, } as any, isDraftMode: isDraft, } @@ -177,5 +180,7 @@ export default async function BlogPostPage({ params }: { params: Promise ) - } catch {} + } catch (err) { + throw err + } } diff --git a/apps/www/components/Blog/BlogCollapsible.tsx b/apps/www/components/Blog/BlogCollapsible.tsx new file mode 100644 index 00000000000..95d0497be25 --- /dev/null +++ b/apps/www/components/Blog/BlogCollapsible.tsx @@ -0,0 +1,35 @@ +'use client' + +import { Triangle } from 'lucide-react' +import type { PropsWithChildren } from 'react' +import { Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_ } from 'ui' + +type BlogCollapsibleProps = PropsWithChildren<{ + title: string + containerClassName?: string +}> + +const BlogCollapsible = ({ title, containerClassName, ...props }: BlogCollapsibleProps) => { + return ( + + + + {title} + + + + ) +} + +export default BlogCollapsible diff --git a/apps/www/components/Blog/BlogPostRenderer.tsx b/apps/www/components/Blog/BlogPostRenderer.tsx index c33415b34fb..a1f07e6e222 100644 --- a/apps/www/components/Blog/BlogPostRenderer.tsx +++ b/apps/www/components/Blog/BlogPostRenderer.tsx @@ -1,9 +1,288 @@ -'use client' +import dayjs from 'dayjs' +import { ChevronLeft } from 'lucide-react' +import Image from 'next/image' +import Link from 'next/link' +import ReactMarkdown from 'react-markdown' +import type { PostReturnType, ProcessedBlogData, StaticAuthor, Tag } from 'types/post' +import { Badge } from 'ui' -import dynamic from 'next/dynamic' +import DraftModeBanner from '@/components/Blog/DraftModeBanner' +import ShareArticleActions from '@/components/Blog/ShareArticleActions' +import CTABanner from '@/components/CTABanner' +import BlogLinks from '@/components/LaunchWeek/7/BlogLinks' +import LW11Summary from '@/components/LaunchWeek/11/LW11Summary' +import LW12Summary from '@/components/LaunchWeek/12/LWSummary' +import LW13Summary from '@/components/LaunchWeek/13/Releases/LWSummary' +import LW14Summary from '@/components/LaunchWeek/14/Releases/LWSummary' +import LW15Summary from '@/components/LaunchWeek/15/LWSummary' +import LWXSummary from '@/components/LaunchWeek/X/LWXSummary' +import DefaultLayout from '@/components/Layouts/Default' +import { BLOG_POST_HERO_IMAGE_SIZES, getBlogThumbnailImage } from '@/lib/blog-images' +import { compileBlogMdx } from '@/lib/mdx/compileBlogMdx' +import mdxComponents from '@/lib/mdx/mdxComponents' -const BlogPostRenderer = dynamic(() => import('./BlogPostRendererClient'), { - ssr: false, -}) +type NextCardProps = { + post: { path: string; title: string; formattedDate: string } + label: string + className?: string +} + +const NextCard = (props: NextCardProps) => { + const { post, label, className } = props + + return ( + +
+
+
+
+

{label}

+
+
+ {'title' in post && ( +

+ {(post as { title?: string }).title} +

+ )} + {'formattedDate' in post && ( +

{(post as { formattedDate?: string }).formattedDate}

+ )} +
+
+
+
+ + ) +} + +const BlogPostRenderer = async ({ + blog, + blogMetaData, + isDraftMode, + prevPost, + nextPost, + authors, +}: { + blog: ProcessedBlogData + blogMetaData: ProcessedBlogData + isDraftMode: boolean + prevPost?: PostReturnType | null + nextPost?: PostReturnType | null + authors: Array +}) => { + const mdxRendered = await compileBlogMdx(blog.content as string, mdxComponents('blog')) + + const isLaunchWeek7 = blogMetaData.launchweek === '7' + const isLaunchWeekX = blogMetaData.launchweek?.toString().toLocaleLowerCase() === 'x' + const isGAWeek = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '11' + const isLaunchWeek12 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '12' + const isLaunchWeek13 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '13' + const isLaunchWeek14 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '14' + const isLaunchWeek15 = blogMetaData.launchweek?.toString().toLocaleLowerCase() === '15' + + const toc = blogMetaData.toc && ( +
+

On this page

+
+ {blogMetaData.toc && ( + + {typeof blogMetaData.toc === 'string' + ? (blogMetaData.toc as string) + : (blogMetaData.toc as { content: string }).content} + + )} +
+
+ ) + + const imageUrl = getBlogThumbnailImage(blogMetaData, { + fallbackToPlaceholder: false, + }) + + return ( + <> + {isDraftMode && } + +
+
+
+ {/* Back button */} + + + Back + +
+
+ {/* Title and description */} +
+
+ + Blog + +

{blogMetaData.title}

+
+

{dayjs(blogMetaData.date).format('DD MMM YYYY')}

+

·

+

{(blogMetaData as any).readingTime}

+
+ {authors.length > 0 && ( +
+
+ {authors.map((author, i: number) => { + const authorImageUrl = author.author_image_url + + const authorId = + (author as any).author_id || + (author as any).username || + author.author.toLowerCase().replace(/\s+/g, '_') + + return ( +
+ +
+ {authorImageUrl && ( +
+ {`${author.author} +
+ )} +
+ + {author.author} + + + {author.position} + +
+
+ +
+ ) + })} +
+
+ )} +
+
+
+ {/* Content */} +
+
+
+ {blogMetaData.youtubeHero ? ( +