Portfolio

A clean portfolio using Astro, Sanity CMS and Vercel. Built with Cursor.

Problem

Hirers care about how candidates frame problems, navigate constraints, and make tradeoffs, but simple portfolios often just show what is made, not how or why. I wanted a site that communicates judgment and decision quality rather than a list of technologies.

Why It Mattered

The site is the first impression for hirers and fellow professionals deciding whether to have a deeper conversation. If a visitor can't get a quick read of how I think, the site has failed. It is a curated body of work structured as case studies (problem, constraints, decisions, outcomes), designed to build trust quickly and provide depth on demand.

Constraints

Limit unnecessary JavaScript: Keep the code lean and performant. I'd consider myself a React developer, but this is for the most part a static site, so we shouldn't send huge amounts of JS to the user without a solid reason. WCAG AA accessibility: Semantic HTML, visible focus states, keyboard-navigable, proper ARIA throughout. Mobile-first. Drive development using Cursor: My intention is to use this to experiment and learn about agent-driven development, which also gives the opportunity to use technologies I've not had exposure to before. That better enables me to focus on picking the best tool for the job, rather than simply what I'm familiar with. Privacy-first: No specific employer names, no PII beyond what's intentionally public. LinkedIn is the source for full work history: the site stays generalized. Content durability: The content model must support easy updates without redesigning pages. Case studies follow a set structure, making adding a new project a content task rather than a code task.

Architecture Decisions

Astro for static rendering: pages generate HTML at build time. React is a build-time dependency (shadcn/ui primitives) but doesn't send JS to the browser except when significantly improving DX. Sanity CMS with normalisation boundary: GROQ queries -> mapper layer to normalise and type. Astro pages never touch raw CMS shapes directly, allowing flexibility to change backend later if required. Deterministic E2E via fixtures: Playwright tests run against mock data so CI never hits the network. Tailwind CSS with design tokens: tokens defined on `:root`/`.dark`, mapped to Tailwind utilities via `@theme inline`. shadcn component variables point at the same tokens, so there's a single source of truth for colour. Class-based dark mode with a blocking inline script: theme preference reads from `localStorage` before first paint to reduce layout shift and pop-in.

Trade-offs

No SSR / no preview mode yet: fully static output means content updates require a rebuild. Acceptable for a low-frequency personal site. Portable Text stored but rendered as plain text: case study body fields are Portable Text in Sanity (preserving future rich-content capability) but the frontend currently uses `pt::text(...)` projections to consume flattened strings. This trades richness now for simplicity and speed, with a clear upgrade path when rich rendering is needed. React for mobile nav: mobile breakpoint uses Radix Dialog for component simplicity + smoother UX (focus trapping, scroll lock, auto-close). The alternative was reimplementing all of that in vanilla JS, which would have been more code and more edge cases for. Downside: this does result in sending React code to the client, which removes one of the benefits of using Astro. oxlint over ESLint: faster lint runs at the cost of fewer available rules. For a small codebase with strong TypeScript coverage, the speed wins outweigh the missing niche rules. No Vitest in CI: Playwright runs in GitHub Actions but unit tests are local-only. Acceptable tradeoff given the small team size; adding a Vitest CI job is trivial when needed.

Outcomes

Sub-second page loads: fully static HTML, no client-side framework runtime, font subsetting via `@fontsource-variable`. Reliable CI: Playwright E2E suite runs against build-time fixtures across Chromium, Firefox, and WebKit without network dependencies. Content independence: adding a new project is entirely a Sanity Studio task. The schema enforces structure (required fields, slug validation, tag limits) so content stays consistent. Both themes verified: light and dark mode tested at every phase; design tokens ensure contrast ratios meet WCAG AA. Clean separation of concerns: CMS schemas, GROQ queries, data normalization, and presentation are in distinct layers. Changes in one don't ripple through the others.

What I'd Improve

Portable Text rendering: replace the `pt::text(...)` flattening with proper `astro-portabletext` rendering so case study sections can include inline code, links, and structured lists. Visual regression testing: add screenshot-based Playwright tests to catch unintended style changes across themes and breakpoints. Content preview: wire up Sanity's draft/preview perspective so content changes can be reviewed before publishing and rebuilding. Image support: the project schema has no image fields yet. Adding a hero image or inline diagrams (e.g. with Astro's `<Image>` optimization, Mermaid support) would make case studies more engaging. Vitest in CI: add a GitHub Actions job for unit tests alongside the existing Playwright workflow. Component-level Storybook or catalog: as the design system grows, a living reference of atoms (TagPill, AccentStripe, CardSurface) would help maintain consistency. Improve E2E testing: run against actual backend data rather than mock data to improve confidence. Potential: Update the project data entity: currently projects all expect a set number of fixed sections. While useful for structure and consistency, it might be useful to allow more flexibility.