From d498131dd801f74d8da9148f7ae880167b015596 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 24 Jun 2026 00:24:32 +0000 Subject: [PATCH] Centralize public route authority --- .../run-hypertwist-web-surface-validation.sh | 1 + website/README.md | 1 + website/package.json | 3 +- website/scripts/render-public-sitemap.mjs | 24 ++++ .../__tests__/public-route-registry.test.ts | 46 +++++++ .../src/components/layout/MarketingShell.tsx | 2 +- website/src/public-route-registry.json | 122 ++++++++++++++++++ website/src/public-route-registry.ts | 37 ++++++ website/src/router/PublicRoutes.tsx | 41 +++--- website/src/site-data.ts | 21 --- 10 files changed, 260 insertions(+), 38 deletions(-) create mode 100644 website/scripts/render-public-sitemap.mjs create mode 100644 website/src/__tests__/public-route-registry.test.ts create mode 100644 website/src/public-route-registry.json create mode 100644 website/src/public-route-registry.ts diff --git a/scripts/run-hypertwist-web-surface-validation.sh b/scripts/run-hypertwist-web-surface-validation.sh index 8ad70f3..0c70c98 100644 --- a/scripts/run-hypertwist-web-surface-validation.sh +++ b/scripts/run-hypertwist-web-surface-validation.sh @@ -144,6 +144,7 @@ run_step \ src/__tests__/protected-app-pages.test.tsx \ src/__tests__/DashboardOverviewPage.test.tsx \ src/__tests__/app-route-tree.test.tsx \ + src/__tests__/public-route-registry.test.ts \ src/__tests__/route-shells.test.tsx \ src/__tests__/release-manifest.test.ts \ src/__tests__/public-marketing-pages.test.tsx \ diff --git a/website/README.md b/website/README.md index bc4d331..1c3f25c 100644 --- a/website/README.md +++ b/website/README.md @@ -14,6 +14,7 @@ First-party `hypertwist.app` surface for HyperTwist: - public launch-status callouts across pricing, download, and notices surfaces - route-aware metadata, canonical, Open Graph, Twitter-card, and protected-route noindex posture for the live `hypertwist.app` surface - first-party `robots.txt` plus `sitemap.xml` for the public crawlable route set while keeping `/app`, `/login`, and `/register` out of crawler posture +- a shared public-route registry that now drives marketing navigation, footer links, router entries, and sitemap generation from one source of truth - runtime diagnostics that distinguish local, mixed, and public auth deployment posture - public open-source notices surface required by HyperTwist's MPL distribution doctrine diff --git a/website/package.json b/website/package.json index 66afdc2..9edafcd 100644 --- a/website/package.json +++ b/website/package.json @@ -6,10 +6,11 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "npm run render:sitemap && tsc && vite build", "preview": "vite preview", "type-check": "tsc --noEmit", "test": "vitest run", + "render:sitemap": "node scripts/render-public-sitemap.mjs", "check:runtime-readiness": "node scripts/check-runtime-readiness.mjs", "render:same-origin-deployment": "node scripts/render-same-origin-deployment.mjs", "render:same-origin-bundle": "node scripts/render-same-origin-bundle.mjs", diff --git a/website/scripts/render-public-sitemap.mjs b/website/scripts/render-public-sitemap.mjs new file mode 100644 index 0000000..b92257b --- /dev/null +++ b/website/scripts/render-public-sitemap.mjs @@ -0,0 +1,24 @@ +import { readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const registryPath = path.join(__dirname, '..', 'src', 'public-route-registry.json') +const sitemapPath = path.join(__dirname, '..', 'public', 'sitemap.xml') +const baseUrl = 'https://hypertwist.app' + +const routeRegistry = JSON.parse(await readFile(registryPath, 'utf8')) +const crawlableRoutes = routeRegistry.filter((route) => route && route.crawlable === true) + +const xml = [ + '', + '', + ...crawlableRoutes.map((route) => ( + ` \n ${baseUrl}${route.path === '/' ? '/' : route.path}\n ` + )), + '', + '', +].join('\n') + +await writeFile(sitemapPath, xml, 'utf8') +console.log(`Rendered sitemap for ${crawlableRoutes.length} crawlable routes to ${sitemapPath}`) diff --git a/website/src/__tests__/public-route-registry.test.ts b/website/src/__tests__/public-route-registry.test.ts new file mode 100644 index 0000000..22cd67c --- /dev/null +++ b/website/src/__tests__/public-route-registry.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest' +import { + buildPublicSitemapXml, + crawlablePublicRoutes, + footerLinks, + marketingNavLinks, + publicRouteRegistry, +} from '../public-route-registry' + +describe('public route registry', () => { + it('keeps unique route definitions for the public shell', () => { + const uniquePaths = new Set(publicRouteRegistry.map((route) => route.path)) + + expect(uniquePaths.size).toBe(publicRouteRegistry.length) + }) + + it('exposes the intended primary navigation and footer surfaces', () => { + expect(marketingNavLinks).toEqual([ + { to: '/', label: 'Home' }, + { to: '/features', label: 'Features' }, + { to: '/about', label: 'About' }, + { to: '/resources', label: 'Resources' }, + { to: '/pricing', label: 'Pricing' }, + { to: '/download', label: 'Download' }, + ]) + + expect(footerLinks).toContainEqual({ + to: '/open-source-notices', + label: 'Open Source Notices', + }) + }) + + it('keeps auth routes out of crawlable sitemap posture', () => { + expect(crawlablePublicRoutes.map((route) => route.path)).not.toContain('/login') + expect(crawlablePublicRoutes.map((route) => route.path)).not.toContain('/register') + }) + + it('renders sitemap XML from the shared crawlable route registry', () => { + const xml = buildPublicSitemapXml() + + expect(xml).toContain('https://hypertwist.app/') + expect(xml).toContain('https://hypertwist.app/features') + expect(xml).not.toContain('/login') + expect(xml).not.toContain('/register') + }) +}) diff --git a/website/src/components/layout/MarketingShell.tsx b/website/src/components/layout/MarketingShell.tsx index 642ee81..62cb000 100644 --- a/website/src/components/layout/MarketingShell.tsx +++ b/website/src/components/layout/MarketingShell.tsx @@ -2,7 +2,7 @@ import { Link, NavLink } from 'react-router-dom' import { usePlatformAuth } from '../../auth/platform-auth' import { MarketingLaunchStatusBanner } from '../ui/PublicLaunchStatus' import { brandConfig } from '../../site-config' -import { footerLinks, marketingNavLinks } from '../../site-data' +import { footerLinks, marketingNavLinks } from '../../public-route-registry' export function MarketingShell({ title, diff --git a/website/src/public-route-registry.json b/website/src/public-route-registry.json new file mode 100644 index 0000000..ffb0baa --- /dev/null +++ b/website/src/public-route-registry.json @@ -0,0 +1,122 @@ +[ + { + "path": "/", + "label": "Home", + "loaderTitle": "Loading HyperTwist...", + "nav": true, + "footer": false, + "crawlable": true + }, + { + "path": "/features", + "label": "Features", + "loaderTitle": "Feature atlas", + "nav": true, + "footer": true, + "crawlable": true + }, + { + "path": "/about", + "label": "About", + "loaderTitle": "About HyperTwist", + "nav": true, + "footer": true, + "crawlable": true + }, + { + "path": "/resources", + "label": "Resources", + "loaderTitle": "Resources", + "nav": true, + "footer": true, + "crawlable": true + }, + { + "path": "/docs", + "label": "Docs", + "loaderTitle": "Documentation", + "nav": false, + "footer": false, + "crawlable": true + }, + { + "path": "/support", + "label": "Support", + "loaderTitle": "Support", + "nav": false, + "footer": false, + "crawlable": true + }, + { + "path": "/changelog", + "label": "Release notes", + "loaderTitle": "Release notes", + "nav": false, + "footer": false, + "crawlable": true + }, + { + "path": "/pricing", + "label": "Pricing", + "loaderTitle": "Pricing", + "nav": true, + "footer": true, + "crawlable": true + }, + { + "path": "/download", + "label": "Download", + "loaderTitle": "Download center", + "nav": true, + "footer": true, + "crawlable": true + }, + { + "path": "/open-source-notices", + "label": "Open Source Notices", + "loaderTitle": "Open source notices", + "nav": false, + "footer": true, + "crawlable": true + }, + { + "path": "/privacy", + "label": "Privacy", + "loaderTitle": "Privacy", + "nav": false, + "footer": true, + "crawlable": true + }, + { + "path": "/terms", + "label": "Terms", + "loaderTitle": "Terms", + "nav": false, + "footer": true, + "crawlable": true + }, + { + "path": "/shipping-payment", + "label": "Shipping & Payment", + "loaderTitle": "Shipping & payment", + "nav": false, + "footer": true, + "crawlable": true + }, + { + "path": "/login", + "label": "Log in", + "loaderTitle": "Log in", + "nav": false, + "footer": false, + "crawlable": false + }, + { + "path": "/register", + "label": "Create account", + "loaderTitle": "Create account", + "nav": false, + "footer": false, + "crawlable": false + } +] diff --git a/website/src/public-route-registry.ts b/website/src/public-route-registry.ts new file mode 100644 index 0000000..172ee9a --- /dev/null +++ b/website/src/public-route-registry.ts @@ -0,0 +1,37 @@ +import routeRegistry from './public-route-registry.json' + +export type PublicRouteDefinition = { + path: string + label: string + loaderTitle: string + nav: boolean + footer: boolean + crawlable: boolean +} + +export const publicRouteRegistry = routeRegistry as PublicRouteDefinition[] + +export const marketingNavLinks = publicRouteRegistry + .filter((route) => route.nav) + .map((route) => ({ + to: route.path, + label: route.label, + })) + +export const footerLinks = publicRouteRegistry + .filter((route) => route.footer) + .map((route) => ({ + to: route.path, + label: route.label, + })) + +export const crawlablePublicRoutes = publicRouteRegistry.filter((route) => route.crawlable) + +export function buildPublicSitemapXml(baseUrl = 'https://hypertwist.app') { + const normalizedBaseUrl = baseUrl.replace(/\/$/, '') + const urlEntries = crawlablePublicRoutes + .map((route) => ` \n ${normalizedBaseUrl}${route.path === '/' ? '/' : route.path}\n `) + .join('\n') + + return `\n\n${urlEntries}\n\n` +} diff --git a/website/src/router/PublicRoutes.tsx b/website/src/router/PublicRoutes.tsx index 76238e3..a2e25a6 100644 --- a/website/src/router/PublicRoutes.tsx +++ b/website/src/router/PublicRoutes.tsx @@ -1,6 +1,7 @@ import { Suspense, lazy, type ReactNode } from 'react' import { Route } from 'react-router-dom' import { GeneralPageLoader } from '../components/ui/Skeletons' +import { publicRouteRegistry } from '../public-route-registry' const HomeLanding = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.HomeLanding }))) const FeaturesPage = lazy(() => import('../pages/public-pages-features').then((m) => ({ default: m.FeaturesPage }))) @@ -18,6 +19,24 @@ const ShippingPaymentPage = lazy(() => import('../pages/public-pages-commerce'). const LoginPage = lazy(() => import('../pages/auth-pages').then((m) => ({ default: m.LoginPage }))) const RegisterPage = lazy(() => import('../pages/auth-pages').then((m) => ({ default: m.RegisterPage }))) +const publicRouteElements: Record = { + '/': , + '/features': , + '/about': , + '/resources': , + '/docs': , + '/support': , + '/changelog': , + '/pricing': , + '/download': , + '/open-source-notices': , + '/privacy': , + '/terms': , + '/shipping-payment': , + '/login': , + '/register': , +} + function withGeneralLoader(title: string, element: ReactNode) { return }>{element} } @@ -25,21 +44,13 @@ function withGeneralLoader(title: string, element: ReactNode) { export function renderPublicRoutes() { return ( <> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> - )} /> + {publicRouteRegistry.map((route) => ( + + ))} ) } diff --git a/website/src/site-data.ts b/website/src/site-data.ts index 69020e7..dc83cac 100644 --- a/website/src/site-data.ts +++ b/website/src/site-data.ts @@ -26,15 +26,6 @@ export const capabilityPillars = [ }, ] as const -export const marketingNavLinks = [ - { to: '/', label: 'Home' }, - { to: '/features', label: 'Features' }, - { to: '/about', label: 'About' }, - { to: '/resources', label: 'Resources' }, - { to: '/pricing', label: 'Pricing' }, - { to: '/download', label: 'Download' }, -] as const - export const deliverySurfaceCards = [ { title: 'Public website', @@ -648,18 +639,6 @@ export const sourceAvailability = { releaseNotesUrl, } -export const footerLinks = [ - { to: '/features', label: 'Features' }, - { to: '/about', label: 'About' }, - { to: '/resources', label: 'Resources' }, - { to: '/pricing', label: 'Pricing' }, - { to: '/download', label: 'Download' }, - { to: '/open-source-notices', label: 'Open Source Notices' }, - { to: '/privacy', label: 'Privacy' }, - { to: '/terms', label: 'Terms' }, - { to: '/shipping-payment', label: 'Shipping & Payment' }, -] as const - export const supportFaqs = [ { question: 'Is the simulator fully in the browser?',