8 min read / ,

Rebuilding joshsalway.com: From Statamic to Astro

TLDR Rebuilt from Statamic to Astro 6 with zero-JS static pages and React islands.

I rebuilt this site from the ground up. The old stack was Statamic CMS running as a static site generator with Alpine.js, Tailwind CSS, and Vite, deployed on Netlify. The new stack is Astro 6 with React islands, Tailwind CSS 4, and the same Netlify deployment.

The core design is identical: same WebGL hero, same 8 colour themes, same Spotify-inspired dark palette. But the architecture is fundamentally different, and the site picked up a lot of new features along the way.

Why the rebuild

Three reasons:

  1. Statamic is overkill for a static blog. I was running a full Laravel application with a CMS control panel, PHP runtime, and Composer dependencies just to generate HTML files. The content is markdown files in a folder.

  2. JavaScript budget. The old site loaded Alpine.js on every page even though most pages had no interactivity. The About page doesn’t need a JavaScript framework.

  3. Developer experience. I wanted type-safe content schemas, better markdown handling, and a faster build pipeline.

The new stack

  • Astro 6 with islands architecture
  • React only for interactive components (theme switcher, search, contact form)
  • Tailwind CSS 4 via the Vite plugin, no PostCSS config needed
  • Astro Content Collections with type-safe Zod schemas
  • Netlify same host, same domain, same form handling

What ships zero JavaScript

This is the key architectural win. In Astro, pages are static HTML by default. JavaScript only loads for components that explicitly opt in with client: directives.

Pages that ship zero JavaScript:

  • About
  • Projects
  • Articles index
  • 404 page

Pages that ship only the JavaScript they need:

  • Home: WebGL canvas + theme initializer
  • Article pages: reading progress bar, table of contents, code copy button
  • Contact: form submission handler
  • Every page: nav (search modal, theme picker, dark mode toggle)

The nav components hydrate with client:load (immediately) while the Konami code easter egg uses client:idle (when the browser is idle). This is granular control you don’t get with a full SPA framework.

View Transitions

The site uses Astro’s ClientRouter for SPA-like navigation between pages. Page transitions are smooth with no full reload flash. The inline <head> script handles theme persistence before paint, so accent colours never flicker between pages, even on Windows where the scrollbar gutter is reserved with scrollbar-gutter: stable to prevent layout shifts.

New features

Command palette search (Ctrl+K)

A Fuse.js-powered search modal accessible via Ctrl+K, Cmd+K, or the search icon in the nav. Searches across all pages, articles, and topic tags with fuzzy matching. Clickable tag filter chips let you browse by topic (Laravel, React, Tutorial, etc.). Shows all content on open with keyboard navigation (arrow keys + enter).

Article metadata

Each article card shows topic tags, depth and length bars (1-5 scale), and reading time. The depth bar indicates how technical the article is, the length bar shows how long it is. Inspired by the complexity bars on the projects page.

Reading progress bar

A thin accent-coloured bar fixed to the top of the viewport that tracks scroll position on article pages. Uses a passive scroll listener for performance.

Table of contents

Auto-generated from article headings (h2/h3). Only appears when an article has 3 or more headings. Highlights the current section using IntersectionObserver.

Code copy button

Hover over any code block to reveal a copy button. Provides visual feedback (“Copied!” / “Failed”) and matches the dark code block aesthetic. Inline code elements break words on mobile instead of overflowing the page.

Previous/next article navigation

Links to adjacent articles at the bottom of each article page, ordered chronologically.

RSS feed

Available at /feed.xml. RSS readers auto-detect it via the <link rel="alternate"> tag in the HTML head. There’s an RSS link inline with the Articles heading on the articles page.

Sitemap

Auto-generated by @astrojs/sitemap at /sitemap-index.xml with priority weighting for each page type.

JSON-LD structured data

Every article page includes Schema.org Article markup for better search engine results.

Dynamic text selection colour

Text selection colour in dark mode now matches the active theme accent. Switch to Ocean theme and your selection highlight turns blue. Switch to Gold and it turns yellow.

Custom 404 page

A styled error page matching the site design with links back to the home page and articles.

Every article has a short URL for sharing. For example, this post-mortem can be shared as joshsalway.com/post-mortem-pr-59323.

Projects page redesign

Project cards now show the project icon (FragHub’s yellow PUBG helmet, Barcoder’s barcode scan icon, Poker Timer’s clock), full tech stack tags, a complexity bar, hosting info, pill-style action buttons, and “Read Article” links to the matching blog post. FragHub gets a rotating rainbow border as the featured project. Non-featured cards have a green border, background lift, and shadow glow on hover. The whole card is clickable. Tech filter pills at the top let you filter projects by stack (React, Laravel, Tailwind CSS, etc.).

The “JS” logo in the nav has a blinking cursor. Hover over it and it deletes “JS”, then types a random word from a list of 180+ options (Zigzagging, Brewing, Flibbertigibbeting…) in red with a spinning asterisk. Move away and it types “JS” back.

Clickable tags

Topic tags on article cards and article headers open the search modal pre-filtered to that tag. The articles page has a tag filter bar that filters articles in place without a page reload. Same for the tech filter on the projects page.

About page

Profile photo pulled from GitHub, open source contribution mention, and links to get in touch.

Konami code

There’s a hidden easter egg on the site. Press Up, Up, Down, Down, Left, Right, Left, Right, B, A on your keyboard (the classic Konami code from 1986) and watch what happens. You’ll get a confetti cannon in all 8 theme colours, followed by a playable Space Invaders game. Use arrow keys to move, space to shoot, ESC to close. A small nod to the gaming roots that got me into programming.

SEO and redirects

The old Statamic site served articles at root-level URLs (/laravel-pr-59323-post-mortem). The new site uses /articles/ as a prefix. All 14 old URLs have 301 permanent redirects configured in netlify.toml so existing links from X, Google, and backlinks resolve correctly.

Canonical URLs, Open Graph tags, X/Twitter cards, and Google Analytics carry over from the old site.

Build performance

The full site builds in under 2 seconds. The old Statamic SSG pipeline took 8-10 seconds including the PHP runtime, Vite asset compilation, and static generation step.

Performance and accessibility

Lighthouse scores: 91 performance, 95 accessibility, 100 best practices, 100 SEO on the home page.

A few things that helped get there:

  • Lazy-loaded highlight.js: instead of bundling the full 969KB library on every page, it only loads via dynamic import() when the page actually has code blocks. Non-article pages ship zero highlight.js.
  • Touch targets: tag filter pills and nav buttons meet the 44px minimum on touch devices for WCAG compliance.
  • Service worker: the site works offline. A network-first strategy caches pages as you browse. Core pages (home, about, articles, projects, contact) are precached on first visit. If you lose connection, cached pages still load. Useful on dodgy mobile connections.
  • Image preloads: profile photo and project icons are preloaded in the HTML head so they render instantly without a pop-in flash.

No CMS, better security

There’s no CMS. No admin panel, no database-backed editor, no login screen. When I want to write or edit an article, I open Claude Code and work directly with .md files. It commits to git, Netlify auto-deploys, and the article is live. The entire content workflow is the terminal.

Statamic’s control panel was nice, but I never used it. I was always editing markdown files directly anyway. Removing the CMS removed an entire category of complexity and security surface: no PHP runtime, no authentication, no session management, no database, no admin panel to exploit, no plugins to patch. The attack surface is a CDN serving static HTML files. The attack surface is about as small as it gets.

What I’d do differently

If I were starting from scratch today, I’d skip the intermediate step. I initially rebuilt in Next.js before realising a static blog doesn’t need a full React SPA framework. The Astro rebuild was the right call: it’s the purpose-built tool for content sites with selective interactivity.

The Next.js version still exists on the master branch if I ever need it. The Astro version is on feat/astro-rebuild.