updated look
All checks were successful
Deploy Website / build-and-deploy (push) Successful in 2m15s
All checks were successful
Deploy Website / build-and-deploy (push) Successful in 2m15s
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
const lineWidths = ['78%', '92%', '85%', '88%', '74%', '96%', '82%', '69%'];
|
||||
|
||||
export default function BlogPostLoading() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-24 animate-pulse">
|
||||
<div className="space-y-4 mb-12">
|
||||
<div className="h-4 bg-zinc-200 dark:bg-zinc-800 rounded w-1/4" />
|
||||
<div className="h-12 bg-zinc-200 dark:bg-zinc-800 rounded w-3/4" />
|
||||
<div className="h-6 bg-zinc-200 dark:bg-zinc-800 rounded w-2/3" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="h-5 bg-zinc-200 dark:bg-zinc-800 rounded" style={{ width: `${70 + Math.random() * 30}%` }} />
|
||||
))}
|
||||
<div className="page-frame py-20 sm:py-24">
|
||||
<div className="mx-auto max-w-[70rem] animate-pulse xl:grid xl:grid-cols-[11rem_minmax(0,44rem)] xl:gap-x-10">
|
||||
<div className="hidden xl:block" />
|
||||
<div>
|
||||
<div className="mb-8 space-y-4 border-b border-line pb-8">
|
||||
<div className="h-3 w-1/3 rounded-full bg-accent-soft" />
|
||||
<div className="h-14 w-3/4 rounded-[1rem] bg-paper-strong" />
|
||||
<div className="h-5 w-2/3 rounded-full bg-paper-strong" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{lineWidths.map((width) => (
|
||||
<div key={width} className="h-4 rounded-full bg-paper-strong" style={{ width }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,12 @@ import { MDXRemote } from 'next-mdx-remote/rsc';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
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';
|
||||
|
||||
// Utility to ensure consistent IDs
|
||||
const slugify = (text: React.ReactNode): string => {
|
||||
if (!text) return '';
|
||||
const str = typeof text === 'string' ? text : String(text);
|
||||
@@ -21,16 +19,16 @@ const slugify = (text: React.ReactNode): string => {
|
||||
};
|
||||
|
||||
const components = {
|
||||
h1: (props: React.ComponentPropsWithoutRef<'h1'>) => <h1 {...props} id={slugify(props.children)} className="text-3xl font-bold mt-8 mb-4 text-zinc-900 dark:text-zinc-50 scroll-mt-24" />,
|
||||
h2: (props: React.ComponentPropsWithoutRef<'h2'>) => <h2 {...props} id={slugify(props.children)} className="text-2xl font-bold mt-8 mb-4 text-zinc-900 dark:text-zinc-50 scroll-mt-24" />,
|
||||
h3: (props: React.ComponentPropsWithoutRef<'h3'>) => <h3 {...props} id={slugify(props.children)} className="text-xl font-bold mt-6 mb-3 text-zinc-900 dark:text-zinc-50 scroll-mt-24" />,
|
||||
p: (props: React.ComponentPropsWithoutRef<'p'>) => <p {...props} className="mb-4 leading-relaxed text-zinc-700 dark:text-zinc-300" />,
|
||||
ul: (props: React.ComponentPropsWithoutRef<'ul'>) => <ul {...props} className="list-disc pl-5 mb-4 space-y-2 text-zinc-700 dark:text-zinc-300" />,
|
||||
ol: (props: React.ComponentPropsWithoutRef<'ol'>) => <ol {...props} className="list-decimal pl-5 mb-4 space-y-2 text-zinc-700 dark:text-zinc-300" />,
|
||||
blockquote: (props: React.ComponentPropsWithoutRef<'blockquote'>) => <blockquote {...props} className="border-l-4 border-zinc-200 dark:border-zinc-700 pl-4 italic my-6 text-zinc-600 dark:text-zinc-400" />,
|
||||
code: (props: React.ComponentPropsWithoutRef<'code'>) => <code {...props} className="bg-zinc-100 dark:bg-zinc-800 text-pink-600 dark:text-pink-400 px-1 py-0.5 rounded text-sm font-mono" />,
|
||||
pre: (props: React.ComponentPropsWithoutRef<'pre'>) => <pre {...props} className="bg-zinc-900 dark:bg-zinc-900 text-zinc-100 p-4 rounded-lg overflow-x-auto my-6 text-sm font-mono" />,
|
||||
a: (props: React.ComponentPropsWithoutRef<'a'>) => <a {...props} className="text-zinc-900 dark:text-zinc-100 underline decoration-zinc-300 dark:decoration-zinc-700 hover:decoration-zinc-900 dark:hover:decoration-zinc-100 transition-all font-medium" />,
|
||||
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,
|
||||
@@ -82,7 +80,6 @@ export default async function BlogPost({ params }: Props) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Extract headings for TOC
|
||||
const headingLines = post.content.match(/^#{2,3}\s+(.+)$/gm) || [];
|
||||
const headings = headingLines.map((line) => {
|
||||
const level = line.match(/^#+/)?.[0].length || 2;
|
||||
@@ -90,47 +87,40 @@ export default async function BlogPost({ params }: Props) {
|
||||
const id = slugify(text);
|
||||
return { id, text, level };
|
||||
});
|
||||
const tags = Array.isArray(post.metadata.tags) ? post.metadata.tags : [];
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-6 py-24 animate-fade-in relative">
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[250px_1fr] gap-12">
|
||||
{/* Left Column: TOC */}
|
||||
<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-32">
|
||||
<div className="sticky top-24">
|
||||
<TableOfContents headings={headings} />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right Column: Content */}
|
||||
<article className="max-w-3xl mx-auto xl:mx-0 w-full">
|
||||
|
||||
{/* Mobile TOC */}
|
||||
<MobileTableOfContents headings={headings} />
|
||||
|
||||
<header className="mb-12 text-center sm:text-left">
|
||||
<div className="flex items-center gap-3 text-sm text-zinc-400 dark:text-zinc-500 mb-4 justify-center sm:justify-start">
|
||||
<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>
|
||||
<span className="w-1 h-1 rounded-full bg-zinc-300 dark:bg-zinc-700" />
|
||||
<div className="flex gap-2">
|
||||
{Array.isArray(post.metadata.tags) && post.metadata.tags.map((tag: string) => (
|
||||
<span key={tag} className="text-xs uppercase tracking-wider">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
{tags.map((tag: string) => (
|
||||
<span key={tag}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-50 mb-6 leading-tight">
|
||||
<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="text-xl text-zinc-500 dark:text-zinc-400 font-light leading-relaxed">
|
||||
<p className="max-w-[34rem] text-[1.04rem] leading-8 text-muted">
|
||||
{post.metadata.description}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="prose prose-zinc dark:prose-invert prose-lg max-w-none relative">
|
||||
<MobileTableOfContents headings={headings} />
|
||||
|
||||
<div className="essay-prose relative mt-8 max-w-none xl:max-w-[44rem]">
|
||||
<MDXRemote
|
||||
source={post.content}
|
||||
components={components}
|
||||
@@ -144,11 +134,11 @@ export default async function BlogPost({ params }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 pt-8 border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center text-sm text-zinc-500">
|
||||
<Link href="/blog" className="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">
|
||||
<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">
|
||||
← Back to all posts
|
||||
</Link>
|
||||
<a href="#" className="hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors">
|
||||
<a href="#" className="transition-colors hover:text-ink">
|
||||
Scroll to top ↑
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
export default function BlogLoading() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-24 space-y-12 animate-pulse">
|
||||
<div className="space-y-4">
|
||||
<div className="h-10 bg-zinc-200 dark:bg-zinc-800 rounded w-1/3" />
|
||||
<div className="h-5 bg-zinc-200 dark:bg-zinc-800 rounded w-2/3" />
|
||||
</div>
|
||||
<div className="grid gap-10">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="space-y-3">
|
||||
<div className="h-4 bg-zinc-200 dark:bg-zinc-800 rounded w-1/4" />
|
||||
<div className="h-7 bg-zinc-200 dark:bg-zinc-800 rounded w-3/4" />
|
||||
<div className="h-5 bg-zinc-200 dark:bg-zinc-800 rounded w-full" />
|
||||
<div className="h-5 bg-zinc-200 dark:bg-zinc-800 rounded w-5/6" />
|
||||
</div>
|
||||
))}
|
||||
<div className="page-frame py-20 sm:py-24">
|
||||
<div className="mx-auto max-w-[72rem] animate-pulse space-y-10">
|
||||
<div className="space-y-4 border-b border-line pb-10">
|
||||
<div className="h-3 w-20 rounded-full bg-accent-soft" />
|
||||
<div className="h-14 w-2/3 rounded-[1rem] bg-paper-strong" />
|
||||
<div className="h-5 w-1/2 rounded-full bg-paper-strong" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<div key={i} className="grid gap-3 border-b border-line pb-6 md:grid-cols-[8rem_minmax(0,1fr)] md:gap-6">
|
||||
<div className="h-3 w-20 rounded-full bg-accent-soft" />
|
||||
<div className="space-y-3">
|
||||
<div className="h-9 w-3/4 rounded-[0.9rem] bg-paper-strong" />
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-full rounded-full bg-paper-strong" />
|
||||
<div className="h-4 w-4/5 rounded-full bg-paper-strong" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { getAllPosts } from '@/lib/mdx';
|
||||
import { format } from 'date-fns';
|
||||
import { getAllPosts } from '@/lib/mdx';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
@@ -16,46 +16,61 @@ export default function BlogIndex() {
|
||||
const posts = getAllPosts();
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-24 space-y-12 animate-fade-in">
|
||||
<header className="space-y-4 text-center sm:text-left">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-50">Writing</h1>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 font-light">
|
||||
Thoughts on software, design, and minimalism.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-10">
|
||||
{posts.map((post) => (
|
||||
<article key={post.slug} className="group relative flex flex-col space-y-3">
|
||||
<div className="flex items-center gap-3 text-sm text-zinc-400 dark:text-zinc-500">
|
||||
<time dateTime={post.date}>
|
||||
{format(new Date(post.date), 'MMMM d, yyyy')}
|
||||
</time>
|
||||
<span className="w-1 h-1 rounded-full bg-zinc-300 dark:bg-zinc-700" />
|
||||
<div className="flex gap-2">
|
||||
{post.tags?.map(tag => (
|
||||
<span key={tag} className="text-xs uppercase tracking-wider">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href={`/blog/${post.slug}`} className="block">
|
||||
<h2 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-600 dark:group-hover:text-zinc-300 transition-colors">
|
||||
{post.title}
|
||||
</h2>
|
||||
</Link>
|
||||
|
||||
<p className="text-zinc-600 dark:text-zinc-400 font-light leading-relaxed">
|
||||
{post.description}
|
||||
<div className="page-frame py-20 sm:py-24">
|
||||
<div className="mx-auto max-w-[72rem] space-y-10">
|
||||
<header className="space-y-4 border-b border-line pb-10">
|
||||
<div className="space-y-4">
|
||||
<p className="eyebrow">Writing</p>
|
||||
<h1 className="max-w-[40rem] text-balance font-sans text-[clamp(3rem,6vw,5rem)] font-medium leading-[0.94] tracking-[-0.08em] text-ink">
|
||||
Notes on software, deep learning, and research.
|
||||
</h1>
|
||||
<p className="max-w-[34rem] text-[1rem] leading-8 text-muted">
|
||||
A small archive of ideas, experiments, and things worth slowing down enough to explain.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="pt-2">
|
||||
<Link href={`/blog/${post.slug}`} className="text-sm font-medium text-zinc-900 dark:text-zinc-100 underline decoration-zinc-300 dark:decoration-zinc-700 underline-offset-4 hover:decoration-zinc-900 dark:hover:decoration-zinc-100 transition-all">
|
||||
Read more
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<div className="divide-y divide-line">
|
||||
{posts.map((post) => (
|
||||
<article key={post.slug} className="grid gap-3 py-6 md:grid-cols-[8rem_minmax(0,1fr)] md:gap-6">
|
||||
<div className="pt-1">
|
||||
<time dateTime={post.date} className="block font-mono text-[0.72rem] uppercase tracking-[0.18em] text-muted-strong">
|
||||
{format(new Date(post.date), 'MMMM d, yyyy')}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Link href={`/blog/${post.slug}`} className="block">
|
||||
<h2 className="max-w-[38rem] font-sans text-[1.8rem] font-medium leading-tight tracking-[-0.05em] text-ink transition-colors hover:text-accent sm:text-[2.1rem]">
|
||||
{post.title}
|
||||
</h2>
|
||||
</Link>
|
||||
|
||||
<p className="max-w-[34rem] text-[0.98rem] leading-7 text-muted">
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 pt-1 text-[0.76rem] font-mono uppercase tracking-[0.14em] text-muted-strong">
|
||||
{post.tags?.map((tag) => (
|
||||
<span key={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
<Link href={`/blog/${post.slug}`} className="text-ink transition-colors hover:text-accent">
|
||||
Read essay
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="grid gap-4 border-t border-line pt-6 md:grid-cols-[8rem_minmax(0,1fr)]">
|
||||
<p className="eyebrow md:pt-1">Archive</p>
|
||||
<p className="max-w-[34rem] text-[0.96rem] leading-7 text-muted">
|
||||
{posts.length} published {posts.length === 1 ? 'essay' : 'essays'}. The archive stays selective.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user