diff --git a/web/DetailPanel.tsx b/web/DetailPanel.tsx index 51661c6..526691f 100644 --- a/web/DetailPanel.tsx +++ b/web/DetailPanel.tsx @@ -153,12 +153,13 @@ export const DetailPanel = () => { const [params] = useSearchParams(); const raw = params.get('section'); /* - A page selected inside Pages opens the BUILDER, not the list with a row lit up. + A page selected inside Pages opens the BUILDER, not the list with a row lit up. By SLUG, so the URL + says which page rather than which row — see pageParam in usePages. The nav says the same thing — Pages is a section and the pages are rows under it — so the detail panel follows it. `?page=` is what distinguishes the two, which keeps both addressable. */ - const selectedPage = Number(params.get('page')) || null; + const selectedPage = params.get('page'); const selected: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION; const { ready, rootSet, loading } = useReady(); diff --git a/web/NavPanel.tsx b/web/NavPanel.tsx index 84d0a04..a42942e 100644 --- a/web/NavPanel.tsx +++ b/web/NavPanel.tsx @@ -2,7 +2,7 @@ import { Link, useSearchParams } from 'react-router'; import { Lock } from 'lucide-react'; import { DEFAULT_SECTION, SECTIONS, isSectionKey, sectionOpen, type SectionKey } from './sections'; import { useReady } from './useReady'; -import { usePages } from './usePages'; +import { pageParam, usePages } from './usePages'; /** * The left column: which part of the fofos site the detail panel is showing. @@ -53,7 +53,7 @@ export const NavPanel = () => { // Announced by PagesPanel rather than fetched here — see usePages. Null until it has spoken, which // is why nothing is drawn instead of an empty list. const { pages } = usePages(); - const selectedPage = Number(params.get('page')) || null; + const selectedPage = params.get('page'); const raw = params.get('section'); const active: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION; const { ready, rootSet } = useReady(); @@ -89,6 +89,15 @@ export const NavPanel = () => { */ search.delete('page'); + /* + `record` is dead and rides along in every URL until something removes it. + + It belonged to the per-page record panels — Gallery, Drops, Collabs — which are unreachable now. + Nothing reads it, so it is not a bug, but every link anybody copies carries a parameter naming a + row in a section that no longer exists. + */ + search.delete('record'); + const body = ( <> @@ -144,10 +153,10 @@ export const NavPanel = () => { {pages.map((page) => { const childSearch = new URLSearchParams(params); childSearch.set('section', 'pages'); - childSearch.set('page', String(page.id)); + childSearch.set('page', pageParam(page)); childSearch.delete('media'); - const current = active === 'pages' && selectedPage === page.id; + const current = active === 'pages' && selectedPage === pageParam(page); return ( { clientRef.current = client; const [params] = useSearchParams(); - const id = Number(params.get('page')) || null; + const slug = params.get('page'); const devPort = useDevPort(); const { pages, announce } = usePages(); @@ -157,7 +157,7 @@ export const PageBuilderPanel = () => { .catch((failure: unknown) => setError(readableError(failure))); }, [pages, announce]); - const page = pages?.find((row) => row.id === id) ?? null; + const page = pageFromParam(pages, slug); /* The page's sections, which the canvas needs to name and outline what it finds in the document. diff --git a/web/usePages.ts b/web/usePages.ts index 2e4f693..4580b37 100644 --- a/web/usePages.ts +++ b/web/usePages.ts @@ -41,3 +41,22 @@ export const usePages = () => { return { pages, announce }; }; + +/** + * The value `?page=` carries, and how to read it back. + * + * The SLUG rather than the id, so a URL says which page it is: `?page=about` can be read, sent to + * somebody, and still means the right thing after a database is restored from a dump with different + * ids — which happened once already in this project. + * + * Home is the exception and needs a word, because its slug is empty by design. `home` is reserved by + * the site's API, so no page can take that address and the mapping is total in both directions. + */ +export const HOME_PARAM = 'home'; + +export const pageParam = (page: Page): string => page.slug || HOME_PARAM; + +export const pageFromParam = (pages: Page[] | null, value: string | null): Page | null => { + if (!pages || !value) return null; + return pages.find((page) => pageParam(page) === value) ?? null; +};