Skip to main content

Architecture

This site is a personal website built with Vue and Nuxt. Since most of my professional work lives in private enterprise systems, I use this project to show how I structure a frontend application, manage content, and handle performance and accessibility.

  • Nuxt 3 & Static Pre-rendering: Every page is pre-rendered to static HTML at build time, which keeps the site fast, simple to host, and easy for search engines to crawl.
  • Vue 3 Composition API: I use the Composition API and script setup to keep component logic easier to follow.
  • Pinia State Management: Dedicated stores manage static content distribution and global application theme states without prop drilling.
  • SCSS & CSS Custom Properties: The design system uses scoped SCSS modules and CSS custom properties to support theming and responsive layout.
  • SCSS Auto-injection via Vite: Variables and mixins are injected into each component’s style block through Vite config, which keeps shared styling tools available without repetitive imports.
  • Page Transitions: A simple global fade transition is configured in nuxt.config.ts, with a dedicated partial handling the route transition classes.

Performance & Lighthouse

This site is pre-rendered, keeps JavaScript light, and avoids extra dependencies where native browser behavior is sufficient. Lighthouse CI runs against every push and pull request, gating deploys behind 100 Performance, 95+ Accessibility, 90+ Best Practices, and 95+ SEO across every prerendered route.

100
Performance
100
Accessibility
100
Best Practices
100
SEO
  • Pre-rendered HTML: Nuxt generates fully-formed HTML at build time, so browsers render content immediately without waiting for client-side JavaScript.
  • Optimized Image Pipeline: @nuxt/image generates responsive WebP variants at build time. Each NuxtImg ships a srcset so the browser pulls the smallest variant for the actual display size, and the hero image uses fetchpriority="high" to win the LCP race.
  • Self-Hosted Fonts: @nuxt/fonts downloads Roboto Condensed at build time, inlines the @font-face declarations into the HTML, and generates size-adjusted fallback fonts so any pre-swap render does not shift the layout. No render-blocking request to Google Fonts.
  • Deferred Analytics: Google Analytics loads through a client plugin scheduled via requestIdleCallback, so the gtag script never blocks the initial paint.
  • Inline SVG Icons: Every icon on the site (engineering competencies, beyond-work cards, social links, theme toggle, and architecture gauges) is inlined directly into the component. Zero icon-font or icon-library requests.
  • Minimal JavaScript: No heavy UI libraries or animation frameworks. The bundle stays light by leaning on native browser APIs and Vite's default tree-shaking.

Content-Driven Architecture

All page content lives in a single data/content.js file as a plain JavaScript export. A Pinia store wraps it so components can access the content they need without passing data through multiple layers.

It is a simple content-driven setup that keeps structure and presentation separate. If I ever wanted to swap this for a CMS or an external API, that change would mostly happen in the store layer rather than across the component tree.

The pre-render config in nuxt.config.ts only includes routes that exist in the data layer, which keeps the build output predictable.

Multi-Theme Design System

Theming is treated as a first-class architecture concern rather than a single light/dark switch. The site ships multiple complete themes that swap live, changing colors, type, and in some cases entire layouts. The floating Customize panel and a command palette (⌘K on Mac, Ctrl+K elsewhere) both drive the same engine, and you are looking at it right now.

  • Token-Driven by Default: No component hard-codes a color. Every surface reads CSS custom properties like --color-bg-primary and --color-accent-line. Switching a theme rewrites those token values on the root <html> element through a single data-theme attribute, so the whole site re-skins in one paint with zero per-component work.
  • Two Orthogonal Axes: data-theme selects the skin and data-mode selects light or dark. Each theme declares whether light/dark is even meaningful for it, so the mode toggle only appears when a theme actually supports one.
  • Themes Can Swap Components, Not Just Colors: A theme registry maps each theme to a set of Vue components, and a <Themed> resolver renders the active theme's version while falling back to the default for anything a theme leaves untouched. The “Reel-to-Reel” theme ships its own masthead, footer, and page layouts. That is a different information design, not a re-paint.
  • Hydration-Safe Swapping: Because every page is prerendered with the default theme, applying a saved theme too early would mismatch the static HTML and blank the view. Instead the saved theme is applied once hydration settles, on Nuxt’s app:suspense:resolve hook, and the component tree swaps in a single reactive update behind a brief cross-fade, the same transition used between routes.
  • The Engine on Display: The Customize drawer reads the live computed token values straight off the DOM and renders them as a read-only code block, so the design variables visibly change as you switch themes. That command palette makes the same switch keyboard-first, the way a real design tool would.

Anti-FOUC Theme Initialization

Theme persistence is mirrored to both localStorage and cookies. Dynamic SSR can read the cookie and render the selected theme immediately; static prerendered pages still have a deterministic default HTML payload.

For static pages, a tiny inline script in nuxt.config.ts runs before first paint, reads the saved preference, sets data-theme and data-mode, and only adds a short boot cloak when the saved theme's component tree differs from the prerendered tree. After hydration, the client plugin applies the saved theme and removes that cloak on the next Vue patch tick, so returning visitors never see a flash of the wrong layout.

Hash-Based Deep Linking

The resume timeline supports URL hash navigation, so a link like /resume#best-egg opens the relevant employer section and scrolls directly to it.

The implementation watches route.hash, matches it against a slugified employer heading, and adds the relevant entries to an expandedRoles Set. A custom useScrollToHash composable waits for the DOM to finish updating before calculating scroll position, and it accounts for the sticky header so section headings are not hidden.

CI/CD & Edge Delivery

Every push and pull request runs through GitHub Actions before anything reaches production. Two workflows guard the main branch:

Production deploys are handled by Netlify. A push to main triggers a fresh static generate that gets served from Netlify’s global CDN.

  • Unit Tests: Vitest runs the full suite on every commit, then uploads coverage to Codecov so the README badge reflects current state.
  • Lighthouse CI: The workflow builds the site, serves it on the runner, and audits each route. A build fails if any page drops below 100 Performance, 95 Accessibility, 90 Best Practices, or 95 SEO. Regressions never reach main.

URL Handling & SEO

URLs that lead to the same page should resolve in one hop, not two. Two pieces work together to make that true.

  • Trailing-Slash Rewrites: Netlify’s default behavior 301-redirects /music to /music/ when it finds a subdirectory. The netlify.toml rewrites use status 200 instead, so the no-trailing-slash form serves the file directly. A direct hit or a Lighthouse audit gets the content immediately, with no redirect round-trip.
  • Canonical Tags: A small useHead setup in app.vue injects a rel="canonical" link on every prerendered route, computed from the current path. Search engines that follow either URL form consolidate ranking signals onto the canonical version.

Testing & Coverage

The site is covered by a Vitest unit suite running in the Nuxt test environment with happy-dom. Components, pages, stores, the layout, the router scroll behavior, the theme plugin, and the scroll-to-hash composable all have their own specs.

  • Vitest + @nuxt/test-utils: Tests boot a real Nuxt context via mountSuspended, so auto-imports, composables, and Pinia all resolve the same way they do at runtime.
  • Coverage Targets: The v8 coverage reporter tracks every source file under components/, pages/, stores/, composables/, plugins/, layouts/, and app/. The suite hits 99% lines, 99% statements, 99% functions, and 91% branches. Remaining gaps are defensive optional chains and the SSR guard.
  • Focus on Behavior: Tests assert rendered output, route-driven state (hash-based timeline expansion), and user interactions (theme toggle, mobile menu open/close, focus trap) rather than implementation details.
  • Fast Local Loop: The full suite runs in about a second and a half via npm run test, with npm run test:watch for iteration and npm run test:coverage to refresh the report.

Accessibility (A11Y)

Accessibility was part of the build from the start.

  • Skip Navigation: A skip-to-content link lets keyboard and screen reader users bypass the header and jump straight to the page content.
  • Semantic HTML: Proper heading hierarchy, landmark elements, and native interactive controls throughout.
  • Keyboard Navigation: Every interactive element, including the expanding resume timeline and mobile navigation, is fully operable via keyboard with visible focus indicators. The mobile menu traps focus while open and closes on Escape.
  • Inert on Collapsed Panels: The timeline uses the inert attribute on collapsed content panels rather than just aria-hidden. inert removes keyboard focus, pointer events, and accessibility tree presence in one attribute, which is what this pattern actually needs.
  • Reduced Motion: A global prefers-reduced-motion media query disables animations and transitions for users who request it.
  • ARIA and State Attributes: Custom components like the timeline toggle, theme switch, and mobile menu use appropriate roles, labels, and state attributes for assistive technologies.
  • Dual-Theme Contrast: Both the dark and light themes use color pairings chosen to maintain readable contrast ratios.