Site builder: security & data-loss hardening + unified asset picker + audit backlog #3

Merged
jknapp merged 61 commits from builder-hardening-2026-07 into main 2026-07-13 01:13:28 +00:00
2 changed files with 38 additions and 1 deletions
Showing only changes of commit e12fb89ada - Show all commits
+32
View File
@@ -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<typeof usePages> | null = null;
const Consumer: React.FC = () => {
ctx = usePages();
return null;
};
render(
<PageProvider>
<Consumer />
</PageProvider>,
);
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<typeof usePages> | null = null;
const Consumer: React.FC = () => {
+6 -1
View File
@@ -181,12 +181,17 @@ const PageContext = createContext<PageContextValue>({
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';
}
/**