unhead 1.0.21 → 1.1.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/dist/index.cjs CHANGED
@@ -2,300 +2,53 @@
2
2
 
3
3
  const hookable = require('hookable');
4
4
  const dom = require('@unhead/dom');
5
+ const shared = require('@unhead/shared');
6
+ const packrup = require('packrup');
5
7
 
6
- const ValidHeadTags = [
7
- "title",
8
- "titleTemplate",
9
- "base",
10
- "htmlAttrs",
11
- "bodyAttrs",
12
- "meta",
13
- "link",
14
- "style",
15
- "script",
16
- "noscript"
17
- ];
18
- const TagConfigKeys = ["tagPosition", "tagPriority", "tagDuplicateStrategy"];
19
-
20
- async function normaliseTag(tagName, input) {
21
- const tag = { tag: tagName, props: {} };
22
- if (tagName === "title" || tagName === "titleTemplate") {
23
- tag.children = input instanceof Promise ? await input : input;
24
- return tag;
25
- }
26
- tag.props = await normaliseProps({ ...input });
27
- ["children", "innerHtml", "innerHTML"].forEach((key) => {
28
- if (typeof tag.props[key] !== "undefined") {
29
- tag.children = tag.props[key];
30
- if (typeof tag.children === "object")
31
- tag.children = JSON.stringify(tag.children);
32
- delete tag.props[key];
33
- }
34
- });
35
- Object.keys(tag.props).filter((k) => TagConfigKeys.includes(k)).forEach((k) => {
36
- tag[k] = tag.props[k];
37
- delete tag.props[k];
38
- });
39
- if (typeof tag.props.class === "object" && !Array.isArray(tag.props.class)) {
40
- tag.props.class = Object.keys(tag.props.class).filter((k) => tag.props.class[k]);
41
- }
42
- if (Array.isArray(tag.props.class))
43
- tag.props.class = tag.props.class.join(" ");
44
- if (tag.props.content && Array.isArray(tag.props.content)) {
45
- return tag.props.content.map((v, i) => {
46
- const newTag = { ...tag, props: { ...tag.props } };
47
- newTag.props.content = v;
48
- newTag.key = `${tag.props.name || tag.props.property}:${i}`;
49
- return newTag;
50
- });
51
- }
52
- return tag;
53
- }
54
- async function normaliseProps(props) {
55
- for (const k of Object.keys(props)) {
56
- if (props[k] instanceof Promise) {
57
- props[k] = await props[k];
58
- }
59
- if (String(props[k]) === "true") {
60
- props[k] = "";
61
- } else if (String(props[k]) === "false") {
62
- delete props[k];
63
- }
8
+ const TAG_WEIGHTS = {
9
+ // aliases
10
+ critical: 2,
11
+ high: 9,
12
+ low: 12,
13
+ // tags
14
+ base: -1,
15
+ title: 1,
16
+ meta: 10
17
+ };
18
+ function tagWeight(tag) {
19
+ if (typeof tag.tagPriority === "number")
20
+ return tag.tagPriority;
21
+ if (tag.tag === "meta") {
22
+ if (tag.props.charset)
23
+ return -2;
24
+ if (tag.props["http-equiv"] === "content-security-policy")
25
+ return 0;
64
26
  }
65
- return props;
66
- }
67
-
68
- function unpackToArray(input, options) {
69
- const unpacked = [];
70
- const kFn = options.resolveKeyData || ((ctx) => ctx.key);
71
- const vFn = options.resolveValueData || ((ctx) => ctx.value);
72
- for (const [k, v] of Object.entries(input)) {
73
- unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
74
- const ctx = { key: k, value: i };
75
- const val = vFn(ctx);
76
- if (typeof val === "object")
77
- return unpackToArray(val, options);
78
- if (Array.isArray(val))
79
- return val;
80
- return {
81
- [typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
82
- [typeof options.value === "function" ? options.value(ctx) : options.value]: val
83
- };
84
- }).flat());
27
+ const key = tag.tagPriority || tag.tag;
28
+ if (key in TAG_WEIGHTS) {
29
+ return TAG_WEIGHTS[key];
85
30
  }
86
- return unpacked;
31
+ return 10;
87
32
  }
88
-
89
- function unpackToString(value, options) {
90
- return Object.entries(value).map(([key, value2]) => {
91
- if (typeof value2 === "object")
92
- value2 = unpackToString(value2, options);
93
- if (options.resolve) {
94
- const resolved = options.resolve({ key, value: value2 });
95
- if (resolved)
96
- return resolved;
97
- }
98
- if (typeof value2 === "number")
99
- value2 = value2.toString();
100
- if (typeof value2 === "string" && options.wrapValue) {
101
- value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
102
- value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
103
- }
104
- return `${key}${options.keyValueSeparator || ""}${value2}`;
105
- }).join(options.entrySeparator || "");
106
- }
107
-
108
- const MetaPackingSchema = {
109
- robots: {
110
- unpack: {
111
- keyValueSeparator: ":"
112
- }
113
- },
114
- // Pragma directives
115
- contentSecurityPolicy: {
116
- unpack: {
117
- keyValueSeparator: " ",
118
- entrySeparator: "; "
119
- },
120
- metaKey: "http-equiv"
121
- },
122
- fbAppId: {
123
- keyValue: "fb:app_id",
124
- metaKey: "property"
125
- },
126
- msapplicationTileImage: {
127
- keyValue: "msapplication-TileImage"
128
- },
129
- /**
130
- * Tile colour for windows
131
- */
132
- msapplicationTileColor: {
133
- keyValue: "msapplication-TileColor"
134
- },
135
- /**
136
- * URL of a config for windows tile.
137
- */
138
- msapplicationConfig: {
139
- keyValue: "msapplication-Config"
140
- },
141
- charset: {
142
- metaKey: "charset"
143
- },
144
- contentType: {
145
- metaKey: "http-equiv"
146
- },
147
- defaultStyle: {
148
- metaKey: "http-equiv"
149
- },
150
- xUaCompatible: {
151
- metaKey: "http-equiv"
152
- },
153
- refresh: {
154
- metaKey: "http-equiv"
155
- }
156
- };
157
- function resolveMetaKeyType(key) {
158
- return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
159
- }
160
-
161
- const ArrayableInputs = ["Image", "Video", "Audio"];
162
- function unpackMeta(input) {
163
- const extras = [];
164
- ArrayableInputs.forEach((key) => {
165
- const ogKey = `og:${key.toLowerCase()}`;
166
- const inputKey = `og${key}`;
167
- const val = input[inputKey];
168
- if (typeof val === "object") {
169
- (Array.isArray(val) ? val : [val]).forEach((entry) => {
170
- if (!entry)
171
- return;
172
- const unpackedEntry = unpackToArray(entry, {
173
- key: "property",
174
- value: "content",
175
- resolveKeyData({ key: key2 }) {
176
- return fixKeyCase(`${ogKey}${key2 !== "url" ? `:${key2}` : ""}`);
177
- },
178
- resolveValueData({ value }) {
179
- return typeof value === "number" ? value.toString() : value;
180
- }
181
- });
182
- extras.push(
183
- ...unpackedEntry.sort((a, b) => a.property === ogKey ? -1 : b.property === ogKey ? 1 : 0)
184
- );
185
- });
186
- delete input[inputKey];
187
- }
188
- });
189
- const meta = unpackToArray(input, {
190
- key({ key }) {
191
- return resolveMetaKeyType(key);
192
- },
193
- value({ key }) {
194
- return key === "charset" ? "charset" : "content";
195
- },
196
- resolveKeyData({ key }) {
197
- return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
198
- },
199
- resolveValueData({ value, key }) {
200
- if (value === null)
201
- return "_null";
202
- if (typeof value === "object") {
203
- const definition = MetaPackingSchema[key];
204
- if (key === "refresh")
205
- return `${value.seconds};url=${value.url}`;
206
- return unpackToString(
207
- changeKeyCasingDeep(value),
208
- {
209
- entrySeparator: ", ",
210
- keyValueSeparator: "=",
211
- resolve({ value: value2, key: key2 }) {
212
- if (value2 === null)
213
- return "";
214
- if (typeof value2 === "boolean")
215
- return `${key2}`;
216
- },
217
- ...definition?.unpack
33
+ const SortModifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
34
+ function SortTagsPlugin() {
35
+ return shared.defineHeadPlugin({
36
+ hooks: {
37
+ "tags:resolve": (ctx) => {
38
+ const tagPositionForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
39
+ for (const { prefix, offset } of SortModifiers) {
40
+ for (const tag of ctx.tags.filter((tag2) => typeof tag2.tagPriority === "string" && tag2.tagPriority.startsWith(prefix))) {
41
+ const position = tagPositionForKey(
42
+ tag.tagPriority.replace(prefix, "")
43
+ );
44
+ if (typeof position !== "undefined")
45
+ tag._p = position + offset;
218
46
  }
219
- );
47
+ }
48
+ ctx.tags.sort((a, b) => a._p - b._p).sort((a, b) => tagWeight(a) - tagWeight(b));
220
49
  }
221
- return typeof value === "number" ? value.toString() : value;
222
50
  }
223
51
  });
224
- return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
225
- }
226
-
227
- const PropertyPrefixKeys = /^(og|fb)/;
228
- const ColonPrefixKeys = /^(og|twitter|fb)/;
229
- function fixKeyCase(key) {
230
- key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
231
- if (ColonPrefixKeys.test(key)) {
232
- key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
233
- }
234
- return key;
235
- }
236
- function changeKeyCasingDeep(input) {
237
- if (Array.isArray(input)) {
238
- return input.map((entry) => changeKeyCasingDeep(entry));
239
- }
240
- if (typeof input !== "object" || Array.isArray(input))
241
- return input;
242
- const output = {};
243
- for (const [key, value] of Object.entries(input))
244
- output[fixKeyCase(key)] = changeKeyCasingDeep(value);
245
- return output;
246
- }
247
-
248
- const tagWeight = (tag) => {
249
- if (typeof tag.tagPriority === "number")
250
- return tag.tagPriority;
251
- switch (tag.tagPriority) {
252
- case "critical":
253
- return 2;
254
- case "high":
255
- return 9;
256
- case "low":
257
- return 12;
258
- }
259
- switch (tag.tag) {
260
- case "base":
261
- return -1;
262
- case "title":
263
- return 1;
264
- case "meta":
265
- if (tag.props.charset)
266
- return -2;
267
- if (tag.props["http-equiv"] === "content-security-policy")
268
- return 0;
269
- return 10;
270
- default:
271
- return 10;
272
- }
273
- };
274
- const sortTags = (aTag, bTag) => {
275
- return tagWeight(aTag) - tagWeight(bTag);
276
- };
277
-
278
- const UniqueTags = ["base", "title", "titleTemplate", "bodyAttrs", "htmlAttrs"];
279
- function tagDedupeKey(tag, fn) {
280
- const { props, tag: tagName } = tag;
281
- if (UniqueTags.includes(tagName))
282
- return tagName;
283
- if (tagName === "link" && props.rel === "canonical")
284
- return "canonical";
285
- if (props.charset)
286
- return "charset";
287
- const name = ["id"];
288
- if (tagName === "meta")
289
- name.push(...["name", "property", "http-equiv"]);
290
- for (const n of name) {
291
- if (typeof props[n] !== "undefined") {
292
- const val = String(props[n]);
293
- if (fn && !fn(val))
294
- return false;
295
- return `${tagName}:${n}:${val}`;
296
- }
297
- }
298
- return false;
299
52
  }
300
53
 
301
54
  const renderTitleTemplate = (template, title) => {
@@ -305,127 +58,44 @@ const renderTitleTemplate = (template, title) => {
305
58
  return template(title);
306
59
  return template.replace("%s", title ?? "");
307
60
  };
308
- function resolveTitleTemplateFromTags(tags) {
309
- let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
310
- const titleIdx = tags.findIndex((i) => i.tag === "title");
311
- if (titleIdx !== -1 && titleTemplateIdx !== -1) {
312
- const newTitle = renderTitleTemplate(
313
- tags[titleTemplateIdx].children,
314
- tags[titleIdx].children
315
- );
316
- if (newTitle !== null) {
317
- tags[titleIdx].children = newTitle || tags[titleIdx].children;
318
- } else {
319
- delete tags[titleIdx];
320
- }
321
- } else if (titleTemplateIdx !== -1) {
322
- const newTitle = renderTitleTemplate(
323
- tags[titleTemplateIdx].children
324
- );
325
- if (newTitle !== null) {
326
- tags[titleTemplateIdx].children = newTitle;
327
- tags[titleTemplateIdx].tag = "title";
328
- titleTemplateIdx = -1;
329
- }
330
- }
331
- if (titleTemplateIdx !== -1) {
332
- delete tags[titleTemplateIdx];
333
- }
334
- return tags.filter(Boolean);
335
- }
336
-
337
- const DedupesTagsPlugin = (options) => {
338
- options = options || {};
339
- const dedupeKeys = options.dedupeKeys || ["hid", "vmid", "key"];
340
- return defineHeadPlugin({
341
- hooks: {
342
- "tag:normalise": function({ tag }) {
343
- dedupeKeys.forEach((key) => {
344
- if (tag.props[key]) {
345
- tag.key = tag.props[key];
346
- delete tag.props[key];
347
- }
348
- });
349
- const dedupe = tag.key ? `${tag.tag}:${tag.key}` : tagDedupeKey(tag);
350
- if (dedupe)
351
- tag._d = dedupe;
352
- },
353
- "tags:resolve": function(ctx) {
354
- const deduping = {};
355
- ctx.tags.forEach((tag) => {
356
- let dedupeKey = tag._d || tag._p;
357
- const dupedTag = deduping[dedupeKey];
358
- if (dupedTag) {
359
- let strategy = tag?.tagDuplicateStrategy;
360
- if (!strategy && (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs"))
361
- strategy = "merge";
362
- if (strategy === "merge") {
363
- const oldProps = dupedTag.props;
364
- ["class", "style"].forEach((key) => {
365
- if (tag.props[key] && oldProps[key]) {
366
- if (key === "style" && !oldProps[key].endsWith(";"))
367
- oldProps[key] += ";";
368
- tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
369
- }
370
- });
371
- deduping[dedupeKey].props = {
372
- ...oldProps,
373
- ...tag.props
374
- };
375
- return;
376
- } else if (tag._e === dupedTag._e) {
377
- dedupeKey = tag._d = `${dedupeKey}:${tag._p}`;
378
- }
379
- const propCount = Object.keys(tag.props).length;
380
- if ((propCount === 0 || propCount === 1 && typeof tag.props["data-h-key"] !== "undefined") && !tag.children) {
381
- delete deduping[dedupeKey];
382
- return;
383
- }
384
- }
385
- deduping[dedupeKey] = tag;
386
- });
387
- ctx.tags = Object.values(deduping);
388
- }
389
- }
390
- });
391
- };
392
-
393
- const SortTagsPlugin = () => {
394
- return defineHeadPlugin({
61
+ const TitleTemplatePlugin = () => {
62
+ return shared.defineHeadPlugin({
395
63
  hooks: {
396
64
  "tags:resolve": (ctx) => {
397
- const tagIndexForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
398
- for (const tag of ctx.tags) {
399
- if (!tag.tagPriority || typeof tag.tagPriority === "number")
400
- continue;
401
- const modifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
402
- for (const { prefix, offset } of modifiers) {
403
- if (tag.tagPriority.startsWith(prefix)) {
404
- const key = tag.tagPriority.replace(prefix, "");
405
- const index = tagIndexForKey(key);
406
- if (typeof index !== "undefined")
407
- tag._p = index + offset;
408
- }
65
+ const { tags } = ctx;
66
+ let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
67
+ const titleIdx = tags.findIndex((i) => i.tag === "title");
68
+ if (titleIdx !== -1 && titleTemplateIdx !== -1) {
69
+ const newTitle = renderTitleTemplate(
70
+ tags[titleTemplateIdx].textContent,
71
+ tags[titleIdx].textContent
72
+ );
73
+ if (newTitle !== null) {
74
+ tags[titleIdx].textContent = newTitle || tags[titleIdx].textContent;
75
+ } else {
76
+ delete tags[titleIdx];
77
+ }
78
+ } else if (titleTemplateIdx !== -1) {
79
+ const newTitle = renderTitleTemplate(
80
+ tags[titleTemplateIdx].textContent
81
+ );
82
+ if (newTitle !== null) {
83
+ tags[titleTemplateIdx].textContent = newTitle;
84
+ tags[titleTemplateIdx].tag = "title";
85
+ titleTemplateIdx = -1;
409
86
  }
410
87
  }
411
- ctx.tags.sort((a, b) => a._p - b._p).sort(sortTags);
412
- }
413
- }
414
- });
415
- };
416
-
417
- const TitleTemplatePlugin = () => {
418
- return defineHeadPlugin({
419
- hooks: {
420
- "tags:resolve": (ctx) => {
421
- ctx.tags = resolveTitleTemplateFromTags(ctx.tags);
88
+ if (titleTemplateIdx !== -1) {
89
+ delete tags[titleTemplateIdx];
90
+ }
91
+ ctx.tags = tags.filter(Boolean);
422
92
  }
423
93
  }
424
94
  });
425
95
  };
426
96
 
427
97
  const DeprecatedTagAttrPlugin = () => {
428
- return defineHeadPlugin({
98
+ return shared.defineHeadPlugin({
429
99
  hooks: {
430
100
  "tag:normalise": function({ tag }) {
431
101
  if (typeof tag.props.body !== "undefined") {
@@ -440,18 +110,21 @@ const DeprecatedTagAttrPlugin = () => {
440
110
  const IsBrowser = typeof window !== "undefined";
441
111
 
442
112
  const ProvideTagHashPlugin = () => {
443
- return defineHeadPlugin({
113
+ return shared.defineHeadPlugin({
444
114
  hooks: {
445
115
  "tag:normalise": (ctx) => {
446
- const { tag, entry } = ctx;
116
+ const { tag, entry, resolvedOptions } = ctx;
117
+ if (resolvedOptions.experimentalHashHydration === true) {
118
+ tag._h = shared.hashTag(tag);
119
+ }
447
120
  const isDynamic = typeof tag.props._dynamic !== "undefined";
448
- if (!HasElementTags.includes(tag.tag) || !tag.key)
121
+ if (!shared.HasElementTags.includes(tag.tag) || !tag.key)
449
122
  return;
450
- tag._hash = dom.hashCode(JSON.stringify({ tag: tag.tag, key: tag.key }));
451
123
  if (IsBrowser || getActiveHead()?.resolvedOptions?.document)
452
124
  return;
453
125
  if (entry._m === "server" || isDynamic) {
454
- tag.props[`data-h-${tag._hash}`] = "";
126
+ tag._h = tag._h || shared.hashTag(tag);
127
+ tag.props[`data-h-${tag._h}`] = "";
455
128
  }
456
129
  },
457
130
  "tags:resolve": (ctx) => {
@@ -464,23 +137,7 @@ const ProvideTagHashPlugin = () => {
464
137
  });
465
138
  };
466
139
 
467
- const PatchDomOnEntryUpdatesPlugin = (options) => {
468
- return defineHeadPlugin({
469
- hooks: {
470
- "entries:updated": function(head) {
471
- if (typeof options?.document === "undefined" && typeof window === "undefined")
472
- return;
473
- let delayFn = options?.delayFn;
474
- if (!delayFn && typeof requestAnimationFrame !== "undefined")
475
- delayFn = requestAnimationFrame;
476
- import('@unhead/dom').then(({ debouncedRenderDOMHead }) => {
477
- debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
478
- });
479
- }
480
- }
481
- });
482
- };
483
-
140
+ const ValidEventTags = ["script", "link", "bodyAttrs"];
484
141
  const EventHandlersPlugin = () => {
485
142
  const stripEventHandlers = (mode, tag) => {
486
143
  const props = {};
@@ -496,65 +153,175 @@ const EventHandlersPlugin = () => {
496
153
  delayedSrc = props.src;
497
154
  delete props.src;
498
155
  }
499
- return { props, eventHandlers, delayedSrc };
156
+ return { props, eventHandlers, delayedSrc };
157
+ };
158
+ return shared.defineHeadPlugin({
159
+ hooks: {
160
+ "ssr:render": function(ctx) {
161
+ ctx.tags = ctx.tags.map((tag) => {
162
+ if (!ValidEventTags.includes(tag.tag))
163
+ return tag;
164
+ if (!Object.entries(tag.props).find(([key, value]) => key.startsWith("on") && typeof value === "function"))
165
+ return tag;
166
+ tag.props = stripEventHandlers("ssr", tag).props;
167
+ return tag;
168
+ });
169
+ },
170
+ "dom:beforeRenderTag": function(ctx) {
171
+ if (!ValidEventTags.includes(ctx.tag.tag))
172
+ return;
173
+ if (!Object.entries(ctx.tag.props).find(([key, value]) => key.startsWith("on") && typeof value === "function"))
174
+ return;
175
+ const { props, eventHandlers, delayedSrc } = stripEventHandlers("dom", ctx.tag);
176
+ if (!Object.keys(eventHandlers).length)
177
+ return;
178
+ ctx.tag.props = props;
179
+ ctx.tag._eventHandlers = eventHandlers;
180
+ ctx.tag._delayedSrc = delayedSrc;
181
+ },
182
+ "dom:renderTag": function(ctx) {
183
+ const $el = ctx.$el;
184
+ if (!ctx.tag._eventHandlers || !$el)
185
+ return;
186
+ const $eventListenerTarget = ctx.tag.tag === "bodyAttrs" && typeof window !== "undefined" ? window : $el;
187
+ Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
188
+ const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
189
+ const eventName = k.slice(2).toLowerCase();
190
+ const eventDedupeKey = `data-h-${eventName}`;
191
+ ctx.markSideEffect(sdeKey, () => {
192
+ });
193
+ if ($el.hasAttribute(eventDedupeKey))
194
+ return;
195
+ const handler = value;
196
+ $el.setAttribute(eventDedupeKey, "");
197
+ $eventListenerTarget.addEventListener(eventName, handler);
198
+ if (ctx.entry) {
199
+ ctx.entry._sde[sdeKey] = () => {
200
+ $eventListenerTarget.removeEventListener(eventName, handler);
201
+ $el.removeAttribute(eventDedupeKey);
202
+ };
203
+ }
204
+ });
205
+ if (ctx.tag._delayedSrc) {
206
+ $el.setAttribute("src", ctx.tag._delayedSrc);
207
+ }
208
+ }
209
+ }
210
+ });
211
+ };
212
+
213
+ const UsesMergeStrategy = ["templateParams", "htmlAttrs", "bodyAttrs"];
214
+ const DedupesTagsPlugin = (options) => {
215
+ options = options || {};
216
+ const dedupeKeys = options.dedupeKeys || ["hid", "vmid", "key"];
217
+ return shared.defineHeadPlugin({
218
+ hooks: {
219
+ "tag:normalise": function({ tag }) {
220
+ dedupeKeys.forEach((key) => {
221
+ if (tag.props[key]) {
222
+ tag.key = tag.props[key];
223
+ delete tag.props[key];
224
+ }
225
+ });
226
+ const dedupe = tag.key ? `${tag.tag}:${tag.key}` : shared.tagDedupeKey(tag);
227
+ if (dedupe)
228
+ tag._d = dedupe;
229
+ },
230
+ "tags:resolve": function(ctx) {
231
+ const deduping = {};
232
+ ctx.tags.forEach((tag) => {
233
+ const dedupeKey = tag._d || tag._p;
234
+ const dupedTag = deduping[dedupeKey];
235
+ if (dupedTag) {
236
+ let strategy = tag?.tagDuplicateStrategy;
237
+ if (!strategy && UsesMergeStrategy.includes(tag.tag))
238
+ strategy = "merge";
239
+ if (strategy === "merge") {
240
+ const oldProps = dupedTag.props;
241
+ ["class", "style"].forEach((key) => {
242
+ if (tag.props[key] && oldProps[key]) {
243
+ if (key === "style" && !oldProps[key].endsWith(";"))
244
+ oldProps[key] += ";";
245
+ tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
246
+ }
247
+ });
248
+ deduping[dedupeKey].props = {
249
+ ...oldProps,
250
+ ...tag.props
251
+ };
252
+ return;
253
+ } else if (tag._e === dupedTag._e) {
254
+ dupedTag._duped = dupedTag._duped || [];
255
+ tag._d = `${dupedTag._d}:${dupedTag._duped.length + 1}`;
256
+ dupedTag._duped.push(tag);
257
+ return;
258
+ }
259
+ const propCount = Object.keys(tag.props).length;
260
+ if ((propCount === 0 || propCount === 1 && typeof tag.props["data-h-key"] !== "undefined") && !tag.innerHTML && !tag.textContent) {
261
+ delete deduping[dedupeKey];
262
+ return;
263
+ }
264
+ }
265
+ deduping[dedupeKey] = tag;
266
+ });
267
+ const newTags = [];
268
+ Object.values(deduping).forEach((tag) => {
269
+ const dupes = tag._duped;
270
+ delete tag._duped;
271
+ newTags.push(tag);
272
+ if (dupes)
273
+ newTags.push(...dupes);
274
+ });
275
+ ctx.tags = newTags;
276
+ }
277
+ }
278
+ });
279
+ };
280
+
281
+ function processTemplateParams(s, config) {
282
+ const replacer = (preserveToken) => (_, token) => {
283
+ if (token === "pageTitle" || token === "s")
284
+ return "%s";
285
+ let val;
286
+ if (token.includes("."))
287
+ val = token.split(".").reduce((acc, key) => acc[key] || {}, config);
288
+ else
289
+ val = config[token];
290
+ return val || (preserveToken ? token : "");
500
291
  };
501
- return defineHeadPlugin({
292
+ let template = s.replace(/%(\w+\.?\w+)%/g, replacer()).replace(/%(\w+\.?\w+)/g, replacer(true)).trim();
293
+ if (config.separator) {
294
+ if (template.endsWith(config.separator))
295
+ template = template.slice(0, -config.separator.length).trim();
296
+ if (template.startsWith(config.separator))
297
+ template = template.slice(config.separator.length).trim();
298
+ template = template.replace(new RegExp(`\\${config.separator}\\s*\\${config.separator}`, "g"), config.separator);
299
+ }
300
+ return template;
301
+ }
302
+ function TemplateParamsPlugin() {
303
+ return shared.defineHeadPlugin({
502
304
  hooks: {
503
- "ssr:render": function(ctx) {
504
- ctx.tags = ctx.tags.map((tag) => {
505
- tag.props = stripEventHandlers("ssr", tag).props;
506
- return tag;
507
- });
508
- },
509
- "dom:beforeRenderTag": function(ctx) {
510
- const { props, eventHandlers, delayedSrc } = stripEventHandlers("dom", ctx.tag);
511
- if (!Object.keys(eventHandlers).length)
512
- return;
513
- ctx.tag.props = props;
514
- ctx.tag._eventHandlers = eventHandlers;
515
- ctx.tag._delayedSrc = delayedSrc;
516
- },
517
- "dom:renderTag": function(ctx) {
518
- const $el = ctx.$el;
519
- if (!ctx.tag._eventHandlers || !$el)
520
- return;
521
- const $eventListenerTarget = ctx.tag.tag === "bodyAttrs" && typeof window !== "undefined" ? window : $el;
522
- Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
523
- const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
524
- const eventName = k.slice(2).toLowerCase();
525
- const eventDedupeKey = `data-h-${eventName}`;
526
- delete ctx.staleSideEffects[sdeKey];
527
- if ($el.hasAttribute(eventDedupeKey))
528
- return;
529
- const handler = value;
530
- $el.setAttribute(eventDedupeKey, "");
531
- $eventListenerTarget.addEventListener(eventName, handler);
532
- if (ctx.entry) {
533
- ctx.entry._sde[sdeKey] = () => {
534
- $eventListenerTarget.removeEventListener(eventName, handler);
535
- $el.removeAttribute(eventDedupeKey);
536
- };
305
+ "tags:resolve": (ctx) => {
306
+ const { tags } = ctx;
307
+ const templateParamsIdx = tags.findIndex((tag) => tag.tag === "templateParams");
308
+ if (templateParamsIdx !== -1) {
309
+ const templateParams = tags[templateParamsIdx].textContent;
310
+ delete tags[templateParamsIdx];
311
+ for (const tag of tags) {
312
+ if (tag) {
313
+ if (["titleTemplate", "title"].includes(tag.tag) && typeof tag.textContent === "string")
314
+ tag.textContent = processTemplateParams(tag.textContent, templateParams);
315
+ if (tag.tag === "meta" && typeof tag.props.content === "string")
316
+ tag.props.content = processTemplateParams(tag.props.content, templateParams);
317
+ }
537
318
  }
538
- });
539
- if (ctx.tag._delayedSrc) {
540
- $el.setAttribute("src", ctx.tag._delayedSrc);
541
319
  }
320
+ ctx.tags = tags.filter(Boolean);
542
321
  }
543
322
  }
544
323
  });
545
- };
546
-
547
- function asArray(value) {
548
- return Array.isArray(value) ? value : [value];
549
324
  }
550
- const HasElementTags = [
551
- "base",
552
- "meta",
553
- "link",
554
- "style",
555
- "script",
556
- "noscript"
557
- ];
558
325
 
559
326
  exports.activeHead = void 0;
560
327
  const setActiveHead = (head) => exports.activeHead = head;
@@ -569,57 +336,404 @@ function useHead(input, options = {}) {
569
336
  return head.push(input, options);
570
337
  }
571
338
  }
339
+
340
+ function useHeadSafe(input, options = {}) {
341
+ return useHead(input, {
342
+ ...options || {},
343
+ transform: whitelistSafeInput
344
+ });
345
+ }
346
+
347
+ function useServerHead(input, options = {}) {
348
+ return useHead(input, { ...options, mode: "server" });
349
+ }
350
+
351
+ function useServerHeadSafe(input, options = {}) {
352
+ return useHeadSafe(input, { ...options, mode: "server" });
353
+ }
354
+
355
+ function useSeoMeta(input, options) {
356
+ const { title, titleTemplate, ...meta } = input;
357
+ return useHead({
358
+ title,
359
+ titleTemplate,
360
+ meta: unpackMeta(meta)
361
+ }, options);
362
+ }
363
+
364
+ function useServerSeoMeta(input, options) {
365
+ return useSeoMeta(input, {
366
+ ...options || {},
367
+ mode: "server"
368
+ });
369
+ }
370
+
572
371
  const useTagTitle = (title) => useHead({ title });
573
372
  const useTagBase = (base) => useHead({ base });
574
- const useTagMeta = (meta) => useHead({ meta: asArray(meta) });
373
+ const useTagMeta = (meta) => useHead({ meta: shared.asArray(meta) });
575
374
  const useTagMetaFlat = (meta) => useTagMeta(unpackMeta(meta));
576
- const useTagLink = (link) => useHead({ link: asArray(link) });
577
- const useTagScript = (script) => useHead({ script: asArray(script) });
578
- const useTagStyle = (style) => useHead({ style: asArray(style) });
579
- const useTagNoscript = (noscript) => useHead({ noscript: asArray(noscript) });
375
+ const useTagLink = (link) => useHead({ link: shared.asArray(link) });
376
+ const useTagScript = (script) => useHead({ script: shared.asArray(script) });
377
+ const useTagStyle = (style) => useHead({ style: shared.asArray(style) });
378
+ const useTagNoscript = (noscript) => useHead({ noscript: shared.asArray(noscript) });
580
379
  const useHtmlAttrs = (attrs) => useHead({ htmlAttrs: attrs });
581
380
  const useBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
582
381
  const useTitleTemplate = (titleTemplate) => useHead({ titleTemplate });
583
-
584
- function useServerHead(input, options = {}) {
585
- return useHead(input, { ...options, mode: "server" });
586
- }
587
382
  const useServerTagTitle = (title) => useServerHead({ title });
588
383
  const useServerTagBase = (base) => useServerHead({ base });
589
- const useServerTagMeta = (meta) => useServerHead({ meta: asArray(meta) });
384
+ const useServerTagMeta = (meta) => useServerHead({ meta: shared.asArray(meta) });
590
385
  const useServerTagMetaFlat = (meta) => useServerTagMeta(unpackMeta(meta));
591
- const useServerTagLink = (link) => useServerHead({ link: asArray(link) });
592
- const useServerTagScript = (script) => useServerHead({ script: asArray(script) });
593
- const useServerTagStyle = (style) => useServerHead({ style: asArray(style) });
594
- const useServerTagNoscript = (noscript) => useServerHead({ noscript: asArray(noscript) });
386
+ const useServerTagLink = (link) => useServerHead({ link: shared.asArray(link) });
387
+ const useServerTagScript = (script) => useServerHead({ script: shared.asArray(script) });
388
+ const useServerTagStyle = (style) => useServerHead({ style: shared.asArray(style) });
389
+ const useServerTagNoscript = (noscript) => useServerHead({ noscript: shared.asArray(noscript) });
595
390
  const useServerHtmlAttrs = (attrs) => useServerHead({ htmlAttrs: attrs });
596
391
  const useServerBodyAttrs = (attrs) => useServerHead({ bodyAttrs: attrs });
597
392
  const useServerTitleTemplate = (titleTemplate) => useServerHead({ titleTemplate });
598
393
 
599
- const useSeoMeta = (input) => {
600
- const { title, titleTemplate, ...meta } = input;
601
- return useHead({
602
- title,
603
- titleTemplate,
604
- meta: unpackMeta(meta)
605
- });
394
+ const MetaPackingSchema = {
395
+ robots: {
396
+ unpack: {
397
+ keyValueSeparator: ":"
398
+ }
399
+ },
400
+ // Pragma directives
401
+ contentSecurityPolicy: {
402
+ unpack: {
403
+ keyValueSeparator: " ",
404
+ entrySeparator: "; "
405
+ },
406
+ metaKey: "http-equiv"
407
+ },
408
+ fbAppId: {
409
+ keyValue: "fb:app_id",
410
+ metaKey: "property"
411
+ },
412
+ msapplicationTileImage: {
413
+ keyValue: "msapplication-TileImage"
414
+ },
415
+ /**
416
+ * Tile colour for windows
417
+ */
418
+ msapplicationTileColor: {
419
+ keyValue: "msapplication-TileColor"
420
+ },
421
+ /**
422
+ * URL of a config for windows tile.
423
+ */
424
+ msapplicationConfig: {
425
+ keyValue: "msapplication-Config"
426
+ },
427
+ charset: {
428
+ metaKey: "charset"
429
+ },
430
+ contentType: {
431
+ metaKey: "http-equiv"
432
+ },
433
+ defaultStyle: {
434
+ metaKey: "http-equiv"
435
+ },
436
+ xUaCompatible: {
437
+ metaKey: "http-equiv"
438
+ },
439
+ refresh: {
440
+ metaKey: "http-equiv"
441
+ }
606
442
  };
443
+ function resolveMetaKeyType(key) {
444
+ return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
445
+ }
607
446
 
608
- const TagEntityBits = 10;
447
+ function packMeta(inputs) {
448
+ const mappedPackingSchema = Object.entries(MetaPackingSchema).map(([key, value]) => [key, value.keyValue]);
449
+ return packrup.packArray(inputs, {
450
+ key: ["name", "property", "httpEquiv", "http-equiv", "charset"],
451
+ value: ["content", "charset"],
452
+ resolveKey(k) {
453
+ let key = mappedPackingSchema.filter((sk) => sk[1] === k)?.[0]?.[0] || k;
454
+ const replacer = (_, letter) => letter?.toUpperCase();
455
+ key = key.replace(/:([a-z])/g, replacer).replace(/-([a-z])/g, replacer);
456
+ return key;
457
+ }
458
+ });
459
+ }
460
+
461
+ const ArrayableInputs = ["Image", "Video", "Audio"];
462
+ function unpackMeta(input) {
463
+ const extras = [];
464
+ ArrayableInputs.forEach((key) => {
465
+ const ogKey = `og:${key.toLowerCase()}`;
466
+ const inputKey = `og${key}`;
467
+ const val = input[inputKey];
468
+ if (typeof val === "object") {
469
+ (Array.isArray(val) ? val : [val]).forEach((entry) => {
470
+ if (!entry)
471
+ return;
472
+ const unpackedEntry = packrup.unpackToArray(entry, {
473
+ key: "property",
474
+ value: "content",
475
+ resolveKeyData({ key: key2 }) {
476
+ return fixKeyCase(`${ogKey}${key2 !== "url" ? `:${key2}` : ""}`);
477
+ },
478
+ resolveValueData({ value }) {
479
+ return typeof value === "number" ? value.toString() : value;
480
+ }
481
+ });
482
+ extras.push(
483
+ ...unpackedEntry.sort((a, b) => a.property === ogKey ? -1 : b.property === ogKey ? 1 : 0)
484
+ );
485
+ });
486
+ delete input[inputKey];
487
+ }
488
+ });
489
+ const meta = packrup.unpackToArray(input, {
490
+ key({ key }) {
491
+ return resolveMetaKeyType(key);
492
+ },
493
+ value({ key }) {
494
+ return key === "charset" ? "charset" : "content";
495
+ },
496
+ resolveKeyData({ key }) {
497
+ return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
498
+ },
499
+ resolveValueData({ value, key }) {
500
+ if (value === null)
501
+ return "_null";
502
+ if (typeof value === "object")
503
+ return resolvePackedMetaObjectValue(value, key);
504
+ return typeof value === "number" ? value.toString() : value;
505
+ }
506
+ });
507
+ return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
508
+ }
509
+ function resolvePackedMetaObjectValue(value, key) {
510
+ const definition = MetaPackingSchema[key];
511
+ if (key === "refresh")
512
+ return `${value.seconds};url=${value.url}`;
513
+ return packrup.unpackToString(
514
+ changeKeyCasingDeep(value),
515
+ {
516
+ entrySeparator: ", ",
517
+ keyValueSeparator: "=",
518
+ resolve({ value: value2, key: key2 }) {
519
+ if (value2 === null)
520
+ return "";
521
+ if (typeof value2 === "boolean")
522
+ return `${key2}`;
523
+ },
524
+ ...definition?.unpack
525
+ }
526
+ );
527
+ }
609
528
 
529
+ async function normaliseTag(tagName, input) {
530
+ const tag = { tag: tagName, props: {} };
531
+ if (["title", "titleTemplate", "templateParams"].includes(tagName)) {
532
+ tag.textContent = input instanceof Promise ? await input : input;
533
+ return tag;
534
+ }
535
+ if (typeof input === "string") {
536
+ if (!["script", "noscript", "style"].includes(tagName))
537
+ return false;
538
+ if (tagName === "script" && (/^(https?:)?\/\//.test(input) || input.startsWith("/"))) {
539
+ tag.props.src = input;
540
+ } else {
541
+ tag.innerHTML = input;
542
+ tag.key = shared.hashCode(input);
543
+ }
544
+ return tag;
545
+ }
546
+ tag.props = await normaliseProps(tagName, { ...input });
547
+ if (tag.props.children) {
548
+ tag.props.innerHTML = tag.props.children;
549
+ }
550
+ delete tag.props.children;
551
+ Object.keys(tag.props).filter((k) => shared.TagConfigKeys.includes(k)).forEach((k) => {
552
+ if (!["innerHTML", "textContent"].includes(k) || shared.TagsWithInnerContent.includes(tag.tag)) {
553
+ tag[k] = tag.props[k];
554
+ }
555
+ delete tag.props[k];
556
+ });
557
+ ["innerHTML", "textContent"].forEach((k) => {
558
+ if (tag.tag === "script" && tag[k] && ["application/ld+json", "application/json"].includes(tag.props.type)) {
559
+ try {
560
+ tag[k] = JSON.parse(tag[k]);
561
+ } catch (e) {
562
+ tag[k] = "";
563
+ }
564
+ }
565
+ if (typeof tag[k] === "object")
566
+ tag[k] = JSON.stringify(tag[k]);
567
+ });
568
+ if (tag.props.class)
569
+ tag.props.class = normaliseClassProp(tag.props.class);
570
+ if (tag.props.content && Array.isArray(tag.props.content))
571
+ return tag.props.content.map((v) => ({ ...tag, props: { ...tag.props, content: v } }));
572
+ return tag;
573
+ }
574
+ function normaliseClassProp(v) {
575
+ if (typeof v === "object" && !Array.isArray(v)) {
576
+ v = Object.keys(v).filter((k) => v[k]);
577
+ }
578
+ return (Array.isArray(v) ? v.join(" ") : v).split(" ").filter((c) => c.trim()).filter(Boolean).join(" ");
579
+ }
580
+ async function normaliseProps(tagName, props) {
581
+ for (const k of Object.keys(props)) {
582
+ const isDataKey = k.startsWith("data-");
583
+ if (props[k] instanceof Promise) {
584
+ props[k] = await props[k];
585
+ }
586
+ if (String(props[k]) === "true") {
587
+ props[k] = isDataKey ? "true" : "";
588
+ } else if (String(props[k]) === "false") {
589
+ if (isDataKey) {
590
+ props[k] = "false";
591
+ } else {
592
+ delete props[k];
593
+ }
594
+ }
595
+ }
596
+ return props;
597
+ }
598
+ const TagEntityBits = 10;
610
599
  async function normaliseEntryTags(e) {
611
600
  const tagPromises = [];
612
- Object.entries(e.resolvedInput || e.input).filter(([k, v]) => typeof v !== "undefined" && ValidHeadTags.includes(k)).forEach(([k, value]) => {
613
- const v = asArray(value);
601
+ Object.entries(e.resolvedInput).filter(([k, v]) => typeof v !== "undefined" && shared.ValidHeadTags.includes(k)).forEach(([k, value]) => {
602
+ const v = shared.asArray(value);
614
603
  tagPromises.push(...v.map((props) => normaliseTag(k, props)).flat());
615
604
  });
616
- return (await Promise.all(tagPromises)).flat().map((t, i) => {
605
+ return (await Promise.all(tagPromises)).flat().filter(Boolean).map((t, i) => {
617
606
  t._e = e._i;
618
607
  t._p = (e._i << TagEntityBits) + i;
619
608
  return t;
620
609
  });
621
610
  }
622
611
 
612
+ const PropertyPrefixKeys = /^(og|fb)/;
613
+ const ColonPrefixKeys = /^(og|twitter|fb)/;
614
+ function fixKeyCase(key) {
615
+ key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
616
+ if (ColonPrefixKeys.test(key)) {
617
+ key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
618
+ }
619
+ return key;
620
+ }
621
+ function changeKeyCasingDeep(input) {
622
+ if (Array.isArray(input)) {
623
+ return input.map((entry) => changeKeyCasingDeep(entry));
624
+ }
625
+ if (typeof input !== "object" || Array.isArray(input))
626
+ return input;
627
+ const output = {};
628
+ for (const [key, value] of Object.entries(input))
629
+ output[fixKeyCase(key)] = changeKeyCasingDeep(value);
630
+ return output;
631
+ }
632
+
633
+ const WhitelistAttributes = {
634
+ htmlAttrs: ["id", "class", "lang", "dir"],
635
+ bodyAttrs: ["id", "class"],
636
+ meta: ["id", "name", "property", "charset", "content"],
637
+ noscript: ["id", "textContent"],
638
+ script: ["id", "type", "textContent"],
639
+ link: ["id", "color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"]
640
+ };
641
+ function whitelistSafeInput(input) {
642
+ const filtered = {};
643
+ Object.keys(input).forEach((key) => {
644
+ const tagValue = input[key];
645
+ if (!tagValue)
646
+ return;
647
+ switch (key) {
648
+ case "title":
649
+ case "titleTemplate":
650
+ case "templateParams":
651
+ filtered[key] = tagValue;
652
+ break;
653
+ case "htmlAttrs":
654
+ case "bodyAttrs":
655
+ filtered[key] = {};
656
+ WhitelistAttributes[key].forEach((a) => {
657
+ if (tagValue[a])
658
+ filtered[key][a] = tagValue[a];
659
+ });
660
+ Object.keys(tagValue || {}).filter((a) => a.startsWith("data-")).forEach((a) => {
661
+ filtered[key][a] = tagValue[a];
662
+ });
663
+ break;
664
+ case "meta":
665
+ if (Array.isArray(tagValue)) {
666
+ filtered[key] = tagValue.map((meta) => {
667
+ const safeMeta = {};
668
+ WhitelistAttributes.meta.forEach((key2) => {
669
+ if (meta[key2] || key2.startsWith("data-"))
670
+ safeMeta[key2] = meta[key2];
671
+ });
672
+ return safeMeta;
673
+ }).filter((meta) => Object.keys(meta).length > 0);
674
+ }
675
+ break;
676
+ case "link":
677
+ if (Array.isArray(tagValue)) {
678
+ filtered[key] = tagValue.map((meta) => {
679
+ const link = {};
680
+ WhitelistAttributes.link.forEach((key2) => {
681
+ if (key2 === "rel" && ["stylesheet", "canonical", "modulepreload", "prerender", "preload", "prefetch"].includes(meta[key2]))
682
+ return;
683
+ if (key2 === "href") {
684
+ try {
685
+ const url = new URL(meta[key2]);
686
+ if (["javascript:", "data:"].includes(url.protocol))
687
+ return;
688
+ link[key2] = meta[key2];
689
+ } catch (e) {
690
+ }
691
+ } else if (meta[key2] || key2.startsWith("data-")) {
692
+ link[key2] = meta[key2];
693
+ }
694
+ });
695
+ return link;
696
+ }).filter((link) => Object.keys(link).length > 1 && !!link.rel);
697
+ }
698
+ break;
699
+ case "noscript":
700
+ if (Array.isArray(tagValue)) {
701
+ filtered[key] = tagValue.map((meta) => {
702
+ const noscript = {};
703
+ WhitelistAttributes.noscript.forEach((key2) => {
704
+ if (meta[key2] || key2.startsWith("data-"))
705
+ noscript[key2] = meta[key2];
706
+ });
707
+ return noscript;
708
+ }).filter((meta) => Object.keys(meta).length > 0);
709
+ }
710
+ break;
711
+ case "script":
712
+ if (Array.isArray(tagValue)) {
713
+ filtered[key] = tagValue.map((script) => {
714
+ const safeScript = {};
715
+ WhitelistAttributes.script.forEach((s) => {
716
+ if (script[s] || s.startsWith("data-")) {
717
+ if (s === "textContent") {
718
+ try {
719
+ const jsonVal = typeof script[s] === "string" ? JSON.parse(script[s]) : script[s];
720
+ safeScript[s] = JSON.stringify(jsonVal, null, 0);
721
+ } catch (e) {
722
+ }
723
+ } else {
724
+ safeScript[s] = script[s];
725
+ }
726
+ }
727
+ });
728
+ return safeScript;
729
+ }).filter((meta) => Object.keys(meta).length > 0);
730
+ }
731
+ break;
732
+ }
733
+ });
734
+ return filtered;
735
+ }
736
+
623
737
  const CorePlugins = () => [
624
738
  // dedupe needs to come first
625
739
  DedupesTagsPlugin(),
@@ -627,16 +741,19 @@ const CorePlugins = () => [
627
741
  TitleTemplatePlugin(),
628
742
  ProvideTagHashPlugin(),
629
743
  EventHandlersPlugin(),
630
- DeprecatedTagAttrPlugin()
744
+ DeprecatedTagAttrPlugin(),
745
+ TemplateParamsPlugin()
631
746
  ];
632
747
  const DOMPlugins = (options = {}) => [
633
- PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
748
+ dom.PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
634
749
  ];
635
750
  function createHead(options = {}) {
636
751
  const head = createHeadCore({
637
752
  ...options,
638
753
  plugins: [...DOMPlugins(options), ...options?.plugins || []]
639
754
  });
755
+ if (options.experimentalHashHydration && head.resolvedOptions.document)
756
+ head._hash = dom.maybeGetSSRHash(head.resolvedOptions.document);
640
757
  setActiveHead(head);
641
758
  return head;
642
759
  }
@@ -652,6 +769,7 @@ function createHeadCore(options = {}) {
652
769
  ...options?.plugins || []
653
770
  ];
654
771
  options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
772
+ options.document = options.document || (IsBrowser ? document : void 0);
655
773
  const updated = () => hooks.callHook("entries:updated", head);
656
774
  const head = {
657
775
  resolvedOptions: options,
@@ -673,6 +791,9 @@ function createHeadCore(options = {}) {
673
791
  };
674
792
  if (options2?.mode)
675
793
  activeEntry._m = options2?.mode;
794
+ if (options2?.transform) {
795
+ activeEntry._t = options2?.transform;
796
+ }
676
797
  entries.push(activeEntry);
677
798
  updated();
678
799
  return {
@@ -702,8 +823,10 @@ function createHeadCore(options = {}) {
702
823
  const resolveCtx = { tags: [], entries: [...entries] };
703
824
  await hooks.callHook("entries:resolve", resolveCtx);
704
825
  for (const entry of resolveCtx.entries) {
826
+ const transformer = entry._t || ((i) => i);
827
+ entry.resolvedInput = transformer(entry.resolvedInput || entry.input);
705
828
  for (const tag of await normaliseEntryTags(entry)) {
706
- const tagCtx = { tag, entry };
829
+ const tagCtx = { tag, entry, resolvedOptions: head.resolvedOptions };
707
830
  await hooks.callHook("tag:normalise", tagCtx);
708
831
  resolveCtx.tags.push(tagCtx.tag);
709
832
  }
@@ -711,32 +834,32 @@ function createHeadCore(options = {}) {
711
834
  await hooks.callHook("tags:resolve", resolveCtx);
712
835
  return resolveCtx.tags;
713
836
  },
714
- _elMap: {},
715
837
  _popSideEffectQueue() {
716
838
  const sde = { ..._sde };
717
839
  _sde = {};
718
840
  return sde;
719
- }
841
+ },
842
+ _elMap: {}
720
843
  };
721
844
  head.hooks.callHook("init", head);
722
845
  return head;
723
846
  }
724
847
 
725
- function defineHeadPlugin(plugin) {
726
- return plugin;
727
- }
728
-
729
848
  const coreComposableNames = [
730
849
  "getActiveHead"
731
850
  ];
732
851
  const composableNames = [
733
852
  "useHead",
853
+ "useSeoMeta",
854
+ "useHeadSafe",
855
+ "useServerHead",
856
+ "useServerSeoMeta",
857
+ "useServerHeadSafe",
858
+ // deprecated
734
859
  "useTagTitle",
735
860
  "useTagBase",
736
861
  "useTagMeta",
737
862
  "useTagMetaFlat",
738
- // alias
739
- "useSeoMeta",
740
863
  "useTagLink",
741
864
  "useTagScript",
742
865
  "useTagStyle",
@@ -744,8 +867,6 @@ const composableNames = [
744
867
  "useHtmlAttrs",
745
868
  "useBodyAttrs",
746
869
  "useTitleTemplate",
747
- // server only composables
748
- "useServerHead",
749
870
  "useServerTagTitle",
750
871
  "useServerTagBase",
751
872
  "useServerTagMeta",
@@ -765,32 +886,49 @@ const unheadComposablesImports = [
765
886
  }
766
887
  ];
767
888
 
889
+ exports.ColonPrefixKeys = ColonPrefixKeys;
768
890
  exports.CorePlugins = CorePlugins;
769
891
  exports.DOMPlugins = DOMPlugins;
770
892
  exports.DedupesTagsPlugin = DedupesTagsPlugin;
771
893
  exports.DeprecatedTagAttrPlugin = DeprecatedTagAttrPlugin;
772
894
  exports.EventHandlersPlugin = EventHandlersPlugin;
773
- exports.HasElementTags = HasElementTags;
774
- exports.PatchDomOnEntryUpdatesPlugin = PatchDomOnEntryUpdatesPlugin;
895
+ exports.MetaPackingSchema = MetaPackingSchema;
896
+ exports.PropertyPrefixKeys = PropertyPrefixKeys;
775
897
  exports.ProvideTagHashPlugin = ProvideTagHashPlugin;
898
+ exports.SortModifiers = SortModifiers;
776
899
  exports.SortTagsPlugin = SortTagsPlugin;
900
+ exports.TAG_WEIGHTS = TAG_WEIGHTS;
901
+ exports.TagEntityBits = TagEntityBits;
902
+ exports.TemplateParamsPlugin = TemplateParamsPlugin;
777
903
  exports.TitleTemplatePlugin = TitleTemplatePlugin;
778
- exports.asArray = asArray;
904
+ exports.changeKeyCasingDeep = changeKeyCasingDeep;
779
905
  exports.composableNames = composableNames;
780
906
  exports.createHead = createHead;
781
907
  exports.createHeadCore = createHeadCore;
782
- exports.defineHeadPlugin = defineHeadPlugin;
908
+ exports.fixKeyCase = fixKeyCase;
783
909
  exports.getActiveHead = getActiveHead;
910
+ exports.normaliseClassProp = normaliseClassProp;
784
911
  exports.normaliseEntryTags = normaliseEntryTags;
912
+ exports.normaliseProps = normaliseProps;
913
+ exports.normaliseTag = normaliseTag;
914
+ exports.packMeta = packMeta;
915
+ exports.renderTitleTemplate = renderTitleTemplate;
916
+ exports.resolveMetaKeyType = resolveMetaKeyType;
917
+ exports.resolvePackedMetaObjectValue = resolvePackedMetaObjectValue;
785
918
  exports.setActiveHead = setActiveHead;
919
+ exports.tagWeight = tagWeight;
786
920
  exports.unheadComposablesImports = unheadComposablesImports;
921
+ exports.unpackMeta = unpackMeta;
787
922
  exports.useBodyAttrs = useBodyAttrs;
788
923
  exports.useHead = useHead;
924
+ exports.useHeadSafe = useHeadSafe;
789
925
  exports.useHtmlAttrs = useHtmlAttrs;
790
926
  exports.useSeoMeta = useSeoMeta;
791
927
  exports.useServerBodyAttrs = useServerBodyAttrs;
792
928
  exports.useServerHead = useServerHead;
929
+ exports.useServerHeadSafe = useServerHeadSafe;
793
930
  exports.useServerHtmlAttrs = useServerHtmlAttrs;
931
+ exports.useServerSeoMeta = useServerSeoMeta;
794
932
  exports.useServerTagBase = useServerTagBase;
795
933
  exports.useServerTagLink = useServerTagLink;
796
934
  exports.useServerTagMeta = useServerTagMeta;
@@ -809,3 +947,7 @@ exports.useTagScript = useTagScript;
809
947
  exports.useTagStyle = useTagStyle;
810
948
  exports.useTagTitle = useTagTitle;
811
949
  exports.useTitleTemplate = useTitleTemplate;
950
+ exports.whitelistSafeInput = whitelistSafeInput;
951
+ Object.keys(shared).forEach(function (k) {
952
+ if (k !== 'default' && !exports.hasOwnProperty(k)) exports[k] = shared[k];
953
+ });