Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
700 changes: 697 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
Binary file added inspiration/blogs/alistapart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/creativebloq.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/csstricks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/devto.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/dzone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/figma.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/freecodecamp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/github.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/ishadeed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/joshwcomeau.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/alistapart.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/creativebloq.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/csstricks.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/devto.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/dzone.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/figma.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/freecodecamp.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/github.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/ishadeed.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/joshwcomeau.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/medium.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/nngroup.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/overreacted.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/smashingmagazine.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/stripe.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added inspiration/blogs/jpg/taniarascia.jpg
Binary file added inspiration/blogs/jpg/uxdesign.jpg
Binary file added inspiration/blogs/jpg/vercel.jpg
Binary file added inspiration/blogs/jpg/webdev.jpg
Binary file added inspiration/blogs/medium.png
Binary file added inspiration/blogs/nngroup.png
Binary file added inspiration/blogs/overreacted.png
Binary file added inspiration/blogs/smashingmagazine.png
Binary file added inspiration/blogs/stripe.png
Binary file added inspiration/blogs/taniarascia.png
Binary file added inspiration/blogs/uxdesign.png
Binary file added inspiration/blogs/vercel.png
Binary file added inspiration/blogs/webdev.png
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,31 @@
},
"dependencies": {
"@14islands/r3f-scroll-rig": "^8.15.0",
"@base-ui/react": "^1.1.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.1.6",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@t3-oss/env-nextjs": "^0.13.10",
"@tailwindcss/typography": "^0.5.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"feed": "^4.2.2",
"lucide-react": "^0.563.0",
"mermaid": "^11.5.0",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"shadcn": "^3.8.2",
"shiki": "^1.29.1",
"tailwind-merge": "^3.4.0",
"three": "^0.182.0",
"unist-util-visit": "^5.0.0",
"zod": "^4.3.6"
Expand All @@ -47,6 +54,7 @@
"babel-plugin-react-compiler": "1.0.0",
"lefthook": "^1.13.0",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "5.9.3"
}
}
107 changes: 92 additions & 15 deletions src/app/(blog)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import type { ComponentType } from "react";

import AuthorCard from "@/components/blog/AuthorCard";
import PostCard from "@/components/blog/PostCard";
import MDXImage from "@/components/mdx/MDXImage";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import {
findMetadataImageAsset,
getBlogPost,
Expand All @@ -20,6 +24,29 @@ type BlogPostModule = {
metadata: unknown;
};

const getSuggestedPosts = (
posts: Awaited<ReturnType<typeof getBlogPosts>>,
currentSlug: string,
) => {
const current = posts.find((post) => post.slug === currentSlug);
if (!current) return [];
const currentTags = new Set(current.metadata.tags ?? []);

return posts
.filter((post) => post.slug !== currentSlug)
.map((post) => {
const tagMatches =
post.metadata.tags?.filter((tag) => currentTags.has(tag)) ?? [];
return { post, score: tagMatches.length };
})
.sort(
(a, b) =>
b.score - a.score || b.post.date.getTime() - a.post.date.getTime(),
)
.slice(0, 3)
.map(({ post }) => post);
};

export async function generateStaticParams() {
const posts = await getBlogPosts();
return posts.map(({ slug }) => ({ slug }));
Expand Down Expand Up @@ -90,6 +117,7 @@ export default async function BlogPostPage({ params }: { params: PageParams }) {

const Content = module.default;
const url = absoluteUrl(`/blog/${slug}`);
const suggestedPosts = getSuggestedPosts(await getBlogPosts(), slug);

const ogAsset = await findMetadataImageAsset(importPath, "opengraph");
const resolvedImageUrl = ogAsset
Expand Down Expand Up @@ -119,26 +147,75 @@ export default async function BlogPostPage({ params }: { params: PageParams }) {
} satisfies MDXComponents;

return (
<article className="mx-auto flex max-w-2xl flex-col gap-6 px-6 py-16">
<article className="relative">
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD is required for SEO.
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<header className="flex flex-col gap-3">
<h1 className="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
{metadata.title}
</h1>
<p className="text-sm text-zinc-600 dark:text-zinc-400">
{metadata.summary}
</p>
<p className="text-xs text-zinc-500 dark:text-zinc-500">
<time dateTime={date.toISOString()}>{formatDate(date)}</time> ·{" "}
{metadata.author} · {readingTime}
</p>
</header>
<div className="prose prose-zinc dark:prose-invert">
<Content components={mdxComponents} />
<div className="mx-auto flex w-full max-w-6xl flex-col gap-12 px-6 py-16">
<header className="flex flex-col gap-6">
{metadata.tags?.length ? (
<div className="flex flex-wrap gap-2">
{metadata.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
) : null}
<div className="space-y-4">
<h1 className="text-balance text-4xl font-semibold tracking-tight md:text-5xl">
{metadata.title}
</h1>
<p className="max-w-3xl text-base text-muted-foreground md:text-lg">
{metadata.summary}
</p>
</div>
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
<span>
<time dateTime={date.toISOString()}>{formatDate(date)}</time>
</span>
<Separator orientation="vertical" className="h-4" />
<span>{metadata.author}</span>
<Separator orientation="vertical" className="h-4" />
<span>{readingTime}</span>
<Separator orientation="vertical" className="h-4" />
<span>{post.wordCount.toLocaleString()} words</span>
{updatedAt ? (
<>
<Separator orientation="vertical" className="h-4" />
<span>Updated {formatDate(updatedAt)}</span>
</>
) : null}
</div>
</header>
<div className="grid gap-12 lg:grid-cols-[minmax(0,1fr)_280px]">
<div className="prose prose-neutral max-w-[70ch] dark:prose-invert prose-headings:scroll-mt-24 prose-h2:text-2xl prose-h3:text-xl prose-lead:text-muted-foreground prose-a:text-foreground/90 prose-strong:text-foreground prose-p:leading-relaxed prose-li:marker:text-muted-foreground/70">
<Content components={mdxComponents} />
</div>
<aside className="space-y-6 lg:sticky lg:top-24">
<AuthorCard />
<div className="rounded-xl border border-border/70 bg-muted/40 p-5 text-sm text-muted-foreground">
Want more like this? New posts land weekly with experiments and
craft notes.
</div>
</aside>
</div>
{suggestedPosts.length > 0 ? (
<section className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold tracking-tight">
Suggested next posts
</h2>
</div>
<div className="grid gap-6 md:grid-cols-3">
{suggestedPosts.map((suggested) => (
<PostCard key={suggested.slug} post={suggested} />
))}
</div>
</section>
) : null}
</div>
</article>
);
Expand Down
87 changes: 55 additions & 32 deletions src/app/(blog)/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import Link from "next/link";

import PostCard from "@/components/blog/PostCard";
import { Separator } from "@/components/ui/separator";
import { getBlogPosts } from "@/lib/blog-utils";
import { formatDate } from "@/lib/date";
import { siteConfig } from "@/lib/site";

export const metadata: Metadata = {
Expand All @@ -12,42 +12,65 @@ export const metadata: Metadata = {

export default async function BlogIndexPage() {
const posts = await getBlogPosts();
const [featured, ...rest] = posts;

return (
<main className="mx-auto flex max-w-2xl flex-col gap-6 px-6 py-16">
<div className="flex flex-col gap-2">
<h1 className="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">
Blog
</h1>
<p className="text-sm text-zinc-600 dark:text-zinc-400">
Notes on what I am building and learning.
</p>
</div>
<ul className="flex flex-col gap-6">
{posts.length > 0 ? (
posts.map(({ slug, metadata, date, readingTime }) => (
<li key={slug} className="flex flex-col gap-1">
<Link
href={`/blog/${slug}`}
className="text-lg font-medium text-blue-600 hover:underline dark:text-blue-400"
>
{metadata.title}
</Link>
<p className="text-sm text-zinc-600 dark:text-zinc-400">
{metadata.summary}
<main className="relative">
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top,rgba(120,120,120,0.12),transparent_55%)]" />
<section className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-6 py-16">
<div className="grid gap-8 lg:grid-cols-[1.1fr_0.9fr] lg:items-end">
<div className="space-y-6">
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.25em] text-muted-foreground">
Journal
</p>
<span className="text-xs text-zinc-500 dark:text-zinc-500">
<time dateTime={date.toISOString()}>{formatDate(date)}</time> ·{" "}
{metadata.author} · {readingTime}
</span>
</li>
))
<h1 className="text-balance text-4xl font-semibold tracking-tight md:text-5xl">
Writing about craft, experiments, and what I am learning.
</h1>
<p className="max-w-xl text-base text-muted-foreground md:text-lg">
Notes on what {siteConfig.author.name} is building across
product design, engineering, and creative development.
</p>
</div>
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
<span>{posts.length} posts</span>
<Separator orientation="vertical" className="h-4" />
<span>New notes as they land</span>
<Separator orientation="vertical" className="h-4" />
<span>Deep dives + quick notes</span>
</div>
</div>
{featured ? (
<PostCard post={featured} variant="featured" />
) : (
<div className="rounded-xl border border-dashed border-border/70 p-10 text-sm text-muted-foreground">
First post coming soon.
</div>
)}
</div>
</section>
<section className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-6 pb-20">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold tracking-tight">
Latest writing
</h2>
</div>
{rest.length > 0 ? (
<div className="grid gap-6 md:grid-cols-2">
{rest.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</div>
) : posts.length > 0 ? (
<p className="text-sm text-muted-foreground">
More posts are on the way. In the meantime, check back soon.
</p>
) : (
<li className="text-sm text-zinc-500 dark:text-zinc-400">
<p className="text-sm text-muted-foreground">
No posts yet. Check back soon.
</li>
</p>
)}
</ul>
</section>
</main>
);
}
39 changes: 39 additions & 0 deletions src/app/(blog)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from "next/link";

import ThemeToggle from "@/components/ThemeToggle";

export default function BlogLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const navLinkClassName =
"inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium text-muted-foreground transition hover:bg-muted/60 hover:text-foreground";

return (
<div className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-40 border-b border-border/60 bg-background/80 backdrop-blur">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
<Link href="/blog" className="group">
<div className="text-xs font-semibold uppercase tracking-[0.3em] text-muted-foreground/80">
Journal
</div>
<div className="text-lg font-semibold tracking-tight transition group-hover:text-foreground/80">
F0RR0
</div>
</Link>
<div className="flex items-center gap-2">
<Link href="/rss.xml" className={navLinkClassName}>
RSS
</Link>
<Link href="/" className={navLinkClassName}>
Portfolio
</Link>
<ThemeToggle />
</div>
</div>
</header>
{children}
</div>
);
}
Loading