Overview
This site eats its own dog food: it is a Next.js 16 application where the content is the filesystem. Projects are markdown files, design docs are MDX, and everything becomes static HTML at build time — with a few deliberate server-side pieces.
The one rule
Adding content never means touching code. Drop files in, push, done.
Content Pipeline
| Step | Where | What happens |
|---|---|---|
| Discovery | src/lib/projects.ts | fast-glob scans projects/**/*.{md,mdx} |
| Parsing | same | gray-matter splits YAML frontmatter from the MDX body into a typed Project — apiDocsUrl is validated as an http(s) URL when present |
| Design-doc merge | src/lib/design-docs.ts | content/design-docs/<slug>/meta.yaml is validated (unique ids, local/notion, URL checks) and merged — duplicate ids fail the build loudly |
| Rendering | src/lib/mdx.tsx | Server-side MDXRemote with GFM, callout transformation, heading slugification, and asset-URL rewriting |
| Assets | /project-assets/[...path] | A Node route serves co-located project files with a path-traversal guard and 1-year immutable cache |
Slug nuance worth knowing: the URL slug comes from frontmatter (slug: field, falling back to the file name) — not the folder name. So projects/weather now/project-weather-now.md publishes at /projects/weather-now, and its design docs live in content/design-docs/weather-now/. Three names, one project — intentional but easy to trip on.
Routes
/ home: hero, featured projects, services, skills, contact
/projects/[slug] project page (statically generated)
/projects/[slug]/design-docs doc index cards (+ API Documentation button when apiDocsUrl exists)
/projects/[slug]/design-docs/[docId] local doc rendering (Notion docs are external links only)
/project-assets/[...path] filesystem asset handler
Plus generated metadata routes: opengraph-image, twitter-image, sitemap.xml, robots.txt, and a PWA manifest.
Project Header Actions
The project header (ProjectLinks in src/app/projects/[slug]/page.tsx) renders a single source of truth — project.apiDocsUrl — in two places:
- Project page —
Source Code(outline) ·Design Docs(outline, when docs exist) ·API Documentation(outline withFileCode+ExternalLink,target="_blank") ·Live Demo(solid). - Design Docs index — same
API Documentationbutton appears below the subtitle whenapiDocsUrlis set, so visitors who enter via/design-docsstill see the external spec without backtracking.
apiDocsUrl lives on Project (src/lib/projects.ts) alongside github/demo, is parsed from frontmatter (apiDocsUrl or alias apiDocs), trimmed, and validated with new URL() (http/https only). No new route is created — the button is a conditional external link that hides entirely when the field is absent.
The MDX Toolkit
Every markdown file on the site renders through one component map:
- Callouts — Obsidian-style
> [!tip] Titleblockquotes transformed at build time into ten themed types with Lucide icons. - Mermaid — diagrams declared inline in MDX, rendered client-side.
- Styled defaults — tables, code, lists, links (internal links use
<Link>, external ones open safely), all wrapped in scroll-reveal animations. - Heading anchors — every heading gets a slugified id for the table of contents and deep links.
Diagrams Without the Bundle Penalty
Mermaid is a heavy library, so it never ships in the initial JavaScript:
- SSR renders an empty placeholder box.
- After hydration,
Mermaid.tsxlazy-imports the library inside an effect. - Renders are serialized through a module-level queue; theme changes trigger re-render without reload.
- A full-screen viewer with pan/zoom/pinch opens on click.
The trade-off: diagram content isn't in the server HTML (no SEO value), which is acceptable — prose carries the meaning, diagrams illustrate it.
Theming & Motion
- Light/dark via
next-themes(system default); tokens inglobals.cssusing Tailwind v4@theme inline. - Theme toggle uses the View Transitions API for a circular wipe from the click point, with graceful fallback.
- All animation runs through Motion with
reducedMotion="user"globally —prefers-reduced-motionis respected everywhere, including Mermaid viewers.
SEO Artifacts
| Artifact | Source |
|---|---|
| Per-page metadata + canonicals | Next Metadata API across layouts/pages |
| Share images | Generated 1200×630 PNGs via next/og |
| Structured data | JSON-LD: Person (layout), WebSite + ProfilePage (home), Article (project/doc pages) |
| Sitemap / robots / manifest | Route conventions, regenerated each build |
Honest Gaps
- Sitemap misses design-doc pages — only home and projects are listed today.
NEXT_PUBLIC_SITE_URLfalls back to localhost — unset in production, canonical URLs silently degrade.- A dormant Letterboxd integration exists — a complete RSS-parsing "recently watched" feature (30-min cached fetch, poster extraction) sits unreferenced by any page; the site currently performs zero runtime fetching.
- Deployment requires a Node runtime — the asset-serving route reads the filesystem per request, so this cannot be a pure static export. On Vercel this just works.
Result
A documentation-grade portfolio: git-push publishing, zero-database content, real design docs per project, and honest engineering about what's static, what's dynamic, and what's not finished.