nuxt-seo-utils 8.4.3 → 8.5.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/cli.mjs CHANGED
@@ -9,7 +9,7 @@ import { p as pngToIco } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
9
9
  import 'node:buffer';
10
10
 
11
11
  const name = "nuxt-seo-utils";
12
- const version = "8.4.3";
12
+ const version = "8.5.0";
13
13
  const description = "SEO utilities to improve your Nuxt sites discoverability and shareability";
14
14
 
15
15
  const ICON_SIZES = [
package/dist/module.d.mts CHANGED
@@ -25,6 +25,12 @@ interface ModuleOptions {
25
25
  * @default true
26
26
  */
27
27
  automaticOgAndTwitterTags: boolean;
28
+ /**
29
+ * Whether to infer Twitter metadata from Open Graph metadata.
30
+ *
31
+ * @default true
32
+ */
33
+ automaticTwitterTags: boolean;
28
34
  /**
29
35
  * Attempts to treeshake the `useSeoMeta` function. Can save around 5kb in the client bundle.
30
36
  *
@@ -127,6 +133,13 @@ interface ModuleOptions {
127
133
  build?: boolean;
128
134
  runtime?: boolean;
129
135
  };
136
+ /**
137
+ * Checks the `app.head` in your Nuxt config for conflicts, redundant tags, and dead tags.
138
+ * Warnings print on dev server start and on build.
139
+ *
140
+ * @default true
141
+ */
142
+ validateAppHead: boolean;
130
143
  /**
131
144
  * Enables debug logs and a debug endpoint.
132
145
  *
package/dist/module.d.ts CHANGED
@@ -25,6 +25,12 @@ interface ModuleOptions {
25
25
  * @default true
26
26
  */
27
27
  automaticOgAndTwitterTags: boolean;
28
+ /**
29
+ * Whether to infer Twitter metadata from Open Graph metadata.
30
+ *
31
+ * @default true
32
+ */
33
+ automaticTwitterTags: boolean;
28
34
  /**
29
35
  * Attempts to treeshake the `useSeoMeta` function. Can save around 5kb in the client bundle.
30
36
  *
@@ -127,6 +133,13 @@ interface ModuleOptions {
127
133
  build?: boolean;
128
134
  runtime?: boolean;
129
135
  };
136
+ /**
137
+ * Checks the `app.head` in your Nuxt config for conflicts, redundant tags, and dead tags.
138
+ * Warnings print on dev server start and on build.
139
+ *
140
+ * @default true
141
+ */
142
+ validateAppHead: boolean;
130
143
  /**
131
144
  * Enables debug logs and a debug endpoint.
132
145
  *
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "8.4.3",
7
+ "version": "8.5.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import fs, { existsSync } from 'node:fs';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { useNuxt, addTypeTemplate, useLogger, defineNuxtModule, createResolver, addServerHandler, hasNuxtModule, addPlugin, addImports, hasNuxtCompatibility, directoryToURL, addVitePlugin } from '@nuxt/kit';
4
+ import { unpackMeta } from '@unhead/vue/utils';
4
5
  import { defu } from 'defu';
5
6
  import { resolveModulePath } from 'exsolve';
6
- import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
7
+ import { installNuxtSiteConfig, useSiteConfig } from 'nuxt-site-config/kit';
7
8
  import { useModuleLogger, setupNitroRuntimeCompatibility, renderNitroTypeAugmentations, resolveHostUnheadMajor } from 'nuxtseo-shared/kit';
8
9
  import { resolve, extname, relative, dirname, basename } from 'pathe';
9
10
  import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
10
- import { unpackMeta } from '@unhead/vue/utils';
11
11
  import { glob } from 'tinyglobby';
12
12
  import { encodePath, joinURL } from 'ufo';
13
13
  import { c as classifyIconFilename, g as getIconRel, n as normalizeIconSizes, a as partitionColorModeIconLinks } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
@@ -703,6 +703,249 @@ function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
703
703
  nuxt.options.app.head = headConfig;
704
704
  }
705
705
 
706
+ const NUXT_DEFAULT_VIEWPORT = "width=device-width, initial-scale=1";
707
+ const UNDERSCORE_RE = /_/g;
708
+ const HYPHEN_RE = /-/g;
709
+ const WS_RE = /\s+/g;
710
+ const ABSOLUTE_SRC_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
711
+ const NOOP_ROBOTS = /* @__PURE__ */ new Set(["index", "follow", "all", "index,follow", "follow,index"]);
712
+ const META_TAGS_ARRAYABLE = /* @__PURE__ */ new Set([
713
+ "theme-color",
714
+ "google-site-verification",
715
+ "author",
716
+ "og:locale:alternate",
717
+ "og:image",
718
+ "og:video",
719
+ "og:audio",
720
+ "article:author",
721
+ "article:tag",
722
+ "book:author",
723
+ "book:tag",
724
+ "twitter:image"
725
+ ]);
726
+ function isMetaArrayable(key) {
727
+ const dedupeKey = `meta:${key}`;
728
+ const suffix = dedupeKey.slice(dedupeKey.indexOf(":") + 1);
729
+ return META_TAGS_ARRAYABLE.has(suffix) || suffix.startsWith("og:image:") || suffix.startsWith("og:video:") || suffix.startsWith("og:audio:") || suffix.startsWith("twitter:image:");
730
+ }
731
+ function normalizeWhitespace(value) {
732
+ return value.trim().replace(WS_RE, " ");
733
+ }
734
+ function asString(value) {
735
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
736
+ }
737
+ function metaTagKey(entry) {
738
+ if (asString(entry.charset))
739
+ return "charset";
740
+ const key = asString(entry.name) || asString(entry.property) || asString(entry["http-equiv"]);
741
+ return key?.toLowerCase();
742
+ }
743
+ function findMeta(meta, key) {
744
+ return meta.find((entry) => metaTagKey(entry) === key);
745
+ }
746
+ function metaContent(entry) {
747
+ return entry ? asString(entry.content) : void 0;
748
+ }
749
+ function baseLanguage(locale) {
750
+ return locale.toLowerCase().replace(UNDERSCORE_RE, "-").split("-")[0];
751
+ }
752
+ function checkLocale(ctx, meta, diagnostics) {
753
+ const lang = asString(ctx.head.htmlAttrs?.lang);
754
+ const ogLocale = metaContent(findMeta(meta, "og:locale"));
755
+ if (ogLocale && ogLocale.includes("-")) {
756
+ diagnostics.push({
757
+ _tag: "OgLocaleNotUnderscored",
758
+ level: "warn",
759
+ content: ogLocale,
760
+ resolved: ogLocale.replace(HYPHEN_RE, "_")
761
+ });
762
+ }
763
+ if (!lang)
764
+ return;
765
+ if (lang.includes("_")) {
766
+ diagnostics.push({
767
+ _tag: "LangNotBcp47",
768
+ level: "warn",
769
+ lang,
770
+ resolved: lang.replace(UNDERSCORE_RE, "-")
771
+ });
772
+ }
773
+ if (ctx.hasI18n) {
774
+ diagnostics.push({ _tag: "StaticLangWithI18n", level: "warn", lang });
775
+ return;
776
+ }
777
+ const configured = asString(ctx.siteConfig?.defaultLocale);
778
+ const defaultLocale = configured || "en";
779
+ if (baseLanguage(lang) !== baseLanguage(defaultLocale)) {
780
+ diagnostics.push({
781
+ _tag: "LocaleMismatch",
782
+ level: "warn",
783
+ lang,
784
+ defaultLocale,
785
+ configured: Boolean(configured)
786
+ });
787
+ }
788
+ }
789
+ function renderedIndexes(groups) {
790
+ const rendered = /* @__PURE__ */ new Map();
791
+ let index = -1;
792
+ for (const group of groups) {
793
+ for (const entry of group) {
794
+ index++;
795
+ const key = metaTagKey(entry);
796
+ if (key && !isMetaArrayable(key))
797
+ rendered.set(key, index);
798
+ }
799
+ }
800
+ return rendered;
801
+ }
802
+ function checkMeta(ctx, groups, diagnostics) {
803
+ const rendered = renderedIndexes(groups);
804
+ let index = -1;
805
+ for (const group of groups) {
806
+ const seen = /* @__PURE__ */ new Map();
807
+ for (const entry of group) {
808
+ index++;
809
+ const key = metaTagKey(entry);
810
+ if (!key) {
811
+ diagnostics.push({ _tag: "MalformedTag", level: "warn", index, layered: groups.length > 1 });
812
+ continue;
813
+ }
814
+ const content = key === "charset" ? asString(entry.charset) : metaContent(entry);
815
+ if (seen.has(key) && seen.get(key) !== content && !isMetaArrayable(key))
816
+ diagnostics.push({ _tag: "DuplicateTag", level: "warn", tag: key });
817
+ seen.set(key, content);
818
+ if (!isMetaArrayable(key) && rendered.get(key) !== index)
819
+ continue;
820
+ if (key === "charset" && content?.toLowerCase() === "utf-8")
821
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "charset", reason: "Nuxt sets it by default." });
822
+ if (key === "viewport" && content && normalizeWhitespace(content) === NUXT_DEFAULT_VIEWPORT)
823
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "viewport", reason: "Nuxt sets the same value by default." });
824
+ if (key === "og:type" && content === "website" && ctx.defaultsActive !== false)
825
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "og:type", reason: "nuxt-seo-utils sets it by default." });
826
+ if (key === "og:url" && ctx.defaultsActive !== false)
827
+ diagnostics.push({ _tag: "GlobalPageTag", level: "warn", tag: "og:url", reason: "It applies the same URL to every page. nuxt-seo-utils sets it per route." });
828
+ if (key === "robots" && content) {
829
+ if (ctx.hasRobotsModule)
830
+ diagnostics.push({ _tag: "RobotsModuleConflict", level: "warn", content });
831
+ else if (NOOP_ROBOTS.has(content.toLowerCase().replace(WS_RE, "")))
832
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "robots", reason: "Crawlers index and follow by default." });
833
+ }
834
+ if (key === "og:site_name" && content && ctx.defaultsActive !== false) {
835
+ const siteName = asString(ctx.siteConfig?.name);
836
+ if (!siteName) {
837
+ diagnostics.push({ _tag: "SiteNameMismatch", level: "warn", head: content });
838
+ } else if (siteName === content) {
839
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "og:site_name", reason: "nuxt-seo-utils sets it from your site config." });
840
+ } else {
841
+ diagnostics.push({ _tag: "SiteNameMismatch", level: "warn", head: content, site: siteName });
842
+ }
843
+ }
844
+ if (key === "description" && content && ctx.mergeWithSiteConfig !== false && content === asString(ctx.siteConfig?.description))
845
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "description", reason: "nuxt-seo-utils sets it from your site config." });
846
+ if ((key === "og:image" || key === "twitter:image") && content && !ABSOLUTE_SRC_RE.test(content) && !asString(ctx.siteConfig?.url))
847
+ diagnostics.push({ _tag: "RelativeSocialImage", level: "warn", tag: key, src: content });
848
+ }
849
+ }
850
+ }
851
+ function checkShorthand(ctx, diagnostics) {
852
+ const charset = asString(ctx.head.charset);
853
+ if (charset?.toLowerCase() === "utf-8")
854
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "app.head.charset", reason: "Nuxt sets `utf-8` by default." });
855
+ const viewport = asString(ctx.head.viewport);
856
+ if (viewport && normalizeWhitespace(viewport) === NUXT_DEFAULT_VIEWPORT)
857
+ diagnostics.push({ _tag: "RedundantTag", level: "info", tag: "app.head.viewport", reason: "Nuxt sets the same value by default." });
858
+ const titleTemplate = ctx.head.titleTemplate;
859
+ if (typeof titleTemplate === "string" && !titleTemplate.includes("%s")) {
860
+ diagnostics.push({ _tag: "TitleTemplateMissingPlaceholder", level: "warn", titleTemplate });
861
+ }
862
+ }
863
+ function checkLinks(ctx, diagnostics) {
864
+ for (const link of ctx.head.link || []) {
865
+ const rel = asString(link.rel)?.toLowerCase();
866
+ if (rel === "canonical") {
867
+ if (ctx.defaultsActive !== false) {
868
+ diagnostics.push({
869
+ _tag: "GlobalPageTag",
870
+ level: "error",
871
+ tag: 'link[rel="canonical"]',
872
+ reason: "It points every page at one URL. nuxt-seo-utils sets a canonical per route."
873
+ });
874
+ }
875
+ } else if (rel === "alternate" && asString(link.hreflang)) {
876
+ diagnostics.push({
877
+ _tag: "GlobalPageTag",
878
+ level: "warn",
879
+ tag: 'link[rel="alternate"][hreflang]',
880
+ reason: "It repeats the same alternate URL on every page. Let @nuxtjs/i18n generate it."
881
+ });
882
+ }
883
+ }
884
+ }
885
+ function validateAppHead(ctx) {
886
+ const diagnostics = [];
887
+ const groups = ctx.head.metaByLayer ?? (ctx.head.meta ? [ctx.head.meta] : []);
888
+ checkLocale(ctx, groups.flat(), diagnostics);
889
+ checkShorthand(ctx, diagnostics);
890
+ checkMeta(ctx, groups, diagnostics);
891
+ checkLinks(ctx, diagnostics);
892
+ return diagnostics;
893
+ }
894
+ function formatAppHeadDiagnostic(diagnostic) {
895
+ switch (diagnostic._tag) {
896
+ case "LocaleMismatch":
897
+ return diagnostic.configured ? `\`app.head.htmlAttrs.lang\` is "${diagnostic.lang}" but site \`defaultLocale\` is "${diagnostic.defaultLocale}". og:locale, canonical casing, and Schema.org will use "${diagnostic.defaultLocale}". Set \`site.defaultLocale\` to "${diagnostic.lang}".` : `\`app.head.htmlAttrs.lang\` is "${diagnostic.lang}" but no site \`defaultLocale\` is set, so it falls back to "en". og:locale, canonical casing, and Schema.org will use "en". Add \`site: { defaultLocale: '${diagnostic.lang}' }\` to your Nuxt config.`;
898
+ case "LangNotBcp47":
899
+ return `\`app.head.htmlAttrs.lang\` is "${diagnostic.lang}". The HTML lang attribute needs a hyphen. Use "${diagnostic.resolved}".`;
900
+ case "OgLocaleNotUnderscored":
901
+ return `\`og:locale\` is "${diagnostic.content}". Open Graph needs an underscore. Use "${diagnostic.resolved}".`;
902
+ case "StaticLangWithI18n":
903
+ return `\`app.head.htmlAttrs.lang\` is "${diagnostic.lang}" while an i18n module is installed. It pins every locale to one language. Remove it and set the locale in your i18n config.`;
904
+ case "RedundantTag":
905
+ return `\`${diagnostic.tag}\` repeats a default. ${diagnostic.reason} You can remove it.`;
906
+ case "GlobalPageTag":
907
+ return `\`${diagnostic.tag}\` is set in \`app.head\`. ${diagnostic.reason}`;
908
+ case "DuplicateTag":
909
+ return `\`${diagnostic.tag}\` is set more than once with different content. Only the last value renders.`;
910
+ case "MalformedTag":
911
+ return diagnostic.layered ? `An \`app.head.meta\` entry has no \`name\`, \`property\`, \`http-equiv\`, or \`charset\`. It renders an empty tag.` : `\`app.head.meta[${diagnostic.index}]\` has no \`name\`, \`property\`, \`http-equiv\`, or \`charset\`. It renders an empty tag.`;
912
+ case "TitleTemplateMissingPlaceholder":
913
+ return `\`titleTemplate\` is "${diagnostic.titleTemplate}" and has no \`%s\`. Every page renders the same title. Add \`%s\` where the page title belongs.`;
914
+ case "SiteNameMismatch":
915
+ return diagnostic.site ? `\`og:site_name\` is "${diagnostic.head}" but site \`name\` is "${diagnostic.site}". Keep one source of truth. Set \`site.name\` and remove the meta tag.` : `\`og:site_name\` is "${diagnostic.head}" but no site \`name\` is set. Set \`site.name\` to "${diagnostic.head}" and remove the meta tag, nuxt-seo-utils then sets it from your site config.`;
916
+ case "RelativeSocialImage":
917
+ return `\`${diagnostic.tag}\` is "${diagnostic.src}" and no site \`url\` is set. Crawlers need an absolute URL. Set \`site.url\`.`;
918
+ case "RobotsModuleConflict":
919
+ return `\`robots\` is "${diagnostic.content}" in \`app.head\` while @nuxtjs/robots is installed. The two fight over the tag. Configure it through the robots module instead.`;
920
+ }
921
+ }
922
+ function collectUserAppHead(nuxt) {
923
+ const head = { metaByLayer: [], link: [] };
924
+ const layers = nuxt.options._layers || [];
925
+ for (let i = layers.length - 1; i >= 0; i--) {
926
+ const layerHead = layers[i]?.config?.app?.head;
927
+ if (!layerHead)
928
+ continue;
929
+ if (layerHead.meta?.length)
930
+ head.metaByLayer.push(layerHead.meta);
931
+ const seoMeta = layerHead.seoMeta;
932
+ if (seoMeta && Object.keys(seoMeta).length)
933
+ head.metaByLayer.push(unpackMeta(seoMeta));
934
+ head.link.push(...layerHead.link || []);
935
+ }
936
+ for (const layer of layers) {
937
+ const layerHead = layer.config?.app?.head;
938
+ if (!layerHead)
939
+ continue;
940
+ head.charset ??= layerHead.charset;
941
+ head.viewport ??= layerHead.viewport;
942
+ head.titleTemplate ??= layerHead.titleTemplate;
943
+ if (layerHead.htmlAttrs)
944
+ head.htmlAttrs = { ...layerHead.htmlAttrs, ...head.htmlAttrs };
945
+ }
946
+ return head;
947
+ }
948
+
706
949
  function setupDevToolsUI(resolve, nuxt = useNuxt()) {
707
950
  setupDevToolsUI$1({
708
951
  route: "/__nuxt-seo-utils",
@@ -748,7 +991,9 @@ const module$1 = defineNuxtModule({
748
991
  extendNuxtConfigAppHeadTypes: true,
749
992
  setupNuxtConfigAppHeadWithMoreDefaults: true,
750
993
  automaticOgAndTwitterTags: true,
994
+ automaticTwitterTags: true,
751
995
  canonicalLowercase: true,
996
+ validateAppHead: true,
752
997
  tagPriority: "low",
753
998
  minify: { build: true, runtime: false }
754
999
  },
@@ -865,6 +1110,7 @@ const module$1 = defineNuxtModule({
865
1110
  "tag"
866
1111
  ],
867
1112
  canonicalLowercase: config.canonicalLowercase,
1113
+ automaticTwitterTags: config.automaticTwitterTags,
868
1114
  tagPriority: config.tagPriority,
869
1115
  separator: headTemplateParams?.separator,
870
1116
  titleSeparator: headTemplateParams?.titleSeparator
@@ -1008,6 +1254,43 @@ export {}
1008
1254
  handler: resolve("./runtime/server/routes/__nuxt-seo-utils/debug")
1009
1255
  });
1010
1256
  }
1257
+ if (config.validateAppHead) {
1258
+ nuxt.hook("modules:done", () => {
1259
+ try {
1260
+ const userHead = collectUserAppHead(nuxt);
1261
+ const seoMeta = unpackMeta(config.meta || {});
1262
+ if (seoMeta.length)
1263
+ userHead.metaByLayer.push(seoMeta);
1264
+ const siteConfig = useSiteConfig(nuxt);
1265
+ const hasUserI18n = hasI18n && (nuxt.options._layers || []).some((layer) => Boolean(layer.config?.i18n));
1266
+ const diagnostics = validateAppHead({
1267
+ head: userHead,
1268
+ siteConfig: {
1269
+ name: siteConfig.name,
1270
+ url: siteConfig.url,
1271
+ description: siteConfig.description,
1272
+ defaultLocale: siteConfig.defaultLocale
1273
+ },
1274
+ hasI18n: hasUserI18n,
1275
+ hasRobotsModule: hasNuxtModule("@nuxtjs/robots", nuxt),
1276
+ defaultsActive: config.automaticDefaults,
1277
+ mergeWithSiteConfig: config.mergeWithSiteConfig
1278
+ });
1279
+ if (!diagnostics.length)
1280
+ return;
1281
+ const lines = diagnostics.map((d) => ` ${d.level === "info" ? "\xB7" : "\u2716"} ${formatAppHeadDiagnostic(d)}`);
1282
+ const message = `Found ${diagnostics.length} issue${diagnostics.length > 1 ? "s" : ""} in your Nuxt config head.
1283
+ ${lines.join("\n")}
1284
+ Set \`seo: { validateAppHead: false }\` to turn off this check.`;
1285
+ if (diagnostics.some((d) => d.level !== "info"))
1286
+ logger.warn(message);
1287
+ else
1288
+ logger.info(message);
1289
+ } catch (e) {
1290
+ logger.warn(`Skipped the head config check. ${e}`);
1291
+ }
1292
+ });
1293
+ }
1011
1294
  if (nuxt.options.dev) {
1012
1295
  setupDevToolsUI(resolve, nuxt);
1013
1296
  }
@@ -1,10 +1,28 @@
1
1
  import { injectHead } from "@unhead/vue";
2
2
  import { InferSeoMetaPlugin, TemplateParamsPlugin } from "@unhead/vue/plugins";
3
- import { defineNuxtPlugin } from "nuxt/app";
3
+ import { defineNuxtPlugin, useRuntimeConfig } from "nuxt/app";
4
+ const disabledTwitterCard = "x-nuxt-seo-utils-disabled-twitter-card";
4
5
  export default defineNuxtPlugin(() => {
5
6
  const head = injectHead();
6
7
  if (!head)
7
8
  return;
9
+ const { automaticTwitterTags } = useRuntimeConfig().public["seo-utils"];
8
10
  head.use(TemplateParamsPlugin);
9
- head.use(InferSeoMetaPlugin());
11
+ if (automaticTwitterTags === false) {
12
+ head.use(InferSeoMetaPlugin({ twitterCard: disabledTwitterCard }));
13
+ head.use({
14
+ key: "nuxt-seo-utils:twitter-card-suppression",
15
+ hooks: {
16
+ "tags:beforeResolve": ({ tags }) => {
17
+ for (let i = tags.length - 1; i >= 0; i--) {
18
+ const tag = tags[i];
19
+ if (tag.tag === "meta" && tag.props.name === "twitter:card" && tag.props.content === disabledTwitterCard)
20
+ tags.splice(i, 1);
21
+ }
22
+ }
23
+ }
24
+ });
25
+ } else {
26
+ head.use(InferSeoMetaPlugin());
27
+ }
10
28
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-seo-utils",
3
3
  "type": "module",
4
- "version": "8.4.3",
4
+ "version": "8.5.0",
5
5
  "description": "SEO utilities to improve your Nuxt sites discoverability and shareability",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/harlan-zw",
@@ -68,8 +68,8 @@
68
68
  "defu": "^6.1.7",
69
69
  "escape-string-regexp": "^5.0.0",
70
70
  "exsolve": "^1.1.1",
71
- "nuxt-site-config": "^4.2.2",
72
- "nuxtseo-shared": "^5.3.12",
71
+ "nuxt-site-config": "^4.2.3",
72
+ "nuxtseo-shared": "^5.3.14",
73
73
  "pathe": "^2.0.3",
74
74
  "pkg-types": "^2.3.1",
75
75
  "scule": "^1.3.0",
@@ -77,39 +77,39 @@
77
77
  "ufo": "^1.6.4"
78
78
  },
79
79
  "optionalDependencies": {
80
- "sharp": "^0.35.3"
80
+ "sharp": "^0.35.4"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@antfu/eslint-config": "^9.3.0",
84
84
  "@nuxt/devtools-kit": "4.0.0-alpha.9",
85
85
  "@nuxt/module-builder": "^1.0.3",
86
86
  "@nuxt/schema": "^4.5.2",
87
- "@nuxt/test-utils": "^4.1.0",
88
- "@nuxt/ui": "^4.10.0",
87
+ "@nuxt/test-utils": "^4.2.0",
88
+ "@nuxt/ui": "^4.11.0",
89
89
  "@nuxtjs/i18n": "^10.6.0",
90
- "@types/node": "^26.2.0",
90
+ "@types/node": "^26.4.0",
91
91
  "@unhead/vue": "3.2.3",
92
- "@vue/test-utils": "^2.4.11",
93
- "bumpp": "^12.2.0",
92
+ "@vue/test-utils": "^2.5.0",
93
+ "bumpp": "^12.2.2",
94
94
  "case-police": "^2.2.1",
95
95
  "cheerio": "^1.2.0",
96
- "eslint": "^10.8.1",
97
- "eslint-plugin-harlanzw": "^0.19.2",
98
- "happy-dom": "^20.11.2",
99
- "lint-staged": "^17.3.0",
96
+ "eslint": "^10.9.1",
97
+ "eslint-plugin-harlanzw": "^0.20.0",
98
+ "happy-dom": "^20.12.0",
99
+ "lint-staged": "^17.4.1",
100
100
  "markdownlint-cli": "^0.49.1",
101
101
  "nuxt": "^4.5.2",
102
102
  "nuxt-security": "^2.6.0",
103
- "nuxtseo-layer-devtools": "^5.3.12",
103
+ "nuxtseo-layer-devtools": "^5.3.14",
104
104
  "playwright-core": "^1.62.1",
105
- "sass": "^1.102.0",
105
+ "sass": "^1.103.1",
106
106
  "typescript": "6.0.3",
107
107
  "ultrahtml": "^1.7.0",
108
108
  "unbuild": "^3.6.1",
109
109
  "unhead": "3.2.3",
110
- "vitest": "^4.1.10",
111
- "vue": "^3.5.41",
112
- "vue-tsc": "^3.3.9"
110
+ "vitest": "^4.1.11",
111
+ "vue": "^3.5.42",
112
+ "vue-tsc": "^3.3.11"
113
113
  },
114
114
  "scripts": {
115
115
  "client:build": "node -e \"require('fs').cpSync('devtools','dist/devtools',{recursive:true})\"",