nuxt-seo-utils 8.4.3 → 8.5.1
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 +1 -1
- package/dist/module.d.mts +13 -0
- package/dist/module.d.ts +13 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +286 -3
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.js +20 -2
- package/package.json +18 -18
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.
|
|
12
|
+
const version = "8.5.1";
|
|
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
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';
|
|
@@ -377,7 +377,7 @@ function addGeneratedRouteRule(nuxt, route, generatedRule) {
|
|
|
377
377
|
nitroRouteRules[route] = defu(nitroRouteRules[route], structuredClone(rule));
|
|
378
378
|
}
|
|
379
379
|
async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
380
|
-
const pagesDirs = nuxt.options._layers.map((layer) => resolve(layer.config.
|
|
380
|
+
const pagesDirs = nuxt.options._layers.map((layer) => resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.pages || "pages")).filter((dir) => fs.existsSync(dir));
|
|
381
381
|
const appendRouteRules = {};
|
|
382
382
|
const registeredFiles = /* @__PURE__ */ new Set();
|
|
383
383
|
const devMiddlewareMap = {};
|
|
@@ -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
|
-
|
|
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
|
+
"version": "8.5.1",
|
|
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.
|
|
72
|
-
"nuxtseo-shared": "^5.3.
|
|
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.
|
|
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.
|
|
88
|
-
"@nuxt/ui": "^4.
|
|
87
|
+
"@nuxt/test-utils": "^4.2.0",
|
|
88
|
+
"@nuxt/ui": "^4.11.0",
|
|
89
89
|
"@nuxtjs/i18n": "^10.6.0",
|
|
90
|
-
"@types/node": "^26.
|
|
90
|
+
"@types/node": "^26.4.0",
|
|
91
91
|
"@unhead/vue": "3.2.3",
|
|
92
|
-
"@vue/test-utils": "^2.
|
|
93
|
-
"bumpp": "^12.2.
|
|
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.
|
|
97
|
-
"eslint-plugin-harlanzw": "^0.
|
|
98
|
-
"happy-dom": "^20.
|
|
99
|
-
"lint-staged": "^17.
|
|
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.
|
|
103
|
+
"nuxtseo-layer-devtools": "^5.3.14",
|
|
104
104
|
"playwright-core": "^1.62.1",
|
|
105
|
-
"sass": "^1.
|
|
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.
|
|
111
|
-
"vue": "^3.5.
|
|
112
|
-
"vue-tsc": "^3.3.
|
|
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})\"",
|