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:
1 parent
5430f13877
commit
c0fd183c11
12 files changed
+913
-13
No files matched your search
@@ -5,6 +5,7 @@ import {
|
||||
buildAnimationScript,
|
||||
ExportOptions,
|
||||
injectAttrs,
|
||||
buildSeoMeta,
|
||||
RESPONSIVE_CSS_MINIFIED,
|
||||
VISIBILITY_CSS_MINIFIED,
|
||||
} from './html-export';
|
||||
@@ -738,3 +739,37 @@ describe('injectAttrs skips leading <style> blocks (W0-breakpoints)', () => {
|
||||
expect(html).toContain('</style><a href="#"');
|
||||
});
|
||||
});
|
||||
|
||||
/** #54: `<link rel="canonical">`. The backend `generateCompiledHTML` must emit this exact line. */
|
||||
describe('#54: canonical link in <head>', () => {
|
||||
const STATE = JSON.stringify({
|
||||
ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { style: {}, tag: 'div' }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {} },
|
||||
});
|
||||
const exp = (o: ExportOptions) => exportToHtml(STATE, o).html;
|
||||
|
||||
test('emitted only when provided, as the exact line', () => {
|
||||
expect(exp({ title: 'P', canonicalUrl: 'https://example.com/about' })).toContain('<link rel="canonical" href="https://example.com/about">');
|
||||
expect(exp({ title: 'P', canonicalUrl: 'https://example.com/' })).toContain('<link rel="canonical" href="https://example.com/">');
|
||||
expect(exp({ title: 'P' })).not.toContain('canonical');
|
||||
expect(exp({ title: 'P', canonicalUrl: '' })).not.toContain('canonical');
|
||||
});
|
||||
|
||||
test('without the option the document is byte-identical to one built with canonicalUrl undefined', () => {
|
||||
const base = { title: 'P', favicon: '/f.png', description: 'd' };
|
||||
expect(exp(base)).toBe(exp({ ...base, canonicalUrl: undefined }));
|
||||
expect(exp(base)).not.toContain('<link rel="canonical"');
|
||||
});
|
||||
|
||||
test('placed directly after the favicon line, and after it only', () => {
|
||||
const block = buildSeoMeta({ title: 'P', favicon: '/f.png', canonicalUrl: 'https://e.com/x' }, 'P');
|
||||
expect(block.endsWith('<link rel="icon" href="/f.png">\n <link rel="canonical" href="https://e.com/x">')).toBe(true);
|
||||
// No favicon: canonical is still last.
|
||||
expect(buildSeoMeta({ canonicalUrl: 'https://e.com/x' }, 'P').endsWith('<link rel="canonical" href="https://e.com/x">')).toBe(true);
|
||||
});
|
||||
|
||||
test('attribute value is escaped; non-http(s) URLs are dropped', () => {
|
||||
expect(buildSeoMeta({ canonicalUrl: 'https://e.com/a"b<c>&d' }, 'P')).toContain('<link rel="canonical" href="https://e.com/a"b<c>&d">');
|
||||
expect(buildSeoMeta({ canonicalUrl: 'javascript:alert(1)' }, 'P')).not.toContain('canonical');
|
||||
expect(buildSeoMeta({ canonicalUrl: '/relative' }, 'P')).not.toContain('canonical');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user