Capstone: Integrating the Predictor Into the Existing Astro Site

Premier League Predictor: Astro

Chapter 12 · Capstone: Integrating the Predictor Into the Existing Astro Site

Every other course in this set — PHP, Django, and FastAPI — closed by mounting a genuinely separate application, written in a different language on a different runtime, alongside an existing Astro site built in something else entirely. This course starts from a real, different position: it's written in the exact same framework the live site already runs. That similarity turns out to matter less than it first appears to — and working out exactly why, precisely, is this capstone's own real job.

Three Real Ways to Mount This

OptionWhat it actually meansReal tradeoff
Same Astro projectAdd this app's own pages/API routes directly into the live site's own existing codebaseSmallest code footprint, but depends entirely on how the live site is actually deployed today
Path-mounted reverse proxyKeep this app fully separate (exactly as built and deployed in Chapter 11), nginx routes /predictor/ to itZero risk to the existing site; the one real path issue this chapter builds out in full
Subdomainpredictor.osztromok.com, a fully independent nginx server blockSimplest possible nginx config; no path-prefix problem at all, at the cost of a second real hostname to manage

A Real Correction to This Course's Own Working Assumption

Before comparing further, it's worth checking a claim this course would otherwise be tempted to make without verifying it: that mounting this app inside the exact same Astro project as the live site would require converting that site's own build from static output to output: 'server' wholesale. Checked directly against Astro's own current documentation, that's not actually true.

A static-output site can already opt individual pages into server rendering
Astro's own real, current model runs the opposite direction from what seemed likely at first: under the default output: 'static', "you may opt out of prerendering on any routes that require server rendering" with a single export const prerender = false — there's no separate named "hybrid" mode to reach for, and no need to convert the whole project to output: 'server' just to gain a handful of genuinely dynamic pages. This is a real, and genuinely useful, correction: the framework-level barrier to adding this predictor's own pages directly into the live site's own codebase is much lower than assumed.
The framework question and the infrastructure question are two separate things
Opting a page into prerender = false only changes what Astro generates — the deployed result still needs an adapter capable of actually running that on-demand code (this course has used @astrojs/node since Chapter 1) and a deployment target that can run it, not simply serve static files. Whether the live site's own current hosting already runs through a Node process, or serves purely static files with no server runtime behind it at all, is a real, separate infrastructure question this capstone can't answer from inside this course alone — it depends on exactly how that site is deployed today, which is outside this predictor's own scope to determine.

Given that real, honest uncertainty, this chapter builds out the option that's already fully proven and doesn't depend on an answer to that open question at all: the path-mounted reverse proxy, reusing Chapter 11's own already-complete, standalone deployment completely unchanged.

The Real Problem, Once Mounted at a Sub-Path

nginx routing /predictor/ to this app's own standalone server strips that prefix before forwarding the request — Astro's own router still only ever sees plain paths like /admin/seasons/1/gameweeks/1/fixtures, exactly as it always has. The problem isn't routing at all; it's every place this app generates a path meant to be followed by the browser, which only ever sees the real, external URL — including the /predictor/ prefix nginx just stripped on the way in.

A genuinely more pervasive version of the FastAPI sibling's own finding
The FastAPI & PostgreSQL course found this exact underlying problem in one specific place: its own client-side fetch('/api/...') calls. This course's own version of the identical issue shows up in three separate places at once, because Astro genuinely generates paths from three different contexts: server-rendered href attributes built in a page's own frontmatter (Chapter 10's gameweek navigation), Astro.redirect(...) calls run entirely server-side (Chapter 10's own season-not-found redirect), and client-side fetch(...) calls (every admin page since Chapter 4). All three use a plain, root-relative /admin/... or /api/... path today, and every one of them would silently resolve to the site's own real root once mounted at /predictor/ — not /predictor/admin/... — exactly the "never surfaces on localhost" trap the FastAPI course already named, just showing up in more places here.

Why the FastAPI Sibling's Own Fix Doesn't Transfer Here

The FastAPI course's own real fix was switching to relative fetch() paths plus one shared <base href="/predictor/"> tag. Checked directly against how a <base> tag actually works: it only changes how the browser resolves relative URLs — anything that doesn't start with a leading /. Every path in this app, in every one of the three places named above, is written as a root-relative path (a leading /admin/..., /api/...), specifically because that's the simplest way to write a path in Astro's own JSX-like templates and in plain fetch() calls. A <base> tag has zero effect on a root-relative path — the browser ignores it completely and resolves straight from the origin regardless. Adding one here would fix nothing at all.

The Real Fix: One BASE_PATH, Threaded Through All Three

A single environment variable, read the same way on the server and in the browser, applied everywhere a path is actually generated:

# .env PUBLIC_BASE_PATH=/predictor
Verified directly against Astro's own documentation: PUBLIC_ vars work in both places
Astro's own real documentation confirms that "only environment variables prefixed with PUBLIC_ are available in client-side code," accessed identically via import.meta.env.PUBLIC_BASE_PATH whether that code is running in a page's own server-side frontmatter or inside a plain, Astro-processed <script> tag — the exact same syntax, resolved correctly in both contexts, with no separate mechanism needed for either.

In Chapter 10's own dynamic route, every server-rendered href and every Astro.redirect() call gets the same prefix:

--- // src/pages/admin/seasons/[seasonId]/gameweeks/[gameweekNumber]/fixtures.astro (updated) const BASE_PATH = import.meta.env.PUBLIC_BASE_PATH ?? ''; // ... if (!season) { return Astro.redirect(`${BASE_PATH}/admin/seasons`); } --- <a href={`${BASE_PATH}/admin/seasons/${seasonId}/gameweeks/${n}/fixtures`}>GW{n}</a>

And in every client-side script since Chapter 4, the identical constant, read the identical way:

<script> const BASE_PATH = import.meta.env.PUBLIC_BASE_PATH ?? ''; async function addFixture() { const res = await fetch(`${BASE_PATH}/api/gameweeks/${gameweekId}/fixtures`, { // ...unchanged }); } </script>
A dev/localhost environment needs no change at all
In development, PUBLIC_BASE_PATH is simply left unset — every template literal above falls back to the empty string via ?? '', and every path resolves exactly as it always has, mounted at the site root. Only the real, deployed .env file behind the reverse proxy sets it to /predictor, meaning this exact same codebase runs correctly in both places with no separate build or code path for either.

The nginx Side, Extending Chapter 11's Own Config

# nginx: strip /predictor/ before forwarding to the exact same standalone server from Chapter 11 location /predictor/ { auth_basic "Restricted"; # Chapter 11's own admin-gating still applies unchanged auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://127.0.0.1:4321/; # the trailing slash is what strips the prefix proxy_set_header Host $host; }
Chapter 11's own Basic Auth gating gets genuinely simpler here, and genuinely less precise
Chapter 11 scoped Basic Auth specifically to /admin/, leaving a real public league-table route open. Gating this entire /predictor/ block instead is a deliberate simplification for this capstone's own example — every route behind the proxy, admin and public alike, sits behind the same password. A real production setup mounting this app under a live, already-public site would likely want two separate location blocks again, mirroring Chapter 11's own original split exactly, just each with /predictor prepended.

Chapter Attribution

ChapterWhat it contributed to the finished app
1Project setup, the shared 4-source spec, output: 'server' + @astrojs/node
2The real SQLite schema — teams, seasons, season_teams, gameweeks, fixtures
3Season/team admin routes, db.transaction()'s own atomicity pattern reused throughout
4The click-to-pair fixture-entry UI; the addEventListener-over-onclick discipline every later script follows
5Recording all four prediction sources per fixture, with the partial unique index correction
6The real 40/10 scoring and guest-averaging principle
7The real league table; getLeagueTable(), reused unchanged in Chapter 9
8The prediction league table, aggregating Chapter 6's own points_awarded across a season
9Promotion & relegation, reusing Chapter 7's own function with no refactor needed
10The real dynamic-route selector and the prerender audit this capstone builds directly on
11The standalone server, WAL mode, backups, and the Basic Auth gating this capstone extends
12BASE_PATH threaded through every generated path, making the whole app relocatable behind a real reverse proxy

What's Still Genuinely Open

Real, unresolved questions tracked in this project's own project_issues.md (P3), none of which this course has needed to answer to reach a genuinely working app:

  • Historical backfill — does tracking start only from whenever this actually launches, or does past data get entered retroactively?
  • Fixture rescheduling — real fixtures do get moved between gameweeks; this app's schema allows it mechanically (Chapter 2), but no real workflow for doing it safely was ever built
  • Single-vs-multi-user support — this remains a genuine solo-hobbyist tool; Chapter 11's own reverse-proxy password is a pragmatic stand-in for real authentication, not a substitute for it

This closes the Premier League Predictor: Astro course — a real, working four-source weekly prediction tracker, deployable standalone behind a reverse proxy today, with a clearly marked, honestly-scoped path toward eventually living inside the same Astro project as the site it was always meant to sit alongside.

Hands-On Exercises

Exercise 1

Explain precisely why a tag would do nothing at all for this app's own paths, using the real distinction between a relative URL and a root-relative URL, and contrast that directly with why the identical fix genuinely worked for the FastAPI sibling.

📄 View solution
Exercise 2

Explain the real distinction this chapter draws between "the framework question" (does adding this app to the live site's own project require converting it to output: 'server') and "the infrastructure question" (can the live site's own current deployment run on-demand code at all), and why this capstone can honestly resolve only the first one.

📄 View solution
Exercise 3

Set PUBLIC_BASE_PATH=/predictor in a real .env file, update every href/Astro.redirect()/fetch() call across Chapters 4-10 to use it, mount the app behind nginx at /predictor/ exactly as shown, and confirm every real user flow (creating a fixture, submitting a prediction, viewing the league table, navigating between gameweeks) works correctly through the mounted path — then unset the variable and confirm the exact same codebase still works correctly at the site root in dev mode with zero code changes.

📄 View solution

Chapter 12 Quick Reference — Course Complete

  • Three real options — same Astro project, path-mounted reverse proxy (built out in full), or a subdomain
  • A real correction — Astro's own export const prerender = false lets a static-output project opt individual pages into server rendering with no full output: 'server' conversion needed
  • Two separate questions — the framework barrier is lower than assumed; the real infrastructure question (can the live site's own hosting run on-demand code at all) stays honestly unresolved
  • A more pervasive version of the FastAPI sibling's own finding — this app's own root-relative paths live in server-rendered hrefs, Astro.redirect() calls, AND client fetch() calls, not client JS alone
  • <base> tags don't fix root-relative paths — verified: they only affect relative URLs, and every path here starts with a leading /
  • PUBLIC_BASE_PATH — one env var, read identically via import.meta.env on the server and in the browser, threaded through every generated path
  • nginx location /predictor/ with a trailing-slash proxy_pass — strips the prefix before forwarding to Chapter 11's own unchanged standalone server
  • Still genuinely open — historical backfill, fixture rescheduling, and single-vs-multi-user support, all tracked honestly in project_issues.md rather than silently assumed away