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
Craft cards support autoplaying video previews with blur placeholders for instant loading. Here's how to add a video for a new craft piece.
- ffmpeg installed (
brew install ffmpeg) - Screen Studio or any screen recorder
- Source video exported as MP4 (H.264), 1080p, 30fps
Record your interaction as a short loop (5–10 seconds). Export from Screen Studio as MP4, HD resolution, 30fps, high compression quality. The source file is just a starting point — ffmpeg handles the real optimization.
From the project root, replace {slug} with your craft piece's slug:
# Create output directory
mkdir -p public/video/{slug}
# Web-optimized MP4 (~500KB–1.5MB for a 5-10s clip)
ffmpeg -i /path/to/source.mp4 \
-c:v libx264 -crf 28 -preset slow \
-vf "scale=1280:-2" \
-an -movflags +faststart -pix_fmt yuv420p \
public/video/{slug}/preview.mp4
# WebM version (30-40% smaller, served to Chrome/Firefox/Edge)
ffmpeg -i /path/to/source.mp4 \
-c:v libvpx-vp9 -crf 35 -b:v 0 \
-vf "scale=1280:-2" \
-an \
public/video/{slug}/preview.webm
# Blur placeholder (base64 string copied to clipboard)
ffmpeg -i /path/to/source.mp4 \
-vframes 1 -vf "scale=40:-1" -q:v 5 \
-update 1 \
/tmp/poster.jpg
base64 -i /tmp/poster.jpg | tr -d '\n' | pbcopyVerify sizes with ls -lh public/video/{slug}/. Target: MP4 under 1.5MB, WebM under 1MB. If larger, increase CRF (e.g., 30 for MP4, 38 for WebM).
In content/craft/{slug}/index.mdx, add the video and poster fields:
---
title: "Your Craft Title"
date: 2025-03-01
description: "What this craft piece demonstrates"
video: "/video/{slug}/preview.mp4"
poster: "data:image/jpeg;base64,<paste from clipboard>"
tags: ["React", "CSS"]
published: true
---The card components automatically handle the rest: the blur placeholder shows instantly while the video loads, then the video plays over it. The browser picks WebM over MP4 when supported.
| 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 |
public/
└── video/
└── {slug}/
├── preview.mp4 # H.264 fallback (Safari, older browsers)
└── preview.webm # VP9 primary (Chrome, Firefox, Edge)
Do not commit source recordings to public/video/ — only the processed preview.mp4 and preview.webm files.
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