diff --git a/src/steps/render.js b/src/steps/render.js index 9b72fd00..c81569c7 100644 --- a/src/steps/render.js +++ b/src/steps/render.js @@ -14,6 +14,7 @@ import { h } from 'hastscript'; import { unified } from 'unified'; import rehypeParse from 'rehype-parse'; +import { cleanupHeaderValue } from '@adobe/helix-shared-utils'; function appendElement($parent, $el) { if ($el) { @@ -34,6 +35,13 @@ function createElement(name, ...attrs) { return h(name, properties); } +function sanitizeJsonLd(jsonLd) { + if (jsonLd.toLowerCase().indexOf('') >= 0) { + throw new Error('script tag not allowed'); + } + return JSON.stringify(JSON.parse(jsonLd.trim())); +} + /** * @type PipelineStep * @param {PipelineState} state @@ -59,7 +67,13 @@ export default async function render(state, req, res) { appendElement($head, createElement('link', 'rel', 'canonical', 'href', meta.canonical)); } + let jsonLd; for (const [name, value] of Object.entries(meta.page)) { + if (name.toLowerCase() === 'json-ld') { + jsonLd = value; + // eslint-disable-next-line no-continue + continue; + } const attr = name.includes(':') && !name.startsWith('twitter:') ? 'property' : 'name'; if (Array.isArray(value)) { for (const v of value) { @@ -71,6 +85,19 @@ export default async function render(state, req, res) { } appendElement($head, createElement('link', 'rel', 'alternate', 'type', 'application/xml+atom', 'href', meta.feed, 'title', `${meta.title} feed`)); + // inject json ld if valid + if (jsonLd) { + const props = { type: 'application/ld+json' }; + try { + jsonLd = sanitizeJsonLd(jsonLd); + } catch (e) { + jsonLd = ''; + props['data-error'] = `error in json-ld: ${cleanupHeaderValue(e.message)}`; + } + const script = h('script', props, jsonLd); + $head.children.push(script); + } + // inject head.html const headHtml = state.config?.head?.html; if (headHtml) { diff --git a/test/fixtures/content/page-metadata-jsonld-error.html b/test/fixtures/content/page-metadata-jsonld-error.html new file mode 100644 index 00000000..62bcf180 --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-error.html @@ -0,0 +1,16 @@ + + Home | Helix Project Boilerplate + + + + + + + + + + + + + + diff --git a/test/fixtures/content/page-metadata-jsonld-error.md b/test/fixtures/content/page-metadata-jsonld-error.md new file mode 100644 index 00000000..5d60edfe --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-error.md @@ -0,0 +1,11 @@ +# JSON LD Test + +This is great. + ++----------------------------------------------------+ +| Metadata | ++================+===================================+ +| title | Home \| Helix Project Boilerplate | ++----------------+-----------------------------------| +| Json-Ld | "@context":"http://schema.org" | ++----------------+-----------------------------------+ diff --git a/test/fixtures/content/page-metadata-jsonld-global.html b/test/fixtures/content/page-metadata-jsonld-global.html new file mode 100644 index 00000000..7004bc9a --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-global.html @@ -0,0 +1,16 @@ + + Global JSON LD Test + + + + + + + + + + + + + + diff --git a/test/fixtures/content/page-metadata-jsonld-global.md b/test/fixtures/content/page-metadata-jsonld-global.md new file mode 100644 index 00000000..4c0fd3ff --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-global.md @@ -0,0 +1,3 @@ +# Global JSON LD Test + +This is great. diff --git a/test/fixtures/content/page-metadata-jsonld-multi.html b/test/fixtures/content/page-metadata-jsonld-multi.html new file mode 100644 index 00000000..e0d9b564 --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-multi.html @@ -0,0 +1,16 @@ + + Home | Helix Project Boilerplate + + + + + + + + + + + + + + diff --git a/test/fixtures/content/page-metadata-jsonld-multi.md b/test/fixtures/content/page-metadata-jsonld-multi.md new file mode 100644 index 00000000..ef188f6c --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-multi.md @@ -0,0 +1,13 @@ +# JSON LD Test + +This is great. + ++-------------------------------------------------------------------------------------------+ +| Metadata | ++================+==========================================================================+ +| title | Home \| Helix Project Boilerplate | ++----------------+--------------------------------------------------------------------------| +| json-ld | {"@context":"http://schema.org","@type":"Product","sku":"BPB-CMON-TABS"} | ++----------------+--------------------------------------------------------------------------| +| json-ld | {"@context":"http://schema.org","@type":"Product","sku":"FOO-BAR-12345"} | ++----------------+--------------------------------------------------------------------------+ diff --git a/test/fixtures/content/page-metadata-jsonld-xss.html b/test/fixtures/content/page-metadata-jsonld-xss.html new file mode 100644 index 00000000..ac6fb7d0 --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-xss.html @@ -0,0 +1,16 @@ + + Home | Helix Project Boilerplate + + + + + + + + + + + + + + diff --git a/test/fixtures/content/page-metadata-jsonld-xss.md b/test/fixtures/content/page-metadata-jsonld-xss.md new file mode 100644 index 00000000..1c3d4ff8 --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld-xss.md @@ -0,0 +1,11 @@ +# JSON LD Test + +This is great. + ++-------------------------------------------------------------------------+ +| Metadata | ++================+========================================================+ +| title | Home \| Helix Project Boilerplate | ++----------------+--------------------------------------------------------| +| Json-Ld | { "foo:": "\ + + + + + diff --git a/test/fixtures/content/page-metadata-jsonld.md b/test/fixtures/content/page-metadata-jsonld.md new file mode 100644 index 00000000..37087279 --- /dev/null +++ b/test/fixtures/content/page-metadata-jsonld.md @@ -0,0 +1,11 @@ +# JSON LD Test + +This is great. + ++-------------------------------------------------------------------------------------------+ +| Metadata | ++================+==========================================================================+ +| title | Home \| Helix Project Boilerplate | ++----------------+--------------------------------------------------------------------------| +| json-ld | {"@context":"http://schema.org","@type":"Product","sku":"BPB-CMON-TABS"} | ++----------------+--------------------------------------------------------------------------+ diff --git a/test/rendering.test.js b/test/rendering.test.js index 9ee7f537..ba13690d 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -193,6 +193,7 @@ describe('Rendering', () => { } const response = await render(url, '', expStatus); const actHtml = response.body; + console.log(actHtml); if (expStatus === 200) { const $actMain = new JSDOM(actHtml).window.document.querySelector(domSelector); const $expMain = new JSDOM(expHtml).window.document.querySelector(domSelector); @@ -407,6 +408,55 @@ describe('Rendering', () => { config = DEFAULT_CONFIG_EMPTY; await testRender('page-metadata-twitter-fallback', 'head'); }); + + it('injects json ld', async () => { + config = DEFAULT_CONFIG_EMPTY; + await testRender('page-metadata-jsonld', 'head'); + }); + + it('chooses last json-ld if multiple', async () => { + config = { + ...DEFAULT_CONFIG_EMPTY, + metadata: { + live: { + data: { + '/**': [{ + key: 'json-ld', + value: '{"@context":"http://schema.org","@type":"Product","sku":"AA-BB-GLOBAL"}', + }], + }, + }, + }, + }; + await testRender('page-metadata-jsonld-multi', 'head'); + }); + + it('injects global json ld', async () => { + config = { + ...DEFAULT_CONFIG_EMPTY, + metadata: { + live: { + data: { + '/**': [{ + key: 'json-ld', + value: '{"@context":"http://schema.org","@type":"Product","sku":"AA-BB-GLOBAL"}', + }], + }, + }, + }, + }; + await testRender('page-metadata-jsonld-global', 'head'); + }); + + it('detects errors in json ld', async () => { + config = DEFAULT_CONFIG_EMPTY; + await testRender('page-metadata-jsonld-error', 'head'); + }); + + it('prevents xss in json ld', async () => { + config = DEFAULT_CONFIG_EMPTY; + await testRender('page-metadata-jsonld-xss', 'head'); + }); }); describe('Miscellaneous', () => {