From 5e4cf21e2d970717f222b11ae5c4de627d88e3be Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Thu, 25 Jun 2026 14:14:11 +0300 Subject: [PATCH] ci: prerender against the internal mesh API + fail loud if empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fedora-44 runner reaches the public internet (the CV gist on api.github.com prerendered fine) but not the public rob.tn — split-horizon DNS — so every moments-API-sourced route baked empty loading states while the build still "succeeded". prefetchQuery swallows fetch errors, so the empty snapshot shipped silently. - Point VITE_API_BASE at the internal API the deploy already reaches (http://nikola.kosherinata.internal:42424/v1), verified reachable over the mesh. Only affects the SSR/prerender build; the browser bundle still uses the relative /api/v1. - run-prerender.mjs now aborts (exit 1) if zero dynamic routes are enumerated, i.e. the API was unreachable — so an empty snapshot can never deploy again. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01X7zF7Kf4JqDwa6M8Qgge9M --- .gitea/workflows/deploy.yml | 9 ++++++--- .gitea/workflows/refresh.yml | 4 +++- ui/run-prerender.mjs | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f24b019..49eb4d0 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -48,9 +48,12 @@ env: API_UPSTREAM_SCHEME: http API_UPSTREAM_ADDR: nikola.kosherinata.internal:42424 MUSL_TARGET: x86_64-unknown-linux-musl - # The prerender fetches the live public API at build time; the client bundle - # keeps the same-origin relative /api/v1 that nginx proxies. - VITE_API_BASE: https://rob.tn/api/v1 + # Build-time prerender base: the runner is on the internal mesh and can't + # resolve/reach the public rob.tn, so fetch the API directly over the mesh + # (same host the deploy reaches). The api binary serves its routes under /v1. + # The client bundle still ships the same-origin relative /api/v1 that nginx + # proxies (VITE_API_BASE only affects the SSR build, not the browser bundle). + VITE_API_BASE: http://nikola.kosherinata.internal:42424/v1 DEPLOY_KEY: | ${{ secrets.RSYNC_SSH_KEY }} diff --git a/.gitea/workflows/refresh.yml b/.gitea/workflows/refresh.yml index e7389ff..6e34f45 100644 --- a/.gitea/workflows/refresh.yml +++ b/.gitea/workflows/refresh.yml @@ -26,7 +26,9 @@ env: API_PORT: "42424" API_UPSTREAM_SCHEME: http API_UPSTREAM_ADDR: nikola.kosherinata.internal:42424 - VITE_API_BASE: https://rob.tn/api/v1 + # Internal mesh API base (the runner can't reach the public rob.tn); see + # deploy.yml. The api serves its routes under /v1. + VITE_API_BASE: http://nikola.kosherinata.internal:42424/v1 DEPLOY_KEY: | ${{ secrets.RSYNC_SSH_KEY }} diff --git a/ui/run-prerender.mjs b/ui/run-prerender.mjs index 05bc7d3..2b2463b 100644 --- a/ui/run-prerender.mjs +++ b/ui/run-prerender.mjs @@ -81,6 +81,22 @@ function outPathFor(route) { const template = await readFile(join(distDir, 'index.html'), 'utf8'); let routes = await collectRoutes(); +// Guard: the dynamic routes (blog posts, projects) are enumerated from the +// moments API. If none came back, the API was unreachable at build time — fail +// loudly rather than bake and deploy an empty, content-free snapshot. (The CV, +// sourced from the public GitHub gist, can still succeed independently.) +const dynamicRouteCount = routes.filter( + (r) => r.startsWith('/project/') || r.startsWith('/blog/'), +).length; +if (dynamicRouteCount === 0) { + console.error( + 'prerender: 0 dynamic routes enumerated — the moments API was unreachable ' + + 'at build time. Aborting so an empty snapshot is never deployed. ' + + `(VITE_API_BASE=${process.env.VITE_API_BASE ?? '(unset)'})`, + ); + process.exit(1); +} + // Optional filter args: `node run-prerender.mjs /activity /cv` bakes only the // routes whose path starts with one of the given prefixes (useful for quick // iteration). With no args, every route is baked.