nuxt-seo-utils 8.5.2 → 8.6.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/README.md +9 -4
- package/dist/cli.mjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +25 -20
- package/dist/runtime/app/composables/schemaOrg.d.ts +2 -0
- package/dist/runtime/app/composables/schemaOrg.js +2 -0
- package/dist/runtime/app/composables/useBreadcrumbItems.js +11 -4
- package/dist/runtime/app/composables/useFallbackTitle.js +0 -8
- package/dist/runtime/app/composables/useShareLinks.js +7 -9
- package/dist/runtime/app/logic/applyDefaults.js +19 -21
- package/dist/runtime/app/logic/syncDerivedTags.d.ts +14 -0
- package/dist/runtime/app/logic/syncDerivedTags.js +43 -0
- package/dist/runtime/app/plugins/0.routeRules.d.ts +1 -1
- package/dist/runtime/app/plugins/0.routeRules.js +2 -3
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.d.ts +1 -1
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.js +1 -2
- package/dist/runtime/app/plugins/colorModeIcons.d.ts +1 -1
- package/dist/runtime/app/plugins/colorModeIcons.js +1 -1
- package/dist/runtime/app/plugins/defaults.d.ts +1 -1
- package/dist/runtime/app/plugins/defaultsWaitI18n.d.ts +1 -1
- package/dist/runtime/app/plugins/derivedTags.d.ts +2 -0
- package/dist/runtime/app/plugins/derivedTags.js +17 -0
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.d.ts +1 -1
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.js +1 -2
- package/dist/runtime/app/plugins/minifyScripts.server.d.ts +1 -1
- package/dist/runtime/app/plugins/minifyScripts.server.js +1 -2
- package/dist/runtime/app/plugins/siteConfig.d.ts +1 -1
- package/dist/runtime/app/plugins/siteConfig.js +1 -2
- package/dist/runtime/app/plugins/titles.d.ts +1 -1
- package/dist/runtime/app/plugins/titles.js +1 -2
- package/dist/runtime/app/plugins/titlesWaitI18n.d.ts +1 -1
- package/dist/runtime/app/plugins/titlesWaitI18n.js +1 -2
- package/dist/runtime/shared/canonicalUrl.d.ts +17 -0
- package/dist/runtime/shared/canonicalUrl.js +26 -0
- package/dist/runtime/shared/derivedTagKeys.d.ts +10 -0
- package/dist/runtime/shared/derivedTagKeys.js +2 -0
- package/package.json +3 -2
- package/skills/nuxt-seo-utils/SKILL.md +144 -0
package/README.md
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
[![License][license-src]][license-href]
|
|
6
6
|
[![Nuxt][nuxt-src]][nuxt-href]
|
|
7
7
|
|
|
8
|
+
<a href="https://skilld.dev/gh/harlan-zw/nuxt-seo-utils">
|
|
9
|
+
<picture>
|
|
10
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://skilld.dev/b/harlan-zw/nuxt-seo-utils?theme=dark">
|
|
11
|
+
<source media="(prefers-color-scheme: light)" srcset="https://skilld.dev/b/harlan-zw/nuxt-seo-utils?theme=light">
|
|
12
|
+
<img alt="Skill repository on skilld.dev" src="https://skilld.dev/b/harlan-zw/nuxt-seo-utils?theme=light">
|
|
13
|
+
</picture>
|
|
14
|
+
</a>
|
|
15
|
+
|
|
8
16
|
Nuxt SEO Utils is a collection of defaults and utilities to improve your Nuxt site's discoverability and shareability.
|
|
9
17
|
|
|
10
18
|
While there are several features covering many aspects of SEO, it covers important defaults such as [automatic canonical URLs](https://nuxtseo.com/learn/controlling-crawlers/canonical-urls) and
|
|
@@ -40,10 +48,7 @@ npx nuxi@latest module add nuxt-seo-utils
|
|
|
40
48
|
```
|
|
41
49
|
|
|
42
50
|
> [!TIP]
|
|
43
|
-
>
|
|
44
|
-
> ```bash
|
|
45
|
-
> npx skilld add nuxt-seo-utils
|
|
46
|
-
> ```
|
|
51
|
+
> Using an AI agent? Get the nuxt-seo-utils Skill on [skilld.dev/gh/harlan-zw/nuxt-seo-utils](https://skilld.dev/gh/harlan-zw/nuxt-seo-utils).
|
|
47
52
|
|
|
48
53
|
💡 Set up your meta tags? Check titles, descriptions and OG tags with a SERP preview using the free [Meta Tag Checker](https://nuxtseo.com/tools/meta-tag-checker), or monitor them across every page with [Nuxt SEO Pro](https://nuxtseo.com/pro).
|
|
49
54
|
|
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.6.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.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { c as classifyIconFilename, g as getIconRel, n as normalizeIconSizes, a
|
|
|
14
14
|
import escapeRE from 'escape-string-regexp';
|
|
15
15
|
import { readFile, readdir } from 'node:fs/promises';
|
|
16
16
|
import { Buffer } from 'node:buffer';
|
|
17
|
+
import { TWITTER_IMAGE_FALLBACK_KEY } from '../dist/runtime/shared/derivedTagKeys.js';
|
|
17
18
|
import { JSON_TYPES, SKIP_JS_TYPES } from '../dist/runtime/shared/minify.js';
|
|
18
19
|
import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
|
|
19
20
|
|
|
@@ -82,9 +83,15 @@ const MetaTagFileDeepGlobs = [
|
|
|
82
83
|
"**/apple-*.{jpg,jpeg,png}",
|
|
83
84
|
"**/*.apple-*.{jpg,jpeg,png}"
|
|
84
85
|
];
|
|
85
|
-
const SOCIAL_IMAGE_RE = /^(
|
|
86
|
+
const SOCIAL_IMAGE_RE = /^(og-image|opengraph-image|twitter-image)\.(?:png|jpe?g|gif)$/;
|
|
87
|
+
function classifySocialImageFilename(filename) {
|
|
88
|
+
const keyword = SOCIAL_IMAGE_RE.exec(filename)?.[1];
|
|
89
|
+
if (!keyword)
|
|
90
|
+
return;
|
|
91
|
+
return keyword === "twitter-image" ? "twitterImage" : "ogImage";
|
|
92
|
+
}
|
|
86
93
|
function isMetaTagFile(filename) {
|
|
87
|
-
return
|
|
94
|
+
return classifySocialImageFilename(filename) !== void 0 || classifyIconFilename(filename) !== void 0;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
const INDEX_SUFFIX_RE = /-index$/;
|
|
@@ -383,10 +390,9 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
|
383
390
|
const devMiddlewareMap = {};
|
|
384
391
|
const nitroOutputMap = [];
|
|
385
392
|
for (const pagesDir of pagesDirs) {
|
|
386
|
-
const files = (await glob(MetaTagFileDeepGlobs, { cwd: pagesDir, onlyFiles: true })).filter((file) => isMetaTagFile(basename(file)));
|
|
393
|
+
const files = (await glob(MetaTagFileDeepGlobs, { cwd: pagesDir, onlyFiles: true })).filter((file) => isMetaTagFile(basename(file))).sort();
|
|
387
394
|
for (const file of files) {
|
|
388
395
|
const fileName = basename(file);
|
|
389
|
-
const keyword = fileName.split(".")[0] || "";
|
|
390
396
|
let { path } = generateNuxtPageFromFile(resolve(pagesDir, dirname(file)), pagesDir);
|
|
391
397
|
if (path.endsWith("/_dir"))
|
|
392
398
|
path = path.replace(DIR_SUFFIX_RE, "");
|
|
@@ -408,9 +414,10 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
|
408
414
|
media: meta.media ? String(meta.media) : void 0
|
|
409
415
|
});
|
|
410
416
|
} else {
|
|
411
|
-
const property =
|
|
412
|
-
routeRule.seoMeta[property]
|
|
413
|
-
|
|
417
|
+
const property = classifySocialImageFilename(fileName);
|
|
418
|
+
if (!property || routeRule.seoMeta[property])
|
|
419
|
+
continue;
|
|
420
|
+
routeRule.seoMeta[property] = { url: href, ...meta, alt: meta.alt || "", sizes: void 0 };
|
|
414
421
|
}
|
|
415
422
|
devMiddlewareMap[routeAssetPath] = resolve(pagesDir, file);
|
|
416
423
|
nitroOutputMap.push({
|
|
@@ -539,7 +546,7 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
|
539
546
|
}
|
|
540
547
|
let hasTwitterImage = hasMetaProperty(headConfig, "twitter:image");
|
|
541
548
|
if (!hasTwitterImage) {
|
|
542
|
-
const twitterImageFiles = rootPublicFiles.filter((file) => file
|
|
549
|
+
const twitterImageFiles = rootPublicFiles.filter((file) => classifySocialImageFilename(file) === "twitterImage").sort();
|
|
543
550
|
if (twitterImageFiles.length) {
|
|
544
551
|
headConfig.meta.push(
|
|
545
552
|
...(await Promise.all(twitterImageFiles.map(async (twitterImageFile) => {
|
|
@@ -557,25 +564,18 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
|
557
564
|
}
|
|
558
565
|
}
|
|
559
566
|
if (!hasMetaProperty(headConfig, "og:image")) {
|
|
560
|
-
const ogImageFiles = rootPublicFiles.filter((file) => file
|
|
567
|
+
const ogImageFiles = rootPublicFiles.filter((file) => classifySocialImageFilename(file) === "ogImage").sort();
|
|
561
568
|
if (ogImageFiles.length) {
|
|
562
569
|
headConfig.meta.push(
|
|
563
570
|
...(await Promise.all(ogImageFiles.map(async (src) => {
|
|
564
571
|
const meta = await getImageMeta(fileEntries.find((e) => e.file === src).dir, src, false);
|
|
565
572
|
delete meta.sizes;
|
|
566
|
-
const
|
|
567
|
-
ogImage: {
|
|
568
|
-
url: src,
|
|
569
|
-
...meta
|
|
570
|
-
}
|
|
571
|
-
};
|
|
573
|
+
const tags = unpackMeta({ ogImage: { url: src, ...meta } });
|
|
572
574
|
if (!hasTwitterImage) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
...meta
|
|
576
|
-
};
|
|
575
|
+
const twitterTags = unpackMeta({ twitterImage: { url: src, ...meta } });
|
|
576
|
+
tags.push(...twitterTags.map((tag) => ({ ...tag, key: `${TWITTER_IMAGE_FALLBACK_KEY}:${src}` })));
|
|
577
577
|
}
|
|
578
|
-
return
|
|
578
|
+
return tags;
|
|
579
579
|
}))).flat()
|
|
580
580
|
);
|
|
581
581
|
}
|
|
@@ -1034,6 +1034,9 @@ const module$1 = defineNuxtModule({
|
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
1036
1036
|
const hasI18n = hasNuxtModule("@nuxtjs/i18n") || hasNuxtModule("nuxt-i18n-micro");
|
|
1037
|
+
addPlugin({
|
|
1038
|
+
src: resolve(runtimeDir, "./app/plugins/derivedTags")
|
|
1039
|
+
});
|
|
1037
1040
|
if (config.automaticDefaults) {
|
|
1038
1041
|
if (hasI18n) {
|
|
1039
1042
|
addPlugin({
|
|
@@ -1098,6 +1101,8 @@ const module$1 = defineNuxtModule({
|
|
|
1098
1101
|
});
|
|
1099
1102
|
}
|
|
1100
1103
|
nuxt.options.alias["#seo-utils"] = runtimeDir;
|
|
1104
|
+
const hasSchemaOrg = hasNuxtModule("nuxt-schema-org") && nuxt.options.schemaOrg?.enable !== false;
|
|
1105
|
+
nuxt.options.alias["#seo-utils-schema"] = hasSchemaOrg ? resolve(runtimeDir, "./app/composables/schemaOrg") : resolve(runtimeDir, "./app/composables/polyfills");
|
|
1101
1106
|
nuxt.options.runtimeConfig.public["nuxt-seo-utils-version"] = nuxt.options.runtimeConfig.public["nuxt-seo-utils-version"] || version || "";
|
|
1102
1107
|
nuxt.options.runtimeConfig.public["seo-utils"] = defu(nuxt.options.runtimeConfig.public["seo-utils"] || {}, {
|
|
1103
1108
|
canonicalQueryWhitelist: config.canonicalQueryWhitelist ?? [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defu } from "defu";
|
|
2
2
|
import { fixSlashes } from "nuxt-site-config/urls";
|
|
3
|
-
import { useNuxtApp, useRoute, useRouter, useState } from "nuxt/app";
|
|
3
|
+
import { useNuxtApp, useRoute, useRouter, useRuntimeConfig, useState } from "nuxt/app";
|
|
4
4
|
import { withoutTrailingSlash } from "ufo";
|
|
5
5
|
import {
|
|
6
6
|
computed,
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
toValue,
|
|
15
15
|
watch
|
|
16
16
|
} from "vue";
|
|
17
|
-
import { defineBreadcrumb,
|
|
17
|
+
import { defineBreadcrumb, useSchemaOrg } from "#seo-utils-schema";
|
|
18
18
|
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
19
19
|
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
20
20
|
import { pathBreadcrumbSegments, resolveBreadcrumbRoot } from "../../shared/breadcrumbs.js";
|
|
21
|
+
import { normaliseCanonicalUrl } from "../../shared/canonicalUrl.js";
|
|
22
|
+
import { useI18n as useI18nPolyfill } from "./polyfills.js";
|
|
21
23
|
function withoutQuery(path) {
|
|
22
24
|
return path.split("?")[0] || "";
|
|
23
25
|
}
|
|
@@ -77,7 +79,7 @@ export function useBreadcrumbItems(_options = {}) {
|
|
|
77
79
|
}
|
|
78
80
|
const route = useRoute();
|
|
79
81
|
const router = useRouter();
|
|
80
|
-
const i18n =
|
|
82
|
+
const i18n = nuxtApp.$i18n || useI18nPolyfill();
|
|
81
83
|
const siteResolver = createSitePathResolver({
|
|
82
84
|
canonical: true,
|
|
83
85
|
absolute: true
|
|
@@ -182,12 +184,17 @@ export function useBreadcrumbItems(_options = {}) {
|
|
|
182
184
|
}, { immediate: true, flush: "sync" });
|
|
183
185
|
const schemaOrgEnabled = typeof _options.schemaOrg === "undefined" ? true : _options.schemaOrg;
|
|
184
186
|
if ((import.meta.server || import.meta.env?.NODE_ENV === "test") && schemaOrgEnabled) {
|
|
187
|
+
const { canonicalLowercase } = useRuntimeConfig().public["seo-utils"];
|
|
188
|
+
const schemaItemUrl = (to) => normaliseCanonicalUrl(toValue(siteResolver(to)), {
|
|
189
|
+
lowercase: canonicalLowercase,
|
|
190
|
+
locale: toValue(siteConfig.currentLocale) || toValue(siteConfig.defaultLocale)
|
|
191
|
+
});
|
|
185
192
|
useSchemaOrg([
|
|
186
193
|
defineBreadcrumb({
|
|
187
194
|
"@id": `#${id}`,
|
|
188
195
|
"itemListElement": computed(() => items.value.map((item) => ({
|
|
189
196
|
name: item.label || item.ariaLabel,
|
|
190
|
-
item: item.to ?
|
|
197
|
+
item: item.to ? schemaItemUrl(String(item.to)) : void 0
|
|
191
198
|
})))
|
|
192
199
|
})
|
|
193
200
|
]);
|
|
@@ -2,7 +2,6 @@ import { useError, useNuxtApp, useRoute } from "nuxt/app";
|
|
|
2
2
|
import { titleCase } from "scule";
|
|
3
3
|
import { withoutTrailingSlash } from "ufo";
|
|
4
4
|
import { computed } from "vue";
|
|
5
|
-
import { useI18n } from "#imports";
|
|
6
5
|
export function useFallbackTitle() {
|
|
7
6
|
const route = useRoute();
|
|
8
7
|
const err = useError();
|
|
@@ -11,13 +10,6 @@ export function useFallbackTitle() {
|
|
|
11
10
|
i18n = useNuxtApp().$i18n;
|
|
12
11
|
} catch {
|
|
13
12
|
}
|
|
14
|
-
if (!i18n) {
|
|
15
|
-
try {
|
|
16
|
-
i18n = useI18n();
|
|
17
|
-
} catch {
|
|
18
|
-
console.warn("[nuxt-seo-utils] useFallbackTitle: could not resolve the i18n instance. Titles will not be resolved from i18n messages.");
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
13
|
return computed(() => {
|
|
22
14
|
if (err.value?.statusCode && [404, 500].includes(err.value.statusCode)) {
|
|
23
15
|
return `${err.value.statusCode} - ${err.value.message}`;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useRoute, useRuntimeConfig } from "nuxt/app";
|
|
2
|
-
import {
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
3
|
import { computed, toValue } from "vue";
|
|
4
4
|
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
5
5
|
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
6
|
+
import { resolveCanonicalUrl } from "../../shared/canonicalUrl.js";
|
|
6
7
|
function buildShareUrl(platform, url, title, options) {
|
|
7
8
|
const encodedUrl = encodeURIComponent(url);
|
|
8
9
|
const encodedTitle = encodeURIComponent(title);
|
|
@@ -53,14 +54,11 @@ export function useShareLinks(options = {}) {
|
|
|
53
54
|
if (customUrl) {
|
|
54
55
|
canonicalUrl = customUrl;
|
|
55
56
|
} else {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Object.entries(route.query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
|
|
62
|
-
);
|
|
63
|
-
canonicalUrl = Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
|
|
57
|
+
canonicalUrl = resolveCanonicalUrl(resolveUrl(route.path || "/").value || route.path, route.query, {
|
|
58
|
+
lowercase: canonicalLowercase,
|
|
59
|
+
locale: toValue(siteConfig.currentLocale) || toValue(siteConfig.defaultLocale),
|
|
60
|
+
queryWhitelist: canonicalQueryWhitelist
|
|
61
|
+
});
|
|
64
62
|
}
|
|
65
63
|
const utm = toValue(options.utm) ?? true;
|
|
66
64
|
const title = toValue(options.title) || siteConfig.name || "";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { injectHead, useHead, useSeoMeta } from "@unhead/vue";
|
|
2
1
|
import { TemplateParamsPlugin } from "@unhead/vue/plugins";
|
|
3
|
-
import { useError, useRoute, useRuntimeConfig } from "nuxt/app";
|
|
4
|
-
import { stringifyQuery } from "ufo";
|
|
2
|
+
import { injectHead, useError, useHead, useRoute, useRuntimeConfig, useSeoMeta } from "nuxt/app";
|
|
5
3
|
import { computed, toValue } from "vue";
|
|
6
4
|
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
7
5
|
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
6
|
+
import { resolveCanonicalUrl } from "../../shared/canonicalUrl.js";
|
|
7
|
+
import { OG_URL_KEY } from "../../shared/derivedTagKeys.js";
|
|
8
8
|
const LOCALE_UNDERSCORE_RE = /_/g;
|
|
9
9
|
export function applyDefaults() {
|
|
10
10
|
const siteConfig = useSiteConfig({
|
|
@@ -26,20 +26,12 @@ export function applyDefaults() {
|
|
|
26
26
|
if (err.value) {
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
url = url.toLowerCase();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const filteredQuery = Object.fromEntries(
|
|
39
|
-
Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
|
|
40
|
-
// Sort params
|
|
41
|
-
);
|
|
42
|
-
const href = Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
|
|
29
|
+
const url = resolveUrl(route.path || "/").value || route.path;
|
|
30
|
+
const href = resolveCanonicalUrl(url, route.query, {
|
|
31
|
+
lowercase: canonicalLowercase,
|
|
32
|
+
locale: resolveCurrentLocale(),
|
|
33
|
+
queryWhitelist: canonicalQueryWhitelist
|
|
34
|
+
});
|
|
43
35
|
return { rel: "canonical", href };
|
|
44
36
|
});
|
|
45
37
|
const minimalPriority = {
|
|
@@ -74,10 +66,6 @@ export function applyDefaults() {
|
|
|
74
66
|
}, minimalPriority);
|
|
75
67
|
const seoMeta = {
|
|
76
68
|
ogType: "website",
|
|
77
|
-
ogUrl: () => {
|
|
78
|
-
const url = canonicalUrl.value;
|
|
79
|
-
return url ? url.href : false;
|
|
80
|
-
},
|
|
81
69
|
ogSiteName: siteConfig.name
|
|
82
70
|
};
|
|
83
71
|
if (import.meta.server && siteConfig.description)
|
|
@@ -88,4 +76,14 @@ export function applyDefaults() {
|
|
|
88
76
|
seoMeta.twitterSite = id;
|
|
89
77
|
}
|
|
90
78
|
useSeoMeta(seoMeta, seoMetaPriority);
|
|
79
|
+
useHead({
|
|
80
|
+
meta: [{
|
|
81
|
+
property: "og:url",
|
|
82
|
+
content: () => {
|
|
83
|
+
const url = canonicalUrl.value;
|
|
84
|
+
return url ? url.href : false;
|
|
85
|
+
},
|
|
86
|
+
key: OG_URL_KEY
|
|
87
|
+
}]
|
|
88
|
+
}, seoMetaPriority);
|
|
91
89
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HeadTag } from '@unhead/vue/types';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the entries whose tags hold an `og:image`, before deduplication.
|
|
4
|
+
*/
|
|
5
|
+
export declare function ogImageEntryIds(entryTags: Iterable<HeadTag[]>): Set<number>;
|
|
6
|
+
/**
|
|
7
|
+
* Keeps tags that describe another tag consistent with the tag that won deduplication.
|
|
8
|
+
*
|
|
9
|
+
* - `og:image:*` tags from an entry whose `og:image` lost are dropped, so a page `ogImage`
|
|
10
|
+
* does not keep the width, height, or type of the `public/` image.
|
|
11
|
+
* - `twitter:image` tags copied from a `public/` og-image follow the same rule.
|
|
12
|
+
* - The default `og:url` takes the href of the resolved canonical link.
|
|
13
|
+
*/
|
|
14
|
+
export declare function syncDerivedTags(tags: HeadTag[], entriesWithOgImage: Set<number>): HeadTag[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { OG_URL_KEY, TWITTER_IMAGE_FALLBACK_KEY } from "../../shared/derivedTagKeys.js";
|
|
2
|
+
function entryOf(tag) {
|
|
3
|
+
return tag._p === void 0 ? void 0 : tag._p >> 10;
|
|
4
|
+
}
|
|
5
|
+
function metaKey(tag) {
|
|
6
|
+
const key = tag.props.property ?? tag.props.name;
|
|
7
|
+
return typeof key === "string" ? key : void 0;
|
|
8
|
+
}
|
|
9
|
+
function isOgImage(tag) {
|
|
10
|
+
return tag.tag === "meta" && metaKey(tag) === "og:image";
|
|
11
|
+
}
|
|
12
|
+
export function ogImageEntryIds(entryTags) {
|
|
13
|
+
const ids = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const tags of entryTags) {
|
|
15
|
+
const tag = tags.find(isOgImage);
|
|
16
|
+
const id = tag && entryOf(tag);
|
|
17
|
+
if (id !== void 0)
|
|
18
|
+
ids.add(id);
|
|
19
|
+
}
|
|
20
|
+
return ids;
|
|
21
|
+
}
|
|
22
|
+
export function syncDerivedTags(tags, entriesWithOgImage) {
|
|
23
|
+
const winningOgImageEntries = new Set(tags.filter(isOgImage).map(entryOf));
|
|
24
|
+
const canonicalHref = tags.find((tag) => tag.tag === "link" && tag.props.rel === "canonical")?.props.href;
|
|
25
|
+
const lostOgImage = (tag) => {
|
|
26
|
+
const entry = entryOf(tag);
|
|
27
|
+
return entry !== void 0 && entriesWithOgImage.has(entry) && !winningOgImageEntries.has(entry);
|
|
28
|
+
};
|
|
29
|
+
return tags.flatMap((tag) => {
|
|
30
|
+
if (tag.tag !== "meta")
|
|
31
|
+
return [tag];
|
|
32
|
+
if (metaKey(tag)?.startsWith("og:image:") && lostOgImage(tag))
|
|
33
|
+
return [];
|
|
34
|
+
if (tag.key?.startsWith(TWITTER_IMAGE_FALLBACK_KEY) && lostOgImage(tag))
|
|
35
|
+
return [];
|
|
36
|
+
if (tag.key === OG_URL_KEY) {
|
|
37
|
+
if (typeof canonicalHref !== "string" || !canonicalHref)
|
|
38
|
+
return [];
|
|
39
|
+
return [{ ...tag, props: { ...tag.props, content: canonicalHref } }];
|
|
40
|
+
}
|
|
41
|
+
return [tag];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtPlugin, useHead, useRequestEvent,
|
|
1
|
+
import { defineNuxtPlugin, useHead, useRequestEvent, useSeoMeta, useState } from "nuxt/app";
|
|
2
2
|
function parseRouteRuleState(context) {
|
|
3
3
|
const rules = context._nitro?.routeRules;
|
|
4
4
|
return {
|
|
@@ -10,7 +10,6 @@ export default defineNuxtPlugin({
|
|
|
10
10
|
enforce: "post",
|
|
11
11
|
env: { islands: false },
|
|
12
12
|
setup() {
|
|
13
|
-
const { tagPriority } = useRuntimeConfig().public["seo-utils"];
|
|
14
13
|
const routeRuleState = useState("nuxt-seo-utils:routeRules", () => null);
|
|
15
14
|
if (import.meta.server) {
|
|
16
15
|
const event = useRequestEvent();
|
|
@@ -21,7 +20,7 @@ export default defineNuxtPlugin({
|
|
|
21
20
|
if (headInput)
|
|
22
21
|
useHead(headInput);
|
|
23
22
|
if (seoMeta)
|
|
24
|
-
useSeoMeta(seoMeta
|
|
23
|
+
useSeoMeta(seoMeta);
|
|
25
24
|
}
|
|
26
25
|
}
|
|
27
26
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { injectHead } from "
|
|
2
|
-
import { defineNuxtPlugin } from "nuxt/app";
|
|
1
|
+
import { defineNuxtPlugin, injectHead } from "nuxt/app";
|
|
3
2
|
import { unref } from "vue";
|
|
4
3
|
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
5
4
|
export default defineNuxtPlugin({
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineNuxtPlugin, useHead, useNuxtApp, useRuntimeConfig } from "
|
|
1
|
+
import { defineNuxtPlugin, useHead, useNuxtApp, useRuntimeConfig } from "nuxt/app";
|
|
2
2
|
import { selectColorModeIconLinks } from "../logic/colorModeIcons.js";
|
|
3
3
|
export default defineNuxtPlugin({
|
|
4
4
|
name: "nuxt-seo-utils:color-mode-icons",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineNuxtPlugin, injectHead } from "nuxt/app";
|
|
2
|
+
import { ogImageEntryIds, syncDerivedTags } from "../logic/syncDerivedTags.js";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
name: "nuxt-seo:derived-tags",
|
|
5
|
+
setup() {
|
|
6
|
+
const head = injectHead();
|
|
7
|
+
head.use({
|
|
8
|
+
key: "nuxt-seo-utils:derived-tags",
|
|
9
|
+
hooks: {
|
|
10
|
+
"tags:beforeResolve": (ctx) => {
|
|
11
|
+
const entryTags = [...head.entries.values()].map((entry) => entry._tags || entry._precomputedTags || []);
|
|
12
|
+
ctx.tags = syncDerivedTags(ctx.tags, ogImageEntryIds(entryTags));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { injectHead } from "@unhead/vue";
|
|
2
1
|
import { InferSeoMetaPlugin, TemplateParamsPlugin } from "@unhead/vue/plugins";
|
|
3
|
-
import { defineNuxtPlugin, useRuntimeConfig } from "nuxt/app";
|
|
2
|
+
import { defineNuxtPlugin, injectHead, useRuntimeConfig } from "nuxt/app";
|
|
4
3
|
const disabledTwitterCard = "x-nuxt-seo-utils-disabled-twitter-card";
|
|
5
4
|
export default defineNuxtPlugin(() => {
|
|
6
5
|
const head = injectHead();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { injectHead } from "
|
|
2
|
-
import { defineNuxtPlugin } from "nuxt/app";
|
|
1
|
+
import { defineNuxtPlugin, injectHead } from "nuxt/app";
|
|
3
2
|
import { JSON_TYPES, minifyCSS, minifyJS, minifyJSON, SKIP_JS_TYPES } from "../../shared/minify.js";
|
|
4
3
|
export default defineNuxtPlugin({
|
|
5
4
|
enforce: "post",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { injectHead } from "
|
|
2
|
-
import { defineNuxtPlugin, useRuntimeConfig } from "nuxt/app";
|
|
1
|
+
import { defineNuxtPlugin, injectHead, useRuntimeConfig } from "nuxt/app";
|
|
3
2
|
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
4
3
|
export default defineNuxtPlugin(() => {
|
|
5
4
|
const head = injectHead();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { useHead } from "
|
|
2
|
-
import { defineNuxtPlugin } from "nuxt/app";
|
|
1
|
+
import { defineNuxtPlugin, useHead } from "nuxt/app";
|
|
3
2
|
import { useFallbackTitle } from "../composables/useFallbackTitle.js";
|
|
4
3
|
export default defineNuxtPlugin({
|
|
5
4
|
name: "nuxt-seo:fallback-titles",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { useHead } from "
|
|
2
|
-
import { defineNuxtPlugin } from "nuxt/app";
|
|
1
|
+
import { defineNuxtPlugin, useHead } from "nuxt/app";
|
|
3
2
|
import { useFallbackTitle } from "../composables/useFallbackTitle.js";
|
|
4
3
|
export default defineNuxtPlugin({
|
|
5
4
|
name: "nuxt-seo:fallback-titles",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { QueryObject } from 'ufo';
|
|
2
|
+
export interface CanonicalUrlOptions {
|
|
3
|
+
lowercase: boolean;
|
|
4
|
+
/** BCP 47 or underscore locale. Lowercasing follows its rules, for example Turkish dotless ı. */
|
|
5
|
+
locale?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The one normaliser for absolute URLs that name the page: the canonical link, og:url,
|
|
9
|
+
* share links, and Schema.org breadcrumb items. Navigation links never pass through it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normaliseCanonicalUrl(url: string, { lowercase, locale }: CanonicalUrlOptions): string;
|
|
12
|
+
/**
|
|
13
|
+
* Normalises the page URL and keeps only whitelisted query keys, sorted.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveCanonicalUrl(url: string, query: QueryObject, options: CanonicalUrlOptions & {
|
|
16
|
+
queryWhitelist: string[];
|
|
17
|
+
}): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { parseURL, stringifyParsedURL, stringifyQuery } from "ufo";
|
|
2
|
+
const LOCALE_UNDERSCORE_RE = /_/g;
|
|
3
|
+
const DEFAULT_LOCALE = "en";
|
|
4
|
+
export function normaliseCanonicalUrl(url, { lowercase, locale }) {
|
|
5
|
+
if (!lowercase)
|
|
6
|
+
return url;
|
|
7
|
+
const parsed = parseURL(url);
|
|
8
|
+
if (parsed.host)
|
|
9
|
+
parsed.host = parsed.host.toLowerCase();
|
|
10
|
+
parsed.pathname = lowercasePath(parsed.pathname, locale);
|
|
11
|
+
return stringifyParsedURL(parsed);
|
|
12
|
+
}
|
|
13
|
+
function lowercasePath(path, locale) {
|
|
14
|
+
try {
|
|
15
|
+
return path.toLocaleLowerCase(locale ? locale.replace(LOCALE_UNDERSCORE_RE, "-") : DEFAULT_LOCALE);
|
|
16
|
+
} catch {
|
|
17
|
+
return path.toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function resolveCanonicalUrl(url, query, options) {
|
|
21
|
+
const normalised = normaliseCanonicalUrl(url, options);
|
|
22
|
+
const filteredQuery = Object.fromEntries(
|
|
23
|
+
Object.entries(query).filter(([key]) => options.queryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
|
|
24
|
+
);
|
|
25
|
+
return Object.keys(filteredQuery).length ? `${normalised}?${stringifyQuery(filteredQuery)}` : normalised;
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks the `twitter:image` tags copied from a `public/` og-image.
|
|
3
|
+
* They are dropped when another entry's `og:image` replaces that image.
|
|
4
|
+
* Keys start with this prefix and end with the file name, so two files never merge.
|
|
5
|
+
*/
|
|
6
|
+
export declare const TWITTER_IMAGE_FALLBACK_KEY = "nuxt-seo-utils:twitter-image-fallback";
|
|
7
|
+
/**
|
|
8
|
+
* Marks the default `og:url`. It takes the href of the resolved canonical link.
|
|
9
|
+
*/
|
|
10
|
+
export declare const OG_URL_KEY = "nuxt-seo-utils:og-url";
|
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.6.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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"bin",
|
|
28
|
-
"dist"
|
|
28
|
+
"dist",
|
|
29
|
+
"skills"
|
|
29
30
|
],
|
|
30
31
|
"build": {
|
|
31
32
|
"externals": [
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nuxt-seo-utils
|
|
3
|
+
description: Set up, override, and debug the default SEO head tags in a Nuxt app with the nuxt-seo-utils module. Use when a task mentions canonical URLs, the title template or separator, fallback titles, og:image or twitter:image files, favicons and app icons, seo.meta, seoMeta or head in routeRules, useBreadcrumbItems, useShareLinks, the nuxt-seo-utils icons CLI, or the seo config key. Also use when a page tag does not override a default, html lang ignores useHead, or og:image renders twice.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# nuxt-seo-utils
|
|
7
|
+
|
|
8
|
+
Tested against `nuxt-seo-utils` 8.5.2 on Nuxt 4.5.2 with Unhead v3 (requires Nuxt `>=3.16.0`).
|
|
9
|
+
The module adds default head tags from site config, reads icon and social image files, and ships breadcrumb and share link composables.
|
|
10
|
+
Docs: https://nuxtseo.com/docs/seo-utils
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
The config key is `seo`. `seoUtils` does nothing.
|
|
15
|
+
Set `site.url` and `site.name`. The canonical URL, `og:url`, the title template, and absolute image URLs read them.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
export default defineNuxtConfig({
|
|
19
|
+
modules: ['nuxt-seo-utils'],
|
|
20
|
+
site: { url: 'https://example.com', name: 'Acme', description: 'Default description', twitter: 'acme', defaultLocale: 'en-US' },
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Crawlers read only the SSR response. The defaults render on the server.
|
|
25
|
+
|
|
26
|
+
## Automatic behaviour
|
|
27
|
+
|
|
28
|
+
With the defaults, every page gets:
|
|
29
|
+
|
|
30
|
+
- `<title>` from the template `%s %separator %siteName`. The separator is `|`. Change it with `site.separator`.
|
|
31
|
+
- A fallback title from the last path segment, title cased: `/blog/my-post` becomes `My Post`. `definePageMeta({ title })` sets it with a static string.
|
|
32
|
+
- `<link rel="canonical">` from `site.url` and the route path, lowercased, with only these query keys kept: `page`, `sort`, `filter`, `search`, `q`, `category`, `tag`.
|
|
33
|
+
- `og:url` (the resolved canonical, including one a page sets), `og:type` (`website`), `og:site_name` (`site.name`), and `description` from `site.description`.
|
|
34
|
+
- `og:title`, `og:description`, and `twitter:card` (`summary_large_image`), inferred from the title and description.
|
|
35
|
+
- `twitter:site` and `twitter:creator` from `site.twitter`. The `@` is added.
|
|
36
|
+
- `<html lang>` from `site.currentLocale` or `site.defaultLocale`, else `en`.
|
|
37
|
+
- `og:locale` only when the locale has a region. `en-US` renders `en_US`. `en` renders no tag.
|
|
38
|
+
- Relative `og:image` and `twitter:image` values become absolute against `site.url`.
|
|
39
|
+
|
|
40
|
+
Module tags use `tagPriority: 'low'`, so a page `useSeoMeta()` or `useHead()` call replaces them.
|
|
41
|
+
A page `ogImage` also drops the default image's width, height, type, and copied `twitter:image`. A page `ogImageAlt` alone keeps the default image.
|
|
42
|
+
A page canonical needs no extra priority:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
useHead({ link: [{ rel: 'canonical', href: 'https://example.com/preferred' }] })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Set `seo: { automaticDefaults: false }` to drop the canonical, title template, `lang`, and Open Graph defaults together. No option drops only one of them.
|
|
49
|
+
|
|
50
|
+
## Metadata files
|
|
51
|
+
|
|
52
|
+
Files in `public/` become site-wide head tags at build time:
|
|
53
|
+
|
|
54
|
+
- `favicon.{ico,png,svg}`, `icon*.{ico,png,jpg,svg}`, `apple-icon*.png`, `apple-touch-icon*.png`: `<link rel="icon">` or `apple-touch-icon`, with `type` and `sizes` read from the file.
|
|
55
|
+
- `og-image.{png,jpg,jpeg,gif}` or `opengraph-image.*`: `og:image` with width, height, and type. It also fills `twitter:image` when no `twitter-image.*` file exists. The name must match exactly; `blog-og-image.png` is ignored.
|
|
56
|
+
- `<name>.alt.txt` next to an image sets `og:image:alt`.
|
|
57
|
+
- `-dark` or `-light` in an icon name adds a `prefers-color-scheme` media query.
|
|
58
|
+
|
|
59
|
+
The same file names inside `pages/<route>/` or `pages/<route>/_dir/` apply to that route and its children. They replace a `public/` image there.
|
|
60
|
+
An icon link in `app.head.link` with the same `rel` wins over the files.
|
|
61
|
+
Set `seo: { metaDataFiles: false }` to turn this off.
|
|
62
|
+
|
|
63
|
+
Generate icon files from one source image in `public/`:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pnpm exec nuxt-seo-utils icons --source logo.svg
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It writes `favicon.ico`, `apple-touch-icon.png`, and `icon-{16,32,192,512}` PNGs to `public/`. It needs `sharp`, which installs as an optional dependency.
|
|
70
|
+
|
|
71
|
+
## Site-wide and per-route meta
|
|
72
|
+
|
|
73
|
+
`seo.meta` takes `useSeoMeta()` input and renders on every page:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
export default defineNuxtConfig({
|
|
77
|
+
seo: { meta: { author: 'Acme Team', themeColor: '#18181b' } },
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`routeRules` accept `seoMeta` and `head`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
export default defineNuxtConfig({
|
|
85
|
+
routeRules: {
|
|
86
|
+
'/blog/**': { seoMeta: { author: 'Blog Team' }, head: { meta: [{ name: 'x-section', content: 'blog' }] } },
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Route rule tags override `seo.meta` and `app.head`. A page `useSeoMeta()` or `useHead()` call overrides them. The `tagPriority` option does not change this.
|
|
92
|
+
Route rule tags render on the server only. They do not update on client navigation.
|
|
93
|
+
|
|
94
|
+
## Breadcrumbs
|
|
95
|
+
|
|
96
|
+
`useBreadcrumbItems()` is auto-imported. It returns a ref of items from the route path, ready for Nuxt UI `<UBreadcrumb :items>`.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const items = useBreadcrumbItems({
|
|
100
|
+
overrides: [undefined, undefined, { label: post.value.title }], // index matches the path segment; false removes it
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- With `nuxt-schema-org` installed, it also adds a `BreadcrumbList` node. Without it, no JSON-LD renders and there is no warning.
|
|
105
|
+
- Labels come from `definePageMeta({ breadcrumb: { label } })`, then `title` in page meta, then the title-cased segment. The index route is `Home`.
|
|
106
|
+
- Call it in the page component, not the layout. The page has the data for the last items. Calls in parent components with the same `id` merge their options.
|
|
107
|
+
|
|
108
|
+
## Share links
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const share = useShareLinks({ title: post.value.title, utm: { source: 'auto', campaign: 'launch' } })
|
|
112
|
+
// share.value.twitter, .facebook, .linkedin, .whatsapp, .telegram, .reddit, .pinterest, .email, .canonicalUrl
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`title` defaults to `site.name`, not the page title. Pass it.
|
|
116
|
+
UTM defaults to `utm_source=<platform>` and `utm_medium=social` (or `email`). An object without `source: 'auto'` switches to manual mode: `{ campaign: 'launch' }` sends only `utm_campaign`.
|
|
117
|
+
|
|
118
|
+
## Traps
|
|
119
|
+
|
|
120
|
+
- **`htmlAttrs.lang` in `useHead()` or `app.head` is ignored.** The module sets `lang` from site config. Set `site.defaultLocale`, or use `@nuxtjs/i18n`.
|
|
121
|
+
- **Before nuxt-schema-org 6.4.1, the Schema.org WebPage URL keeps the route case.** `canonicalLowercase` lowercases the canonical, `og:url`, `useShareLinks()` URLs, and `BreadcrumbList` item URLs. nuxt-schema-org before 6.4.1 builds the WebPage `url` from the route path. There, `/blog/My-Post` gets a canonical of `/blog/my-post` and a WebPage URL of `/blog/My-Post`. From 6.4.1, the WebPage `url` follows the canonical. On an older version, upgrade nuxt-schema-org, use lowercase route paths, or set `canonicalLowercase: false`. Breadcrumb `to` links keep their case on purpose.
|
|
122
|
+
- **`definePageMeta({ title })` takes a static string only.** Use `useSeoMeta({ title })` for data.
|
|
123
|
+
|
|
124
|
+
## Version limits
|
|
125
|
+
|
|
126
|
+
- 8.5.2 and earlier: a page `ogImage` keeps the default image's dimensions and `twitter:image`, a page canonical leaves `og:url` on the route URL, a `pages/<route>/og-image.png` renders `og:image:alt` twice, and `public/opengraph-image.*` is ignored.
|
|
127
|
+
- 8.5.2 and earlier: `BreadcrumbList` item URLs keep the route case, and `useShareLinks()` lowercases without the site locale.
|
|
128
|
+
- 8.5.2 and earlier: `seo.meta` and `public/og-image.png` beat route rule `seoMeta` and `pages/**/og-image.png`. Put the route value in the page with `useSeoMeta()`.
|
|
129
|
+
- `treeShakeUseSeoMeta` needs Unhead v3. On Unhead v2 the module skips it with a warning.
|
|
130
|
+
- `automaticTwitterTags: false` (removes `twitter:card`) arrived in 8.5.0.
|
|
131
|
+
|
|
132
|
+
## Config
|
|
133
|
+
|
|
134
|
+
- `redirectToCanonicalSiteUrl` (`false`): in production, send a 301 from other hosts to the `site.url` host.
|
|
135
|
+
- `canonicalQueryWhitelist`: replaces the default list. It does not extend it.
|
|
136
|
+
- `minify` (`{ build: true, runtime: false }`): minify inline scripts and styles in `app.head` and prerendered HTML. `runtime: true` minifies on every SSR request.
|
|
137
|
+
- `automaticOgAndTwitterTags` (`true`): the `og:title`, `og:description`, and `twitter:card` inference.
|
|
138
|
+
- Other options: https://nuxtseo.com/docs/seo-utils/api/config
|
|
139
|
+
|
|
140
|
+
## Debug
|
|
141
|
+
|
|
142
|
+
- On `nuxt dev` and `nuxt build`, the module warns about conflicts in `app.head`, such as a static `lang` or an `og:site_name` that differs from `site.name`. Fix the site config, not the head. Set `validateAppHead: false` to hide the check.
|
|
143
|
+
- `/__nuxt-seo-utils/debug.json` in dev, or in production with `seo: { debug: true }`.
|
|
144
|
+
- Nuxt DevTools has an SEO Utils tab.
|