diff --git a/COMMIT_EDITMSG b/COMMIT_EDITMSG index fdfd1c9..468f607 100644 --- a/COMMIT_EDITMSG +++ b/COMMIT_EDITMSG @@ -1,16 +1,28 @@ -chore: commit pending ZXDB explorer changes prior to index perf work +perf(zxdb): server-render index pages with ISR and initial data -Context -- Housekeeping commit to capture all current ZXDB Explorer work before index-page performance optimizations. +Why +- Reduce time-to-first-content on ZXDB index pages by eliminating the initial client-side fetch and enabling incremental static regeneration. -Includes -- Server-rendered entry detail page with ISR and parallelized DB queries. -- Node runtime for ZXDB API routes and params validation updates for Next 15. -- ZXDB repository extensions (facets, label queries, category queries). -- Cross-linking and Link-based prefetch across ZXDB UI. -- Cache headers on low-churn list APIs. +What +- Main Explorer (/zxdb): + - Server-renders first page of results and lookup lists (genres, languages, machinetypes) and passes them as initial props. + - Keeps client interactivity for subsequent searches/filters. +- Labels index (/zxdb/labels): + - Server-renders first page of empty search and passes as initial props to skip the first fetch. +- Category lists: + - Genres (/zxdb/genres), Languages (/zxdb/languages), Machine Types (/zxdb/machinetypes) now server-render their lists and export revalidate=3600. + - Refactored list components to accept server-provided items; removed on-mount fetching. +- Links & prefetch: + - Replaced remaining anchors with Next Link to enable prefetch where applicable. + +Tech details +- Added revalidate=3600 to the index pages for ISR. +- Updated ZxdbExplorer to accept initial results and initial filter lists; skips first client fetch when initial props are present. +- Updated LabelsSearch to accept initial payload and skip first fetch in default state. +- Updated GenreList, LanguageList, MachineTypeList to be presentational components receiving items from server pages. Notes -- Follow-up commit will focus specifically on speeding up index pages via SSR initial data and ISR. +- Low-churn list APIs already emit Cache-Control for CDN; list pages now render instantly from server. +- Further polish (breadcrumbs, facet counts UI) can build on this foundation without reintroducing initial network waits. Signed-off-by: Junie@lucy.xalior.com \ No newline at end of file diff --git a/src/app/zxdb/ZxdbExplorer.tsx b/src/app/zxdb/ZxdbExplorer.tsx index 1ffcf54..cf89fc7 100644 --- a/src/app/zxdb/ZxdbExplorer.tsx +++ b/src/app/zxdb/ZxdbExplorer.tsx @@ -18,14 +18,24 @@ type Paged = { total: number; }; -export default function ZxdbExplorer() { +export default function ZxdbExplorer({ + initial, + initialGenres, + initialLanguages, + initialMachines, +}: { + initial?: Paged; + initialGenres?: { id: number; name: string }[]; + initialLanguages?: { id: string; name: string }[]; + initialMachines?: { id: number; name: string }[]; +}) { const [q, setQ] = useState(""); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); - const [data, setData] = useState | null>(null); - const [genres, setGenres] = useState<{ id: number; name: string }[]>([]); - const [languages, setLanguages] = useState<{ id: string; name: string }[]>([]); - const [machines, setMachines] = useState<{ id: number; name: string }[]>([]); + const [data, setData] = useState | null>(initial ?? null); + const [genres, setGenres] = useState<{ id: number; name: string }[]>(initialGenres ?? []); + const [languages, setLanguages] = useState<{ id: string; name: string }[]>(initialLanguages ?? []); + const [machines, setMachines] = useState<{ id: number; name: string }[]>(initialMachines ?? []); const [genreId, setGenreId] = useState(""); const [languageId, setLanguageId] = useState(""); const [machinetypeId, setMachinetypeId] = useState(""); @@ -59,18 +69,23 @@ export default function ZxdbExplorer() { } useEffect(() => { + // Avoid immediate client fetch on first paint if server provided initial data + if (initial && page === 1 && q === "" && genreId === "" && languageId === "" && machinetypeId === "" && sort === "title") { + return; + } fetchData(q, page); // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, genreId, languageId, machinetypeId, sort]); - // Load filter lists once + // Load filter lists on mount only if not provided by server useEffect(() => { + if (initialGenres && initialLanguages && initialMachines) return; async function loadLists() { try { const [g, l, m] = await Promise.all([ - fetch("/api/zxdb/genres", { cache: "no-store" }).then((r) => r.json()), - fetch("/api/zxdb/languages", { cache: "no-store" }).then((r) => r.json()), - fetch("/api/zxdb/machinetypes", { cache: "no-store" }).then((r) => r.json()), + fetch("/api/zxdb/genres", { cache: "force-cache" }).then((r) => r.json()), + fetch("/api/zxdb/languages", { cache: "force-cache" }).then((r) => r.json()), + fetch("/api/zxdb/machinetypes", { cache: "force-cache" }).then((r) => r.json()), ]); setGenres(g.items ?? []); setLanguages(l.items ?? []); @@ -78,7 +93,7 @@ export default function ZxdbExplorer() { } catch {} } loadLists(); - }, []); + }, [initialGenres, initialLanguages, initialMachines]); function onSubmit(e: React.FormEvent) { e.preventDefault(); diff --git a/src/app/zxdb/genres/GenreList.tsx b/src/app/zxdb/genres/GenreList.tsx index 9af3383..a12ae3e 100644 --- a/src/app/zxdb/genres/GenreList.tsx +++ b/src/app/zxdb/genres/GenreList.tsx @@ -1,27 +1,10 @@ "use client"; -import { useEffect, useState } from "react"; import Link from "next/link"; type Genre = { id: number; name: string }; -export default function GenreList() { - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - useEffect(() => { - async function load() { - try { - const res = await fetch("/api/zxdb/genres", { cache: "no-store" }); - const json = await res.json(); - setItems(json.items ?? []); - } finally { - setLoading(false); - } - } - load(); - }, []); - - if (loading) return
Loading…
; +export default function GenreList({ items }: { items: Genre[] }) { return (

Genres

diff --git a/src/app/zxdb/genres/page.tsx b/src/app/zxdb/genres/page.tsx index b5d1468..345b810 100644 --- a/src/app/zxdb/genres/page.tsx +++ b/src/app/zxdb/genres/page.tsx @@ -1,7 +1,11 @@ import GenreList from "./GenreList"; +import { listGenres } from "@/server/repo/zxdb"; export const metadata = { title: "ZXDB Genres" }; -export default function Page() { - return ; +export const revalidate = 3600; + +export default async function Page() { + const items = await listGenres(); + return ; } diff --git a/src/app/zxdb/labels/LabelsSearch.tsx b/src/app/zxdb/labels/LabelsSearch.tsx index f496066..5ce2501 100644 --- a/src/app/zxdb/labels/LabelsSearch.tsx +++ b/src/app/zxdb/labels/LabelsSearch.tsx @@ -1,16 +1,17 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; type Label = { id: number; name: string; labeltypeId: string | null }; type Paged = { items: T[]; page: number; pageSize: number; total: number }; -export default function LabelsSearch() { +export default function LabelsSearch({ initial }: { initial?: Paged