diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 39d7cd9..77e2db6 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -68,16 +68,42 @@ const nextConfig: NextConfig = { env: { NEXT_PUBLIC_POSTHOG_ENABLED: analyticsEnabled ? "1" : "", }, + experimental: { + // Inline the stylesheet (about 15 KB compressed) into the HTML, so first paint does not wait for + // a separate render-blocking CSS request. Client-side navigations still load it as a file. + inlineCss: true, + }, images: { unoptimized: true, }, poweredByHeader: false, async redirects() { // The manual's content now lives in the docs as the pseudocode reference. - return ["/manual", "/app/manual"].flatMap((source) => [ + const manual = ["/manual", "/app/manual"].flatMap((source) => [ { source, destination: "/docs", permanent: true }, { source: `/:locale(en|zh)${source}`, destination: "/:locale/docs", permanent: true }, ]); + // English is served at the bare path, so `/en/...` redirects there and every page has one URL. + const englishPrefix = [ + { source: "/en", destination: "/", permanent: true }, + { source: "/en/:path*", destination: "/:path*", permanent: true }, + ]; + return [...manual, ...englishPrefix]; + }, + async rewrites() { + // English pages live under app/[locale] as `/en/...`; the bare path is rewritten there. These run + // in Vercel's routing layer, so cached pages are served from the CDN without invoking the proxy + // (middleware). `afterFiles` runs after public files and static routes (robots.txt, icons, + // /api, /_next) but before dynamic routes, so `/docs` reaches `/en/docs` rather than + // `[locale]` = "docs". + return { + beforeFiles: [], + afterFiles: [ + { source: "/", destination: "/en" }, + { source: "/:path((?!(?:en|zh|api|_next)(?:/|$)).+)", destination: "/en/:path" }, + ], + fallback: [], + }; }, async headers() { const immutableAssetHeaders = [ diff --git a/apps/web/src/app/[locale]/(public)/page.tsx b/apps/web/src/app/[locale]/(public)/page.tsx index af9bc9a..31f7b29 100644 --- a/apps/web/src/app/[locale]/(public)/page.tsx +++ b/apps/web/src/app/[locale]/(public)/page.tsx @@ -157,6 +157,9 @@ export default async function LandingPage({ params }: LocaleParams) { return (
+ {/* The stylesheet is inlined, so the browser could paint before the hero window has arrived and then grow + it. Hold the first paint until the hero has been parsed (browsers without rel=expect ignore this). */} +