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 did not want a portfolio that felt like a graveyard of half-finished ideas or a template I would 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 cannot 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 does not look like 1998
  • Tables that do not 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 cannot, 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 did not 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
  • Tries a hardware-capable WebGL context with failIfMajorPerformanceCaveat before enabling the expensive SVG goo path
  • Falls back to a much cheaper animated soft-blob layer when that probe fails
  • 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 intentional on weaker ones instead of trying to fake the same effect badly.

6) SEO + feeds + robots (because it is 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 is not a bank. But it is also not a WordPress blog from 2009.

Links