// Single source of truth for clean-URL routing, shared by next.config.mjs // (dev + preview rewrites) and scripts/build-static.mjs (clean-path output + // the Cloudflare Pages _redirects file). Each entry maps a public URL to the // built file that serves it. // // url the public path (":seg" marks a dynamic segment) // file the built file under public/ that answers it // next true = a Next route (not a /landing static file) // // Non-param entries become real files at their clean path in the static // bundle (any static host serves them directly). Param entries can't be // pre-generated per value, so the host rewrites the pattern to `file` — on // Cloudflare Pages via the generated out/_redirects (see build-static). export const routes = [ // Home (the static mirror). English serves at the bare root: it is the URL // every inbound link and every nav logo already points at, and the one // Google treats as the site. "/en" 301s here (see build-static step 5). { url: "/", file: "/landing/index.html" }, { url: "/ja", file: "/landing/index.html" }, // Marketing + legal + guides { url: "/experiences", file: "/landing/experiences/index.html" }, { url: "/experiences/enterprise", file: "/landing/experiences/enterprise/index.html" }, { url: "/about", file: "/landing/about/index.html" }, { url: "/press", file: "/landing/press/index.html" }, { url: "/press/images", file: "/landing/press/images/index.html" }, { url: "/affiliate", file: "/landing/affiliate/index.html" }, { url: "/terms", file: "/landing/terms/index.html" }, { url: "/privacy", file: "/landing/privacy/index.html" }, { url: "/hardwareterms", file: "/landing/hardwareterms/index.html" }, { url: "/warranty", file: "/landing/warranty/index.html" }, { url: "/displays", file: "/landing/displays/index.html" }, { url: "/mybeyond", file: "/landing/mybeyond/index.html" }, { url: "/myhalomount", file: "/landing/myhalomount/index.html" }, { url: "/myaudiostrap", file: "/landing/myaudiostrap/index.html" }, // Not-found page. Previewable here; in production Cloudflare Pages serves it // as out/404.html for every unmatched URL (copied there by build-static.mjs). { url: "/404", file: "/landing/404/index.html" }, // Bigscreen-software pages { url: "/remotedesktop", file: "/landing/remotedesktop/index.html" }, { url: "/purchase/:bigMediaId/:url", file: "/landing/purchase/index.html" }, { url: "/ticketpolicy", file: "/landing/ticketpolicy/index.html" }, { url: "/browser/:activityId", file: "/browser", next: true }, // Account system { url: "/account/login", file: "/landing/account/login/index.html" }, { url: "/account/signup", file: "/landing/account/signup/index.html" }, { url: "/account/forgot", file: "/landing/account/forgot/index.html" }, { url: "/account/reset/:token", file: "/landing/account/reset/index.html" }, { url: "/account/home", file: "/landing/account/home/index.html" }, { url: "/account/orders", file: "/landing/account/orders/index.html" }, { url: "/account/order/:id/start", file: "/landing/account/order-start/index.html" }, { url: "/account/order/:id/ipd", file: "/landing/account/order-ipd/index.html" }, // Email-link entry URLs (embedded in transactional emails — do not rename) { url: "/token2/login/:token", file: "/landing/account/bridge/index.html" }, // Legacy v1 token URLs from old emails. The old app's /token/login called a // method that didn't exist (blank page for years); the bridge now tries the // token against the v2 exchange and falls back to the expired-link state. // /token/purchase just forwards into the purchase flow (slug is cosmetic). { url: "/token/login/:token", file: "/landing/account/bridge/index.html" }, { url: "/token/purchase/:bigMediaId", file: "/landing/account/bridge/index.html" }, { url: "/email/verify/:token", file: "/landing/account/bridge/index.html" }, { url: "/email/update/:token", file: "/landing/account/bridge/index.html" }, { url: "/reset/:token", file: "/landing/account/bridge/index.html" }, { url: "/forgot", file: "/landing/account/bridge/index.html" }, { url: "/email_verify", file: "/landing/account/outcome/index.html" }, { url: "/email_verify_failed", file: "/landing/account/outcome/index.html" }, { url: "/email_update", file: "/landing/account/outcome/index.html" }, { url: "/email_update_failed", file: "/landing/account/outcome/index.html" }, // Face-scan + eyetracking (URLs embedded in emails, the app clip, the ET client) { url: "/scans", file: "/landing/scans/start/index.html" }, { url: "/scans/start", file: "/landing/scans/start/index.html" }, { url: "/scans/failedtoken", file: "/landing/scans/failedtoken/index.html" }, { url: "/scan/:token/process", file: "/landing/scans/process/index.html" }, { url: "/bset/pushtoken/:etToken", file: "/landing/bset/token/index.html" }, ]; /* ---- Locale mirrors ------------------------------------------------------- * English is the source and keeps the canonical URLs (/displays). Every other * locale is a prefixed mirror pointing at the matching file that * scripts/build-pages.mjs emits under /landing//. * * Generated rather than hand-listed: this table is already the single source of * truth for routing, and a second hand-maintained copy per locale is a list * that goes stale the first time someone adds a page. * * Skipped deliberately: * - transactional entry URLs (email links, the app clip, the ET client). Those * strings are embedded in already-sent emails and shipped clients, so a * locale prefix buys nothing and the surface is not marketing copy. * - "/" and "/ja", which are the home page itself, already listed above. */ const LOCALE_MIRRORS = ["ja"]; const NO_MIRROR = /^\/(token2|token|email|reset|forgot|email_verify|email_update|scans?|scan|bset|account|purchase|browser)\b/; // "/ja" exists above pointing at the English home file — repoint it now that // build-pages emits a Japanese one. for (const r of routes) if (r.url === "/ja") r.file = "/landing/ja/index.html"; routes.push( ...LOCALE_MIRRORS.flatMap((loc) => routes .filter((r) => !r.url.startsWith(`/${loc}`) && r.url !== "/" && !r.next && !NO_MIRROR.test(r.url)) .map((r) => ({ url: `/${loc}${r.url}`, file: r.file.replace("/landing/", `/landing/${loc}/`) })), ), ); export const isParam = (url) => url.includes(":"); /* Sitemap membership (scripts/build-static.mjs writes out/sitemap.xml from * this): the marketing/document surface only. Excluded: param routes, the * transactional family (NO_MIRROR's set plus the /email_*_failed outcomes its * \b boundary can't catch), and the 404 page — those either serve one visitor * once or aren't pages at all as far as search is concerned. */ const NO_INDEX = /^\/(token2|email|reset|forgot|email_verify|email_update|scan|scans|bset|account|purchase|browser|404)([_/]|$)/; export const isIndexable = (url) => { const base = url.replace(new RegExp(`^/(?:${LOCALE_MIRRORS.join("|")})(?=/|$)`), "") || "/"; return !isParam(url) && !NO_INDEX.test(base); }; // Next.js rewrite form (source/destination), for next.config.mjs dev + preview. export const toRewrites = () => routes.map((r) => ({ source: r.url, destination: r.next ? r.file : r.file }));