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;
+};