feat(site-builder): pre-publish checklist and canonical link

Publish now runs client-side checks (image alt, meta description, title
length, H1, empty links, contrast, noindex landing, empty page) over the
current state and shows a grouped checklist with Fix links and
"Publish anyway". Adds ExportOptions.canonicalUrl -> <link rel="canonical">
after the favicon line in both Preview and export.

Refs #54
This commit is contained in:
shadowdao committed 2026-10-07 12:05:28 -07:00
1 parent 5430f13877
commit c0fd183c11
12 files changed
+913 -13

No files matched your search

+8 -1
View File
@@ -204,7 +204,7 @@ describe('conflict modal', () => {
loadedThen('r1', [conflict409]);
mount();
await flush();
await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); });
await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await confirmChecklist();
await flush();
expect(q('[data-testid="conflict-modal"]')).not.toBeNull();
expect(fetchMock.mock.calls.filter(([u]) => String(u).includes('action=publish'))).toHaveLength(0);
@@ -264,3 +264,10 @@ describe('revision and overwrite edge cases', () => {
expect(saveBody(2).base_rev).toBe('r1');
});
});
/** #54: the pre-publish checklist opens for these fixtures (an empty Home with no
* meta description); these tests are about the publish path, so confirm it. */
async function confirmChecklist() {
const b = q('[data-testid="publish-checklist-confirm"]') as HTMLElement | null;
if (b) await act(async () => { b.click(); });
}
+8 -1
View File
@@ -138,7 +138,7 @@ afterEach(() => {
});
const edit = () => act(() => { guard.markDirty(); });
const clickPublish = async () => { await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await flush(); };
const clickPublish = async () => { await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await confirmChecklist(); await flush(); };
describe('saves are serialised', () => {
test('a save requested during an in-flight publish waits, then saves with the rev the first save returned', async () => {
@@ -235,3 +235,10 @@ describe('autosave timer', () => {
expect(calls('save').length).toBeGreaterThanOrEqual(2);
});
});
/** #54: the pre-publish checklist opens for these fixtures (an empty Home with no
* meta description); these tests are about the publish path, so confirm it. */
async function confirmChecklist() {
const b = q('[data-testid="publish-checklist-confirm"]') as HTMLElement | null;
if (b) await act(async () => { b.click(); });
}
+10 -3
View File
@@ -69,7 +69,7 @@ const flush = () => act(async () => { for (let i = 0; i < 10; i++) await Promise
const calls = (action: string) => fetchMock.mock.calls.filter(([u]) => String(u).includes(`action=${action}`));
const edit = () => act(() => { guard.markDirty(); });
const clickSave = async () => { await act(async () => { q('button[aria-label="Save Draft"]')!.click(); }); await flush(); };
const clickPublish = async () => { await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await flush(); };
const clickPublish = async () => { await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await confirmChecklist(); await flush(); };
function respondWith(handlers: { save?: unknown[]; publish?: unknown[] }) {
const save = [...(handlers.save ?? [])];
@@ -368,7 +368,7 @@ describe('publish racing an edit', () => {
});
await mount();
edit();
await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); });
await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await confirmChecklist();
await flush();
expect(calls('publish')).toHaveLength(1);
@@ -392,7 +392,7 @@ describe('publish racing an edit', () => {
});
await mount();
edit();
await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); });
await act(async () => { q('button[aria-label="Publish to live site"]')!.click(); }); await confirmChecklist();
await flush();
const savesBefore = calls('save').length; // the publish's own save
await act(async () => { vi.advanceTimersByTime(30_000); });
@@ -433,3 +433,10 @@ describe('populated project load', () => {
expect(pill()).toBe('unsaved');
});
});
/** #54: the pre-publish checklist opens for these fixtures (an empty Home with no
* meta description); these tests are about the publish path, so confirm it. */
async function confirmChecklist() {
const b = q('[data-testid="publish-checklist-confirm"]') as HTMLElement | null;
if (b) await act(async () => { b.click(); });
}