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', () => {