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
| Option | What it actually means | Real tradeoff |
|---|---|---|
| Same Astro project | Add this app's own pages/API routes directly into the live site's own existing codebase | Smallest code footprint, but depends entirely on how the live site is actually deployed today |
| Path-mounted reverse proxy | Keep this app fully separate (exactly as built and deployed in Chapter 11), nginx routes /predictor/ to it | Zero risk to the existing site; the one real path issue this chapter builds out in full |
| Subdomain | predictor.osztromok.com, a fully independent nginx server block | Simplest 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.
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.
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.
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:
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:
And in every client-side script since Chapter 4, the identical constant, read the identical way:
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
/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
| Chapter | What it contributed to the finished app |
|---|---|
| 1 | Project setup, the shared 4-source spec, output: 'server' + @astrojs/node |
| 2 | The real SQLite schema — teams, seasons, season_teams, gameweeks, fixtures |
| 3 | Season/team admin routes, db.transaction()'s own atomicity pattern reused throughout |
| 4 | The click-to-pair fixture-entry UI; the addEventListener-over-onclick discipline every later script follows |
| 5 | Recording all four prediction sources per fixture, with the partial unique index correction |
| 6 | The real 40/10 scoring and guest-averaging principle |
| 7 | The real league table; getLeagueTable(), reused unchanged in Chapter 9 |
| 8 | The prediction league table, aggregating Chapter 6's own points_awarded across a season |
| 9 | Promotion & relegation, reusing Chapter 7's own function with no refactor needed |
| 10 | The real dynamic-route selector and the prerender audit this capstone builds directly on |
| 11 | The standalone server, WAL mode, backups, and the Basic Auth gating this capstone extends |
| 12 | BASE_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
Explain precisely why a
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 solutionSet 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 solutionChapter 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