diff --git a/index.html b/index.html index 9fae366..411f22f 100644 --- a/index.html +++ b/index.html @@ -2,8 +2,8 @@ - - + + @@ -20,23 +20,23 @@ - - - - + + + + - - - + + + - - - - + + + + @@ -48,10 +48,10 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Playfair+Display:ital,wght@0,900;1,900&family=JetBrains+Mono&display=swap" /> - OneHook — Intentional Messaging App for One Real Connection + OneHook — Intentional Dating App for One Real Connection - + \n `); + async closeBundle() { + const indexPath = resolve(distDir, 'index.html'); + if (!existsSync(indexPath)) return; // the build failed; let its own error surface + const base = readFileSync(indexPath, 'utf8'); + + // Build the page renderer as an SSR bundle (Rollup resolves CommonJS dependencies the same way + // the browser build does), then import it in Node. Written to a temp dir and removed afterwards, + // so nothing from it can reach dist/. + const ssrOutDir = resolve(__dirname, 'node_modules/.cache/prerender'); + await build({ + configFile: false, + root: __dirname, + mode, + logLevel: 'error', + plugins: [react()], + resolve: { alias: appAliases }, + define: { 'process.env.NODE_ENV': JSON.stringify(mode) }, + ssr: { noExternal: true }, + build: { + ssr: resolve(__dirname, 'src/prerender-entry.tsx'), + outDir: ssrOutDir, + emptyOutDir: true, + copyPublicDir: false, + rollupOptions: { output: { entryFileNames: 'prerender-entry.mjs', format: 'es', inlineDynamicImports: true } }, + }, + }); + try { + const { renderRouteBody, PRERENDER_BODY_PAGES } = (await import( + pathToFileURL(resolve(ssrOutDir, 'prerender-entry.mjs')).href + )) as typeof import('./src/prerender-entry'); + + const setAttr = (html: string, selector: RegExp, value: string) => { + if (!selector.test(html)) throw new Error(`[prerender-pages] tag not found in index.html: ${selector}`); + return html.replace(selector, (_m, pre: string, post: string) => `${pre}${escapeHtml(value)}${post}`); + }; + const banner = smartAppBannerContent(); + + const renderPage = (route: string, seo: RouteSeo, canonical: string | null): string => { + const image = `${SITE_URL}${seo.image ?? DEFAULT_SHARE_IMAGE}`; + const imageAlt = seo.imageAlt ?? DEFAULT_SHARE_IMAGE_ALT; + let html = base; + html = html.replace(/[^<]*<\/title>/, `<title>${escapeHtml(seo.title)}`); + html = setAttr(html, /(/, '').replace(/\s*/, ''); + } + html = setAttr(html, /(`); + if (seo.jsonLd) { + const ld = JSON.stringify(seo.jsonLd).replace(/${ld}`); + } + if (extraHead.length) html = html.replace('', ` ${extraHead.join('\n ')}\n `); + if (PRERENDER_BODY_PAGES[route]) { + if (!html.includes('
')) throw new Error('[prerender-pages]
not found in index.html'); + const body = revealMotionInitialStyles(renderRouteBody(route)); + html = html.replace('
', () => `
${body}
`); + } + return html; + }; + + for (const route of ['/', ...PRERENDER_ROUTES]) { + const html = renderPage(route, ROUTE_SEO[route], `${SITE_URL}${route}`); + const dir = route === '/' ? distDir : resolve(distDir, route.replace(/^\//, '')); + mkdirSync(dir, { recursive: true }); + writeFileSync(resolve(dir, 'index.html'), html); } - const dir = resolve(distDir, route.replace(/^\//, '')); - mkdirSync(dir, { recursive: true }); - writeFileSync(resolve(dir, 'index.html'), html); + writeFileSync(resolve(distDir, '404.html'), renderPage('/404', NOT_FOUND_SEO, null)); + writeFileSync(resolve(distDir, 'sitemap.xml'), buildSitemap()); + } finally { + await rm(ssrOutDir, { recursive: true, force: true }); } }, }); +const appAliases = [ + { find: '@', replacement: resolve(__dirname, 'src') }, + { find: /onehook-api-client\/dist-es\/runtimeConfig$/, replacement: 'onehook-api-client/dist-es/runtimeConfig.browser' }, +]; + const leanMediaPrunePlugin = (): Plugin => ({ name: 'lean-media-prune', apply: 'build', @@ -140,37 +240,9 @@ export default defineConfig(({ mode }) => { tailwindcss(), ensureOutDirPlugin(), leanMediaPrunePlugin(), - prerenderRouteHeadsPlugin(), - Sitemap({ - hostname: 'https://onehook.club', - // Public marketing pages are discovered from the pre-built dist//index.html files written - // by prerenderRouteHeadsPlugin (which runs first), so they are not listed again here — listing - // them would duplicate every entry. '/' comes from dist/index.html. - dynamicRoutes: [], - exclude: ['/app', '/onboarding', '/auth/setup', '/login', '/redeem'], - // Generated robots.txt must mirror public/robots.txt (the plugin overwrites dist/robots.txt). - robots: [ - { - userAgent: '*', - allow: '/', - disallow: ['/app', '/onboarding', '/auth/', '/redeem'], - }, - ], - // compact output: no whitespace inside , which some validators reject - readable: false, - changefreq: 'weekly', - priority: { - '/': 1.0, - '/science': 0.8, - '/messenger': 0.8, - '/pricing': 0.8, - '/careers': 0.6, - '/contact': 0.6, - '/privacy': 0.4, - '/terms': 0.4, - }, - outDir: 'dist', - }), + // Writes per-route HTML (head + rendered body), 404.html and sitemap.xml. robots.txt is + // public/robots.txt, copied as-is. + prerenderPagesPlugin(mode), ], server: { fs: { @@ -263,10 +335,7 @@ export default defineConfig(({ mode }) => { }, }, resolve: { - alias: [ - { find: '@', replacement: resolve(__dirname, 'src') }, - { find: /onehook-api-client\/dist-es\/runtimeConfig$/, replacement: 'onehook-api-client/dist-es/runtimeConfig.browser' }, - ], + alias: appAliases, }, define: { 'process.env.NODE_ENV': JSON.stringify(mode),