Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/components/JsonLd.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
---
import { serializeJsonLd } from '../lib/json-ld.mjs';

const { data } = Astro.props;
---
<script type="application/ld+json" set:html={JSON.stringify(data)} />
<script type="application/ld+json" set:html={serializeJsonLd(data)} />
11 changes: 11 additions & 0 deletions src/lib/json-ld.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// JSON-LD for a <script type="application/ld+json"> element, which the browser ends at the first `</script`.

const UNSAFE = /[<>&\u2028\u2029]/g;

/**
* `data` as JSON, with `<`, `>`, `&`, U+2028 and U+2029 written as `\u` escapes. A string holding `</script>` or `<!--`
* then stays inside the script element, and the JSON still parses to the same value.
*/
export function serializeJsonLd(data) {
return JSON.stringify(data).replace(UNSAFE, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
23 changes: 23 additions & 0 deletions tests/json-ld.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { serializeJsonLd } from '../src/lib/json-ld.mjs';

test('JSON-LD escapes <, >, &, U+2028 and U+2029 and still parses to the same value', () => {
const data = {
'@type': 'BlogPosting',
headline: 'Ends early </script><b>x</b> & <!-- comment --> > done',
description: 'Line\u2028and paragraph\u2029separators',
keywords: ['</SCRIPT >', 'a&b'],
};
const json = serializeJsonLd(data);
// With no `<` left, the browser can't find `</script` or `<!--`, so the whole JSON stays in the script element.
assert.doesNotMatch(json, /[<>&\u2028\u2029]/);
assert.match(json, /"Ends early \\u003c\/script\\u003e\\u003cb\\u003ex\\u003c\/b\\u003e \\u0026 \\u003c!-- comment/);
assert.match(json, /"Line\\u2028and paragraph\\u2029separators"/);
assert.deepEqual(JSON.parse(json), data);
});

test('JSON-LD without those characters is plain JSON.stringify', () => {
const data = { '@context': 'https://schema.org', name: 'meaningfool', sameAs: ['https://github.com/meaningfool'] };
assert.equal(serializeJsonLd(data), JSON.stringify(data));
});
Loading