From c420bc9322979b6281288db3ac791713dc16c4d2 Mon Sep 17 00:00:00 2001 From: Joan Code <172996447+joan-code6@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:54:54 +0200 Subject: [PATCH 1/2] Skip prerendering and school cache for PR previews --- .github/workflows/build.yml | 12 +++++++----- README.md | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14f2133d..87c24f46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,19 +36,21 @@ jobs: - name: Install locked frontend dependencies run: npm ci --no-audit --no-fund - name: Cache public school data and rendered markup + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/school-pages - # PRs can only write caches for their own ref. Production restores - # the separate main prefix; all entries are validated by the renderer. + # Only production builds use this cache; PR previews skip rendering. + # All entries are validated by the renderer. # A unique save key persists data refreshed during each successful run. - key: school-pages-v1-${{ runner.os }}-node24-${{ github.event_name == 'pull_request' && 'pr' || 'main' }}-${{ github.run_id }}-${{ github.run_attempt }} + key: school-pages-v1-${{ runner.os }}-node24-main-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | - school-pages-v1-${{ runner.os }}-node24-${{ github.event_name == 'pull_request' && 'pr' || 'main' }}- - - name: Build and render public HTML + school-pages-v1-${{ runner.os }}-node24-main- + - name: Build frontend run: npm run build env: NODE_ENV: production + SKIP_PRERENDER: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') && '0' || '1' }} VITE_API_URL: https://lanis-backend.joancode.dev - name: Check deployment security rules run: node --test scripts/cloudflare/*.test.cjs diff --git a/README.md b/README.md index 000d5d0d..9c0cf4ce 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,9 @@ browser. The client mounts normally, preserving stored themes and backend selection. This is build-time generation, not a server needed at runtime. Rendering failures fail the build. `SKIP_PRERENDER=1` explicitly skips generation -for local troubleshooting; CI requires the generated public pages for deployment. +for local troubleshooting and PR previews. CI prerenders only production builds +from pushes and scheduled runs on `main`; PRs still run TypeScript and Vite and +deploy a client-rendered preview without fetching or caching school data. `PRERENDER_STRICT` is no longer needed or used. Host the contents of **`dist/`** as a static site over HTTPS. Configure the host to serve existing assets and prerendered pages first, then fall back to **`/index.html`** for client-side routes such as `/login`, `/dashboard`, and `/demo/messages`. The app uses root-relative routes and assets, so deploy it at the domain root unless you also adapt those paths. @@ -183,8 +185,8 @@ from lanis_api (`GET /schools`, `GET /schools//data`); image traffic uses it - A daily Actions run at **03:17 UTC** refreshes and deploys public data even without a code change. The deployment validator accepts only a successful scheduled run for the current main commit and the trusted repository. -- PR and main cache prefixes are separate; untrusted PRs cannot write the - default branch's cache. Missing/corrupt cache entries are rebuilt. +- Only production builds on `main` restore and save the school-page cache; + PR previews skip it. Missing/corrupt cache entries are rebuilt. No runtime rendering service, headless browser, or school-by-school image download is needed for generation. Source code is in From ecb2c4234d272735aef2cbd28d523b7dbad71b60 Mon Sep 17 00:00:00 2001 From: Joan Code <172996447+joan-code6@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:56:48 +0200 Subject: [PATCH 2/2] Allow preview deployment without prerendered status page --- scripts/cloudflare/deploy.cjs | 8 ++++++-- scripts/cloudflare/deploy.test.cjs | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/scripts/cloudflare/deploy.cjs b/scripts/cloudflare/deploy.cjs index b567b018..54ba5c97 100644 --- a/scripts/cloudflare/deploy.cjs +++ b/scripts/cloudflare/deploy.cjs @@ -26,8 +26,12 @@ function validateFiles(directory) { } } validateFiles(assets); -for (const file of ['index.html', 'status/index.html', 'sw.js']) { - if (!fs.statSync(path.join(assets, file)).isFile()) throw new Error(`Missing ${file}`); +// PR previews use the SPA fallback; only production requires prerendered HTML. +const requiredFiles = ['index.html', 'sw.js']; +if (kind === 'production') requiredFiles.push('status/index.html'); +for (const file of requiredFiles) { + const target = path.join(assets, file); + if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error(`Missing ${file}`); } // Preserve previews from branches predating school pages. For school builds, // ensure every URL advertised in the sitemap has a static page before publishing. diff --git a/scripts/cloudflare/deploy.test.cjs b/scripts/cloudflare/deploy.test.cjs index 491a2a09..bc65cd75 100644 --- a/scripts/cloudflare/deploy.test.cjs +++ b/scripts/cloudflare/deploy.test.cjs @@ -49,6 +49,35 @@ test('artifact scripts, dotenv and configuration cannot change the deploy comman assert.match(fs.readFileSync(path.join(assets, '_headers'), 'utf8'), /noindex/); }); +test('client-rendered previews deploy without prerendered status HTML', t => { + const {root, assets, invoke} = fixture(t); + fs.rmSync(path.join(assets, 'status'), {recursive: true}); + const result = invoke(); + assert.equal(result.status, 0, result.stderr); + const invocation = JSON.parse(fs.readFileSync(path.join(root, 'invocation.json'))); + assert.equal(invocation.config.name, 'lanis-ui-preview'); + assert.equal(invocation.config.assets.not_found_handling, 'single-page-application'); + assert.match(fs.readFileSync(path.join(assets, '_headers'), 'utf8'), /noindex/); +}); + +test('production still requires prerendered status HTML', t => { + const {root, assets, invoke} = fixture(t); + fs.rmSync(path.join(assets, 'status'), {recursive: true}); + const result = invoke({DEPLOY_KIND: 'production'}); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Missing status\/index.html/); + assert.equal(fs.existsSync(path.join(root, 'invocation.json')), false); +}); + +test('production with prerendered status HTML still deploys', t => { + const {root, invoke} = fixture(t); + const result = invoke({DEPLOY_KIND: 'production'}); + assert.equal(result.status, 0, result.stderr); + const invocation = JSON.parse(fs.readFileSync(path.join(root, 'invocation.json'))); + assert.equal(invocation.config.name, 'lanis-ui'); + assert.equal(invocation.args[0], 'deploy'); +}); + test('symlinked artifacts fail before invoking Wrangler', t => { const {root, assets, invoke} = fixture(t); fs.symlinkSync(__filename, path.join(assets, 'linked-file'));