Files
Webserver/app/blog/[slug]/page.tsx
Akshay Kolli 382edaab75
All checks were successful
Deploy Website / build-and-deploy (push) Successful in 2m15s
updated look
2026-04-11 23:27:29 -04:00

150 lines
7.0 KiB
TypeScript

import { getPostBySlug, getPostSlugs } from '@/lib/mdx';
import { MDXRemote } from 'next-mdx-remote/rsc';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { format } from 'date-fns';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import { TableOfContents } from '@/components/mdx/TableOfContents';
import { SideNote } from '@/components/mdx/SideNote';
import { Citation, Bibliography } from '@/components/mdx/Citation';
import { MobileTableOfContents } from '@/components/mdx/MobileTableOfContents';
const slugify = (text: React.ReactNode): string => {
if (!text) return '';
const str = typeof text === 'string' ? text : String(text);
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
};
const components = {
h1: (props: React.ComponentPropsWithoutRef<'h1'>) => <h1 {...props} id={slugify(props.children)} className="mt-12 scroll-mt-24 font-sans text-[2rem] font-medium leading-tight tracking-[-0.05em] text-ink sm:text-[2.5rem]" />,
h2: (props: React.ComponentPropsWithoutRef<'h2'>) => <h2 {...props} id={slugify(props.children)} className="mt-14 scroll-mt-24 font-sans text-[1.65rem] font-medium leading-tight tracking-[-0.05em] text-ink sm:text-[2rem]" />,
h3: (props: React.ComponentPropsWithoutRef<'h3'>) => <h3 {...props} id={slugify(props.children)} className="mt-10 scroll-mt-24 font-sans text-[1.22rem] font-medium tracking-[-0.03em] text-ink sm:text-[1.42rem]" />,
p: (props: React.ComponentPropsWithoutRef<'p'>) => <p {...props} className="mb-6 text-[1.02rem] leading-[1.85] text-ink-soft" />,
ul: (props: React.ComponentPropsWithoutRef<'ul'>) => <ul {...props} className="mb-6 list-disc space-y-2 pl-5 text-[0.98rem] leading-8 text-ink-soft marker:text-accent" />,
ol: (props: React.ComponentPropsWithoutRef<'ol'>) => <ol {...props} className="mb-6 list-decimal space-y-2 pl-5 text-[0.98rem] leading-8 text-ink-soft marker:text-accent" />,
blockquote: (props: React.ComponentPropsWithoutRef<'blockquote'>) => <blockquote {...props} className="my-10 border-l border-line-strong pl-5 text-[1.05rem] italic leading-8 text-ink-soft" />,
code: (props: React.ComponentPropsWithoutRef<'code'>) => <code {...props} className="rounded bg-paper-strong px-1.5 py-0.5 font-mono text-[0.9em] text-ink" />,
pre: (props: React.ComponentPropsWithoutRef<'pre'>) => <pre {...props} className="my-8 overflow-x-auto rounded-[1rem] border border-line bg-[#161412] p-5 text-sm text-paper shadow-[0_16px_40px_rgba(17,16,15,0.12)]" />,
a: (props: React.ComponentPropsWithoutRef<'a'>) => <a {...props} className="font-medium text-ink underline decoration-line-strong underline-offset-4 transition-colors hover:text-accent hover:decoration-accent" />,
SideNote,
Citation,
Bibliography,
};
type Props = {
params: Promise<{ slug: string }>;
};
export function generateStaticParams() {
return getPostSlugs()
.filter((slug) => slug.endsWith('.mdx'))
.map((slug) => ({ slug: slug.replace(/\.mdx$/, '') }));
}
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
try {
const { metadata } = getPostBySlug(slug);
return {
title: metadata.title,
description: metadata.description,
openGraph: {
title: metadata.title,
description: metadata.description,
type: 'article' as const,
publishedTime: metadata.date,
},
twitter: {
card: 'summary' as const,
title: metadata.title,
description: metadata.description,
},
};
} catch {
return {
title: 'Post Not Found',
};
}
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
let post;
try {
post = getPostBySlug(slug);
} catch {
notFound();
}
const headingLines = post.content.match(/^#{2,3}\s+(.+)$/gm) || [];
const headings = headingLines.map((line) => {
const level = line.match(/^#+/)?.[0].length || 2;
const text = line.replace(/^#+\s+/, '').trim();
const id = slugify(text);
return { id, text, level };
});
const tags = Array.isArray(post.metadata.tags) ? post.metadata.tags : [];
return (
<div className="page-frame py-20 sm:py-24">
<div className="mx-auto max-w-[78rem] xl:grid xl:grid-cols-[11rem_minmax(0,44rem)] xl:gap-x-10">
<aside className="hidden xl:block">
<div className="sticky top-24">
<TableOfContents headings={headings} />
</div>
</aside>
<article className="w-full">
<header className="mb-8 space-y-4 border-b border-line pb-8">
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-[0.72rem] font-mono uppercase tracking-[0.14em] text-muted-strong">
<time dateTime={post.metadata.date}>
{format(new Date(post.metadata.date), 'MMMM d, yyyy')}
</time>
{tags.map((tag: string) => (
<span key={tag}>{tag}</span>
))}
</div>
<h1 className="max-w-[38rem] text-balance font-sans text-[clamp(2.7rem,5.8vw,4.4rem)] font-medium leading-[0.94] tracking-[-0.08em] text-ink">
{post.metadata.title}
</h1>
<p className="max-w-[34rem] text-[1.04rem] leading-8 text-muted">
{post.metadata.description}
</p>
</header>
<MobileTableOfContents headings={headings} />
<div className="essay-prose relative mt-8 max-w-none xl:max-w-[44rem]">
<MDXRemote
source={post.content}
components={components}
options={{
mdxOptions: {
rehypePlugins: [
[rehypeAutolinkHeadings, { behavior: 'wrap' }]
]
}
}}
/>
</div>
<div className="mt-14 flex items-center justify-between border-t border-line pt-6 text-sm text-muted">
<Link href="/blog" className="transition-colors hover:text-ink">
&larr; Back to all posts
</Link>
<a href="#" className="transition-colors hover:text-ink">
Scroll to top &uarr;
</a>
</div>
</article>
</div>
</div>
);
}