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 ? ( +