unhead 1.0.22 → 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
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const hookable = require('hookable');
4
- const shared = require('@unhead/shared');
5
4
  const dom = require('@unhead/dom');
5
+ const shared = require('@unhead/shared');
6
+ const packrup = require('packrup');
6
7
 
7
8
  const TAG_WEIGHTS = {
8
9
  // aliases
@@ -66,20 +67,20 @@ const TitleTemplatePlugin = () => {
66
67
  const titleIdx = tags.findIndex((i) => i.tag === "title");
67
68
  if (titleIdx !== -1 && titleTemplateIdx !== -1) {
68
69
  const newTitle = renderTitleTemplate(
69
- tags[titleTemplateIdx].children,
70
- tags[titleIdx].children
70
+ tags[titleTemplateIdx].textContent,
71
+ tags[titleIdx].textContent
71
72
  );
72
73
  if (newTitle !== null) {
73
- tags[titleIdx].children = newTitle || tags[titleIdx].children;
74
+ tags[titleIdx].textContent = newTitle || tags[titleIdx].textContent;
74
75
  } else {
75
76
  delete tags[titleIdx];
76
77
  }
77
78
  } else if (titleTemplateIdx !== -1) {
78
79
  const newTitle = renderTitleTemplate(
79
- tags[titleTemplateIdx].children
80
+ tags[titleTemplateIdx].textContent
80
81
  );
81
82
  if (newTitle !== null) {
82
- tags[titleTemplateIdx].children = newTitle;
83
+ tags[titleTemplateIdx].textContent = newTitle;
83
84
  tags[titleTemplateIdx].tag = "title";
84
85
  titleTemplateIdx = -1;
85
86
  }
@@ -112,15 +113,18 @@ const ProvideTagHashPlugin = () => {
112
113
  return shared.defineHeadPlugin({
113
114
  hooks: {
114
115
  "tag:normalise": (ctx) => {
115
- const { tag, entry } = ctx;
116
+ const { tag, entry, resolvedOptions } = ctx;
117
+ if (resolvedOptions.experimentalHashHydration === true) {
118
+ tag._h = shared.hashTag(tag);
119
+ }
116
120
  const isDynamic = typeof tag.props._dynamic !== "undefined";
117
121
  if (!shared.HasElementTags.includes(tag.tag) || !tag.key)
118
122
  return;
119
- tag._hash = shared.hashCode(JSON.stringify({ tag: tag.tag, key: tag.key }));
120
123
  if (IsBrowser || getActiveHead()?.resolvedOptions?.document)
121
124
  return;
122
125
  if (entry._m === "server" || isDynamic) {
123
- tag.props[`data-h-${tag._hash}`] = "";
126
+ tag._h = tag._h || shared.hashTag(tag);
127
+ tag.props[`data-h-${tag._h}`] = "";
124
128
  }
125
129
  },
126
130
  "tags:resolve": (ctx) => {
@@ -184,7 +188,8 @@ const EventHandlersPlugin = () => {
184
188
  const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
185
189
  const eventName = k.slice(2).toLowerCase();
186
190
  const eventDedupeKey = `data-h-${eventName}`;
187
- delete ctx.staleSideEffects[sdeKey];
191
+ ctx.markSideEffect(sdeKey, () => {
192
+ });
188
193
  if ($el.hasAttribute(eventDedupeKey))
189
194
  return;
190
195
  const handler = value;
@@ -205,6 +210,119 @@ const EventHandlersPlugin = () => {
205
210
  });
206
211
  };
207
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 : "");
291
+ };
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({
304
+ hooks: {
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
+ }
318
+ }
319
+ }
320
+ ctx.tags = tags.filter(Boolean);
321
+ }
322
+ }
323
+ });
324
+ }
325
+
208
326
  exports.activeHead = void 0;
209
327
  const setActiveHead = (head) => exports.activeHead = head;
210
328
  const getActiveHead = () => exports.activeHead;
@@ -218,6 +336,38 @@ function useHead(input, options = {}) {
218
336
  return head.push(input, options);
219
337
  }
220
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
+
221
371
  const useTagTitle = (title) => useHead({ title });
222
372
  const useTagBase = (base) => useHead({ base });
223
373
  const useTagMeta = (meta) => useHead({ meta: shared.asArray(meta) });
@@ -229,10 +379,6 @@ const useTagNoscript = (noscript) => useHead({ noscript: shared.asArray(noscript
229
379
  const useHtmlAttrs = (attrs) => useHead({ htmlAttrs: attrs });
230
380
  const useBodyAttrs = (attrs) => useHead({ bodyAttrs: attrs });
231
381
  const useTitleTemplate = (titleTemplate) => useHead({ titleTemplate });
232
-
233
- function useServerHead(input, options = {}) {
234
- return useHead(input, { ...options, mode: "server" });
235
- }
236
382
  const useServerTagTitle = (title) => useServerHead({ title });
237
383
  const useServerTagBase = (base) => useServerHead({ base });
238
384
  const useServerTagMeta = (meta) => useServerHead({ meta: shared.asArray(meta) });
@@ -245,111 +391,6 @@ const useServerHtmlAttrs = (attrs) => useServerHead({ htmlAttrs: attrs });
245
391
  const useServerBodyAttrs = (attrs) => useServerHead({ bodyAttrs: attrs });
246
392
  const useServerTitleTemplate = (titleTemplate) => useServerHead({ titleTemplate });
247
393
 
248
- const useSeoMeta = (input) => {
249
- const { title, titleTemplate, ...meta } = input;
250
- return useHead({
251
- title,
252
- titleTemplate,
253
- meta: unpackMeta(meta)
254
- });
255
- };
256
-
257
- function asArray(input) {
258
- return Array.isArray(input) ? input : [input];
259
- }
260
- const InternalKeySymbol = "_$key";
261
- function packObject(input, options) {
262
- const keys = Object.keys(input);
263
- let [k, v] = keys;
264
- options = options || {};
265
- options.key = options.key || k;
266
- options.value = options.value || v;
267
- options.resolveKey = options.resolveKey || ((k2) => k2);
268
- const resolveKey = (index) => {
269
- const arr = asArray(options?.[index]);
270
- return arr.find((k2) => {
271
- if (typeof k2 === "string" && k2.includes(".")) {
272
- return k2;
273
- }
274
- return k2 && keys.includes(k2);
275
- });
276
- };
277
- const resolveValue = (k2, input2) => {
278
- if (k2.includes(".")) {
279
- const paths = k2.split(".");
280
- let val = input2;
281
- for (const path of paths)
282
- val = val[path];
283
- return val;
284
- }
285
- return input2[k2];
286
- };
287
- k = resolveKey("key") || k;
288
- v = resolveKey("value") || v;
289
- const dedupeKeyPrefix = input.key ? `${InternalKeySymbol}${input.key}-` : "";
290
- let keyValue = resolveValue(k, input);
291
- keyValue = options.resolveKey(keyValue);
292
- return {
293
- [`${dedupeKeyPrefix}${keyValue}`]: resolveValue(v, input)
294
- };
295
- }
296
-
297
- function packArray(input, options) {
298
- const packed = {};
299
- for (const i of input) {
300
- const packedObj = packObject(i, options);
301
- const pKey = Object.keys(packedObj)[0];
302
- const isDedupeKey = pKey.startsWith(InternalKeySymbol);
303
- if (!isDedupeKey && packed[pKey]) {
304
- packed[pKey] = Array.isArray(packed[pKey]) ? packed[pKey] : [packed[pKey]];
305
- packed[pKey].push(Object.values(packedObj)[0]);
306
- } else {
307
- packed[isDedupeKey ? pKey.split("-").slice(1).join("-") || pKey : pKey] = packedObj[pKey];
308
- }
309
- }
310
- return packed;
311
- }
312
-
313
- function unpackToArray(input, options) {
314
- const unpacked = [];
315
- const kFn = options.resolveKeyData || ((ctx) => ctx.key);
316
- const vFn = options.resolveValueData || ((ctx) => ctx.value);
317
- for (const [k, v] of Object.entries(input)) {
318
- unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
319
- const ctx = { key: k, value: i };
320
- const val = vFn(ctx);
321
- if (typeof val === "object")
322
- return unpackToArray(val, options);
323
- if (Array.isArray(val))
324
- return val;
325
- return {
326
- [typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
327
- [typeof options.value === "function" ? options.value(ctx) : options.value]: val
328
- };
329
- }).flat());
330
- }
331
- return unpacked;
332
- }
333
-
334
- function unpackToString(value, options) {
335
- return Object.entries(value).map(([key, value2]) => {
336
- if (typeof value2 === "object")
337
- value2 = unpackToString(value2, options);
338
- if (options.resolve) {
339
- const resolved = options.resolve({ key, value: value2 });
340
- if (resolved)
341
- return resolved;
342
- }
343
- if (typeof value2 === "number")
344
- value2 = value2.toString();
345
- if (typeof value2 === "string" && options.wrapValue) {
346
- value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
347
- value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
348
- }
349
- return `${key}${options.keyValueSeparator || ""}${value2}`;
350
- }).join(options.entrySeparator || "");
351
- }
352
-
353
394
  const MetaPackingSchema = {
354
395
  robots: {
355
396
  unpack: {
@@ -405,7 +446,7 @@ function resolveMetaKeyType(key) {
405
446
 
406
447
  function packMeta(inputs) {
407
448
  const mappedPackingSchema = Object.entries(MetaPackingSchema).map(([key, value]) => [key, value.keyValue]);
408
- return packArray(inputs, {
449
+ return packrup.packArray(inputs, {
409
450
  key: ["name", "property", "httpEquiv", "http-equiv", "charset"],
410
451
  value: ["content", "charset"],
411
452
  resolveKey(k) {
@@ -428,7 +469,7 @@ function unpackMeta(input) {
428
469
  (Array.isArray(val) ? val : [val]).forEach((entry) => {
429
470
  if (!entry)
430
471
  return;
431
- const unpackedEntry = unpackToArray(entry, {
472
+ const unpackedEntry = packrup.unpackToArray(entry, {
432
473
  key: "property",
433
474
  value: "content",
434
475
  resolveKeyData({ key: key2 }) {
@@ -445,7 +486,7 @@ function unpackMeta(input) {
445
486
  delete input[inputKey];
446
487
  }
447
488
  });
448
- const meta = unpackToArray(input, {
489
+ const meta = packrup.unpackToArray(input, {
449
490
  key({ key }) {
450
491
  return resolveMetaKeyType(key);
451
492
  },
@@ -458,74 +499,98 @@ function unpackMeta(input) {
458
499
  resolveValueData({ value, key }) {
459
500
  if (value === null)
460
501
  return "_null";
461
- if (typeof value === "object") {
462
- const definition = MetaPackingSchema[key];
463
- if (key === "refresh")
464
- return `${value.seconds};url=${value.url}`;
465
- return unpackToString(
466
- changeKeyCasingDeep(value),
467
- {
468
- entrySeparator: ", ",
469
- keyValueSeparator: "=",
470
- resolve({ value: value2, key: key2 }) {
471
- if (value2 === null)
472
- return "";
473
- if (typeof value2 === "boolean")
474
- return `${key2}`;
475
- },
476
- ...definition?.unpack
477
- }
478
- );
479
- }
502
+ if (typeof value === "object")
503
+ return resolvePackedMetaObjectValue(value, key);
480
504
  return typeof value === "number" ? value.toString() : value;
481
505
  }
482
506
  });
483
507
  return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
484
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
+ }
485
528
 
486
529
  async function normaliseTag(tagName, input) {
487
530
  const tag = { tag: tagName, props: {} };
488
- if (tagName === "title" || tagName === "titleTemplate") {
489
- tag.children = input instanceof Promise ? await input : input;
531
+ if (["title", "titleTemplate", "templateParams"].includes(tagName)) {
532
+ tag.textContent = input instanceof Promise ? await input : input;
490
533
  return tag;
491
534
  }
492
- tag.props = await normaliseProps({ ...input });
493
- ["children", "innerHtml", "innerHTML"].forEach((key) => {
494
- if (typeof tag.props[key] !== "undefined") {
495
- tag.children = tag.props[key];
496
- if (typeof tag.children === "object")
497
- tag.children = JSON.stringify(tag.children);
498
- delete tag.props[key];
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);
499
543
  }
500
- });
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;
501
551
  Object.keys(tag.props).filter((k) => shared.TagConfigKeys.includes(k)).forEach((k) => {
502
- tag[k] = tag.props[k];
552
+ if (!["innerHTML", "textContent"].includes(k) || shared.TagsWithInnerContent.includes(tag.tag)) {
553
+ tag[k] = tag.props[k];
554
+ }
503
555
  delete tag.props[k];
504
556
  });
505
- if (typeof tag.props.class === "object" && !Array.isArray(tag.props.class)) {
506
- tag.props.class = Object.keys(tag.props.class).filter((k) => tag.props.class[k]);
507
- }
508
- if (Array.isArray(tag.props.class))
509
- tag.props.class = tag.props.class.join(" ");
510
- if (tag.props.content && Array.isArray(tag.props.content)) {
511
- return tag.props.content.map((v, i) => {
512
- const newTag = { ...tag, props: { ...tag.props } };
513
- newTag.props.content = v;
514
- newTag.key = `${tag.props.name || tag.props.property}:${i}`;
515
- return newTag;
516
- });
517
- }
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 } }));
518
572
  return tag;
519
573
  }
520
- async function normaliseProps(props) {
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) {
521
581
  for (const k of Object.keys(props)) {
582
+ const isDataKey = k.startsWith("data-");
522
583
  if (props[k] instanceof Promise) {
523
584
  props[k] = await props[k];
524
585
  }
525
586
  if (String(props[k]) === "true") {
526
- props[k] = "";
587
+ props[k] = isDataKey ? "true" : "";
527
588
  } else if (String(props[k]) === "false") {
528
- delete props[k];
589
+ if (isDataKey) {
590
+ props[k] = "false";
591
+ } else {
592
+ delete props[k];
593
+ }
529
594
  }
530
595
  }
531
596
  return props;
@@ -533,11 +598,11 @@ async function normaliseProps(props) {
533
598
  const TagEntityBits = 10;
534
599
  async function normaliseEntryTags(e) {
535
600
  const tagPromises = [];
536
- Object.entries(e.resolvedInput || e.input).filter(([k, v]) => typeof v !== "undefined" && shared.ValidHeadTags.includes(k)).forEach(([k, value]) => {
601
+ Object.entries(e.resolvedInput).filter(([k, v]) => typeof v !== "undefined" && shared.ValidHeadTags.includes(k)).forEach(([k, value]) => {
537
602
  const v = shared.asArray(value);
538
603
  tagPromises.push(...v.map((props) => normaliseTag(k, props)).flat());
539
604
  });
540
- return (await Promise.all(tagPromises)).flat().map((t, i) => {
605
+ return (await Promise.all(tagPromises)).flat().filter(Boolean).map((t, i) => {
541
606
  t._e = e._i;
542
607
  t._p = (e._i << TagEntityBits) + i;
543
608
  return t;
@@ -565,14 +630,119 @@ function changeKeyCasingDeep(input) {
565
630
  return output;
566
631
  }
567
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
+
568
737
  const CorePlugins = () => [
569
738
  // dedupe needs to come first
570
- shared.DedupesTagsPlugin(),
739
+ DedupesTagsPlugin(),
571
740
  SortTagsPlugin(),
572
741
  TitleTemplatePlugin(),
573
742
  ProvideTagHashPlugin(),
574
743
  EventHandlersPlugin(),
575
- DeprecatedTagAttrPlugin()
744
+ DeprecatedTagAttrPlugin(),
745
+ TemplateParamsPlugin()
576
746
  ];
577
747
  const DOMPlugins = (options = {}) => [
578
748
  dom.PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
@@ -582,6 +752,8 @@ function createHead(options = {}) {
582
752
  ...options,
583
753
  plugins: [...DOMPlugins(options), ...options?.plugins || []]
584
754
  });
755
+ if (options.experimentalHashHydration && head.resolvedOptions.document)
756
+ head._hash = dom.maybeGetSSRHash(head.resolvedOptions.document);
585
757
  setActiveHead(head);
586
758
  return head;
587
759
  }
@@ -597,6 +769,7 @@ function createHeadCore(options = {}) {
597
769
  ...options?.plugins || []
598
770
  ];
599
771
  options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
772
+ options.document = options.document || (IsBrowser ? document : void 0);
600
773
  const updated = () => hooks.callHook("entries:updated", head);
601
774
  const head = {
602
775
  resolvedOptions: options,
@@ -618,6 +791,9 @@ function createHeadCore(options = {}) {
618
791
  };
619
792
  if (options2?.mode)
620
793
  activeEntry._m = options2?.mode;
794
+ if (options2?.transform) {
795
+ activeEntry._t = options2?.transform;
796
+ }
621
797
  entries.push(activeEntry);
622
798
  updated();
623
799
  return {
@@ -647,8 +823,10 @@ function createHeadCore(options = {}) {
647
823
  const resolveCtx = { tags: [], entries: [...entries] };
648
824
  await hooks.callHook("entries:resolve", resolveCtx);
649
825
  for (const entry of resolveCtx.entries) {
826
+ const transformer = entry._t || ((i) => i);
827
+ entry.resolvedInput = transformer(entry.resolvedInput || entry.input);
650
828
  for (const tag of await normaliseEntryTags(entry)) {
651
- const tagCtx = { tag, entry };
829
+ const tagCtx = { tag, entry, resolvedOptions: head.resolvedOptions };
652
830
  await hooks.callHook("tag:normalise", tagCtx);
653
831
  resolveCtx.tags.push(tagCtx.tag);
654
832
  }
@@ -656,12 +834,12 @@ function createHeadCore(options = {}) {
656
834
  await hooks.callHook("tags:resolve", resolveCtx);
657
835
  return resolveCtx.tags;
658
836
  },
659
- _elMap: {},
660
837
  _popSideEffectQueue() {
661
838
  const sde = { ..._sde };
662
839
  _sde = {};
663
840
  return sde;
664
- }
841
+ },
842
+ _elMap: {}
665
843
  };
666
844
  head.hooks.callHook("init", head);
667
845
  return head;
@@ -672,12 +850,16 @@ const coreComposableNames = [
672
850
  ];
673
851
  const composableNames = [
674
852
  "useHead",
853
+ "useSeoMeta",
854
+ "useHeadSafe",
855
+ "useServerHead",
856
+ "useServerSeoMeta",
857
+ "useServerHeadSafe",
858
+ // deprecated
675
859
  "useTagTitle",
676
860
  "useTagBase",
677
861
  "useTagMeta",
678
862
  "useTagMetaFlat",
679
- // alias
680
- "useSeoMeta",
681
863
  "useTagLink",
682
864
  "useTagScript",
683
865
  "useTagStyle",
@@ -685,8 +867,6 @@ const composableNames = [
685
867
  "useHtmlAttrs",
686
868
  "useBodyAttrs",
687
869
  "useTitleTemplate",
688
- // server only composables
689
- "useServerHead",
690
870
  "useServerTagTitle",
691
871
  "useServerTagBase",
692
872
  "useServerTagMeta",
@@ -709,14 +889,17 @@ const unheadComposablesImports = [
709
889
  exports.ColonPrefixKeys = ColonPrefixKeys;
710
890
  exports.CorePlugins = CorePlugins;
711
891
  exports.DOMPlugins = DOMPlugins;
892
+ exports.DedupesTagsPlugin = DedupesTagsPlugin;
712
893
  exports.DeprecatedTagAttrPlugin = DeprecatedTagAttrPlugin;
713
894
  exports.EventHandlersPlugin = EventHandlersPlugin;
895
+ exports.MetaPackingSchema = MetaPackingSchema;
714
896
  exports.PropertyPrefixKeys = PropertyPrefixKeys;
715
897
  exports.ProvideTagHashPlugin = ProvideTagHashPlugin;
716
898
  exports.SortModifiers = SortModifiers;
717
899
  exports.SortTagsPlugin = SortTagsPlugin;
718
900
  exports.TAG_WEIGHTS = TAG_WEIGHTS;
719
901
  exports.TagEntityBits = TagEntityBits;
902
+ exports.TemplateParamsPlugin = TemplateParamsPlugin;
720
903
  exports.TitleTemplatePlugin = TitleTemplatePlugin;
721
904
  exports.changeKeyCasingDeep = changeKeyCasingDeep;
722
905
  exports.composableNames = composableNames;
@@ -724,22 +907,28 @@ exports.createHead = createHead;
724
907
  exports.createHeadCore = createHeadCore;
725
908
  exports.fixKeyCase = fixKeyCase;
726
909
  exports.getActiveHead = getActiveHead;
910
+ exports.normaliseClassProp = normaliseClassProp;
727
911
  exports.normaliseEntryTags = normaliseEntryTags;
728
912
  exports.normaliseProps = normaliseProps;
729
913
  exports.normaliseTag = normaliseTag;
730
914
  exports.packMeta = packMeta;
731
915
  exports.renderTitleTemplate = renderTitleTemplate;
916
+ exports.resolveMetaKeyType = resolveMetaKeyType;
917
+ exports.resolvePackedMetaObjectValue = resolvePackedMetaObjectValue;
732
918
  exports.setActiveHead = setActiveHead;
733
919
  exports.tagWeight = tagWeight;
734
920
  exports.unheadComposablesImports = unheadComposablesImports;
735
921
  exports.unpackMeta = unpackMeta;
736
922
  exports.useBodyAttrs = useBodyAttrs;
737
923
  exports.useHead = useHead;
924
+ exports.useHeadSafe = useHeadSafe;
738
925
  exports.useHtmlAttrs = useHtmlAttrs;
739
926
  exports.useSeoMeta = useSeoMeta;
740
927
  exports.useServerBodyAttrs = useServerBodyAttrs;
741
928
  exports.useServerHead = useServerHead;
929
+ exports.useServerHeadSafe = useServerHeadSafe;
742
930
  exports.useServerHtmlAttrs = useServerHtmlAttrs;
931
+ exports.useServerSeoMeta = useServerSeoMeta;
743
932
  exports.useServerTagBase = useServerTagBase;
744
933
  exports.useServerTagLink = useServerTagLink;
745
934
  exports.useServerTagMeta = useServerTagMeta;
@@ -758,6 +947,7 @@ exports.useTagScript = useTagScript;
758
947
  exports.useTagStyle = useTagStyle;
759
948
  exports.useTagTitle = useTagTitle;
760
949
  exports.useTitleTemplate = useTitleTemplate;
950
+ exports.whitelistSafeInput = whitelistSafeInput;
761
951
  Object.keys(shared).forEach(function (k) {
762
952
  if (k !== 'default' && !exports.hasOwnProperty(k)) exports[k] = shared[k];
763
953
  });