Overview
This portfolio is a fully static site where projects and skills are written as
MDX files. Any file dropped into projects/ is automatically discovered at
build time and rendered — no code changes, no registry, no database. A git push
triggers a rebuild on the hosting provider.
It is also documentation-aware: projects can attach multiple Design Docs
(local MDX or notion links) that appear as a Design Docs button beside
Source Code, with an index page and per-document rendering — all without a
second content system.
Key capabilities:
- Projects and skills authored as Markdown/MDX with YAML frontmatter.
- Automatic project discovery via
fast-glob+gray-matter. - Server-rendered MDX with a custom styled component map.
- Light/dark theming with
next-themes(follows system by default). - Mermaid diagrams rendered directly inside MDX, theme-aware.
- Obsidian-style callouts with typed icons and theme tokens.
- Design Docs for any project — local MDX or external Notion — via
content/design-docs/<slug>/meta.yaml. - Build-time SEO: sitemap, robots, manifest, Open Graph, and JSON-LD.
Tech Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Server Components) |
| Language | TypeScript (strict) |
| Content | MDX via next-mdx-remote (RSC) + yaml |
| Frontmatter | gray-matter + yaml |
| Discovery | fast-glob |
| Styling | Tailwind CSS v4 + shadcn/ui (Radix primitives) |
| Theming | next-themes |
| Diagrams | Mermaid (client-rendered, light/dark aware) |
| Animation | Motion (Framer Motion) |
| Icons | lucide-react |
| Deploy | Vercel |
Project Structure
portfolio/
├── projects/ # project content lives here
│ └── <project-folder>/
│ ├── <slug>.md / .mdx # frontmatter + MDX body (auto-discovered)
│ └── banner.png # co-located images (referenced relatively)
├── content/
│ ├── skills.mdx # skills shown on the home page
│ └── design-docs/
│ └── <project-slug>/
│ ├── meta.yaml # DesignDoc[] for that project
│ ├── how-to-use.mdx # local doc example
│ └── architecture.mdx
├── public/ # site-wide static assets (logos, placeholders)
└── src/
├── app/ # App Router routes + generated metadata
│ ├── layout.tsx # fonts, ThemeProvider, global metadata, JSON-LD
│ ├── page.tsx # home page
│ ├── projects/[slug]/page.tsx # project detail page
│ ├── projects/[slug]/design-docs/page.tsx # Design Docs index
│ ├── projects/[slug]/design-docs/[docId]/page.tsx # local doc page
│ ├── project-assets/[...path]/route.ts # serves co-located assets
│ ├── sitemap.ts / robots.ts / manifest.ts
│ └── opengraph-image.tsx / twitter-image.tsx
├── components/
│ ├── Header.tsx / Footer.tsx
│ ├── ProjectCard.tsx / ProjectGrid.tsx
│ ├── DesignDocCard.tsx # doc index card (local vs Notion)
│ ├── Mermaid.tsx # client-side Mermaid renderer
│ ├── MermaidViewer.tsx
│ ├── Callout.tsx
│ ├── Reveal.tsx / Stagger.tsx / ThemeToggle.tsx
│ └── ui/ # shadcn/ui primitives
└── lib/
├── config.ts # name, tagline, links, site URL
├── projects.ts # glob -> parse -> typed Project + merge designDocs
├── design-docs.ts # DesignDoc type, validation, loader
├── skills.tsx # reads content/skills.mdx
├── mermaid.ts # centralized Mermaid theme config
├── mdx.tsx # MDXRemote + styled component map + remark plugins
└── utils.ts # cn(), formatDate()
How It Works
- Discovery —
src/lib/projects.tsrunsfast-globoverprojects/and returns every.mdand.mdxfile, in any subfolder. - Parsing — each file is passed through
gray-matter; YAML frontmatter becomes the typedProjectobject, the rest becomes the MDX body. If the frontmatter containsdesignDocs[], it is validated (uniqueid,localornotion, URL check). - Design Docs merge — for each slug,
loadDesignDocsFromMeta()triescontent/design-docs/<slug>/meta.yaml(viayaml). Frontmatter docs and YAML docs are merged; duplicateidthrows at build time. - Listing — the home page sorts projects by
date(thenorder) and renders aProjectCardgrid.featured: truefiles go into a separate "Featured" section. Cards show a Docs button only whenproject.designDocs?.length > 0. - Detail —
generateStaticParams()derives every slug up front;/projects/[slug]renders the body withnext-mdx-remote(RSC) using a custom component map (src/lib/mdx.tsx). The Design Docs button beside Source Code links to the index. - Assets — co-located files are referenced with relative paths in MDX. The
remarkProjectAssetsplugin rewrites them to/project-assets/<folder>/<file>, served straight fromprojects/.
Project File Schema
Add projects/<your-project>/<your-project>.md (or .mdx). Frontmatter:
---
title: My Project # required — shown on card + page
slug: my-project # optional — defaults to the file name
description: Short blurb. # card + meta description
tags: [react, typescript] # optional — rendered as badges
image: banner.png # optional — card + hero image
github: https://github.com/… # optional — "Source" link
demo: https://demo.example.com # optional — "Live Demo" link
featured: true # optional — pin to Featured on home page
visible: true # optional — hide from the site (defaults true)
date: 2026-08-01 # sort key, shown on the page
order: 1 # tiebreaker when dates match
# optional — inline docs (or use content/design-docs/<slug>/meta.yaml)
designDocs:
- id: architecture
title: Architecture
description: System design.
type: local
source: architecture.mdx
- id: api
title: API Design
type: notion
source: https://www.notion.so/your-page-id
updatedAt: "2026-08-24"
---
Body content… # any MDX: headings, lists, code, <Component/>
Prefer content/design-docs/<slug>/meta.yaml for local docs so MDX files live
next to their metadata. Frontmatter designDocs is ideal for a quick Notion link.
Both sources merge — the UI just sees Project.designDocs[].
Design Docs
Design Docs are the portfolio's answer to "where is the architecture?" without turning every project into a docs site.
Adding docs to a project
- Create
content/design-docs/<slug>/(slug fromprojects/<folder>/<file>.md). - Add
meta.yaml:
designDocs:
- id: how-to-use
title: How to Use
description: Two-minute setup.
type: local
source: how-to-use.mdx
- id: architecture
title: Architecture
description: How it works.
type: local
source: architecture.mdx
- Add the MDX files (
how-to-use.mdx,architecture.mdx) besidemeta.yaml. They support the same MDX features as project pages — Mermaid, code, tables, callouts, images, custom components, TOC. - For Notion:
- id: api
title: API Design
type: notion
source: https://www.notion.so/your-page-id
No build-code changes needed. The next npm run build validates and emits
/projects/<slug>/design-docs and /projects/<slug>/design-docs/<id> for local docs.
Source-agnostic
The index just receives DesignDoc[]. local renders MDX, notion links out.
Adding github or pdf later means extending the union type, not the UI.
Validation
idunique within the project.typemust belocalornotion.notionrequires a validhttp(s)URL.localrequires the file atcontent/design-docs/<slug>/<source>.updatedAtis optional, shown asUpdated …when present.
This portfolio's own docs live at content/design-docs/portfolio/ — see
How to Use and Architecture there for a live example.
Callouts
Documentation blocks support Obsidian-style callouts. A blockquote whose first
line starts with [!<type>] is transformed at build time into a themed,
icon-bearing callout — the type is parsed automatically and the body renders
full Markdown (lists, links, code, tables, even nested callouts).
Syntax:
> [!warning] Title
> Body content with **Markdown** support.
Omitting the title falls back to the capitalized type name ([!tip] → "Tip"),
and unknown types fall back to note styling. Icons come from Lucide; colors
are design tokens defined per theme in src/app/globals.css.
| Type | Icon | Use for |
|---|---|---|
note | StickyNote | Generic notes |
info | Info | Neutral, helpful context |
tip | Lightbulb | Hints and best practices |
warning | TriangleAlert | Caution — things that can go wrong |
danger | CircleAlert | Critical risks and destructive actions |
success | CircleCheck | Confirmations and completed outcomes |
question | CircleHelp | FAQs and open questions |
bug | Bug | Known issues and regressions |
example | BookOpen | Worked examples |
quote | Quote | Quotations |
Example output
Note
This is a plain note.
Info
Neutral context that is helpful but not critical.
Tip
Omit the custom title and the callout simply reads "Tip".
Watch out
Warnings use a soft orange tint in light mode and a darker one at night.
Danger zone
Critical information stands out without relying on color alone — each type also has its own icon and label.
Deployed
The build passed and shipped successfully.
Can callouts contain rich content?
Yes. Bodies support full Markdown:
- Bold, italic,
inline code, and links- Lists, tables, and fenced code blocks
const ready = true;
A second paragraph after a blank line.
Known issue
Callouts can flag regressions in release notes.
Multiline example
First paragraph of the example.
- Step one
- Step two
Quote
Documentation should be part of the product.
Callouts can be nested by indenting an inner blockquote:
Do callouts nest?
Yes — place another callout inside the body:
Inner callout
Fully themed, with its own icon and tokens.
Adding a New Project
- Create
projects/my-new-project/and addmy-new-project.md(or.mdx) with the frontmatter above. - Drop screenshots/documents next to the file and reference them with relative
paths — no need to touch
public/. - Optional: add
content/design-docs/my-new-project/meta.yaml+ MDX files for deep dives. - Run
npm run devand open the project page. When ready, commit and push — the hosting provider rebuilds and ships it automatically.
Theming
Design tokens live in src/app/globals.css (Tailwind v4 + shadcn variables).
ThemeProvider in layout.tsx uses next-themes with defaultTheme="system",
so dark mode follows the device; ThemeToggle.tsx overrides to light/dark
explicitly. Mermaid diagrams adapt automatically: the diagram theme is mapped
from the page theme in src/lib/mermaid.ts and re-renders on theme change
without a reload. The view-transition API powers the theme toggle's circular wipe.
SEO & Metadata
Generated at build time by Next.js route conventions:
sitemap.ts→/sitemap.xml— rebuilt every build; one URL per project and per Design Doc page.robots.ts→/robots.txt— allows crawling, points to the sitemap.manifest.ts→/manifest.webmanifest— PWA metadata.- Open Graph / Twitter —
opengraph-image.tsx+twitter-image.tsxgenerate default share images; project and doc pages set per-pageopenGraphmetadata. - JSON-LD —
Person(layout),WebSite+ProfilePage(home),Article(project and doc pages) viasrc/components/JsonLd.tsx.
Set NEXT_PUBLIC_SITE_URL to the production URL — it is used for the sitemap,
canonical URLs, Open Graph, and JSON-LD.
Performance & Accessibility
- Static generation for every project and doc (SSG via
generateStaticParams). Reveal/Staggeranimations respectprefers-reduced-motion.- TOC via
TableOfContentswithDeepLinkhighlight and scroll-spy. - Keyboard-navigable cards and buttons, visible focus rings,
aria-labelfor external Notion links ("opens in new tab"). next/imagefor hero images,loading="eager"+fetchPriority="high"for the first card.
Commands
| Command | Purpose |
|---|---|
npm run dev | Local dev server (hot reload) |
npm run build | Type-check + static build |
npm start | Serve the production build |
npm run lint | ESLint |