the URL says which page, not which row
`?page=about` instead of `?page=2`. A URL that names the page 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 in this project already, earlier today. Home needs a word, because its slug is empty by design: that is the check constraint, and it is what makes `/` fall out of joining a slug to the root. An empty string is no use in a query parameter that has to name a page unambiguously, so the admin says `home` — and the site's API now REFUSES that slug, which is what makes the mapping total in both directions rather than merely usual. One reserved address costs nothing. Anybody who wanted /home wanted the front page, which they have. One place maps a page to its URL value and back — pageParam and pageFromParam — so the nav, the detail panel and the builder cannot disagree about what `?page=` means. `record` is deleted on every navigation. It belonged to the per-page record panels, which are unreachable now; nothing reads it, so it was not a bug, but every URL anybody copied carried a parameter naming a row in a section that no longer exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -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();
|
||||
|
||||
+13
-4
@@ -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 = (
|
||||
<>
|
||||
<Icon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||||
@@ -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 (
|
||||
<Link
|
||||
|
||||
@@ -7,7 +7,7 @@ import { usePreviewable, useViewMode } from '../ViewMode';
|
||||
import { paintCanvas, type CanvasComponent, type CanvasSection } from '../canvas';
|
||||
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
|
||||
import { FreeTextDialog } from './FreeTextDialog';
|
||||
import { usePages, type Page } from '../usePages';
|
||||
import { pageFromParam, usePages, type Page } from '../usePages';
|
||||
|
||||
/**
|
||||
* One page, being built.
|
||||
@@ -30,7 +30,7 @@ export const PageBuilderPanel = () => {
|
||||
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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user