From 9bbe70824276aff226ef8ae90ffe113a688a3c72 Mon Sep 17 00:00:00 2001 From: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:20:20 -0700 Subject: [PATCH 1/3] Optimize generated variant routes and site performance - Generate deterministic model-specific route entries before dev and build - Reduce compare-page payloads and unnecessary client work - Improve caching, image delivery, and gallery loading behavior --- .gitignore | 3 + next.config.ts | 39 +++- package.json | 5 +- scripts/generate-variant-routes.mjs | 92 ++++++++++ src/app/[group]/[model]/[iteration]/page.tsx | 17 -- src/app/[group]/[model]/page.tsx | 5 +- src/app/compare/page.tsx | 12 +- src/app/experiments/page.tsx | 2 +- src/app/globals.css | 27 ++- src/app/lab-guess/page.tsx | 5 +- src/app/layout.tsx | 5 +- src/app/page.tsx | 11 +- .../[group]/[model]/[iteration]/page.tsx | 17 -- src/app/rankings/page.tsx | 4 +- src/components/compare/compare-page.tsx | 143 ++++++++------- src/components/compare/compare-selects.tsx | 4 +- .../gallery/gallery-archive-grid.tsx | 70 +++++++ src/components/gallery/gallery-card.tsx | 13 +- .../gallery/gallery-group-section.tsx | 75 ++++---- .../gallery/gallery-iteration-view.tsx | 12 +- .../gallery/gallery-rankings-nav.tsx | 16 +- .../gallery/gallery-theme-provider.tsx | 74 -------- .../gallery/gallery-theme-toggle.tsx | 36 +++- src/components/gallery/generation-prompt.tsx | 35 ++-- src/components/gallery/theme-aware-logo.tsx | 35 +++- src/components/gallery/variant-header.tsx | 81 +++++--- src/components/game/model-lab-wordle.tsx | 26 +-- src/lib/compare-server.ts | 114 ++++++++++++ src/lib/compare.ts | 173 ++---------------- src/lib/gallery-anthropic-skill.ts | 3 + src/lib/gallery-catalog.ts | 15 ++ src/lib/gallery-model-order.ts | 4 +- src/lib/gallery-registry.ts | 122 ------------ src/lib/gallery-types.ts | 8 + tests/compare-routes.spec.ts | 39 ++++ 35 files changed, 750 insertions(+), 592 deletions(-) create mode 100644 scripts/generate-variant-routes.mjs delete mode 100644 src/app/[group]/[model]/[iteration]/page.tsx delete mode 100644 src/app/preview/[group]/[model]/[iteration]/page.tsx create mode 100644 src/components/gallery/gallery-archive-grid.tsx delete mode 100644 src/components/gallery/gallery-theme-provider.tsx create mode 100644 src/lib/compare-server.ts create mode 100644 src/lib/gallery-catalog.ts delete mode 100644 src/lib/gallery-registry.ts diff --git a/.gitignore b/.gitignore index 12f73ae..e8406ef 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ playwright-report/ .agents/ .claude/ .windsurf/ + +# Deterministic model-specific Next.js route entries generated before dev/build. +/src/app/(generated-variant-routes)/ diff --git a/next.config.ts b/next.config.ts index 96ee744..0c2d8ca 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,8 +2,45 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { allowedDevOrigins: ["127.0.0.1"], + poweredByHeader: false, images: { - unoptimized: true, + deviceSizes: [640, 750, 828, 1080, 1200, 1600, 1920], + imageSizes: [32, 48, 64, 96, 128, 256, 384], + minimumCacheTTL: 86_400, + qualities: [70, 75], + remotePatterns: [ + { + protocol: "https", + hostname: "picsum.photos", + }, + ], + }, + experimental: { + optimizePackageImports: ["@phosphor-icons/react"], + }, + async headers() { + return [ + { + // Compare output is deterministic for the full URL and contains no user data. + // Cache it at Vercel's edge while keeping the browser response revalidated. + source: "/compare", + headers: [ + { + key: "Vercel-CDN-Cache-Control", + value: "public, s-maxage=31536000, stale-while-revalidate=86400", + }, + ], + }, + ...["/gallery-previews/:path*", "/variants/:path*"].map((source) => ({ + source, + headers: [ + { + key: "Cache-Control", + value: "public, max-age=86400, stale-while-revalidate=604800", + }, + ], + })), + ]; }, }; diff --git a/package.json b/package.json index 70a151c..cc0b446 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "scripts": { "dev": "next dev", "build": "next build", - "predev": "node scripts/scope-variant-css.mjs", - "prebuild": "node scripts/scope-variant-css.mjs", + "predev": "node scripts/generate-variant-routes.mjs && node scripts/scope-variant-css.mjs", + "prebuild": "node scripts/generate-variant-routes.mjs && node scripts/scope-variant-css.mjs", "start": "next start", "lint": "eslint", "capture-previews": "node scripts/capture-previews.mjs", "scope:variant-css": "node scripts/scope-variant-css.mjs", + "generate:variant-routes": "node scripts/generate-variant-routes.mjs", "test:routes": "playwright test tests/gallery-routes.spec.ts", "test:visual": "playwright test tests/gallery-visual-smoke.spec.ts" }, diff --git a/scripts/generate-variant-routes.mjs b/scripts/generate-variant-routes.mjs new file mode 100644 index 0000000..fd35676 --- /dev/null +++ b/scripts/generate-variant-routes.mjs @@ -0,0 +1,92 @@ +import { readdir, rm, mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const variantsRoot = path.join(projectRoot, "src", "variants"); +const appRoot = path.join(projectRoot, "src", "app"); +const outputRoot = path.join(appRoot, "(generated-variant-routes)"); +const nextRoot = path.join(projectRoot, ".next"); + +if (path.dirname(outputRoot) !== appRoot) { + throw new Error(`Refusing to generate routes outside the app directory: ${outputRoot}`); +} + +const iterationIds = ["1", "2", "3", "4", "5"]; + +function pageSource({ group, model, preview }) { + const componentName = preview ? "PreviewIterationPage" : "IterationPage"; + return `import "./variant-tailwind.css"; +import { GalleryIterationView } from "@/components/gallery/gallery-iteration-view"; +import variantModule from "@/variants/${group}/${model}"; + +export const dynamicParams = false; + +export function generateStaticParams() { + return ${JSON.stringify(iterationIds)}.map((iteration) => ({ iteration })); +} + +export default async function ${componentName}({ + params, +}: { + params: Promise<{ iteration: string }>; +}) { + const { iteration } = await params; + return ( + + ); +} +`; +} + +await rm(outputRoot, { recursive: true, force: true }); +// Route type validators are incremental and can retain imports for the two +// generic routes this generator replaces. +await Promise.all( + [path.join(nextRoot, "types"), path.join(nextRoot, "dev", "types")].map((target) => + rm(target, { recursive: true, force: true }), + ), +); + +let generatedPages = 0; +const groups = await readdir(variantsRoot, { withFileTypes: true }); +for (const groupEntry of groups) { + if (!groupEntry.isDirectory()) continue; + const group = groupEntry.name; + const models = await readdir(path.join(variantsRoot, group), { withFileTypes: true }); + + for (const modelEntry of models) { + if (!modelEntry.isDirectory()) continue; + const model = modelEntry.name; + const variantDir = path.join(variantsRoot, group, model); + const files = await readdir(variantDir); + if (!files.includes("index.tsx")) continue; + + for (const preview of [false, true]) { + const routeDir = preview + ? path.join(outputRoot, "preview", group, model, "[iteration]") + : path.join(outputRoot, group, model, "[iteration]"); + await mkdir(routeDir, { recursive: true }); + await writeFile( + path.join(routeDir, "page.tsx"), + pageSource({ group, model, preview }), + "utf8", + ); + const relativeVariantDir = path.relative(routeDir, variantDir).split(path.sep).join("/"); + await writeFile( + path.join(routeDir, "variant-tailwind.css"), + `@layer theme, utilities;\n@import "tailwindcss/theme.css" layer(theme);\n@import "tailwindcss/utilities.css" layer(utilities) source(none);\n@source "${relativeVariantDir}";\n`, + "utf8", + ); + generatedPages += 1; + } + } +} + +console.log(`Generated ${generatedPages} model-specific route modules in ${path.relative(projectRoot, outputRoot)}`); diff --git a/src/app/[group]/[model]/[iteration]/page.tsx b/src/app/[group]/[model]/[iteration]/page.tsx deleted file mode 100644 index 97819a8..0000000 --- a/src/app/[group]/[model]/[iteration]/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { GalleryIterationView } from "@/components/gallery/gallery-iteration-view"; -import { getStaticGalleryIterationParams } from "@/lib/gallery-static-params"; - -export const dynamicParams = false; - -export function generateStaticParams() { - return getStaticGalleryIterationParams(); -} - -export default async function IterationPage({ - params, -}: { - params: Promise<{ group: string; model: string; iteration: string }>; -}) { - const { group, model, iteration } = await params; - return ; -} diff --git a/src/app/[group]/[model]/page.tsx b/src/app/[group]/[model]/page.tsx index d4d193e..6a85403 100644 --- a/src/app/[group]/[model]/page.tsx +++ b/src/app/[group]/[model]/page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; import { GalleryBreadcrumb, IterationLinks } from "@/components/gallery/gallery-shell"; -import { buildCompareHrefForSelection } from "@/lib/compare"; +import { buildCompareHrefForSelection } from "@/lib/compare-server"; import { getGalleryEntry } from "@/lib/gallery-manifest"; import { getStaticGalleryModelParams } from "@/lib/gallery-static-params"; import { buildVariantHref, isGalleryGroup } from "@/lib/gallery-paths"; @@ -69,6 +69,9 @@ export default async function ModelPage({ src={iteration.thumbnailPath} alt={`${entry.modelLabel} iteration ${iteration.id}`} fill + sizes="(max-width: 639px) calc(100vw - 2rem), (max-width: 1279px) calc(50vw - 2.5rem), 24rem" + quality={70} + preload={iteration.id === entry.defaultIteration} className="object-cover" /> diff --git a/src/app/compare/page.tsx b/src/app/compare/page.tsx index 0c0f65a..40b31f3 100644 --- a/src/app/compare/page.tsx +++ b/src/app/compare/page.tsx @@ -1,6 +1,9 @@ import { notFound, redirect } from "next/navigation"; import { ComparePage } from "@/components/compare/compare-page"; -import { buildCompareHref, DEFAULT_COMPARE_STATE, parseCompareSearchParams } from "@/lib/compare"; +import { GalleryRankingsNav } from "@/components/gallery/gallery-rankings-nav"; +import { buildCompareHref, DEFAULT_COMPARE_STATE } from "@/lib/compare"; +import { parseCompareSearchParams } from "@/lib/compare-server"; +import { galleryCatalog } from "@/lib/gallery-catalog"; export default async function CompareRoutePage({ searchParams, @@ -18,5 +21,10 @@ export default async function CompareRoutePage({ notFound(); } - return ; + return ( + <> + + + + ); } diff --git a/src/app/experiments/page.tsx b/src/app/experiments/page.tsx index d0a20fe..17f73af 100644 --- a/src/app/experiments/page.tsx +++ b/src/app/experiments/page.tsx @@ -195,7 +195,7 @@ const iterations = [ export default function ExperimentsPage() { return ( <> - +

Exploration ยท not implemented yet

diff --git a/src/app/globals.css b/src/app/globals.css index 8d702d2..36bec76 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,6 @@ -@import "tailwindcss"; +@import "tailwindcss" source(none); +@source "."; +@source "../components"; @custom-variant dark (&:where(.dark, .dark *)); @@ -403,6 +405,29 @@ html { animation: spin-slow 28s linear infinite; } +@keyframes gallery-toast-enter { + from { + opacity: 0; + translate: 0 0.35rem; + scale: 0.97; + } + to { + opacity: 1; + translate: 0 0; + scale: 1; + } +} + +.gallery-toast-enter { + animation: gallery-toast-enter 160ms cubic-bezier(0.22, 1, 0.36, 1) both; +} + +/* Skip paint/layout work for gallery groups well below the viewport. */ +.gallery-group-section { + content-visibility: auto; + contain-intrinsic-size: auto 72rem; +} + @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; diff --git a/src/app/lab-guess/page.tsx b/src/app/lab-guess/page.tsx index 7a6faf4..de027b2 100644 --- a/src/app/lab-guess/page.tsx +++ b/src/app/lab-guess/page.tsx @@ -1,11 +1,12 @@ import { GalleryRankingsNav } from "@/components/gallery/gallery-rankings-nav"; import { ModelLabWordle } from "@/components/game/model-lab-wordle"; +import { galleryCatalog } from "@/lib/gallery-catalog"; export default function LabGuessPage() { return ( <> - - + + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c4f0763..f9fcb74 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,6 @@ import type { Metadata } from "next"; import type { ReactNode } from "react"; import { GeistSans } from "geist/font/sans"; -import { GalleryThemeProvider } from "@/components/gallery/gallery-theme-provider"; import { galleryThemeInitScript } from "@/lib/gallery-theme"; import "./globals.css"; @@ -20,9 +19,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {