You are a senior TypeScript + Next.js engineer and an expert technical blog writer. This repository is a personal blog built with Next.js 16 (App Router), React 19, Tailwind CSS v4, and strict TypeScript. Blog posts are React TSX components rendered as static pages at build-time for maximum performance.
Follow this playbook precisely. Be bold: prefer single, clean solutions. Remove legacy paths rather than maintaining dual systems.
- Use App Router only. Do not introduce Pages Router APIs (no
getStaticProps,getStaticPaths). - Render posts as fully static. Prefer SSG. Enforce with
export const dynamic = "error"andexport const revalidate = falseon blog routes. - Default to Server Components. Mark Client Components explicitly with
"use client"only when necessary. - TypeScript strictly typed. No
any. Clear, descriptive names; early returns; minimal nesting; no unnecessary try/catch. - No fallback code paths. Ever. No graceful degradation, no try/except recovery wrappers, no "fallback to X if Y fails." If the primary code path doesn't work, fix it — don't add a backup. Fallback code means the original code is broken and doesn't deserve to exist.
- Keep dependencies lean. Prefer built-in Next features and well-supported libs only.
- Next.js: 16.x (App Router)
- React: 19.x
- TypeScript: 5.x (strict)
- Styling: Tailwind CSS v4
- Package manager: pnpm
Key scripts:
pnpm dev # start dev server (ONLY run by humans - Claude should NEVER run this)
pnpm build # production build (SSG)
pnpm start # start production server
pnpm lint # run eslint
pnpm optimize-images # optimize images for web (generates AVIF, WebP, optimized originals)IMPORTANT: Claude is NOT ALLOWED to run pnpm dev. Only humans start the dev server. If you need the dev server running for verification, ask the user: "Please start the dev server."
- Content source:
components/blog-posts/**/*.tsx(React components) - Post metadata:
lib/posts.ts(TypeScript data structure) - Routes:
- Index/list:
app/(site)/blog/page.tsx - Detail:
app/(site)/blog/[slug]/page.tsx - Optional tag index:
app/(site)/tags/[tag]/page.tsx
- Index/list:
- Assets for posts:
public/posts/<slug>/*(cover images, diagrams, etc.) - Enforce static generation for all blog pages:
export const dynamic = "error"export const revalidate = falseexport async function generateStaticParams()reads all slugs fromlib/posts.ts
SEO & discoverability:
app/sitemap.tsandapp/robots.tsgenerateMetadatain route files for per-post OG/SEO- Build-time RSS/Atom feed at
/feed.xmland/feed.atomfrom post metadata
Blog posts are defined as TypeScript objects in lib/posts.ts with the following structure:
interface BlogPost {
title: string; // Human-readable title (required)
slug: string; // kebab-case-slug (required, unique)
description: string; // 120-160 chars for SEO (required)
publishedAt: string; // YYYY-MM-DD or ISO date (required)
updatedAt?: string; // ISO date (optional)
tags?: string[]; // e.g., ["nextjs", "typescript", "performance"]
cover?: string; // Path under public/, e.g., "/posts/<slug>/cover.jpg"
draft?: boolean; // Default false
canonicalUrl?: string; // Optional
ogImage?: string; // Path under public/, e.g., "/posts/<slug>/og.png"
component: React.ComponentType; // The TSX component from components/blog-posts/
}Constraints:
slugmust match the component filename (e.g.,components/blog-posts/<slug>.tsx)- Draft posts are excluded from production builds and feeds
- Reading time is computed; do not store it
- Audience: senior/principal engineers. Prioritize clarity, correctness, and depth.
- Structure each post:
- One-paragraph summary (what/why, who should care)
- Key takeaways (3–5 bullets)
- Body with runnable examples and trade-offs
- Pitfalls and edge cases
- Conclusion with next steps or references
- Style:
- Prefer active voice, short paragraphs, and precise terminology.
- Show the minimal viable example first; iterate into variations.
- Cite sources and standards where relevant (RFCs, docs). Use descriptive link text.
- Use proper JSX/React syntax for code blocks, keep lines < 100 chars.
- Add alt text for every image. Use vector/SVG when possible; fall back to optimized PNG/JPEG.
Example post component structure:
// components/blog-posts/static-rendering-next-16.tsx
export function StaticRenderingNext16Content() {
return (
<div className="prose prose-invert prose-base sm:prose-lg md:prose-xl max-w-none">
<p>TL;DR: Use App Router + static params + React components. Avoid Pages Router APIs.</p>
<h2>Key takeaways</h2>
<ul>
<li>Fully static posts are fast, cacheable, and easy to deploy</li>
<li>generateStaticParams drives SSG for dynamic routes</li>
<li>Pure React components keep the stack simple</li>
</ul>
<h2>Example</h2>
<pre>
<code>{`// app/(site)/blog/[slug]/page.tsx
export const dynamic = "error";
export const revalidate = false;
export async function generateStaticParams() {
return getAllPosts().map(post => ({ slug: post.slug }));
}
export default async function PostPage({ params }) {
const post = getPostBySlug(params.slug);
return <post.component />;
}`}</code>
</pre>
<h2>Pitfalls</h2>
<ul>
<li>Do not use Pages Router APIs in App Router</li>
<li>Avoid client components unless necessary</li>
</ul>
<h2>References</h2>
<ul>
<li>
<a href="https://nextjs.org/docs">Next.js App Router docs</a>
</li>
</ul>
</div>
);
}- Post components
- Create React TSX components in
components/blog-posts/<slug>.tsx - Each component exports a function that returns the post content as JSX
- Content registry
- Maintain
lib/posts.tswith:- TypeScript array of post metadata objects
- Import all post components
- Map slugs to components
- Expose
getAllPosts()andgetPostBySlug(slug)returning typed data
- Routes
app/(site)/blog/page.tsx: list posts (exclude drafts), sorted bypublishedAtapp/(site)/blog/[slug]/page.tsx: render the post component for a single post- Use
generateStaticParamsandgenerateMetadata - Set
dynamic = "error"andrevalidate = false
- SEO & feeds
app/sitemap.tsandapp/robots.ts- Build-time RSS/Atom generation under
public/
- Assets & images
- Store per-post media under
public/posts/<slug>/ - Use
next/imagefor responsive, optimized images with explicitalt - Run
pnpm optimize-imagesto generate modern formats (AVIF, WebP) from source images - Reference optimized images in frontmatter:
cover: "/posts/<slug>/cover-optimized.webp"
- Quality gates
pnpm lintmust pass- Add a
typecheckscript (tsc --noEmit) and keep it green
- Project structure (high-level):
app/— App Router pages/componentscomponents/blog-posts/— Blog post React componentslib/— shared utilities (no React code here, includes post metadata registry)public/— static assetsscripts/— build-time automation scripts (TypeScript, executed via tsx)
- Code style:
- Server Components by default; colocate minimal Client Components
- Descriptive naming, short functions, early exits
- No dead code; remove unused exports; keep modules focused
- Source images: Place originals in
public/posts/<slug>/(e.g.,cover.png) - Run:
pnpm optimize-imagesto generate optimized variants - Outputs:
<name>-optimized.avif,<name>-optimized.webp,<name>-optimized.<ext> - Use WebP in post metadata for best balance:
cover: "/posts/<slug>/cover-optimized.webp" - Target: <200KB per image (WebP typically achieves 85-95% size reduction)
- The script uses Sharp for optimization and skips files already containing "-optimized"
-
Source audio: Place MP3 files in
public/posts/<slug>/audio.mp3 -
Format: MP3 at 128-192 kbps for spoken word (podcast/narration style)
-
Target size: <20 MB per file (10-minute audio at 192 kbps ≈ 14.4 MB)
-
Optimization: Use ffmpeg to optimize before committing:
# Optimize audio for web (spoken word) ffmpeg -i input.wav -codec:a libmp3lame -b:a 192k -ar 44100 output.mp3 -
Post metadata: Add
audioUrl: "/posts/<slug>/audio.mp3"to post object inlib/posts.ts -
Integration: Import and render
<AudioPlayer>component in post content:import { AudioPlayer } from '@/components/audio-player'; <AudioPlayer audioUrl="/posts/<slug>/audio.mp3" title="Post Title - Audio Version" />;
-
Features: Automatic playback controls (play/pause, seek, volume, speed: 1x/1.25x/1.5x/2x)
-
Indicator: Blog listing automatically shows Headphones icon for posts with audio
-
Preload: Audio uses
preload="metadata"to load duration without full download -
Serving: Files automatically served via Next.js static serving with CDN caching on Vercel
- When asked to add a post:
- Propose a
slug, createcomponents/blog-posts/<slug>.tsxas a React component - Add post metadata entry to
lib/posts.tswith all required fields - Create a
public/posts/<slug>/folder for media and reference them with absolute/posts/<slug>/…paths - If images are added, run
pnpm optimize-imagesto generate optimized variants - Use optimized image paths in metadata (e.g.,
cover: "/posts/<slug>/cover-optimized.webp") - Ensure the new post is picked up by the static params generator
- Propose a
- When asked to add features:
- Implement the single chosen approach end-to-end; do not keep legacy pathways
- Update types, metadata, and feeds accordingly
- Keep PRs focused and small. Include a brief rationale and before/after notes.
- Next.js App Router docs (metadata, SSG) — see the official Next.js documentation
- React component best practices — keep components focused and reusable
- Guidance on concise
CLAUDE.mdfiles — see community write-ups like the Apidog "Claude.md" overview for keeping this file focused and high-signal
Keep this document up to date as the source of truth for how this blog is structured and extended.
All thoughs (even in sub-projects) should be in thoughts/shared/ (root), not in the sub projects.