← Portfolio

Personal Website

This site is built on Next.js 15 with the App Router and TypeScript. Pages render as server components by default; a small set of leaf components — the navigation, theme toggle, D3 visualizations, and the watchmaker clock — opt into client rendering where they need browser APIs or interactivity. There is no traditional database. Styling is handled exclusively through CSS Modules with design tokens defined as CSS custom properties in a single globals.css. The Inter typeface is loaded via next/font, and Vercel Analytics is integrated at the root layout.

Content Architecture

Content lives in two places depending on how often it changes and how it’s edited.

Long-form prose — writing posts, neighborhood-watch newsletters, and community event listings — is authored as .md files with YAML frontmatter, parsed by gray-matter, and rendered through remark. A module-level in-memory cache in src/lib/content.ts indexes all frontmatter at startup and reads full bodies on demand. Rendered HTML is injected via dangerouslySetInnerHTML — fine here because the input is mine and the pipeline is deterministic.

Structured site sections — portfolio entries, services, social links — are authored as typed TypeScript data modules under src/data/. Each module defines a type, exports a sorted and filtered list, and is consumed by a small reusable wrapper component (PortfolioPage, ServicePage) that handles back-nav, prev/next pagination, and any structured panels (outcomes, includes, FAQ, and so on). Detail pages are still hand-written JSX — the data module owns the metadata; the page owns the narrative. Adding a new entry to either section is a one-record change in the data file plus a single new page.tsx; the nav dropdown, listing grid, and pagination update automatically.

This split is intentional. Markdown is great for prose with light structure; TypeScript is great for structured data with rich consumers. Mixing them in either direction is where most personal sites quietly get unwieldy.

Theming

Light and dark themes are driven by a data-theme attribute on <html>, with all colors defined as CSS custom properties in globals.css and overridden under [data-theme="dark"]. A small ThemeProviderreads and writes the user’s preference; a ThemeToggle flips it. Component CSS Modules consume the same custom properties, so adding a new component never requires re-implementing dark mode — it just inherits.

Images

Project screenshots are compressed to WebP and served through the Next.js Image component, which handles lazy loading and responsive srcset generation. The sizes prop is tuned to the 860px prose content width so the browser requests appropriately-sized variants at each breakpoint.

Testing

Jest with React Testing Library covers the surfaces where a regression would actually be felt — the calendar grid, newsletter listing, and event list. jsdom is the test environment; ts-jest handles TypeScript transformation. Run with npm test, or npm run test:watch while iterating. Most pages are simple enough that TypeScript catches what a unit test would.

Deployment

The site is deployed to Vercel on every merge to master, with a separate nightly build triggered via GitHub Actions to keep prerendered content fresh. All work happens on feature branches and lands through pull requests — master is never committed to directly.

Design Goals

Lightweight and content-first. I wanted a site I could extend without fighting a framework or a CMS — adding a new portfolio entry or service offering means appending a record to the relevant data module and creating a single page.tsx. The architecture is deliberately boring: typed data, reusable wrappers, server-rendered prose, no bespoke build steps. The site is live at paulaklimas.com and serves as both a writing outlet and a working demonstration of my frontend architecture choices.