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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ComputedRef } from 'vue';
|
|
2
|
-
export declare function useSchemaOrg(): void;
|
|
2
|
+
export declare function useSchemaOrg(_input?: unknown): void;
|
|
3
3
|
export declare function defineWebSite(): void;
|
|
4
4
|
export declare function defineWebPage(): void;
|
|
5
|
-
export declare function defineBreadcrumb(): void;
|
|
5
|
+
export declare function defineBreadcrumb(_input?: unknown): void;
|
|
6
6
|
export declare function useI18n(): {
|
|
7
7
|
t: (_: string, fallback: string, _options: any) => string;
|
|
8
8
|
te: (_: string) => boolean;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { computed, toValue } from "vue";
|
|
2
2
|
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
3
|
-
export function useSchemaOrg() {
|
|
3
|
+
export function useSchemaOrg(_input) {
|
|
4
4
|
}
|
|
5
5
|
export function defineWebSite() {
|
|
6
6
|
}
|
|
7
7
|
export function defineWebPage() {
|
|
8
8
|
}
|
|
9
|
-
export function defineBreadcrumb() {
|
|
9
|
+
export function defineBreadcrumb(_input) {
|
|
10
10
|
}
|
|
11
11
|
export function useI18n() {
|
|
12
12
|
const siteConfig = useSiteConfig({
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { BreadcrumbItemProps, BreadcrumbProps, ResolvedBreadcrumbProps } from './composables/useBreadcrumbItems.js';
|
|
2
|
+
export { useBreadcrumbItems } from './composables/useBreadcrumbItems.js';
|
|
3
|
+
export { useFallbackTitle } from './composables/useFallbackTitle.js';
|
|
4
|
+
export type { ShareLinks, ShareLinksOptions, ShareLinkUtmParams, SharePlatform } from './composables/useShareLinks.js';
|
|
5
|
+
export { useShareLinks } from './composables/useShareLinks.js';
|
|
@@ -20,24 +20,34 @@ export function ogImageEntryIds(entryTags) {
|
|
|
20
20
|
return ids;
|
|
21
21
|
}
|
|
22
22
|
export function syncDerivedTags(tags, entriesWithOgImage) {
|
|
23
|
-
const winningOgImageEntries = new Set(
|
|
24
|
-
|
|
23
|
+
const winningOgImageEntries = /* @__PURE__ */ new Set();
|
|
24
|
+
let canonicalTag;
|
|
25
|
+
for (const tag of tags) {
|
|
26
|
+
if (isOgImage(tag))
|
|
27
|
+
winningOgImageEntries.add(entryOf(tag));
|
|
28
|
+
if (!canonicalTag && tag.tag === "link" && tag.props.rel === "canonical")
|
|
29
|
+
canonicalTag = tag;
|
|
30
|
+
}
|
|
31
|
+
const canonicalHref = canonicalTag?.props.href;
|
|
25
32
|
const lostOgImage = (tag) => {
|
|
26
33
|
const entry = entryOf(tag);
|
|
27
34
|
return entry !== void 0 && entriesWithOgImage.has(entry) && !winningOgImageEntries.has(entry);
|
|
28
35
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const result = [];
|
|
37
|
+
for (const tag of tags) {
|
|
38
|
+
if (tag.tag === "meta") {
|
|
39
|
+
if (metaKey(tag)?.startsWith("og:image:") && lostOgImage(tag))
|
|
40
|
+
continue;
|
|
41
|
+
if (tag.key?.startsWith(TWITTER_IMAGE_FALLBACK_KEY) && lostOgImage(tag))
|
|
42
|
+
continue;
|
|
43
|
+
if (tag.key === OG_URL_KEY) {
|
|
44
|
+
if (typeof canonicalHref !== "string" || !canonicalHref)
|
|
45
|
+
continue;
|
|
46
|
+
result.push({ ...tag, props: { ...tag.props, content: canonicalHref } });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
|
-
|
|
42
|
-
}
|
|
50
|
+
result.push(tag);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
43
53
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { InferSeoMetaPlugin, TemplateParamsPlugin } from "@unhead/vue/plugins";
|
|
2
2
|
import { defineNuxtPlugin, injectHead, useRuntimeConfig } from "nuxt/app";
|
|
3
|
-
const disabledTwitterCard = "x-nuxt-seo-utils-disabled-twitter-card";
|
|
4
3
|
export default defineNuxtPlugin(() => {
|
|
5
4
|
const head = injectHead();
|
|
6
5
|
if (!head)
|
|
@@ -8,19 +7,7 @@ export default defineNuxtPlugin(() => {
|
|
|
8
7
|
const { automaticTwitterTags } = useRuntimeConfig().public["seo-utils"];
|
|
9
8
|
head.use(TemplateParamsPlugin);
|
|
10
9
|
if (automaticTwitterTags === false) {
|
|
11
|
-
head.use(InferSeoMetaPlugin({ twitterCard:
|
|
12
|
-
head.use({
|
|
13
|
-
key: "nuxt-seo-utils:twitter-card-suppression",
|
|
14
|
-
hooks: {
|
|
15
|
-
"tags:beforeResolve": ({ tags }) => {
|
|
16
|
-
for (let i = tags.length - 1; i >= 0; i--) {
|
|
17
|
-
const tag = tags[i];
|
|
18
|
-
if (tag.tag === "meta" && tag.props.name === "twitter:card" && tag.props.content === disabledTwitterCard)
|
|
19
|
-
tags.splice(i, 1);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
});
|
|
10
|
+
head.use(InferSeoMetaPlugin({ twitterCard: false }));
|
|
24
11
|
} else {
|
|
25
12
|
head.use(InferSeoMetaPlugin());
|
|
26
13
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineNuxtPlugin, injectHead } from "nuxt/app";
|
|
2
|
-
import { JSON_TYPES, minifyCSS,
|
|
2
|
+
import { JSON_TYPES, minifyCSS, minifyJSON } from "../../shared/minify.js";
|
|
3
3
|
export default defineNuxtPlugin({
|
|
4
4
|
enforce: "post",
|
|
5
5
|
setup() {
|
|
@@ -25,14 +25,6 @@ export default defineNuxtPlugin({
|
|
|
25
25
|
}
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
|
-
if (type && SKIP_JS_TYPES.has(type))
|
|
29
|
-
continue;
|
|
30
|
-
try {
|
|
31
|
-
const minified = minifyJS(content);
|
|
32
|
-
if (minified.length < content.length)
|
|
33
|
-
tag.innerHTML = minified;
|
|
34
|
-
} catch {
|
|
35
|
-
}
|
|
36
28
|
} else if (tag.tag === "style") {
|
|
37
29
|
try {
|
|
38
30
|
const minified = minifyCSS(content);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nuxt/server").EventHandler<string | undefined>;
|
|
2
2
|
export default _default;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { defineEventHandler, getRequestHost, sendRedirect } from "nuxt/server";
|
|
1
2
|
import { joinURL } from "ufo";
|
|
2
|
-
import { defineEventHandler, getRequestHost, sendRedirect, setHeader } from "#nuxtseo/h3";
|
|
3
3
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
4
4
|
export default defineEventHandler((e) => {
|
|
5
5
|
if (import.meta.prerender) {
|
|
6
6
|
return;
|
|
7
7
|
}
|
|
8
|
-
const path = e.
|
|
8
|
+
const path = e.url.pathname;
|
|
9
9
|
if (path.startsWith("/_nuxt") || path.startsWith("/api")) {
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
@@ -16,13 +16,13 @@ export default defineEventHandler((e) => {
|
|
|
16
16
|
if (!siteConfig.url || siteConfig.env !== "production") {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
const siteConfigHostName = new URL(e.
|
|
19
|
+
const siteConfigHostName = new URL(e.url.pathname, siteConfig.url).host;
|
|
20
20
|
const host = getRequestHost(e, { xForwardedHost: true });
|
|
21
|
-
if (e.headers.get("x-nuxt-seo-redirected") || host.includes("http") || host.replace("https://", "").includes(":")) {
|
|
21
|
+
if (e.req.headers.get("x-nuxt-seo-redirected") || host.includes("http") || host.replace("https://", "").includes(":")) {
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
if (host.split(":")[0] !== siteConfigHostName.split(":")[0]) {
|
|
25
|
-
|
|
26
|
-
return sendRedirect(e, joinURL(siteConfig.url, e.
|
|
25
|
+
e.res.headers.set("x-nuxt-seo-redirected", "true");
|
|
26
|
+
return sendRedirect(e, joinURL(siteConfig.url, e.url.pathname + e.url.search), 301);
|
|
27
27
|
}
|
|
28
28
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createError, defineEventHandler, sendRedirect, useRuntimeConfig } from "nuxt/server";
|
|
2
|
+
import { decodePath, joinURL, withoutBase } from "ufo";
|
|
3
|
+
import { appendSearch, createRedirectMatcher, isAbsoluteUrl, REDIRECT_CACHE_CONTROL, resolveTarget } from "../../shared/redirects.js";
|
|
4
|
+
let match;
|
|
5
|
+
export default defineEventHandler((e) => {
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
match ||= createRedirectMatcher(config["seo-utils-redirects"]);
|
|
8
|
+
const baseURL = config.app.baseURL;
|
|
9
|
+
const path = decodePath(withoutBase(e.url.pathname, baseURL));
|
|
10
|
+
const rule = match(path);
|
|
11
|
+
if (!rule)
|
|
12
|
+
return;
|
|
13
|
+
if (rule._tag === "Gone") {
|
|
14
|
+
throw createError({ status: rule.statusCode, statusText: rule.statusCode === 410 ? "Gone" : "Not Found" });
|
|
15
|
+
}
|
|
16
|
+
const target = resolveTarget(rule, path);
|
|
17
|
+
const location = isAbsoluteUrl(target) ? target : joinURL(baseURL, target);
|
|
18
|
+
if (!e.res.headers.has("cache-control"))
|
|
19
|
+
e.res.headers.set("cache-control", REDIRECT_CACHE_CONTROL);
|
|
20
|
+
return sendRedirect(e, appendSearch(location, e.url.search), rule.statusCode);
|
|
21
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nuxt/server").EventHandler<Promise<NonSharedBuffer | undefined>>;
|
|
2
2
|
export default _default;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import { defineEventHandler, setHeader } from "#nuxtseo/h3";
|
|
2
|
+
import { defineEventHandler } from "nuxt/server";
|
|
4
3
|
import { fileMapping } from "#seo-utils-virtual/pageDirImages";
|
|
5
4
|
export default defineEventHandler(async (e) => {
|
|
6
|
-
const path =
|
|
5
|
+
const path = e.url.pathname;
|
|
7
6
|
if (fileMapping[path]) {
|
|
8
7
|
if (path.endsWith(".svg"))
|
|
9
|
-
|
|
8
|
+
e.res.headers.set("Content-Type", "image/svg+xml");
|
|
10
9
|
else if (path.endsWith(".png"))
|
|
11
|
-
|
|
10
|
+
e.res.headers.set("Content-Type", "image/png");
|
|
12
11
|
else if (path.endsWith(".jpg") || path.endsWith(".jpeg"))
|
|
13
|
-
|
|
12
|
+
e.res.headers.set("Content-Type", "image/jpeg");
|
|
14
13
|
return fs.readFileSync(fileMapping[path]);
|
|
15
14
|
}
|
|
16
15
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineNitroPlugin } from "#nuxtseo/nitro";
|
|
2
|
-
import { JSON_TYPES, minifyCSS,
|
|
2
|
+
import { JSON_TYPES, minifyCSS, minifyJSON } from "../../shared/minify.js";
|
|
3
3
|
const INLINE_SCRIPT_RE = /<script(?![^>]+\bsrc\b)([^>]*)>([\s\S]*?)<\/script\s*>/gi;
|
|
4
4
|
const INLINE_STYLE_RE = /<style([^>]*)>([\s\S]*?)<\/style\s*>/gi;
|
|
5
5
|
const TYPE_ATTR_RE = /\btype\s*=\s*["']([^"']*)["']/i;
|
|
@@ -16,14 +16,7 @@ function minifyChunk(chunk) {
|
|
|
16
16
|
return full;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
return full;
|
|
21
|
-
try {
|
|
22
|
-
const m = minifyJS(content);
|
|
23
|
-
return m.length < content.length ? `<script${attrs}>${m}<\/script>` : full;
|
|
24
|
-
} catch {
|
|
25
|
-
return full;
|
|
26
|
-
}
|
|
19
|
+
return full;
|
|
27
20
|
});
|
|
28
21
|
out = out.replace(INLINE_STYLE_RE, (full, attrs, content) => {
|
|
29
22
|
if (!content)
|
|
@@ -1,2 +1,28 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nuxt/server").EventHandler<{
|
|
2
|
+
runtimeConfig: {
|
|
3
|
+
canonicalQueryWhitelist: string[];
|
|
4
|
+
canonicalLowercase: boolean;
|
|
5
|
+
automaticTwitterTags: boolean;
|
|
6
|
+
tagPriority: import("@unhead/vue").UseHeadOptions["tagPriority"];
|
|
7
|
+
separator?: string;
|
|
8
|
+
titleSeparator?: string;
|
|
9
|
+
colorModeIcons?: import("../../../types.js").ColorModeIconLinks;
|
|
10
|
+
version: {};
|
|
11
|
+
appBaseURL: string;
|
|
12
|
+
};
|
|
13
|
+
siteConfig: {
|
|
14
|
+
url: string;
|
|
15
|
+
name: any;
|
|
16
|
+
description: any;
|
|
17
|
+
env: any;
|
|
18
|
+
indexable: any;
|
|
19
|
+
trailingSlash: any;
|
|
20
|
+
titleSeparator: any;
|
|
21
|
+
defaultLocale: any;
|
|
22
|
+
currentLocale: any;
|
|
23
|
+
twitter: any;
|
|
24
|
+
};
|
|
25
|
+
headConfig: {};
|
|
26
|
+
path: string | string[];
|
|
27
|
+
}>;
|
|
2
28
|
export default _default;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { defineEventHandler, getQuery } from "
|
|
2
|
-
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
1
|
+
import { defineEventHandler, getQuery, useRuntimeConfig } from "nuxt/server";
|
|
3
2
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
3
|
export default defineEventHandler((e) => {
|
|
5
4
|
const siteConfig = getSiteConfig(e);
|
|
6
|
-
const runtimeConfig = useRuntimeConfig(
|
|
5
|
+
const runtimeConfig = useRuntimeConfig();
|
|
7
6
|
const seoUtils = runtimeConfig.public?.["seo-utils"] || {};
|
|
8
7
|
const headConfig = runtimeConfig["seo-utils-head"] || { link: [], meta: [] };
|
|
9
8
|
const query = getQuery(e);
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export declare const JSON_TYPES: Set<string>;
|
|
2
|
-
export declare const SKIP_JS_TYPES: Set<string>;
|
|
3
|
-
/**
|
|
4
|
-
* Lightweight JS minifier in pure JS (no native deps).
|
|
5
|
-
* Strips comments and collapses whitespace while preserving string literals.
|
|
6
|
-
*/
|
|
7
|
-
export declare function minifyJS(code: string): string;
|
|
8
2
|
/**
|
|
9
3
|
* Lightweight CSS minifier in pure JS (no native deps).
|
|
10
4
|
* Strips comments and collapses whitespace while preserving string literals.
|
|
@@ -1,62 +1,5 @@
|
|
|
1
1
|
export const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json"]);
|
|
2
|
-
export const SKIP_JS_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
|
|
3
|
-
const JS_MINIFY_CANDIDATE_RE = /[\s/]/;
|
|
4
2
|
const CSS_MINIFY_CANDIDATE_RE = /[\s/]|;\}|0\.\d/;
|
|
5
|
-
export function minifyJS(code) {
|
|
6
|
-
if (!JS_MINIFY_CANDIDATE_RE.test(code))
|
|
7
|
-
return code;
|
|
8
|
-
let result = "";
|
|
9
|
-
let last = "";
|
|
10
|
-
let i = 0;
|
|
11
|
-
const len = code.length;
|
|
12
|
-
const append = (value) => {
|
|
13
|
-
result += value;
|
|
14
|
-
last = value;
|
|
15
|
-
};
|
|
16
|
-
while (i < len) {
|
|
17
|
-
const ch = code[i];
|
|
18
|
-
if (ch === "'" || ch === '"' || ch === "`") {
|
|
19
|
-
const quote = ch;
|
|
20
|
-
append(ch);
|
|
21
|
-
i++;
|
|
22
|
-
while (i < len && code[i] !== quote) {
|
|
23
|
-
if (code[i] === "\\") {
|
|
24
|
-
append(code[i++]);
|
|
25
|
-
}
|
|
26
|
-
append(code[i++]);
|
|
27
|
-
}
|
|
28
|
-
if (i < len)
|
|
29
|
-
append(code[i++]);
|
|
30
|
-
} else if (ch === "/" && code[i + 1] === "/") {
|
|
31
|
-
i += 2;
|
|
32
|
-
while (i < len && code[i] !== "\n")
|
|
33
|
-
i++;
|
|
34
|
-
} else if (ch === "/" && code[i + 1] === "*") {
|
|
35
|
-
i += 2;
|
|
36
|
-
while (i < len && !(code[i] === "*" && code[i + 1] === "/"))
|
|
37
|
-
i++;
|
|
38
|
-
i += 2;
|
|
39
|
-
} else if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
40
|
-
let hasNewline = false;
|
|
41
|
-
while (i < len && (code[i] === " " || code[i] === " " || code[i] === "\n" || code[i] === "\r")) {
|
|
42
|
-
if (code[i] === "\n")
|
|
43
|
-
hasNewline = true;
|
|
44
|
-
i++;
|
|
45
|
-
}
|
|
46
|
-
const next = code[i];
|
|
47
|
-
if (hasNewline && last && next && last !== "{" && last !== "}" && last !== ";" && next !== "}" && next !== ";")
|
|
48
|
-
append("\n");
|
|
49
|
-
else if (last && next && isIdentChar(last) && isIdentChar(next))
|
|
50
|
-
append(" ");
|
|
51
|
-
else if (last && next && (last === "+" && next === "+" || last === "-" && next === "-"))
|
|
52
|
-
append(" ");
|
|
53
|
-
} else {
|
|
54
|
-
append(ch);
|
|
55
|
-
i++;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return result.trim();
|
|
59
|
-
}
|
|
60
3
|
export function minifyCSS(code) {
|
|
61
4
|
if (!CSS_MINIFY_CANDIDATE_RE.test(code))
|
|
62
5
|
return code;
|
|
@@ -132,9 +75,6 @@ export function minifyCSS(code) {
|
|
|
132
75
|
}
|
|
133
76
|
return result.trim();
|
|
134
77
|
}
|
|
135
|
-
function isIdentChar(ch) {
|
|
136
|
-
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "$";
|
|
137
|
-
}
|
|
138
78
|
function isCSSBasePunctuation(ch) {
|
|
139
79
|
return ch === "{" || ch === "}" || ch === ";" || ch === ":" || ch === ",";
|
|
140
80
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** How an entry key matches request paths. `/old/**` matches `/old` and every path below it. */
|
|
2
|
+
export type SourcePattern = {
|
|
3
|
+
_tag: 'Exact';
|
|
4
|
+
path: string;
|
|
5
|
+
} | {
|
|
6
|
+
_tag: 'Prefix';
|
|
7
|
+
prefix: string;
|
|
8
|
+
};
|
|
9
|
+
/** The part of an entry the server needs to answer a request. */
|
|
10
|
+
export type RedirectRule = {
|
|
11
|
+
_tag: 'Moved';
|
|
12
|
+
pattern: SourcePattern;
|
|
13
|
+
to: string;
|
|
14
|
+
statusCode: 301 | 308;
|
|
15
|
+
} | {
|
|
16
|
+
_tag: 'Gone';
|
|
17
|
+
pattern: SourcePattern;
|
|
18
|
+
statusCode: 410 | 404;
|
|
19
|
+
};
|
|
20
|
+
export type MovedRule = Extract<RedirectRule, {
|
|
21
|
+
_tag: 'Moved';
|
|
22
|
+
}>;
|
|
23
|
+
/** Without a max-age, Chromium caches a 301 or 308 forever, so a later fix or removal never reaches returning visitors. */
|
|
24
|
+
export declare const REDIRECT_CACHE_CONTROL = "public, max-age=3600";
|
|
25
|
+
export declare function normalizePath(path: string): string;
|
|
26
|
+
export declare function isAbsoluteUrl(to: string): boolean;
|
|
27
|
+
/** Parse an entry key into a pattern, or return why the key is invalid. */
|
|
28
|
+
export declare function parseSourcePattern(source: string): SourcePattern | string;
|
|
29
|
+
export declare function matchesSource(pattern: SourcePattern, path: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Index entries for lookup. An exact entry wins, then the longest matching prefix.
|
|
32
|
+
* The path must not include `app.baseURL`, the query string, or the hash.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createRedirectMatcher<T extends {
|
|
35
|
+
pattern: SourcePattern;
|
|
36
|
+
}>(entries: readonly T[]): (path: string) => T | undefined;
|
|
37
|
+
/** The target for a request path. A target ending in `/**` gets the path below the source prefix. */
|
|
38
|
+
export declare function resolveTarget(rule: MovedRule, path: string): string;
|
|
39
|
+
/** Keep the request query string. A query on the target comes first. */
|
|
40
|
+
export declare function appendSearch(target: string, search: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { joinURL, withoutTrailingSlash } from "ufo";
|
|
2
|
+
export const REDIRECT_CACHE_CONTROL = "public, max-age=3600";
|
|
3
|
+
export function normalizePath(path) {
|
|
4
|
+
return path === "/" ? path : withoutTrailingSlash(path);
|
|
5
|
+
}
|
|
6
|
+
export function isAbsoluteUrl(to) {
|
|
7
|
+
return /^https?:\/\//i.test(to);
|
|
8
|
+
}
|
|
9
|
+
export function parseSourcePattern(source) {
|
|
10
|
+
if (!source.startsWith("/") || source.startsWith("//"))
|
|
11
|
+
return "it must be a path that starts with `/`";
|
|
12
|
+
if (/[?#]/.test(source))
|
|
13
|
+
return "route matching ignores the query string and hash, so remove them";
|
|
14
|
+
const wildcard = source.endsWith("/**");
|
|
15
|
+
const base = wildcard ? source.slice(0, -3) : source;
|
|
16
|
+
if (/\*|\/:/.test(base))
|
|
17
|
+
return "only a trailing `/**` is supported";
|
|
18
|
+
if (wildcard) {
|
|
19
|
+
const prefix = normalizePath(base || "/");
|
|
20
|
+
return { _tag: "Prefix", prefix: prefix === "/" ? "" : prefix };
|
|
21
|
+
}
|
|
22
|
+
return { _tag: "Exact", path: normalizePath(base) };
|
|
23
|
+
}
|
|
24
|
+
export function matchesSource(pattern, path) {
|
|
25
|
+
const normalized = normalizePath(path);
|
|
26
|
+
if (pattern._tag === "Exact")
|
|
27
|
+
return pattern.path === normalized;
|
|
28
|
+
return normalized === (pattern.prefix || "/") || normalized.startsWith(`${pattern.prefix}/`);
|
|
29
|
+
}
|
|
30
|
+
export function createRedirectMatcher(entries) {
|
|
31
|
+
const exact = /* @__PURE__ */ new Map();
|
|
32
|
+
const prefixes = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.pattern._tag === "Exact")
|
|
35
|
+
exact.set(entry.pattern.path, entry);
|
|
36
|
+
else
|
|
37
|
+
prefixes.push(entry);
|
|
38
|
+
}
|
|
39
|
+
const prefixLength = (entry) => entry.pattern._tag === "Prefix" ? entry.pattern.prefix.length : 0;
|
|
40
|
+
prefixes.sort((a, b) => prefixLength(b) - prefixLength(a));
|
|
41
|
+
return (path) => {
|
|
42
|
+
const normalized = normalizePath(path);
|
|
43
|
+
return exact.get(normalized) ?? prefixes.find((entry) => matchesSource(entry.pattern, normalized));
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function resolveTarget(rule, path) {
|
|
47
|
+
if (!rule.to.endsWith("/**"))
|
|
48
|
+
return rule.to;
|
|
49
|
+
const rest = rule.pattern._tag === "Prefix" ? normalizePath(path).slice(rule.pattern.prefix.length) : "";
|
|
50
|
+
return joinURL(rule.to.slice(0, -3) || "/", rest);
|
|
51
|
+
}
|
|
52
|
+
export function appendSearch(target, search) {
|
|
53
|
+
if (!search || search === "?")
|
|
54
|
+
return target;
|
|
55
|
+
const hashIndex = target.indexOf("#");
|
|
56
|
+
const base = hashIndex === -1 ? target : target.slice(0, hashIndex);
|
|
57
|
+
const hash = hashIndex === -1 ? "" : target.slice(hashIndex);
|
|
58
|
+
return `${base}${base.includes("?") ? "&" : "?"}${search.replace(/^\?/, "")}${hash}`;
|
|
59
|
+
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import type { UseHeadOptions } from '@unhead/vue';
|
|
1
2
|
import type { Head, Link, MetaFlat, RawInput } from '@unhead/vue/types';
|
|
3
|
+
export interface SeoUtilsRuntimeConfig {
|
|
4
|
+
canonicalQueryWhitelist: string[];
|
|
5
|
+
canonicalLowercase: boolean;
|
|
6
|
+
automaticTwitterTags: boolean;
|
|
7
|
+
tagPriority: UseHeadOptions['tagPriority'];
|
|
8
|
+
separator?: string;
|
|
9
|
+
titleSeparator?: string;
|
|
10
|
+
colorModeIcons?: ColorModeIconLinks;
|
|
11
|
+
}
|
|
12
|
+
declare module 'nuxt/schema' {
|
|
13
|
+
interface PublicRuntimeConfig {
|
|
14
|
+
'seo-utils': SeoUtilsRuntimeConfig;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
2
17
|
export interface ColorModeIconLinks {
|
|
3
18
|
dark: Link[];
|
|
4
19
|
light: Link[];
|
package/dist/types.d.mts
CHANGED