diff --git a/craft/src/state/PageContext.slug.test.tsx b/craft/src/state/PageContext.slug.test.tsx index 03469ae..5f807b8 100644 --- a/craft/src/state/PageContext.slug.test.tsx +++ b/craft/src/state/PageContext.slug.test.tsx @@ -121,6 +121,38 @@ describe('PageProvider slug dedupe', () => { unmount(); }); + test('M-4: a punctuation-only page name ("!!!") falls back to slug "page", never empty string', () => { + let ctx: ReturnType | null = null; + const Consumer: React.FC = () => { + ctx = usePages(); + return null; + }; + + render( + + + , + ); + + act(() => { + ctx!.addPage('!!!', ''); + }); + + const added = ctx!.pages.find((p) => p.name === '!!!')!; + expect(added.slug).toBe('page'); + expect(added.slug).not.toBe(''); + + // A second punctuation-only-named page dedupes to 'page-2', not ''. + act(() => { + ctx!.addPage('???', ''); + }); + const second = ctx!.pages.find((p) => p.name === '???')!; + expect(second.slug).toBe('page-2'); + expect(second.slug).not.toBe(''); + + unmount(); + }); + test('landing page slug always stays "index" even if renamed to collide', () => { let ctx: ReturnType | null = null; const Consumer: React.FC = () => { diff --git a/craft/src/state/PageContext.tsx b/craft/src/state/PageContext.tsx index 12474e5..0edbc6a 100644 --- a/craft/src/state/PageContext.tsx +++ b/craft/src/state/PageContext.tsx @@ -181,12 +181,17 @@ const PageContext = createContext({ export const usePages = () => useContext(PageContext); function slugify(name: string): string { - return name + const slug = name .toLowerCase() .trim() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-'); + // M-4: a punctuation-only name (e.g. "!!!") strips down to '' -- without a + // fallback, buildSavePayload would write filename = '' + '.html' for that + // page. uniqueSlug's existing dedupe logic then applies on top of this + // fallback the same way it does for any other base ('page', 'page-2', ...). + return slug || 'page'; } /**