snappy-prerender 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/head/tags.js CHANGED
@@ -11,12 +11,19 @@ const SINGLETON_LINK_RELS = new Set(['canonical']);
11
11
  * caller did not supply are simply absent, so the component never invents metadata the app
12
12
  * did not ask for.
13
13
  */
14
- export function buildHeadEntries(props = {}, defaults = {}) {
14
+ export function buildHeadEntries(props = {}, defaults = {}, id = 'default') {
15
15
  const entries = [];
16
16
  const template = props.titleTemplate ?? defaults.titleTemplate ?? null;
17
17
  const siteName = props.siteName ?? defaults.siteName ?? null;
18
18
  const image = props.image ?? defaults.defaultImage ?? null;
19
19
 
20
+ if (typeof props.lang === 'string' && props.lang.length > 0) {
21
+ entries.push({ kind: 'html', key: 'lang', value: props.lang });
22
+ }
23
+ if (props.dir === 'ltr' || props.dir === 'rtl' || props.dir === 'auto') {
24
+ entries.push({ kind: 'html', key: 'dir', value: props.dir });
25
+ }
26
+
20
27
  const title = props.title ? applyTitleTemplate(props.title, template, siteName) : null;
21
28
  if (title) entries.push({ kind: 'title', text: title });
22
29
  if (title && props.openGraph !== false) entries.push(metaProp('og:title', title));
@@ -50,6 +57,24 @@ export function buildHeadEntries(props = {}, defaults = {}) {
50
57
  entries.push({ kind: 'link', key: 'rel:canonical', attrs: { rel: 'canonical', href: url } });
51
58
  }
52
59
 
60
+ for (const alternate of props.alternates ?? []) {
61
+ const href = pageUrl(alternate.href, defaults);
62
+ const hreflang = alternate.hreflang ?? alternate.lang;
63
+ if (!href || !hreflang) continue;
64
+ const attrs = { rel: 'alternate', hreflang, href };
65
+ if (alternate.type) attrs.type = alternate.type;
66
+ entries.push({ kind: 'link', key: `rel:alternate|${hreflang}|${href}`, attrs });
67
+ }
68
+
69
+ if (props.structuredData !== undefined && props.structuredData !== null) {
70
+ entries.push({
71
+ kind: 'script',
72
+ key: `script:ld+json:${id}`,
73
+ attrs: { type: 'application/ld+json' },
74
+ text: serializeStructuredData(props.structuredData),
75
+ });
76
+ }
77
+
53
78
  for (const extra of props.extra ?? []) entries.push(extraEntry(extra));
54
79
 
55
80
  return entries;
@@ -77,6 +102,8 @@ export function mergeHeadEntries(stack = []) {
77
102
  */
78
103
  export function headKey(entry) {
79
104
  if (entry.kind === 'title') return 'title';
105
+ if (entry.kind === 'html') return `html:${entry.key}`;
106
+ if (entry.kind === 'script') return entry.key;
80
107
  if (entry.kind === 'link') return `link:${entry.key ?? attrsKey(entry.attrs)}`;
81
108
  if (entry.key) return `meta:${entry.key}`;
82
109
  const name = attrsValue(entry.attrs, 'name');
@@ -195,10 +222,29 @@ function attrsValue(attrs = {}, name) {
195
222
  return attrs[name];
196
223
  }
197
224
 
198
- // Keeps output deterministic: title, then name/property metas, then links.
225
+ // Serialises JSON-LD so a value can never close the script element. The escapes remain
226
+ // valid JSON, so crawlers parse the same object the caller supplied.
227
+ function serializeStructuredData(value) {
228
+ return JSON.stringify(value).replace(
229
+ /[<>&\u2028\u2029]/g,
230
+ (char) =>
231
+ ({
232
+ '<': '\\u003C',
233
+ '>': '\\u003E',
234
+ '&': '\\u0026',
235
+ '\u2028': '\\u2028',
236
+ '\u2029': '\\u2029',
237
+ })[char],
238
+ );
239
+ }
240
+
241
+ // Keeps output deterministic: title, metas, links, JSON-LD scripts, then html attributes.
199
242
  function sortMetasFirst(entries) {
200
- return [...entries].sort((a, b) => {
201
- const rank = (entry) => (entry.kind === 'meta' ? 0 : 1);
202
- return rank(a) - rank(b);
203
- });
243
+ const rank = (entry) => {
244
+ if (entry.kind === 'meta') return 0;
245
+ if (entry.kind === 'link') return 1;
246
+ if (entry.kind === 'script') return 2;
247
+ return 3;
248
+ };
249
+ return [...entries].sort((a, b) => rank(a) - rank(b));
204
250
  }