mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 04:20:39 +00:00
The root layout handed every page the plan catalog (lib/rooms.ts configure with lib/data/plans.json), and /pricing, /calculator, /customers and the benchmark pages bundled the 202 KB price list. lib/api is now the one reader: plans() GET /v1/billing/plans, pricing() /v1/pricing, summary(), served() /v1/models, templates(), blueprints() /v1/blueprint/sbom, one promise a path a page load, each drawn as loading, ready or "api.hanzo.ai is unreachable" (components/api) and never an older number. plans.json, pricing.json, models.json, templates.json, blueprints.json and sync-pricing, sync-models, sync-templates, sync-blueprints are gone. What a static export must know before anyone asks, which model pages exist and their titles, is read by the build itself (lib/api/build, Next's fetch cache keyed by the build's stamp: one answer per build) and renders no rate. lib/moved reads the retired model pages against the export, and the 153 pages the old snapshot kept for models the gateway dropped are in retired.json. Kai's price beside Jev's, Enso's tier rates, a model page's rate and the GPU table read the price list in the browser; the site's plan math no longer imports @hanzo/rooms/lib/plans, whose bundled @hanzo/plans catalog rode the home, /pricing and the picker. Home first load 452.7 -> 437.4 KB gzip (budget 440); pages over perf.budget.json 32 -> 9, the rest over for settings code, connectors and the commerce catalog. The gates serve invented answers (e2e/gates/api) and block them; e2e/gates/read.spec.ts holds that no script carries a plan, price or model table.
72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
/**
|
|
* Every retired address on hanzo.ai, and what answers it now.
|
|
*
|
|
* Build-time only: the retired model pages are a table the client never needs,
|
|
* so `lib/publish` keeps MOVED and SHELVES, and this joins them with the model
|
|
* pages the export actually wrote. `scripts/edge.mjs` renders the result as the
|
|
* edge's 301s, the gates check it against the export, and `scripts/crawl.mjs`
|
|
* against production.
|
|
*
|
|
* READ AGAINST THE EXPORT, so run after `pnpm build`. Which models have a page
|
|
* is the gateway's answer at build time (lib/models/served), and the export is
|
|
* the one record of it: a page that is back is not retired, and a target that
|
|
* is gone falls back to its lab's page, then to /models. Nothing here can name
|
|
* a page the build did not make.
|
|
*/
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { MOVED, SHELVES } from './publish.ts'
|
|
import { shipped } from './routes.ts'
|
|
import retired from './data/retired.json' with { type: 'json' }
|
|
|
|
const OUT = fileURLToPath(new URL('../out', import.meta.url))
|
|
|
|
/** The model and lab pages under /models that the export wrote. */
|
|
function written(): Set<string> {
|
|
if (!existsSync(join(OUT, 'index.html'))) {
|
|
throw new Error(`lib/moved reads the model pages the export wrote, and there is no export at ${OUT}: run \`pnpm build\` first`)
|
|
}
|
|
return new Set(
|
|
shipped(OUT)
|
|
.map(({ route }) => route)
|
|
.filter((route) => route.startsWith('/models/')),
|
|
)
|
|
}
|
|
|
|
/** The retired model pages, read against the export. */
|
|
function models(): Record<string, string> {
|
|
const live = written()
|
|
const out: Record<string, string> = {}
|
|
for (const [from, to] of Object.entries(retired as Record<string, string>)) {
|
|
if (live.has(from)) continue
|
|
const lab = from.split('/').slice(0, 3).join('/')
|
|
out[from] = live.has(to) || to === '/models' ? to : live.has(lab) ? lab : '/models'
|
|
}
|
|
return out
|
|
}
|
|
|
|
let table: Readonly<Record<string, string>> | null = null
|
|
|
|
/** Exact addresses: MOVED over the retired model pages. */
|
|
export function redirects(): Readonly<Record<string, string>> {
|
|
table ??= { ...models(), ...MOVED }
|
|
return table
|
|
}
|
|
|
|
/** The shelf a route sits under and is not kept by, if any. */
|
|
export function shelf(route: string) {
|
|
return SHELVES.find(({ from, keep }) => route.startsWith(from) && !keep.some((k) => route === k || route.startsWith(k + '/')))
|
|
}
|
|
|
|
/**
|
|
* Where a retired route goes, or null for a route that is not retired. The
|
|
* route is a path in its canonical form: no query, no trailing slash, no .html.
|
|
*/
|
|
export function forward(route: string): string | null {
|
|
const exact = redirects()[route]
|
|
if (exact) return exact
|
|
const s = shelf(route)
|
|
return s ? s.to + route.slice(s.from.length) : null
|
|
}
|