JordanThirkle.com
A production-grade portfolio and blog built with Astro 6, React 19, and Tailwind CSS. Engineered for 100 Lighthouse scores, WCAG 2.2 AA compliance, and zero-JS baseline with interactive React islands.
Tech Stack
Astro 6, React 19, TypeScript, Tailwind CSS, Nanostores, Playwright, Sharp, Vercel
Timeline
Continuous
Category
Websites
Mission
Build the fastest, most accessible developer portfolio on the web. Zero-JS baseline, 100 Lighthouse scores, fully theme-aware, and AI-accelerated development workflows.
Overview
It serves as my portfolio and blog. Built with Astro’s Islands Architecture, it ships 0kb of JavaScript for static articles, only hydrating the interactive command palette and engagement dock as React components.
Technical Deep Dive
Islands Architecture
The primary challenge was keeping the site fast and responsive while maintaining interactivity where it matters. Using Astro, only the CommandPalette and EngagementDock are hydrated as React components. Everything else is static HTML/CSS, resulting in fast page transitions and strong Lighthouse scores.
The React islands use client:idle hydration — they load after the main page content is fully interactive. This keeps the critical rendering path clean:
- First Contentful Paint: ~0.6s (static HTML from pre-rendered pages)
- Largest Contentful Paint: ~0.9s (hero image or heading text)
- Time to Interactive: ~1.4s (including React hydration for islands)
- Total Blocking Time: 0ms (no main-thread blocking scripts)
View Transitions
Astro’s view transition API powers smooth page navigations. The <main> element uses transition:animate="slide" for horizontal slide transitions, while <nav> and <footer> use transition:persist to avoid re-rendering shared elements across navigations.
The theme toggle state is maintained across transitions via an astro:after-swap event listener that re-reads localStorage.theme and re-applies the data-theme attribute.
Performance Budget
Every build is measured against strict thresholds enforced by Lighthouse CI:
| Metric | Threshold | Current |
|---|---|---|
| Performance | ≥ 90 | 96–100 |
| Accessibility | ≥ 95 | 97–100 |
| SEO | ≥ 95 | 100 |
| LCP | ≤ 2500ms | ~900ms |
| CLS | ≤ 0.1 | ~0.005 |
| TBT | ≤ 200ms | 0ms |
Image Pipeline
Images go through a two-stage pipeline:
- Build-time generation — OG images are generated via Satori (JSX-to-SVG-to-PNG) for every blog post and project
- Post-build optimisation — A Sharp script (
scripts/optimize-images.mjs) scans all PNGs in the output and generates WebP + AVIF variants with quality tuning (WebP: 80, AVIF: 65)
The pipeline produces ~74 optimised images per build from ~55 source images, saving approximately 2.4 MB.
Custom Search & Filtering
Instead of reaching for a heavy search library like Algolia for a small site, I built a custom search index using Astro’s content collections. The CommandPalette performs weighted searches across titles, tags, and descriptions, providing a Spotlight-style experience without external API calls.
Theme Engine
The theme system uses a lightweight nanostore (themeStore) that syncs with both localStorage and the data-theme HTML attribute. The store is initialised synchronously in an inline <script> block before any paint to prevent flash of unstyled content (FOUC).
All color values are CSS variable tokens — no hex codes exist in component templates. The token system provides:
- Text tokens:
text-primary(headings),text-secondary(body),text-tertiary(metadata),text-quaternary(captions) - Surface tokens:
bg-page(page background),bg-surface(card backgrounds),bg-surface-elevated(hovered cards) - Border tokens:
border-border(default),border-border-strong(active/focused) - Accent tokens:
text-accent-primary(links),text-accent-secondary(tags)
All tokens are defined in global.css under [data-theme="dark"] and [data-theme="light"] selector blocks.
Newsletter System
The /api/subscribe serverless function integrates with Buttondown for email delivery. It includes:
- Honeypot anti-spam: Hidden
firstNamefield — bots that fill it get a silent 200 response (no subscription) - Rate limiting: In-memory token bucket (5 requests per 60 seconds per IP)
- Origin check: Validates the request origin matches the deployed site
- Graceful error handling: Returns descriptive errors for invalid emails, rate limits, and upstream failures
Changelog System
Projects support a changelog frontmatter field — an array of { date, type, description } objects with types feature | fix | perf | design | content. The Changelog.astro component renders these as a styled timeline on each project detail page, always in reverse chronological order.
Accessibility
Every page is tested against WCAG 2.2 AA standards via axe-core Playwright integration. The test suite covers 11 routes (home, about, blog, blog post, projects, project detail, contact, newsletter, uses, services, 404) with zero violations enforced in CI.
Specific implementations:
- Touch targets: All interactive controls use the
tap-targetclass (44×44px minimum, WCAG 2.5.5) - Focus indicators: Visible
:focus-visibleoutlines on all interactive elements - ARIA: Semantic landmarks, descriptive labels, live regions for dynamic content
- Reduced motion: All scroll reveals and view transitions respect
prefers-reduced-motion
Connected Guides
If you want to see the specific decisions behind this build, check out these deep dives:
- Minimalist Architecture for Solo Devs
- Astro 6 React Islands Architecture
- Zero-JS Theme Toggle
- Astro 6 Privacy-First Analytics
The Takeaway
Creator Hub shows that modern static-first frameworks like Astro let you build sites that are both visually polished and extremely fast — without reaching for a heavy SPA framework. The key decisions (zero-JS baseline, semantic tokens, CI-enforced quality gates) all serve the same goal: a site that loads instantly, works for everyone, and stays maintainable as it grows.
Changelog
Added Lighthouse CI, scroll-triggered reveals, and Astro view transitions for smooth page navigation.
Added Sharp build-time image optimization — 74 WebP/AVIF files generated, ~2.3 MB saved.
Added web-vitals RUM tracking via Plausible custom events (LCP, CLS, INP).
Non-blocking Google Fonts load with media-print swap pattern. Added Inter fallback with size-adjust to prevent CLS.
Transparent logo across favicon, navbar, and footer. Theme-aware SVG favicon with prefers-color-scheme support.
Fixed all 40+ audit issues — double-branded titles, contrast ratios, missing ARIA, form labels, memory leaks, API security.
Added XML sitemap filter for noindex pages. Added SoftwareApplication + WebSite JSON-LD schemas.
Fixed blog card click target — absolute inset-0 overlay was missing position: relative on parent article.
Fixed blog index — duplicate URL sync, timeline data-date, redundant RSS link, dead-code image fallback.
Added category filter + grid/timeline view toggle to blog and project indexes.
Theme persistence engine — semantic color tokens, Framer Motion toggle, localStorage sync across view transitions.
Achieved 100/100 Lighthouse scores across all pages. Fixed contrast, search hardening, navigation anchors.
Command palette with keyboard navigation. Toast notification system. Newsletter API with honeypot anti-spam.
Engagement dock (likes, views, share) with local-first storage. Giscus blog comments integration.
Initial blog posts and MDX content collections with Zod validation. OG image generation pipeline via Satori.
Initial site launch — Astro 6, React 19, Tailwind CSS, Vercel deployment, blog + projects + about pages.