Astro Islands & Interactivity: Where Client-Side JS Actually Lives in a Static-First Framework

Premier League Predictor: Astro

Chapter 10 · Astro Islands & Interactivity: Where Client-Side JS Actually Lives in a Static-First Framework

Two real promises have been sitting open since earlier chapters: Chapter 4's own hardcoded seasonId/gameweekId, waiting for "a real gameweek/season selector," and Chapter 1's own note that output: 'server' makes every page dynamic by default — "a deliberate, temporary tradeoff" — pending a return to the question of which pages could genuinely opt back into static prerendering. This chapter closes both, and starts with what "Astro Islands" actually means, since this course has never used one.

What an Island Actually Is — and Why This Course Has Never Needed One

Astro's real Islands Architecture is specifically about embedding components from a UI framework — React, Vue, Svelte, Preact, Solid — inside an otherwise static or server-rendered Astro page, and choosing exactly when each one hydrates on the client via a client:* directive: client:load (immediately), client:idle, client:visible, client:media, or client:only. Each one is a genuinely isolated "island" of client-side JavaScript-framework interactivity sitting in a sea of otherwise plain HTML.

Every interactive page this course has built — Chapter 4's fixture-entry grid, Chapter 5's prediction forms — has used a plain <script> tag instead, with no framework component and no client:* directive anywhere. That's a genuinely different, simpler mechanism: Astro bundles that script into a real ES module and ships it to the browser, but there's no framework runtime, no component hydration, and nothing being "reactivated" the way an island's own framework component is. This wasn't an oversight — every interaction this app needs (clicking a team button, submitting a prediction) is a handful of DOM updates driven by plain event listeners, and installing a UI framework specifically to build a five-button grid or a two-input form would be a real, ongoing dependency added for no real gain.

This is the honest, structural reason this chapter never installs React or Vue
Reaching for an island only pays off once a piece of UI genuinely needs a framework's own state-management and re-rendering model — a live-updating form with several interdependent fields, for instance. Nothing this app has built so far crosses that line; every "interactive" page is really just "read some data, update a few DOM nodes when it's clicked." The real selector built in this chapter uses Astro's own actual strength instead — real, server-rendered dynamic routes — which turns out to solve the exact problem a client-side island would otherwise have been reached for.

The Real Selector: A Dynamic Route, Not Client-Side State

Rather than keeping one fixed /admin/fixtures page and switching gameweeks with client-side JavaScript, the season and gameweek become real parts of the URL — a plain Astro dynamic route, resolved server-side on every request:

--- // src/pages/admin/seasons/[seasonId]/gameweeks/[gameweekNumber]/fixtures.astro import { db } from '../../../../../../lib/db'; const seasonId = Number(Astro.params.seasonId); const gameweekNumber = Number(Astro.params.gameweekNumber); const season = db.prepare('SELECT * FROM seasons WHERE id = ?').get(seasonId); if (!season) { return Astro.redirect('/admin/seasons'); } const gameweek = db.prepare( 'SELECT * FROM gameweeks WHERE season_id = ? AND number = ?' ).get(seasonId, gameweekNumber) as { id: number } | undefined; // Every season this app has, for the season