Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion scripts/prerender.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,21 @@ const ROUTE_META = {
title: 'Dashboard · instanode',
description: 'instanode dashboard — manage resources, deployments, and billing.',
},
// UI-3 (2026-05-29): /app/checkout and /app/billing are external CTA
// destinations (from /pricing "Start Pro", magic-link "manage billing",
// and agent-clickable upsell links). Without pre-generated shells they
// hit GH Pages' 404.html fallback and ship HTTP 404 even when the body
// hydrates correctly — analytics noise + tab-strip title flash. Step
// 4.6 emits dist/app/checkout/index.html and dist/app/billing/index.html
// so external entries see HTTP 200.
'/app/checkout': {
title: 'Checkout · instanode',
description: 'Complete your instanode plan upgrade.',
},
'/app/billing': {
title: 'Billing · instanode',
description: 'Manage your instanode subscription, payment method, and invoices.',
},
}

/** escapeHtmlAttr — minimal escaping for text injected into an HTML
Expand Down Expand Up @@ -429,7 +444,16 @@ async function main() {
// appears in the CLI test mock and any stale terminal scrollback /
// chat transcript a user pastes. Without an entry under dist/cli-auth/,
// GH Pages returns its 404 shell and the React Navigate never runs.
const authShellRoutes = ['/login', '/login/callback', '/claim', '/cli-auth']
// UI-3 (2026-05-29): add /app/checkout and /app/billing so external CTA
// entry from /pricing → "Start Pro" or magic-link → "manage billing"
// hits pre-generated dist/app/checkout/index.html and
// dist/app/billing/index.html shells with HTTP 200 instead of the
// GH Pages 404.html fallback (status code 404 even when the body
// hydrates correctly via the catch-all SPA shell). The /app shell
// already covers /app on its own (Step 4.5). Other /app/* deep links
// remain on the 404-status fallback — they are not external CTA
// destinations and aren't worth pre-generating.
const authShellRoutes = ['/login', '/login/callback', '/claim', '/cli-auth', '/app/checkout', '/app/billing']
for (const route of authShellRoutes) {
const p = resolve(DIST, route.replace(/^\//, ''), 'index.html')
await mkdir(dirname(p), { recursive: true })
Expand Down
47 changes: 47 additions & 0 deletions src/prerender.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* prerender.test.ts — UI-3 regression guard for the SPA-fallback HTTP 404
* bug on /app/checkout and /app/billing.
*
* GH Pages serves dist/404.html with HTTP 404 for every URL that doesn't
* match a real file under dist/. Direct hits to /app/checkout?plan=...
* (external CTA from /pricing → "Start Pro") therefore shipped HTTP 404
* even though the SPA shell hydrated and CheckoutPage rendered correctly.
*
* Fix: prerender.mjs Step 4.6 now emits dist/app/checkout/index.html and
* dist/app/billing/index.html as pre-generated SPA shells. GH Pages
* serves those with HTTP 200.
*
* This test pins the source-of-truth list in prerender.mjs so a future
* refactor of Step 4.6 can't silently regress either route. (We don't
* shell out to the prerender script itself — the gate's `npm run build`
* already exercises the writeFile path; this is a fast static check.)
*/

import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, it, expect } from 'vitest'

const PRERENDER = resolve(__dirname, '..', 'scripts', 'prerender.mjs')

describe('prerender.mjs Step 4.6 — authShellRoutes (UI-3)', () => {
const src = readFileSync(PRERENDER, 'utf-8')

it('emits an SPA shell for /app/checkout (external CTA destination from /pricing)', () => {
expect(src).toMatch(/['"]\/app\/checkout['"]/)
})

it('emits an SPA shell for /app/billing (external CTA destination from magic-link)', () => {
expect(src).toMatch(/['"]\/app\/billing['"]/)
})

it('still emits the original auth shells (/login, /login/callback, /claim, /cli-auth)', () => {
expect(src).toMatch(/['"]\/login['"]/)
expect(src).toMatch(/['"]\/login\/callback['"]/)
expect(src).toMatch(/['"]\/claim['"]/)
expect(src).toMatch(/['"]\/cli-auth['"]/)
})

it('ROUTE_META carries titles for both new auth shells (so /app/checkout and /app/billing get sensible <title> tags, not the homepage default)', () => {
expect(src).toMatch(/['"]\/app\/checkout['"]:\s*{[^}]*title:/)
expect(src).toMatch(/['"]\/app\/billing['"]:\s*{[^}]*title:/)
})
})
Loading