Brian · Portfolio
maintainednextjsmdxtailwindseo

Portfolio Site

A fast, content-driven portfolio built with Next.js + MDX, with view transitions, SEO tooling, and a performance-conscious animated backdrop.

Why build a “real” portfolio site

I didn’t want a portfolio that felt like a graveyard of half-finished ideas or a template I’d never touch again.

The goal was to treat the site like a small product:

  • Content-first: adding a project should feel like writing, not “building a page”.
  • Fast + readable: minimal UI friction, strong typography rhythm, and no layout jank.
  • Honest engineering: shipping-minded details (SEO, security headers, error states) without turning the site into a framework science experiment.

Architecture

High-level shape

  • Next.js App Router
    • Server components by default (cheap pages, small client footprint)
    • Dynamic routes for Projects and Writing (/projects/[slug], /writing/[slug])
  • Content layer (MDX on disk)
    • content/projects/*.mdx
    • content/writing/*.mdx
    • Frontmatter drives listing cards, badges, metadata, and visibility
  • Presentation layer
    • Tailwind CSS with a few custom global styles (MDX rhythm, scrollbar polish, view transitions)
    • A small set of “product-y” components: Badge, HeroImage, Lightbox, ScrollProgress, FluidBackdrop

Data flow (content → page)

  1. A project/post starts life as an .mdx file with frontmatter.
  2. lib/content.js loads the collection, applies visibility rules, and sorts:
    • featured first
    • then date (descending)
    • then title
  3. The route (/projects/[slug]) fetches the entry and compiles MDX server-side via next-mdx-remote/rsc.
  4. MDX renders through components/mdx-components.js so images/code/tables look consistent.

Notable technical details

1) A content model with “privacy” baked in

Every content file can be:

  • public: listed and routable
  • unlisted: routable by direct URL, but not listed
  • private: not routable and not listed

That gives me a clean workflow for drafts, “client work I can’t publicize yet”, and works-in-progress without juggling branches.

2) MDX that behaves like a real reading experience

MDX compilation uses:

  • remark-gfm for GitHub-flavored markdown (tables, task lists, etc.)
  • rehype-pretty-code (Shiki) for code highlighting

On top of that, mdx-components.js provides opinionated rendering:

  • Inline code vs block code styling
  • A-link styling that doesn’t look like 1998
  • Tables that don’t explode the layout
  • A consistent “premium rhythm” for headings, paragraphs, and spacing

3) Images: low-jank by default + optional lightbox

For local images in public/, the MDX image component tries to infer width/height on the server (using image-size). When it can, it renders next/image with real dimensions to reduce layout shift. If it can’t, it gracefully falls back to a normal <img>.

On top of that, any image can be wrapped in a Lightbox:

  • Accessible dialog semantics (role="dialog", aria-modal)
  • Escape-to-close
  • Body scroll lock while open
  • Uses a portal so the overlay sits above everything cleanly

4) View transitions without the “my whole page just flashed” vibe

I wanted transitions between pages to feel smooth, but I didn’t want the animated backdrop to restart or crossfade weirdly.

So the CSS explicitly:

  • Transitions only the main content container (.vt-main)
  • Disables the default root snapshot crossfade

Result: navigation feels “native”, but the background stays stable.

5) The goo backdrop is performance-gated (on purpose)

The animated background is fun… until it turns into a laptop fan benchmark.

FluidBackdrop is written like a polite guest:

  • Respects prefers-reduced-motion
  • Checks for a hardware WebGL context (using failIfMajorPerformanceCaveat)
  • Uses passive scroll listeners + requestAnimationFrame
  • Stops animation when the tab is hidden, resumes when visible
  • Still keeps the scroll-based darkening effect even when not animating (so readability stays consistent)

So the site looks “alive” on capable machines, and still looks good (but calmer) on weaker ones.

6) SEO + feeds + robots (because it’s 2026)

Baseline SEO is baked in:

  • Per-page metadata (title/description/canonical/OG/Twitter)
  • JSON-LD on the root layout
  • /sitemap.xml, /rss.xml, /robots.txt generated via route handlers
  • Canonical URL resolver that behaves across local dev, Vercel previews, and production

7) Security headers by default

next.config.mjs sets headers globally:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: … (locked down)
  • X-XSS-Protection: 0 (explicitly disabled; modern browsers ignore it anyway)

No, it’s not a bank. But it’s also not a WordPress blog from 2009.

Links