Personal website and blog built with Next.js 15, featuring MDX-based content and modern design.
This project uses a clean, purpose-driven component organization:
src/
├── components/
│ ├── layout/ # Layout & structure components
│ │ ├── container.tsx # Content width constraint
│ │ ├── section.tsx # Section wrapper with spacing
│ │ └── header.tsx # Site header with navigation
│ ├── brand/ # Branding elements
│ │ └── logo.tsx # Theme-aware logo component
│ ├── content/ # Content-specific components
│ │ ├── post-content.tsx # MDX content wrapper
│ │ ├── post-image.tsx # Post images with aspect ratio
│ │ └── transformer-copy-button.tsx # Code block copy functionality
│ ├── sections/ # Page-specific sections
│ │ └── home/ # Homepage sections
│ │ ├── article-section.tsx
│ │ ├── featured-project-section.tsx
│ │ └── profile-section.tsx
│ ├── theme/ # Theme system
│ │ ├── theme-provider.tsx
│ │ └── theme-switcher.tsx
│ └── ui/ # Reusable UI primitives (shadcn/ui)
│ ├── button.tsx
│ ├── card.tsx
│ ├── avatar.tsx
│ ├── aspect-ratio.tsx
│ └── skeleton.tsx
└── mdx-components.tsx # Global MDX component mapping
public/
└── images/ # Static assets (Next.js requirement)
├── avatar.png # Global assets
├── featured-projects/ # Organized by feature
│ ├── bookmark_landing_page.png
│ ├── room_homepage.png
│ └── [other project images]
└── posts/ # Organized by post
└── hiding-scrollbars-in-tailwind/
├── image_00.jpg
└── image_01.jpg
- Better discoverability - Components grouped by purpose
- Logical structure - Related components together
- Scalable architecture - Easy to add new content types
- Clear separation - Layout, content, and UI components distinct
Static assets must be in the public/ directory due to Next.js requirements:
- Next.js Static Assets: Only files in
public/are served as static assets - Build Process: Next.js optimizes and serves these files at build time
- Hot Reloading: Dev server watches
public/for changes - Deployment: Static assets are deployed to CDN/edge locations
Organization Strategy:
public/images/featured-projects/- Project showcase imagespublic/images/posts/[slug]/- Post-specific imagespublic/images/- Global assets (avatar, icons, etc.)
This project uses Contentlayer2 for type-safe MDX content management:
- Content Directory: All MDX posts are stored in
content/posts/ - Type Safety: Contentlayer generates TypeScript types from your content
- Build Process: Run
pnpm contentlayerto build content or it runs automatically duringpnpm build - Frontmatter: Posts require
title,topic,datefields; optionaldescriptionandpublishedfields - Computed Fields: Automatically generates
slug,url, andreadingTimefor each post
Every craft post follows three phases: Development → Assets → Publishing. This checklist ensures each post ships with the same level of polish.
- Build the interactive demo component in
src/features/craft/playgrounds/ - Create the MDX file at
content/craft/{slug}.mdxwith frontmatter:--- title: "Your Craft Title" date: 2025-03-01 description: "What this craft piece demonstrates" tags: ["React", "CSS"] published: false ---
- Write up the walkthrough content in the MDX body
- Verify on dev server (
pnpm dev) — make sure the demo works and the page renders
Each craft card displays an autoplaying video preview with a blur placeholder that shows instantly while the video loads.
Prerequisites:
- ffmpeg installed (
brew install ffmpeg) - Screen Studio or any screen recorder
Steps:
-
Record a 5–10 second loop of the interaction. Export from Screen Studio as MP4 (H.264, 1080p, 30fps, high quality). The source file is just a starting point — the script handles optimization.
-
Process with the video script:
./scripts/process-craft-video.sh ~/path/to/recording.mp4 {slug}This single command generates:
public/video/{slug}/preview.mp4— H.264 fallback (Safari, older browsers)public/video/{slug}/preview.webm— VP9 primary (Chrome, Firefox, Edge)- Base64 blur poster — copied to clipboard and printed as a frontmatter snippet
-
Update frontmatter — paste the
videoandposterlines the script prints:video: "/video/{slug}/preview.mp4" poster: "data:image/jpeg;base64,<output from script>"
-
Verify sizes — the script prints file sizes. Targets: MP4 under 1.5MB, WebM under 1MB. If larger, you can increase CRF values in the script (e.g., 30 for MP4, 38 for WebM).
-
Check rendering — confirm the blur placeholder appears instantly on the craft card, then the video plays over it.
Note: Do not commit source recordings to
public/video/— only the processedpreview.mp4andpreview.webmfiles.
- Set
published: truein frontmatter - Verify the craft card renders correctly on both the home page and
/craftlist - Deploy
The card components use a 3-tier fallback: video + blur poster → static image → placeholder with first letter. The browser automatically picks WebM over MP4 when supported. The blur poster is a base64-encoded 40px-wide JPEG that renders instantly as a blurred background while the video buffers.
ffmpeg flag reference
| Flag | Purpose |
|---|---|
-crf 28 |
Quality level (lower = better quality, bigger file). 28 is ideal for small card previews |
-preset slow |
Better compression at the cost of longer encode time |
scale=1280:-2 |
Scales to 720p width. -2 ensures even height (required by H.264) |
-an |
Strips audio track (videos are muted anyway) |
-movflags +faststart |
Moves metadata to file start so browser can play before full download |
-pix_fmt yuv420p |
Ensures Safari and hardware decoder compatibility |
-b:v 0 (WebM) |
Lets VP9 use variable bitrate guided purely by CRF |
The src/mdx-components.tsx file must remain at the src/ level due to Next.js conventions:
- Next.js automatically discovers this file for MDX configuration
- Required for App Router MDX integration
- Must be named exactly
mdx-components.tsx/js - Cannot be moved to subdirectories like
src/components/
This file provides global component mapping for all MDX files in the application.
pnpm dev- Start development server with Turbopackpnpm build- Build production bundlepnpm lint- Run ESLintpnpm start- Start production serverpnpm storybook- Start Storybook development server on port 6006pnpm build-storybook- Build static Storybook for deployment
- Framework: Next.js 16 (16.0.10) with App Router
- React: React 19 (19.2.3)
- Language: TypeScript 5.7.2
- Styling: Tailwind CSS 3.4.17
- Content: Contentlayer2 (0.5.8) with MDX
- UI Components: Radix UI primitives + Base UI
- Code Highlighting: rehype-pretty-code with Shiki 1.26.1
- Component Development: Storybook 10.1.10 with Vite 7
- Package Manager: pnpm 9.13.2
Configuration files are organized in the config/ directory:
config/components.json- shadcn/ui configuration
This project uses Storybook 10.1.10 for component development and documentation.
pnpm storybookThis will start the Storybook development server at http://localhost:6006.
To build a static version of Storybook:
pnpm build-storybookThe static files will be generated in the storybook-static/ directory.
Stories are located alongside their components in src/components/ui/. To create a new story:
- Create a file named
[component].stories.tsxnext to your component - Use the CSF (Component Story Format) 3.0 syntax
- Export a default meta object and individual story objects
Example:
import type { Meta, StoryObj } from "@storybook/react";
import { YourComponent } from "./your-component";
const meta = {
title: "UI/YourComponent",
component: YourComponent,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof YourComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
// your component props
},
};.storybook/main.ts- Main Storybook configuration with Vite builder.storybook/preview.tsx- Global decorators, parameters, and theme setup
Note: This project uses @storybook/react-vite with Vite 7 for faster builds and hot module replacement.
-
Create the MDX File:
mkdir -p public/images/posts/[post-slug] touch content/posts/[post-slug].mdx
-
Add Frontmatter and Content:
--- title: "Your Post Title" topic: "Your Topic" date: "2024-01-01" description: "Optional description for SEO" published: true --- Your post content here... <PostImage src="/images/posts/[post-slug]/image.jpg" />
-
Build Content: Run
pnpm contentlayerto generate types and build the content, or it will run automatically duringpnpm build -
Add Images: Place any images in
public/images/posts/[post-slug]/and reference them using/images/posts/[post-slug]/filename.jpg
Note: Posts are automatically discovered by Contentlayer from the content/posts/ directory. No manual listing required!
-
Add Project Images: Place project images in
public/images/featured-projects/ -
Update Featured Projects: Edit
src/components/sections/home/featured-project-section.tsxand add your project to theprojectsarray:{ title: "Your Project Title", description: "Project description...", tags: ["Next.js", "Tailwind", "TypeScript"], image: "/images/featured-projects/your-project-image.png", link: "https://your-project-link.com", }
- Image Formats: Use
.pngfor UI screenshots,.jpgfor photos - Naming Convention: Use lowercase with hyphens (e.g.,
my-project-image.png) - Organization: Group related images in appropriate subdirectories
- Optimization: Consider image optimization for web (WebP, appropriate sizing)
[x] create header elements [x] create code blocks [x] create bulleted lists [x] Upgrade to Next15 [x] Cleaned up the Posts page [x] Remove blog components we no longer need [x] Reorganize component structure [x] Reorganize asset structure for better maintainability [] Add link back to Posts page from the individual post [] create images with captions [] create quote/info sections [] use same icon strategy as Build UI [] post: Ergonomic Interactions example for input with icon https://devouringdetails.com/principles/ergonomic-interactions