nuxt-seo-utils 8.6.1 → 9.0.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/README.md +11 -15
- package/dist/cli.mjs +1 -1
- package/dist/module.d.mts +55 -3
- package/dist/module.d.ts +55 -3
- package/dist/module.json +2 -2
- package/dist/module.mjs +520 -32
- package/dist/runtime/app/composables/polyfills.d.ts +2 -2
- package/dist/runtime/app/composables/polyfills.js +2 -2
- package/dist/runtime/app/index.d.ts +5 -0
- package/dist/runtime/app/index.js +3 -0
- package/dist/runtime/app/logic/syncDerivedTags.js +25 -15
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.js +1 -14
- package/dist/runtime/app/plugins/minifyScripts.server.js +1 -9
- package/dist/runtime/server/middleware/redirectCanonical.d.ts +1 -1
- package/dist/runtime/server/middleware/redirectCanonical.js +6 -6
- package/dist/runtime/server/middleware/redirects.d.ts +2 -0
- package/dist/runtime/server/middleware/redirects.js +21 -0
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.d.ts +1 -1
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +5 -6
- package/dist/runtime/server/plugins/minifyHtml.js +2 -9
- package/dist/runtime/server/routes/__nuxt-seo-utils/debug.d.ts +27 -1
- package/dist/runtime/server/routes/__nuxt-seo-utils/debug.js +2 -3
- package/dist/runtime/shared/minify.d.ts +0 -6
- package/dist/runtime/shared/minify.js +0 -60
- package/dist/runtime/shared/redirects.d.ts +40 -0
- package/dist/runtime/shared/redirects.js +59 -0
- package/dist/runtime/types.d.ts +15 -0
- package/dist/types.d.mts +1 -1
- package/package.json +42 -37
- package/skills/nuxt-seo-utils/SKILL.md +52 -6
package/dist/module.mjs
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import fs, { existsSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
4
|
+
import { useNuxt, addTypeTemplate, getLayerDirectories, useLogger, addServerHandler, hasNuxtModule, extendRouteRules, defineNuxtModule, createResolver, addPlugin, addImports, directoryToURL, addVitePlugin } from '@nuxt/kit';
|
|
4
5
|
import { unpackMeta } from '@unhead/vue/utils';
|
|
5
6
|
import { defu } from 'defu';
|
|
6
7
|
import { resolveModulePath } from 'exsolve';
|
|
7
|
-
import {
|
|
8
|
-
import { useModuleLogger, setupNitroRuntimeCompatibility,
|
|
9
|
-
import { resolve, extname, relative, dirname, basename } from 'pathe';
|
|
10
|
-
import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
|
|
8
|
+
import { useSiteConfig } from 'nuxt-site-config/kit';
|
|
9
|
+
import { useModuleLogger, setupNitroRuntimeCompatibility, setupRuntimeAliases, renderNitroTypeAugmentations } from 'nuxtseo-shared/kit';
|
|
10
|
+
import { resolve, extname, relative, dirname, basename, join } from 'pathe';
|
|
11
11
|
import { glob } from 'tinyglobby';
|
|
12
|
-
import { encodePath, joinURL } from 'ufo';
|
|
12
|
+
import { encodePath, joinURL, withoutTrailingSlash } from 'ufo';
|
|
13
13
|
import { c as classifyIconFilename, g as getIconRel, n as normalizeIconSizes, a as partitionColorModeIconLinks } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
|
|
14
14
|
import escapeRE from 'escape-string-regexp';
|
|
15
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
16
15
|
import { Buffer } from 'node:buffer';
|
|
17
16
|
import { TWITTER_IMAGE_FALLBACK_KEY } from '../dist/runtime/shared/derivedTagKeys.js';
|
|
18
|
-
import { JSON_TYPES
|
|
17
|
+
import { JSON_TYPES } from '../dist/runtime/shared/minify.js';
|
|
18
|
+
import { resolveI18nConfig } from 'nuxtseo-shared/i18n';
|
|
19
|
+
import { isAbsoluteUrl, parseSourcePattern, createRedirectMatcher, resolveTarget, appendSearch, REDIRECT_CACHE_CONTROL } from '../dist/runtime/shared/redirects.js';
|
|
20
|
+
import { createNitroRouteRuleMatcher } from 'nuxtseo-shared/server';
|
|
19
21
|
import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
|
|
20
22
|
|
|
21
23
|
function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
|
|
@@ -384,7 +386,7 @@ function addGeneratedRouteRule(nuxt, route, generatedRule) {
|
|
|
384
386
|
nitroRouteRules[route] = defu(nitroRouteRules[route], structuredClone(rule));
|
|
385
387
|
}
|
|
386
388
|
async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
387
|
-
const pagesDirs = nuxt.
|
|
389
|
+
const pagesDirs = getLayerDirectories(nuxt).map((layer) => layer.appPages).filter((dir) => fs.existsSync(dir));
|
|
388
390
|
const appendRouteRules = {};
|
|
389
391
|
const registeredFiles = /* @__PURE__ */ new Set();
|
|
390
392
|
const devMiddlewareMap = {};
|
|
@@ -499,7 +501,7 @@ function enrichConfiguredIcon(link, baseURL, iconEntries, diagnostics) {
|
|
|
499
501
|
};
|
|
500
502
|
}
|
|
501
503
|
async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
502
|
-
const publicDirs = nuxt.
|
|
504
|
+
const publicDirs = getLayerDirectories(nuxt).map((layer) => layer.public);
|
|
503
505
|
const seen = /* @__PURE__ */ new Set();
|
|
504
506
|
const fileEntries = [];
|
|
505
507
|
for (const dir of publicDirs) {
|
|
@@ -585,6 +587,7 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
|
585
587
|
return { hasIcons, diagnostics };
|
|
586
588
|
}
|
|
587
589
|
|
|
590
|
+
const JS_TYPES = /* @__PURE__ */ new Set(["", "module", "text/javascript", "application/javascript", "text/ecmascript", "application/ecmascript"]);
|
|
588
591
|
const logger = useLogger("nuxt-seo-utils");
|
|
589
592
|
async function minifyJSBuildTime(code) {
|
|
590
593
|
try {
|
|
@@ -633,7 +636,7 @@ function minifyStaticHead() {
|
|
|
633
636
|
if (typeof script === "string")
|
|
634
637
|
continue;
|
|
635
638
|
const content = String(script.innerHTML || script.textContent || "");
|
|
636
|
-
if (!content)
|
|
639
|
+
if (!content || script.src)
|
|
637
640
|
continue;
|
|
638
641
|
const setContent = (val) => {
|
|
639
642
|
if (script.innerHTML)
|
|
@@ -650,7 +653,7 @@ function minifyStaticHead() {
|
|
|
650
653
|
}
|
|
651
654
|
continue;
|
|
652
655
|
}
|
|
653
|
-
if (
|
|
656
|
+
if (!JS_TYPES.has(script.type?.toLowerCase().trim() ?? ""))
|
|
654
657
|
continue;
|
|
655
658
|
promises.push(minifyJSBuildTime(content).then((minified) => {
|
|
656
659
|
if (minified && minified.length < content.length)
|
|
@@ -680,8 +683,6 @@ function minifyStaticHead() {
|
|
|
680
683
|
}
|
|
681
684
|
|
|
682
685
|
function resolveUnheadVitePluginSource(input, resolvePath) {
|
|
683
|
-
if (input.unheadMajor < 3)
|
|
684
|
-
return { _tag: "unsupported-v2" };
|
|
685
686
|
const path = resolvePath("@unhead/vue/vite", {
|
|
686
687
|
try: true,
|
|
687
688
|
from: [...input.hostImportPaths, ...input.importPaths]
|
|
@@ -703,6 +704,394 @@ function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
|
|
|
703
704
|
nuxt.options.app.head = headConfig;
|
|
704
705
|
}
|
|
705
706
|
|
|
707
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
708
|
+
const MAX_HOPS = 10;
|
|
709
|
+
const DAY_MS = 864e5;
|
|
710
|
+
const MISSING_TARGET = "set `to` for a moved URL, or `statusCode: 410` for a gone URL";
|
|
711
|
+
function parseDate(value) {
|
|
712
|
+
if (typeof value !== "string" || !DATE_RE.test(value))
|
|
713
|
+
return void 0;
|
|
714
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
715
|
+
return Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value ? void 0 : date;
|
|
716
|
+
}
|
|
717
|
+
function parseEntry(source, input) {
|
|
718
|
+
const pattern = parseSourcePattern(source);
|
|
719
|
+
if (typeof pattern === "string")
|
|
720
|
+
return { issues: [{ _tag: "InvalidSource", level: "error", source, reason: pattern }] };
|
|
721
|
+
if (!input || typeof input !== "object")
|
|
722
|
+
return { issues: [{ _tag: "InvalidTarget", level: "error", source, to: input, reason: MISSING_TARGET }] };
|
|
723
|
+
const issues = [];
|
|
724
|
+
const addedAt = parseDate(input.addedAt);
|
|
725
|
+
if (!addedAt)
|
|
726
|
+
issues.push({ _tag: "InvalidDate", level: "warn", source, field: "addedAt", value: input.addedAt });
|
|
727
|
+
const reviewedAt = input.reviewedAt === void 0 ? void 0 : parseDate(input.reviewedAt);
|
|
728
|
+
if (input.reviewedAt !== void 0 && !reviewedAt)
|
|
729
|
+
issues.push({ _tag: "InvalidDate", level: "warn", source, field: "reviewedAt", value: input.reviewedAt });
|
|
730
|
+
const reviewFrom = reviewedAt && (!addedAt || reviewedAt > addedAt) ? reviewedAt : addedAt;
|
|
731
|
+
if (input.to === void 0) {
|
|
732
|
+
if (input.statusCode !== 410 && input.statusCode !== 404) {
|
|
733
|
+
issues.push(input.statusCode === void 0 ? { _tag: "InvalidTarget", level: "error", source, to: void 0, reason: MISSING_TARGET } : { _tag: "InvalidStatusCode", level: "error", source, statusCode: input.statusCode });
|
|
734
|
+
return { issues };
|
|
735
|
+
}
|
|
736
|
+
if (pattern._tag === "Prefix" && pattern.prefix === "") {
|
|
737
|
+
issues.push({ _tag: "InvalidSource", level: "error", source, reason: "a gone URL cannot match every path" });
|
|
738
|
+
return { issues };
|
|
739
|
+
}
|
|
740
|
+
return { entry: { _tag: "Gone", source, pattern, statusCode: input.statusCode, reviewFrom }, issues };
|
|
741
|
+
}
|
|
742
|
+
const to = input.to;
|
|
743
|
+
if (typeof to !== "string" || to.startsWith("//") || !(to.startsWith("/") || isAbsoluteUrl(to))) {
|
|
744
|
+
issues.push({ _tag: "InvalidTarget", level: "error", source, to, reason: "use a path that starts with `/`, or an absolute `https://` URL" });
|
|
745
|
+
return { issues };
|
|
746
|
+
}
|
|
747
|
+
if (to.includes("*") && !(to.endsWith("/**") && !to.slice(0, -3).includes("*"))) {
|
|
748
|
+
issues.push({ _tag: "InvalidTarget", level: "error", source, to, reason: "only a trailing `/**` is supported" });
|
|
749
|
+
return { issues };
|
|
750
|
+
}
|
|
751
|
+
if (to.endsWith("/**") && pattern._tag !== "Prefix") {
|
|
752
|
+
issues.push({ _tag: "InvalidTarget", level: "error", source, to, reason: "a target that ends in `/**` needs a source that ends in `/**`" });
|
|
753
|
+
return { issues };
|
|
754
|
+
}
|
|
755
|
+
const statusCode = input.statusCode ?? 301;
|
|
756
|
+
if (statusCode !== 301 && statusCode !== 308) {
|
|
757
|
+
issues.push({ _tag: "InvalidStatusCode", level: "error", source, statusCode });
|
|
758
|
+
return { issues };
|
|
759
|
+
}
|
|
760
|
+
return { entry: { _tag: "Moved", source, pattern, to, statusCode, reviewFrom }, issues };
|
|
761
|
+
}
|
|
762
|
+
function targetPath(to) {
|
|
763
|
+
return isAbsoluteUrl(to) ? void 0 : to.split(/[?#]/)[0];
|
|
764
|
+
}
|
|
765
|
+
function withSuffix(target, suffix) {
|
|
766
|
+
const hashIndex = suffix.indexOf("#");
|
|
767
|
+
const search = hashIndex === -1 ? suffix : suffix.slice(0, hashIndex);
|
|
768
|
+
const hash = hashIndex === -1 ? "" : suffix.slice(hashIndex);
|
|
769
|
+
return appendSearch(target, search) + (target.includes("#") ? "" : hash);
|
|
770
|
+
}
|
|
771
|
+
function flattenChains(entries) {
|
|
772
|
+
const match = createRedirectMatcher(entries);
|
|
773
|
+
const issues = [];
|
|
774
|
+
const loops = /* @__PURE__ */ new Set();
|
|
775
|
+
const resolved = entries.map((entry) => {
|
|
776
|
+
if (entry._tag !== "Moved")
|
|
777
|
+
return entry;
|
|
778
|
+
let to = entry.to;
|
|
779
|
+
const visited = [entry.source];
|
|
780
|
+
const hops = [entry.source, entry.to];
|
|
781
|
+
for (let i = 0; i < MAX_HOPS; i++) {
|
|
782
|
+
const path = targetPath(to);
|
|
783
|
+
if (path === void 0)
|
|
784
|
+
break;
|
|
785
|
+
const wildcard = to.endsWith("/**");
|
|
786
|
+
const lookup = wildcard ? path.slice(0, -3) || "/" : path;
|
|
787
|
+
const next = match(lookup);
|
|
788
|
+
if (!next || wildcard && next.pattern._tag !== "Prefix")
|
|
789
|
+
break;
|
|
790
|
+
if (visited.includes(next.source)) {
|
|
791
|
+
const key = [...new Set(visited)].sort().join("\n");
|
|
792
|
+
if (!loops.has(key)) {
|
|
793
|
+
loops.add(key);
|
|
794
|
+
issues.push({ _tag: "RedirectLoop", level: "error", sources: [...visited, next.source] });
|
|
795
|
+
}
|
|
796
|
+
return entry;
|
|
797
|
+
}
|
|
798
|
+
visited.push(next.source);
|
|
799
|
+
if (next._tag === "Gone") {
|
|
800
|
+
issues.push({ _tag: "TargetGone", level: "warn", source: entry.source, to, statusCode: next.statusCode });
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
if (wildcard)
|
|
804
|
+
to = next.to.endsWith("/**") ? `${resolveTarget(next, lookup).replace(/\/$/, "")}/**` : next.to;
|
|
805
|
+
else
|
|
806
|
+
to = withSuffix(resolveTarget(next, path), to.slice(path.length));
|
|
807
|
+
hops.push(to);
|
|
808
|
+
}
|
|
809
|
+
if (to === entry.to)
|
|
810
|
+
return entry;
|
|
811
|
+
issues.push({ _tag: "ChainFlattened", level: "info", source: entry.source, hops });
|
|
812
|
+
return { ...entry, to };
|
|
813
|
+
});
|
|
814
|
+
return { entries: resolved, issues };
|
|
815
|
+
}
|
|
816
|
+
function findDueForReview(entries, { now, reviewAfterDays }) {
|
|
817
|
+
if (reviewAfterDays === false)
|
|
818
|
+
return [];
|
|
819
|
+
return entries.flatMap((entry) => {
|
|
820
|
+
if (!entry.reviewFrom)
|
|
821
|
+
return [];
|
|
822
|
+
const days = Math.floor((now.getTime() - entry.reviewFrom.getTime()) / DAY_MS);
|
|
823
|
+
if (days < reviewAfterDays)
|
|
824
|
+
return [];
|
|
825
|
+
return [{
|
|
826
|
+
_tag: "DueForReview",
|
|
827
|
+
level: "warn",
|
|
828
|
+
source: entry.source,
|
|
829
|
+
target: entry._tag === "Moved" ? entry.to : String(entry.statusCode),
|
|
830
|
+
since: entry.reviewFrom.toISOString().slice(0, 10),
|
|
831
|
+
days
|
|
832
|
+
}];
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function planRedirects(input, options) {
|
|
836
|
+
const parsed = Object.entries(input || {}).map(([source, value]) => parseEntry(source, value));
|
|
837
|
+
const flattened = flattenChains(parsed.flatMap((p) => p.entry ? [p.entry] : []));
|
|
838
|
+
return {
|
|
839
|
+
entries: flattened.entries,
|
|
840
|
+
issues: [...parsed.flatMap((p) => p.issues), ...flattened.issues, ...findDueForReview(flattened.entries, options)]
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function resolveLocalePrefixes(i18n) {
|
|
844
|
+
if (!i18n.strategy || i18n.strategy === "no_prefix")
|
|
845
|
+
return [];
|
|
846
|
+
return i18n.locales.filter((locale) => i18n.strategy !== "prefix_except_default" || locale.code !== i18n.defaultLocale).map((locale) => `/${locale.code}`);
|
|
847
|
+
}
|
|
848
|
+
function withPrefix(prefix, pattern) {
|
|
849
|
+
return pattern._tag === "Exact" ? { _tag: "Exact", path: pattern.path === "/" ? prefix : `${prefix}${pattern.path}` } : { _tag: "Prefix", prefix: `${prefix}${pattern.prefix}` };
|
|
850
|
+
}
|
|
851
|
+
function sourceKey$2(pattern) {
|
|
852
|
+
return pattern._tag === "Exact" ? pattern.path : `${pattern.prefix}/**`;
|
|
853
|
+
}
|
|
854
|
+
function expandLocalePrefixes(entries, prefixes) {
|
|
855
|
+
if (!prefixes.length)
|
|
856
|
+
return entries;
|
|
857
|
+
const taken = new Set(entries.map((entry) => sourceKey$2(entry.pattern)));
|
|
858
|
+
const isPrefixed = (key) => prefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}/`));
|
|
859
|
+
const expanded = entries.flatMap((entry) => isPrefixed(sourceKey$2(entry.pattern)) ? [] : prefixes.flatMap((prefix) => {
|
|
860
|
+
const pattern = withPrefix(prefix, entry.pattern);
|
|
861
|
+
const key = sourceKey$2(pattern);
|
|
862
|
+
if (taken.has(key))
|
|
863
|
+
return [];
|
|
864
|
+
taken.add(key);
|
|
865
|
+
if (entry._tag === "Gone")
|
|
866
|
+
return [{ ...entry, pattern }];
|
|
867
|
+
return [{ ...entry, pattern, to: isAbsoluteUrl(entry.to) ? entry.to : joinURL(prefix, entry.to) }];
|
|
868
|
+
}));
|
|
869
|
+
return [...entries, ...expanded];
|
|
870
|
+
}
|
|
871
|
+
function toRedirectRules(entries) {
|
|
872
|
+
return entries.map((entry) => entry._tag === "Moved" ? { _tag: "Moved", pattern: entry.pattern, to: entry.to, statusCode: entry.statusCode } : { _tag: "Gone", pattern: entry.pattern, statusCode: entry.statusCode });
|
|
873
|
+
}
|
|
874
|
+
function formatRedirectIssue(issue) {
|
|
875
|
+
switch (issue._tag) {
|
|
876
|
+
case "InvalidSource":
|
|
877
|
+
return `\`${issue.source}\` is not a valid source: ${issue.reason}.`;
|
|
878
|
+
case "InvalidTarget":
|
|
879
|
+
return `\`${issue.source}\` has an invalid target ${JSON.stringify(issue.to)}: ${issue.reason}.`;
|
|
880
|
+
case "InvalidStatusCode":
|
|
881
|
+
return `\`${issue.source}\` has status code ${JSON.stringify(issue.statusCode)}. Use 301 or 308 with \`to\`, and 410 or 404 without it.`;
|
|
882
|
+
case "RedirectLoop":
|
|
883
|
+
return `Redirect loop: ${issue.sources.join(" \u2192 ")}. Remove one of these entries.`;
|
|
884
|
+
case "InvalidDate":
|
|
885
|
+
return `\`${issue.source}\` has ${issue.field} ${JSON.stringify(issue.value)}. Use a date such as '2026-10-13'. The entry still works, but the review check skips it.`;
|
|
886
|
+
case "TargetGone":
|
|
887
|
+
return `\`${issue.source}\` redirects to \`${issue.to}\`, which returns ${issue.statusCode}. Pick a live target, or make \`${issue.source}\` a gone URL.`;
|
|
888
|
+
case "DueForReview":
|
|
889
|
+
return `\`${issue.source}\` \u2192 ${issue.target} (since ${issue.since}, ${issue.days} days)`;
|
|
890
|
+
case "ChainFlattened":
|
|
891
|
+
return `${issue.hops.join(" \u2192 ")}. \`${issue.source}\` now redirects to \`${issue.hops.at(-1)}\` in one hop.`;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function formatRedirectReport(issues, reviewAfterDays) {
|
|
895
|
+
const lines = (level, exclude) => issues.filter((issue) => issue.level === level && issue._tag !== exclude).map((issue) => ` - ${formatRedirectIssue(issue)}`);
|
|
896
|
+
const errors = lines("error");
|
|
897
|
+
const warnings = lines("warn", "DueForReview");
|
|
898
|
+
const due = issues.filter((issue) => issue._tag === "DueForReview").map((issue) => ` - ${formatRedirectIssue(issue)}`);
|
|
899
|
+
const flattened = lines("info");
|
|
900
|
+
const report = {};
|
|
901
|
+
if (errors.length)
|
|
902
|
+
report.error = `Invalid \`seo.redirects\` entries:
|
|
903
|
+
${errors.join("\n")}`;
|
|
904
|
+
const warn = [
|
|
905
|
+
...warnings.length ? [`Check these \`seo.redirects\` entries:
|
|
906
|
+
${warnings.join("\n")}`] : [],
|
|
907
|
+
...due.length ? [[
|
|
908
|
+
`${due.length} \`seo.redirects\` ${due.length === 1 ? "entry is" : "entries are"} due for review (older than ${reviewAfterDays} days):`,
|
|
909
|
+
...due,
|
|
910
|
+
" If an old URL still gets visits or backlinks, keep it and set `reviewedAt` to today.",
|
|
911
|
+
" If it gets none, you can delete the entry."
|
|
912
|
+
].join("\n")] : []
|
|
913
|
+
];
|
|
914
|
+
if (warn.length)
|
|
915
|
+
report.warn = warn.join("\n");
|
|
916
|
+
if (flattened.length)
|
|
917
|
+
report.info = `Flattened \`seo.redirects\` chains:
|
|
918
|
+
${flattened.join("\n")}`;
|
|
919
|
+
return report;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const ERROR_PAGE = "/404.html";
|
|
923
|
+
const HOST_NAMES = {
|
|
924
|
+
Netlify: "Netlify",
|
|
925
|
+
CloudflarePages: "Cloudflare Pages",
|
|
926
|
+
Vercel: "Vercel"
|
|
927
|
+
};
|
|
928
|
+
function isRootBase(baseURL) {
|
|
929
|
+
return (withoutTrailingSlash(baseURL) || "/") === "/";
|
|
930
|
+
}
|
|
931
|
+
function resolveStaticHost(preset, baseURL) {
|
|
932
|
+
switch (preset) {
|
|
933
|
+
case "netlify-static":
|
|
934
|
+
return isRootBase(baseURL) ? { _tag: "Netlify" } : { _tag: "MetaRefresh", reason: "Nitro writes the Netlify `_redirects` file inside `app.baseURL`, and Netlify does not read it there." };
|
|
935
|
+
case "cloudflare-pages-static":
|
|
936
|
+
return { _tag: "CloudflarePages" };
|
|
937
|
+
case "vercel-static":
|
|
938
|
+
return { _tag: "Vercel" };
|
|
939
|
+
default:
|
|
940
|
+
return { _tag: "MetaRefresh", reason: `The \`${preset}\` preset writes no host redirect file.` };
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function sourceKey$1(rule) {
|
|
944
|
+
return rule.pattern._tag === "Exact" ? rule.pattern.path : `${rule.pattern.prefix}/**`;
|
|
945
|
+
}
|
|
946
|
+
function withPrefixPaths(rules) {
|
|
947
|
+
const exact = new Set(rules.flatMap((rule) => rule.pattern._tag === "Exact" ? [rule.pattern.path] : []));
|
|
948
|
+
const prefixPaths = rules.flatMap((rule) => {
|
|
949
|
+
if (rule.pattern._tag !== "Prefix")
|
|
950
|
+
return [];
|
|
951
|
+
const path = rule.pattern.prefix || "/";
|
|
952
|
+
if (exact.has(path))
|
|
953
|
+
return [];
|
|
954
|
+
exact.add(path);
|
|
955
|
+
const pattern = { _tag: "Exact", path };
|
|
956
|
+
return [rule._tag === "Moved" ? { ...rule, pattern, to: resolveTarget(rule, path) } : { ...rule, pattern }];
|
|
957
|
+
});
|
|
958
|
+
const prefixLength = (rule) => rule.pattern._tag === "Prefix" ? rule.pattern.prefix.length : Infinity;
|
|
959
|
+
return [...rules, ...prefixPaths].sort((a, b) => prefixLength(b) - prefixLength(a));
|
|
960
|
+
}
|
|
961
|
+
function toSplat(to) {
|
|
962
|
+
return to.endsWith("/**") ? `${to.slice(0, -3)}/:splat` : to;
|
|
963
|
+
}
|
|
964
|
+
function trimSlash(path) {
|
|
965
|
+
return path === "/" ? "" : withoutTrailingSlash(path);
|
|
966
|
+
}
|
|
967
|
+
function escapeRegExp(value) {
|
|
968
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
969
|
+
}
|
|
970
|
+
function vercelRoute(rule, baseURL) {
|
|
971
|
+
const path = trimSlash(joinURL(baseURL, rule.pattern._tag === "Exact" ? rule.pattern.path : rule.pattern.prefix || "/"));
|
|
972
|
+
const src = rule.pattern._tag === "Prefix" ? `^${escapeRegExp(path)}/(.*)$` : path ? `^${escapeRegExp(path)}/?$` : "^/$";
|
|
973
|
+
if (rule._tag === "Gone")
|
|
974
|
+
return { src, status: rule.statusCode, dest: joinURL(baseURL, ERROR_PAGE) };
|
|
975
|
+
const wildcard = rule.to.endsWith("/**");
|
|
976
|
+
const target = wildcard ? rule.to.slice(0, -3) || "/" : rule.to;
|
|
977
|
+
const location = isAbsoluteUrl(target) ? target : joinURL(baseURL, target);
|
|
978
|
+
return {
|
|
979
|
+
src,
|
|
980
|
+
status: rule.statusCode,
|
|
981
|
+
headers: {
|
|
982
|
+
"Location": wildcard ? `${trimSlash(location)}/$1` : location,
|
|
983
|
+
"cache-control": REDIRECT_CACHE_CONTROL
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function nativeRedirect(to, statusCode) {
|
|
988
|
+
return { to, statusCode, status: statusCode };
|
|
989
|
+
}
|
|
990
|
+
function planNativeRedirects(rules, host, ctx) {
|
|
991
|
+
const servesGone = host._tag !== "CloudflarePages" && ctx.errorPage;
|
|
992
|
+
const served = withPrefixPaths(rules.filter((rule) => rule._tag === "Moved" || servesGone));
|
|
993
|
+
const gone = rules.filter((rule) => rule._tag === "Gone");
|
|
994
|
+
const unservedGone = servesGone ? [] : gone.filter((rule) => host._tag !== "CloudflarePages" || rule.statusCode === 410);
|
|
995
|
+
const unserved = unservedGone.length ? { _tag: host._tag === "CloudflarePages" ? "NoGoneStatus" : "NoErrorPage", sources: unservedGone.map(sourceKey$1) } : void 0;
|
|
996
|
+
if (host._tag === "Vercel")
|
|
997
|
+
return { output: { _tag: "VercelRoutes", routes: served.map((rule) => vercelRoute(rule, ctx.baseURL)) }, unserved };
|
|
998
|
+
const redirects = served.map((rule) => [
|
|
999
|
+
sourceKey$1(rule),
|
|
1000
|
+
rule._tag === "Moved" ? nativeRedirect(toSplat(rule.to), rule.statusCode) : nativeRedirect(ERROR_PAGE, rule.statusCode)
|
|
1001
|
+
]);
|
|
1002
|
+
return { output: { _tag: "RouteRules", redirects: redirects.reverse() }, unserved };
|
|
1003
|
+
}
|
|
1004
|
+
function nativeSourceKeys(rules) {
|
|
1005
|
+
return [...new Set(withPrefixPaths(rules).map(sourceKey$1))];
|
|
1006
|
+
}
|
|
1007
|
+
function formatUnservedGone(host, unserved) {
|
|
1008
|
+
const name = HOST_NAMES[host._tag];
|
|
1009
|
+
const intro = unserved._tag === "NoGoneStatus" ? `${name} cannot send 410 from \`_redirects\`. These gone URLs return the host 404 page instead:` : `The build has no \`404.html\` page, so ${name} cannot serve these gone URLs. They return the host 404 page:`;
|
|
1010
|
+
return [intro, ...unserved.sources.map((source) => ` - ${source}`)].join("\n");
|
|
1011
|
+
}
|
|
1012
|
+
function formatMetaRefreshFallback(reason, unserved) {
|
|
1013
|
+
return [
|
|
1014
|
+
`${reason} So this static build cannot send a redirect or 410 status.`,
|
|
1015
|
+
"Each exact moved URL gets an HTML page with an instant meta refresh.",
|
|
1016
|
+
...unserved.length ? ["These entries need host redirect rules, or they return the host 404 page:", ...unserved.map((source) => ` - ${source}`)] : []
|
|
1017
|
+
].join("\n");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function sourceKey(rule) {
|
|
1021
|
+
return rule.pattern._tag === "Exact" ? rule.pattern.path : `${rule.pattern.prefix}/**`;
|
|
1022
|
+
}
|
|
1023
|
+
function setupRedirects(redirects, reviewAfterDays, ctx) {
|
|
1024
|
+
const { nuxt, logger } = ctx;
|
|
1025
|
+
if (!redirects || !Object.keys(redirects).length || nuxt.options._prepare)
|
|
1026
|
+
return;
|
|
1027
|
+
const plan = planRedirects(redirects, { now: ctx.now, reviewAfterDays });
|
|
1028
|
+
const report = formatRedirectReport(plan.issues, reviewAfterDays);
|
|
1029
|
+
if (report.error)
|
|
1030
|
+
throw new Error(`[nuxt-seo-utils] ${report.error}`);
|
|
1031
|
+
if (report.warn)
|
|
1032
|
+
logger.warn(report.warn);
|
|
1033
|
+
if (report.info)
|
|
1034
|
+
logger.info(report.info);
|
|
1035
|
+
if (!plan.entries.length)
|
|
1036
|
+
return;
|
|
1037
|
+
addServerHandler({ middleware: true, handler: ctx.middleware });
|
|
1038
|
+
let rules = toRedirectRules(plan.entries);
|
|
1039
|
+
nuxt.hook("modules:done", async () => {
|
|
1040
|
+
const i18n = ctx.hasI18n ? await resolveI18nConfig(logger) : false;
|
|
1041
|
+
if (i18n && !i18n.differentDomains && !i18n.multiDomainLocales)
|
|
1042
|
+
rules = expandLocalePrefixes(rules, resolveLocalePrefixes(i18n));
|
|
1043
|
+
nuxt.options.runtimeConfig["seo-utils-redirects"] = rules;
|
|
1044
|
+
if (hasNuxtModule("@nuxtjs/sitemap", nuxt)) {
|
|
1045
|
+
for (const rule of rules)
|
|
1046
|
+
extendRouteRules(sourceKey(rule), { sitemap: false });
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
nuxt.hook("nitro:init", (nitro) => {
|
|
1050
|
+
const { baseURL, routeRules } = nitro.options;
|
|
1051
|
+
const setPrerender = (keys, prerender) => {
|
|
1052
|
+
for (const key of keys)
|
|
1053
|
+
routeRules[key] = defu(routeRules[key], { prerender });
|
|
1054
|
+
};
|
|
1055
|
+
const blockPrerender = (keys) => setPrerender(
|
|
1056
|
+
(withoutTrailingSlash(baseURL) || "/") === "/" ? keys : keys.flatMap((key) => [key, joinURL(baseURL, key)]),
|
|
1057
|
+
false
|
|
1058
|
+
);
|
|
1059
|
+
if (!nitro.options.static) {
|
|
1060
|
+
blockPrerender(rules.map(sourceKey));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const host = resolveStaticHost(nitro.options.preset, baseURL);
|
|
1064
|
+
if (host._tag === "MetaRefresh") {
|
|
1065
|
+
const pages = rules.filter((rule) => rule._tag === "Moved" && rule.pattern._tag === "Exact");
|
|
1066
|
+
const unserved = rules.filter((rule) => !pages.includes(rule)).map(sourceKey);
|
|
1067
|
+
blockPrerender(rules.filter((rule) => rule._tag === "Gone").map(sourceKey));
|
|
1068
|
+
setPrerender(pages.map(sourceKey), true);
|
|
1069
|
+
if (unserved.length)
|
|
1070
|
+
logger.warn(formatMetaRefreshFallback(host.reason, unserved));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
blockPrerender(nativeSourceKeys(rules));
|
|
1074
|
+
nitro.hooks.hook("prerender:done", () => {
|
|
1075
|
+
const plan2 = planNativeRedirects(rules, host, {
|
|
1076
|
+
baseURL,
|
|
1077
|
+
errorPage: existsSync(join(nitro.options.output.publicDir, "404.html"))
|
|
1078
|
+
});
|
|
1079
|
+
if (plan2.output._tag === "VercelRoutes") {
|
|
1080
|
+
const config = (nitro.options.vercel ||= { config: { version: 3 } }).config ||= { version: 3 };
|
|
1081
|
+
config.routes = [...config.routes || [], ...plan2.output.routes];
|
|
1082
|
+
} else {
|
|
1083
|
+
for (const [key, redirect] of plan2.output.redirects) {
|
|
1084
|
+
const existing = nitro.options.routeRules[key];
|
|
1085
|
+
delete nitro.options.routeRules[key];
|
|
1086
|
+
nitro.options.routeRules[key] = defu(existing, { redirect });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (plan2.unserved)
|
|
1090
|
+
logger.warn(formatUnservedGone(host, plan2.unserved));
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
706
1095
|
const NUXT_DEFAULT_VIEWPORT = "width=device-width, initial-scale=1";
|
|
707
1096
|
const UNDERSCORE_RE = /_/g;
|
|
708
1097
|
const HYPHEN_RE = /-/g;
|
|
@@ -946,6 +1335,66 @@ function collectUserAppHead(nuxt) {
|
|
|
946
1335
|
return head;
|
|
947
1336
|
}
|
|
948
1337
|
|
|
1338
|
+
const NOINDEX = /(?:^|,)\s*(?:noindex|none)\s*(?:,|$)/i;
|
|
1339
|
+
const NON_PAGE_PATH = /^\/(?:api|_nuxt|_nitro|__nuxt|\.well-known)(?:\/|$)/;
|
|
1340
|
+
function isIndexable(robots) {
|
|
1341
|
+
if (robots === false)
|
|
1342
|
+
return false;
|
|
1343
|
+
if (typeof robots === "string")
|
|
1344
|
+
return !NOINDEX.test(robots);
|
|
1345
|
+
if (robots && typeof robots === "object") {
|
|
1346
|
+
if (typeof robots.indexable === "boolean")
|
|
1347
|
+
return robots.indexable;
|
|
1348
|
+
return !robots.noindex && !robots.none && !(robots.rule && NOINDEX.test(robots.rule));
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function validateRendering(context) {
|
|
1353
|
+
if (context.indexable === false)
|
|
1354
|
+
return [];
|
|
1355
|
+
const routeRules = context.routeRules || {};
|
|
1356
|
+
const match = createNitroRouteRuleMatcher({ nitro: { routeRules } });
|
|
1357
|
+
const candidates = [
|
|
1358
|
+
...context.ssr === false ? ["/**"] : [],
|
|
1359
|
+
...Object.keys(routeRules)
|
|
1360
|
+
];
|
|
1361
|
+
const diagnostics = [];
|
|
1362
|
+
for (const path of new Set(candidates)) {
|
|
1363
|
+
const rule = match(path);
|
|
1364
|
+
const contentType = Object.entries(rule.headers || {}).find(([name]) => name.toLowerCase() === "content-type")?.[1];
|
|
1365
|
+
if (context.ssr !== false && rule.ssr !== false || !isIndexable(rule.robots) || NON_PAGE_PATH.test(path) || rule.redirect || rule.proxy || typeof contentType === "string" && !/^text\/html(?:;|$)/i.test(contentType)) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (diagnostics.some((diagnostic) => diagnostic.path === "/**"))
|
|
1369
|
+
continue;
|
|
1370
|
+
diagnostics.push({ _tag: "ClientRendering", level: "warn", path });
|
|
1371
|
+
}
|
|
1372
|
+
return diagnostics;
|
|
1373
|
+
}
|
|
1374
|
+
function formatRenderingDiagnostic(diagnostic) {
|
|
1375
|
+
const path = JSON.stringify(diagnostic.path);
|
|
1376
|
+
return [
|
|
1377
|
+
`SSR is disabled for indexable pages matching \`${diagnostic.path}\`.`,
|
|
1378
|
+
"Crawlers and agents that skip JavaScript may miss their page content.",
|
|
1379
|
+
"For public pages, enable global SSR and restore SSR on the route:",
|
|
1380
|
+
"```ts [nuxt.config.ts]",
|
|
1381
|
+
"export default defineNuxtConfig({",
|
|
1382
|
+
" ssr: true,",
|
|
1383
|
+
` routeRules: { ${path}: { ssr: true } },`,
|
|
1384
|
+
"})",
|
|
1385
|
+
"```",
|
|
1386
|
+
"For private app pages, keep their rendering rule and disable indexing with Nuxt Robots:",
|
|
1387
|
+
"```ts [nuxt.config.ts]",
|
|
1388
|
+
"export default defineNuxtConfig({",
|
|
1389
|
+
" modules: ['@nuxtjs/robots'],",
|
|
1390
|
+
` routeRules: { ${path}: { robots: false } },`,
|
|
1391
|
+
"})",
|
|
1392
|
+
"```",
|
|
1393
|
+
"Requires `@nuxtjs/robots`. Install it with `pnpm add @nuxtjs/robots`.",
|
|
1394
|
+
"Merge the chosen example into your existing Nuxt config."
|
|
1395
|
+
].join("\n");
|
|
1396
|
+
}
|
|
1397
|
+
|
|
949
1398
|
function setupDevToolsUI(resolve, nuxt = useNuxt()) {
|
|
950
1399
|
setupDevToolsUI$1({
|
|
951
1400
|
route: "/__nuxt-seo-utils",
|
|
@@ -960,7 +1409,7 @@ const module$1 = defineNuxtModule({
|
|
|
960
1409
|
name: "nuxt-seo-utils",
|
|
961
1410
|
configKey: "seo",
|
|
962
1411
|
compatibility: {
|
|
963
|
-
nuxt: "
|
|
1412
|
+
nuxt: "^4.6.0 || ^5.0.0-0"
|
|
964
1413
|
}
|
|
965
1414
|
},
|
|
966
1415
|
moduleDependencies: {
|
|
@@ -972,14 +1421,17 @@ const module$1 = defineNuxtModule({
|
|
|
972
1421
|
version: ">=1",
|
|
973
1422
|
optional: true
|
|
974
1423
|
},
|
|
975
|
-
|
|
976
|
-
|
|
1424
|
+
// Keep package-relative resolution until the minimum Nuxt version includes this fix:
|
|
1425
|
+
// https://github.com/nuxt/nuxt/pull/36542
|
|
1426
|
+
[fileURLToPath(import.meta.resolve("nuxt-site-config"))]: {
|
|
1427
|
+
version: ">=5.0.0"
|
|
977
1428
|
}
|
|
978
1429
|
},
|
|
979
1430
|
defaults: {
|
|
980
1431
|
enabled: true,
|
|
981
1432
|
debug: false,
|
|
982
1433
|
redirectToCanonicalSiteUrl: false,
|
|
1434
|
+
redirectReviewAfterDays: 365,
|
|
983
1435
|
automaticDefaults: true,
|
|
984
1436
|
fallbackTitle: true,
|
|
985
1437
|
metaDataFiles: true,
|
|
@@ -1004,9 +1456,8 @@ const module$1 = defineNuxtModule({
|
|
|
1004
1456
|
return;
|
|
1005
1457
|
}
|
|
1006
1458
|
const nitroCompatibility = setupNitroRuntimeCompatibility(nuxt);
|
|
1007
|
-
const { resolve } = createResolver(import.meta.url);
|
|
1008
|
-
const { version } = await
|
|
1009
|
-
await installNuxtSiteConfig();
|
|
1459
|
+
const { resolve, resolvePath } = createResolver(import.meta.url);
|
|
1460
|
+
const { version } = JSON.parse(await readFile(resolve("../package.json"), "utf8"));
|
|
1010
1461
|
const runtimeDir = resolve("./runtime");
|
|
1011
1462
|
if (config.metaDataFiles) {
|
|
1012
1463
|
const { hasIcons, diagnostics } = await generateTagsFromPublicFiles();
|
|
@@ -1079,7 +1530,7 @@ const module$1 = defineNuxtModule({
|
|
|
1079
1530
|
});
|
|
1080
1531
|
const polyfills = [
|
|
1081
1532
|
// @ts-expect-error runtime type
|
|
1082
|
-
...!hasNuxtModule("nuxt-schema-org") || nuxt.options.schemaOrg?.
|
|
1533
|
+
...!hasNuxtModule("nuxt-schema-org") || nuxt.options.schemaOrg?.enabled === false ? [
|
|
1083
1534
|
"useSchemaOrg",
|
|
1084
1535
|
"defineWebSite",
|
|
1085
1536
|
"defineWebPage",
|
|
@@ -1100,8 +1551,15 @@ const module$1 = defineNuxtModule({
|
|
|
1100
1551
|
middleware: true
|
|
1101
1552
|
});
|
|
1102
1553
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1554
|
+
setupRedirects(config.redirects, config.redirectReviewAfterDays ?? 365, {
|
|
1555
|
+
nuxt,
|
|
1556
|
+
logger,
|
|
1557
|
+
middleware: resolve(runtimeDir, "./server/middleware/redirects"),
|
|
1558
|
+
now: /* @__PURE__ */ new Date(),
|
|
1559
|
+
hasI18n
|
|
1560
|
+
});
|
|
1561
|
+
setupRuntimeAliases({ namespace: "#seo-utils", app: resolve(runtimeDir, "./app") }, nuxt);
|
|
1562
|
+
const hasSchemaOrg = hasNuxtModule("nuxt-schema-org") && nuxt.options.schemaOrg?.enabled !== false;
|
|
1105
1563
|
nuxt.options.alias["#seo-utils-schema"] = hasSchemaOrg ? resolve(runtimeDir, "./app/composables/schemaOrg") : resolve(runtimeDir, "./app/composables/polyfills");
|
|
1106
1564
|
nuxt.options.runtimeConfig.public["nuxt-seo-utils-version"] = nuxt.options.runtimeConfig.public["nuxt-seo-utils-version"] || version || "";
|
|
1107
1565
|
nuxt.options.runtimeConfig.public["seo-utils"] = defu(nuxt.options.runtimeConfig.public["seo-utils"] || {}, {
|
|
@@ -1176,17 +1634,15 @@ export {}
|
|
|
1176
1634
|
{ vite: config.treeShakeUseSeoMeta ? { validate: !nuxt.options.test } : false }
|
|
1177
1635
|
);
|
|
1178
1636
|
const viteOpts = nuxt.options.unhead.vite;
|
|
1179
|
-
const nuxtRegistersUnheadVite = nuxt.options.future?.compatibilityVersion >= 5 && nuxt.options.builder === "@nuxt/vite-builder"
|
|
1637
|
+
const nuxtRegistersUnheadVite = nuxt.options.future?.compatibilityVersion >= 5 && nuxt.options.builder === "@nuxt/vite-builder";
|
|
1180
1638
|
if (!nuxtRegistersUnheadVite && viteOpts !== false) {
|
|
1181
|
-
const unheadMajor = await resolveHostUnheadMajor(nuxt.options.rootDir);
|
|
1182
1639
|
const importPaths = nuxt.options.modulesDir.map((d) => directoryToURL(d));
|
|
1183
1640
|
const rolldownPath = resolveModulePath("rolldown/experimental", { try: true, from: importPaths });
|
|
1184
1641
|
const lightningcssPath = resolveModulePath("lightningcss", { try: true, from: importPaths });
|
|
1185
1642
|
const rolldownURL = rolldownPath ? pathToFileURL(rolldownPath).href : void 0;
|
|
1186
1643
|
const lightningcssURL = lightningcssPath ? pathToFileURL(lightningcssPath).href : void 0;
|
|
1187
|
-
const hostImportPaths =
|
|
1644
|
+
const hostImportPaths = [directoryToURL(dirname(resolveModulePath("nuxt/package.json", { from: importPaths })))];
|
|
1188
1645
|
const vitePluginSource = resolveUnheadVitePluginSource({
|
|
1189
|
-
unheadMajor,
|
|
1190
1646
|
hostImportPaths,
|
|
1191
1647
|
importPaths
|
|
1192
1648
|
}, resolveModulePath);
|
|
@@ -1204,9 +1660,7 @@ export {}
|
|
|
1204
1660
|
}).code).trim();
|
|
1205
1661
|
} : void 0
|
|
1206
1662
|
};
|
|
1207
|
-
if (vitePluginSource._tag === "
|
|
1208
|
-
logger.warn("`treeShakeUseSeoMeta` requires Unhead v3; skipping the transform on Unhead v2.");
|
|
1209
|
-
} else if (vitePluginSource._tag === "missing-vue") {
|
|
1663
|
+
if (vitePluginSource._tag === "missing-vue") {
|
|
1210
1664
|
logger.warn("`treeShakeUseSeoMeta` requires `@unhead/vue` with the `/vite` export. Install Unhead v3 or disable `treeShakeUseSeoMeta`.");
|
|
1211
1665
|
} else {
|
|
1212
1666
|
addVitePlugin(async () => {
|
|
@@ -1217,8 +1671,25 @@ export {}
|
|
|
1217
1671
|
});
|
|
1218
1672
|
}
|
|
1219
1673
|
}
|
|
1220
|
-
if (config.automaticOgAndTwitterTags)
|
|
1674
|
+
if (config.automaticOgAndTwitterTags) {
|
|
1675
|
+
const minifyImports = ["unhead/minify", await resolvePath("unhead/minify")];
|
|
1676
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
1677
|
+
if (nitroCompatibility._tag === "nitro-v3") {
|
|
1678
|
+
const nitro3Config = nitroConfig;
|
|
1679
|
+
if (nitro3Config.noExternals !== true) {
|
|
1680
|
+
nitro3Config.noExternals = [
|
|
1681
|
+
...Array.isArray(nitro3Config.noExternals) ? nitro3Config.noExternals : [],
|
|
1682
|
+
...minifyImports
|
|
1683
|
+
];
|
|
1684
|
+
}
|
|
1685
|
+
} else {
|
|
1686
|
+
nitroConfig.externals ||= {};
|
|
1687
|
+
nitroConfig.externals.inline ||= [];
|
|
1688
|
+
nitroConfig.externals.inline.push(...minifyImports);
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1221
1691
|
addPlugin({ src: resolve(appRuntimeDir, "plugins", "inferSeoMetaPlugin") });
|
|
1692
|
+
}
|
|
1222
1693
|
if (config.mergeWithSiteConfig)
|
|
1223
1694
|
addPlugin({ src: resolve(appRuntimeDir, "plugins", "siteConfig") });
|
|
1224
1695
|
if (config.extendRouteRules)
|
|
@@ -1259,6 +1730,23 @@ export {}
|
|
|
1259
1730
|
handler: resolve("./runtime/server/routes/__nuxt-seo-utils/debug")
|
|
1260
1731
|
});
|
|
1261
1732
|
}
|
|
1733
|
+
if (!nuxt.options._prepare) {
|
|
1734
|
+
nuxt.hooks.afterEach((event) => {
|
|
1735
|
+
if (event.name !== "nitro:config")
|
|
1736
|
+
return;
|
|
1737
|
+
const [nitroConfig] = event.args;
|
|
1738
|
+
const site = nuxt.options.site;
|
|
1739
|
+
const robots = nuxt.options.robots;
|
|
1740
|
+
const diagnostics = validateRendering({
|
|
1741
|
+
ssr: nuxt.options.ssr,
|
|
1742
|
+
indexable: site?.indexable === false || robots && robots.indexable === false ? false : void 0,
|
|
1743
|
+
routeRules: defu(nitroConfig.routeRules, nuxt.options.routeRules)
|
|
1744
|
+
});
|
|
1745
|
+
if (diagnostics.length)
|
|
1746
|
+
logger.warn(`Rendering configuration issues:
|
|
1747
|
+
${diagnostics.map((d) => ` ${formatRenderingDiagnostic(d)}`).join("\n")}`);
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1262
1750
|
if (config.validateAppHead) {
|
|
1263
1751
|
nuxt.hook("modules:done", () => {
|
|
1264
1752
|
try {
|