nuxt-seo-utils 7.0.15 → 7.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +115 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +560 -0
- package/dist/runtime/app/composables/polyfills.d.ts +11 -0
- package/dist/runtime/app/composables/polyfills.js +27 -0
- package/dist/runtime/app/composables/useBreadcrumbItems.d.ts +112 -0
- package/dist/runtime/app/composables/useBreadcrumbItems.js +193 -0
- package/dist/runtime/app/logic/applyDefaults.d.ts +1 -0
- package/dist/runtime/app/logic/applyDefaults.js +78 -0
- package/dist/runtime/app/plugins/0.routeRules.d.ts +2 -0
- package/dist/runtime/app/plugins/0.routeRules.js +24 -0
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.d.ts +2 -0
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.js +33 -0
- package/dist/runtime/app/plugins/defaults.d.ts +2 -0
- package/dist/runtime/app/plugins/defaults.js +12 -0
- package/dist/runtime/app/plugins/defaultsWaitI18n.d.ts +2 -0
- package/dist/runtime/app/plugins/defaultsWaitI18n.js +13 -0
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.d.ts +2 -0
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.js +11 -0
- package/dist/runtime/app/plugins/siteConfig.d.ts +2 -0
- package/dist/runtime/app/plugins/siteConfig.js +33 -0
- package/dist/runtime/app/plugins/titles.d.ts +2 -0
- package/dist/runtime/app/plugins/titles.js +30 -0
- package/dist/runtime/server/middleware/redirectCanonical.d.ts +2 -0
- package/dist/runtime/server/middleware/redirectCanonical.js +28 -0
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.d.ts +2 -0
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +16 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/shared/breadcrumbs.d.ts +1 -0
- package/dist/runtime/shared/breadcrumbs.js +18 -0
- package/dist/runtime/types.d.ts +6 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { MetaFlatSerializable } from '../dist/runtime/types.js';
|
|
3
|
+
|
|
4
|
+
interface ModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the SEO experiements should run.
|
|
7
|
+
*
|
|
8
|
+
* @default true
|
|
9
|
+
*/
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Should the files in the public directory be used to infer tags such as favicon, apple-touch-icon, and
|
|
13
|
+
* open graph images.
|
|
14
|
+
*
|
|
15
|
+
* @default true
|
|
16
|
+
*/
|
|
17
|
+
metaDataFiles: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Should head data be inferred from the current input to fill in the gaps.
|
|
20
|
+
*
|
|
21
|
+
* For example:
|
|
22
|
+
* - If you supply a title, this will automatically add an og:title.
|
|
23
|
+
* - If you provide an og:image, it will automatically add a twitter:image.
|
|
24
|
+
*
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
automaticOgAndTwitterTags: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Attempts to treeshake the `useSeoMeta` function. Can save around 5kb in the client bundle.
|
|
30
|
+
*
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
treeShakeUseSeoMeta: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Injects site config into the `useHead` composable such as setting the title template
|
|
36
|
+
*
|
|
37
|
+
* @default true
|
|
38
|
+
*/
|
|
39
|
+
mergeWithSiteConfig: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Adds `head` and `seoMeta` to the route rules and app config.
|
|
42
|
+
*
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
extendRouteRules: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Tries to convert relative image paths to absolute paths in meta tags.
|
|
48
|
+
*
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
fixRequiredAbsoluteMetaTagsLinks: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Extends the `head` in the Nuxt config with the `seoMeta` object.
|
|
54
|
+
*
|
|
55
|
+
* @default true
|
|
56
|
+
*/
|
|
57
|
+
extendNuxtConfigAppHeadSeoMeta: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Augments the head schema with `/public` files making it easier to reference them in the head.
|
|
60
|
+
*
|
|
61
|
+
* @default true
|
|
62
|
+
*/
|
|
63
|
+
extendNuxtConfigAppHeadTypes: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* @dead
|
|
66
|
+
*/
|
|
67
|
+
setupNuxtConfigAppHeadWithMoreDefaults: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Will ensure a title is always set by providing a fallback title based on the casing the last slug segment.
|
|
70
|
+
*
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
fallbackTitle?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Will set up a number of defaults for meta tags and Schema.org, if the modules and config are available.
|
|
76
|
+
*
|
|
77
|
+
* @default true
|
|
78
|
+
*/
|
|
79
|
+
automaticDefaults?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* When enabled, it will whitelist the query parameters that are allowed in the canonical URL.
|
|
82
|
+
*/
|
|
83
|
+
canonicalQueryWhitelist?: string[];
|
|
84
|
+
/**
|
|
85
|
+
* When enabled, it will lowercase the canonical URL.
|
|
86
|
+
*
|
|
87
|
+
* @default true
|
|
88
|
+
*/
|
|
89
|
+
canonicalLowercase?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
|
|
92
|
+
*
|
|
93
|
+
* E.g if the site url is 'www.example.com' and the user visits 'example.com',
|
|
94
|
+
* they will be redirected to 'www.example.com'.
|
|
95
|
+
*
|
|
96
|
+
* This is useful for SEO as it prevents duplicate content and consolidates page rank.
|
|
97
|
+
*
|
|
98
|
+
* @default false
|
|
99
|
+
*/
|
|
100
|
+
redirectToCanonicalSiteUrl?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The SEO meta object to be used as defaults.
|
|
103
|
+
*/
|
|
104
|
+
meta: MetaFlatSerializable;
|
|
105
|
+
/**
|
|
106
|
+
* Enables debug logs and a debug endpoint.
|
|
107
|
+
*
|
|
108
|
+
* @default false
|
|
109
|
+
*/
|
|
110
|
+
debug: boolean;
|
|
111
|
+
}
|
|
112
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
113
|
+
|
|
114
|
+
export { _default as default };
|
|
115
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { useNuxt, addTypeTemplate, defineNuxtModule, useLogger, createResolver, addServerHandler, hasNuxtModule, addPlugin, addImports, addVitePlugin } from '@nuxt/kit';
|
|
2
|
+
import UnheadVite from '@unhead/addons/vite';
|
|
3
|
+
import { defu } from 'defu';
|
|
4
|
+
import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
5
|
+
import { resolve, relative, extname, dirname, basename } from 'pathe';
|
|
6
|
+
import { unpackMeta } from '@unhead/vue/utils';
|
|
7
|
+
import fg from 'fast-glob';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { encodePath, joinURL } from 'ufo';
|
|
10
|
+
import escapeRE from 'escape-string-regexp';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import imageSize from 'image-size';
|
|
13
|
+
|
|
14
|
+
function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
|
|
15
|
+
const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || {};
|
|
16
|
+
const configSeoMeta = nuxt.options.seo?.meta || nuxt.options.seo?.seoMeta || {};
|
|
17
|
+
if (!seoMeta && !configSeoMeta)
|
|
18
|
+
return;
|
|
19
|
+
nuxt.options.app.head = nuxt.options.app.head || {};
|
|
20
|
+
nuxt.options.app.head = {
|
|
21
|
+
...nuxt.options.app.head,
|
|
22
|
+
meta: [
|
|
23
|
+
...nuxt.options.app.head.meta || [],
|
|
24
|
+
// @ts-expect-error untyped
|
|
25
|
+
...unpackMeta(seoMeta),
|
|
26
|
+
...unpackMeta(configSeoMeta || {})
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
delete nuxt.options.app.seoMeta;
|
|
30
|
+
delete nuxt.options.app.head.seoMeta;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extendNuxtConfigAppHeadTypes(nuxt = useNuxt()) {
|
|
34
|
+
addTypeTemplate({
|
|
35
|
+
filename: "module/nuxt-seo-utils.assets.d.ts",
|
|
36
|
+
getContents: async () => {
|
|
37
|
+
const paths = (await fg(["**/*"], { cwd: resolve(nuxt.options.srcDir, "public") })).map((p) => `/${p}`);
|
|
38
|
+
const jsPaths = paths.filter((p) => p.endsWith(".js") || p.endsWith(".mjs"));
|
|
39
|
+
return `// Generated by nuxt-seo-utils
|
|
40
|
+
declare module '#app' {
|
|
41
|
+
import { HeadEntry, HeadTag } from '@unhead/vue/types'
|
|
42
|
+
|
|
43
|
+
interface RuntimeNuxtHooks {
|
|
44
|
+
'head:tags': (tag: HeadTag[]) => Promise<void> | void
|
|
45
|
+
'head:entries': (entries: HeadEntry[]) => Promise<void> | void
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
declare module '@unhead/schema' {
|
|
49
|
+
|
|
50
|
+
type PublicFiles = ${[...paths.map((p) => `'${p}'`), "(string & Record<never, never>)"].join(" | ")}
|
|
51
|
+
type JsFiles = ${[...jsPaths.map((p) => `'${p}'`), "(string & Record<never, never>)"].join(" | ")}
|
|
52
|
+
|
|
53
|
+
interface SchemaAugmentations {
|
|
54
|
+
link: import('@unhead/schema').UserTagConfigWithoutInnerContent & {
|
|
55
|
+
href: PublicFiles
|
|
56
|
+
}
|
|
57
|
+
script: import('@unhead/schema').TagUserProperties & {
|
|
58
|
+
src: JsFiles
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
}, {
|
|
67
|
+
nuxt: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const MetaTagFileGlobs = [
|
|
72
|
+
"**/*og-image.{png,jpg,jpeg,gif}",
|
|
73
|
+
"**/*opengraph-image.{png,jpg,jpeg,gif}",
|
|
74
|
+
"**/*twitter-image.{png,jpg,jpeg,gif}",
|
|
75
|
+
"**/*apple-icon.{jpg,jpeg,png}",
|
|
76
|
+
"**/*apple-touch.{jpg,jpeg,png}",
|
|
77
|
+
"**/*apple-touch-*.{jpg,jpeg,png}",
|
|
78
|
+
"**/*apple-touch-icon.{jpg,jpeg,png}",
|
|
79
|
+
"**/*apple-touch-icon-*.{jpg,jpeg,png}",
|
|
80
|
+
"**/*icon.{ico,jpg,jpeg,png,svg}",
|
|
81
|
+
"**/*icon-*.{ico,jpg,jpeg,png,svg}"
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function generateNuxtPageFromFile(file, pagesDir) {
|
|
85
|
+
const segments = relative(pagesDir, file).replace(new RegExp(`${escapeRE(extname(file))}$`), "").split("/");
|
|
86
|
+
const route = {
|
|
87
|
+
name: "",
|
|
88
|
+
path: "",
|
|
89
|
+
file,
|
|
90
|
+
children: []
|
|
91
|
+
};
|
|
92
|
+
let parent = [];
|
|
93
|
+
for (let i = 0; i < segments.length; i++) {
|
|
94
|
+
const segment = segments[i] || "";
|
|
95
|
+
const tokens = parseSegment(segment);
|
|
96
|
+
const segmentName = tokens.map(({ value }) => value).join("");
|
|
97
|
+
route.name += (route.name && "-") + segmentName;
|
|
98
|
+
const child = parent.find((parentRoute) => parentRoute.name === route.name && !parentRoute.path.endsWith("(.*)*"));
|
|
99
|
+
if (child && child.children) {
|
|
100
|
+
parent = child.children;
|
|
101
|
+
route.path = "";
|
|
102
|
+
} else if (segmentName === "index" && !route.path) {
|
|
103
|
+
route.path += "/";
|
|
104
|
+
} else if (segmentName !== "index") {
|
|
105
|
+
route.path += getRoutePath(tokens);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (route.name)
|
|
109
|
+
route.name = route.name.replace(/-index$/, "");
|
|
110
|
+
return route;
|
|
111
|
+
}
|
|
112
|
+
function getRoutePath(tokens) {
|
|
113
|
+
return tokens.reduce((path, token) => {
|
|
114
|
+
return path + (token.type === 2 /* optional */ ? `:${token.value}?` : token.type === 1 /* dynamic */ ? `:${token.value}` : token.type === 3 /* catchall */ ? `:${token.value}(.*)*` : encodePath(token.value));
|
|
115
|
+
}, "/");
|
|
116
|
+
}
|
|
117
|
+
const PARAM_CHAR_RE = /[\w.]/;
|
|
118
|
+
function parseSegment(segment) {
|
|
119
|
+
let state = 0 /* initial */;
|
|
120
|
+
let i = 0;
|
|
121
|
+
let buffer = "";
|
|
122
|
+
const tokens = [];
|
|
123
|
+
function consumeBuffer() {
|
|
124
|
+
if (!buffer)
|
|
125
|
+
return;
|
|
126
|
+
if (state === 0 /* initial */)
|
|
127
|
+
throw new Error("wrong state");
|
|
128
|
+
tokens.push({
|
|
129
|
+
type: state === 1 /* static */ ? 0 /* static */ : state === 2 /* dynamic */ ? 1 /* dynamic */ : state === 3 /* optional */ ? 2 /* optional */ : 3 /* catchall */,
|
|
130
|
+
value: buffer
|
|
131
|
+
});
|
|
132
|
+
buffer = "";
|
|
133
|
+
}
|
|
134
|
+
while (i < segment.length) {
|
|
135
|
+
const c = segment[i] || "";
|
|
136
|
+
switch (state) {
|
|
137
|
+
case 0 /* initial */:
|
|
138
|
+
buffer = "";
|
|
139
|
+
if (c === "[") {
|
|
140
|
+
state = 2 /* dynamic */;
|
|
141
|
+
} else {
|
|
142
|
+
i--;
|
|
143
|
+
state = 1 /* static */;
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
case 1 /* static */:
|
|
147
|
+
if (c === "[") {
|
|
148
|
+
consumeBuffer();
|
|
149
|
+
state = 2 /* dynamic */;
|
|
150
|
+
} else {
|
|
151
|
+
buffer += c;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 4 /* catchall */:
|
|
155
|
+
case 2 /* dynamic */:
|
|
156
|
+
case 3 /* optional */:
|
|
157
|
+
if (buffer === "...") {
|
|
158
|
+
buffer = "";
|
|
159
|
+
state = 4 /* catchall */;
|
|
160
|
+
}
|
|
161
|
+
if (c === "[" && state === 2 /* dynamic */)
|
|
162
|
+
state = 3 /* optional */;
|
|
163
|
+
if (c === "]" && (state !== 3 /* optional */ || buffer[buffer.length - 1] === "]")) {
|
|
164
|
+
if (!buffer)
|
|
165
|
+
throw new Error("Empty param");
|
|
166
|
+
else
|
|
167
|
+
consumeBuffer();
|
|
168
|
+
state = 0 /* initial */;
|
|
169
|
+
} else if (PARAM_CHAR_RE.test(c)) {
|
|
170
|
+
buffer += c;
|
|
171
|
+
} else ;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
if (state === 2 /* dynamic */)
|
|
177
|
+
throw new Error(`Unfinished param "${buffer}"`);
|
|
178
|
+
consumeBuffer();
|
|
179
|
+
return tokens;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hasLinkRel(input, rel) {
|
|
183
|
+
return input.link?.some((link) => link.rel === rel);
|
|
184
|
+
}
|
|
185
|
+
function hasMetaProperty(input, property) {
|
|
186
|
+
return input.meta?.some((meta) => meta.property === property);
|
|
187
|
+
}
|
|
188
|
+
async function getImageMeta(base, path, isIcon = false) {
|
|
189
|
+
const absolutePath = resolve(base, path);
|
|
190
|
+
const file = absolutePath.split("/").pop();
|
|
191
|
+
const keyword = file.split(".")[0];
|
|
192
|
+
let ext = absolutePath.split(".").pop();
|
|
193
|
+
if (ext === "jpg")
|
|
194
|
+
ext = "jpeg";
|
|
195
|
+
const { width, height } = await getImageDimensions(absolutePath);
|
|
196
|
+
const payload = {
|
|
197
|
+
type: ext === "svg" ? `image/svg+xml` : `image/${ext}`
|
|
198
|
+
};
|
|
199
|
+
if (!isIcon) {
|
|
200
|
+
payload.width = width;
|
|
201
|
+
payload.height = height;
|
|
202
|
+
const altTextPath = resolve(dirname(absolutePath), `${keyword}.alt.txt`);
|
|
203
|
+
if (fs.existsSync(altTextPath)) {
|
|
204
|
+
payload.alt = fs.readFileSync(altTextPath, "utf8");
|
|
205
|
+
payload.alt = String(payload.alt).replace(/\n/g, " ").trim();
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
if (path.includes(".dark") || path.includes("-dark"))
|
|
209
|
+
payload.media = "(prefers-color-scheme: dark)";
|
|
210
|
+
else if (path.includes(".light") || path.includes("-light"))
|
|
211
|
+
payload.media = "(prefers-color-scheme: light)";
|
|
212
|
+
if (ext !== "svg") {
|
|
213
|
+
payload.sizes = `${width}x${height}`;
|
|
214
|
+
} else {
|
|
215
|
+
payload.sizes = "any";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return payload;
|
|
219
|
+
}
|
|
220
|
+
async function getImageDimensions(absolutePath) {
|
|
221
|
+
const buffer = await readFile(absolutePath);
|
|
222
|
+
return imageSize(buffer);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
226
|
+
const pagesDir = resolve(nuxt.options.rootDir, nuxt.options.dir.pages);
|
|
227
|
+
const files = await fg(MetaTagFileGlobs, { cwd: pagesDir, onlyFiles: true });
|
|
228
|
+
const appendRouteRules = {};
|
|
229
|
+
const devMiddlewareMap = {};
|
|
230
|
+
const nitroOutputMap = [];
|
|
231
|
+
for (const file of files) {
|
|
232
|
+
const fileName = basename(file);
|
|
233
|
+
const keyword = fileName.split(".")[0] || "";
|
|
234
|
+
let { path } = generateNuxtPageFromFile(resolve(pagesDir, dirname(file)), pagesDir);
|
|
235
|
+
const meta = await getImageMeta(pagesDir, file);
|
|
236
|
+
if (path.endsWith("/_dir"))
|
|
237
|
+
path = path.replace(/\/_dir$/, "");
|
|
238
|
+
const src = joinURL(path, fileName);
|
|
239
|
+
if (["icon", "apple-touch-icon", "apple-icon"].includes(keyword) || keyword.startsWith("icon-")) {
|
|
240
|
+
appendRouteRules[`${path}/**`] = {
|
|
241
|
+
head: {
|
|
242
|
+
link: [
|
|
243
|
+
{ rel: keyword, type: meta.type, href: src, sizes: meta.sizes }
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
} else {
|
|
248
|
+
appendRouteRules[`${path}/**`] = {
|
|
249
|
+
seoMeta: {
|
|
250
|
+
[["opengraph-image", "og-image"].includes(keyword) ? "ogImage" : "twitterImage"]: [{ url: src, ...meta, sizes: void 0 }]
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
devMiddlewareMap[src] = resolve(pagesDir, file);
|
|
255
|
+
nitroOutputMap.push({
|
|
256
|
+
src: resolve(pagesDir, file),
|
|
257
|
+
dest: src.slice(1)
|
|
258
|
+
// remove leading slash
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
nuxt.options.routeRules = defu(appendRouteRules, nuxt.options.routeRules);
|
|
262
|
+
nuxt.options.nitro.routeRules = defu(appendRouteRules, nuxt.options.nitro.routeRules);
|
|
263
|
+
if (nuxt.options.dev) {
|
|
264
|
+
nuxt.hooks.hook("nitro:config", async (nitroConfig) => {
|
|
265
|
+
nitroConfig.virtual["#seo-utils-virtual/pageDirImages"] = `export const fileMapping = ${JSON.stringify(devMiddlewareMap)}`;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
nuxt.hooks.hook("nitro:build:public-assets", (_nitro) => {
|
|
269
|
+
const publicDir = resolve(_nitro.options.output.dir, _nitro.options.output.publicDir);
|
|
270
|
+
nitroOutputMap.forEach(({ src, dest }) => {
|
|
271
|
+
const resolvedDest = resolve(publicDir, dest);
|
|
272
|
+
const destFolder = dirname(resolvedDest);
|
|
273
|
+
if (!fs.existsSync(destFolder))
|
|
274
|
+
fs.mkdirSync(destFolder, { recursive: true });
|
|
275
|
+
if (!fs.existsSync(resolvedDest))
|
|
276
|
+
fs.copyFileSync(src, resolvedDest);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
282
|
+
const publicDirPath = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
|
|
283
|
+
const rootPublicFiles = (await fg(MetaTagFileGlobs, { cwd: publicDirPath, onlyFiles: true, deep: 1 })).map((file) => basename(file));
|
|
284
|
+
const headConfig = defu(nuxt.options.app.head, {
|
|
285
|
+
link: [],
|
|
286
|
+
meta: []
|
|
287
|
+
});
|
|
288
|
+
if (!hasLinkRel(headConfig, "icon")) {
|
|
289
|
+
if (rootPublicFiles.includes("favicon.ico") && nuxt.options.app.baseURL !== "/") {
|
|
290
|
+
headConfig.link.push({
|
|
291
|
+
rel: "icon",
|
|
292
|
+
href: joinURL(nuxt.options.app.baseURL, "favicon.ico"),
|
|
293
|
+
sizes: "any"
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const isIcon = (file) => file.includes("icon") && !file.endsWith(".ico");
|
|
297
|
+
const isAppleTouchIcon = (file) => file.includes("apple-icon") || file.includes("apple-touch-icon") || file.includes("apple-touch");
|
|
298
|
+
headConfig.link.push(
|
|
299
|
+
...await Promise.all([
|
|
300
|
+
...rootPublicFiles.filter((file) => isIcon(file) && !isAppleTouchIcon(file)).sort().map(async (iconFile) => {
|
|
301
|
+
const meta = await getImageMeta(publicDirPath, iconFile, true);
|
|
302
|
+
return {
|
|
303
|
+
rel: "icon",
|
|
304
|
+
href: joinURL(nuxt.options.app.baseURL, iconFile),
|
|
305
|
+
...meta
|
|
306
|
+
};
|
|
307
|
+
}),
|
|
308
|
+
...rootPublicFiles.filter((file) => isAppleTouchIcon(file)).sort().map(async (appleIconFile) => {
|
|
309
|
+
const meta = await getImageMeta(publicDirPath, appleIconFile, true);
|
|
310
|
+
return {
|
|
311
|
+
rel: "apple-touch-icon",
|
|
312
|
+
href: joinURL(nuxt.options.app.baseURL, appleIconFile),
|
|
313
|
+
...meta
|
|
314
|
+
};
|
|
315
|
+
})
|
|
316
|
+
])
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
let hasTwitterImage = hasMetaProperty(headConfig, "twitter:image");
|
|
320
|
+
if (!hasTwitterImage) {
|
|
321
|
+
const twitterImageFiles = rootPublicFiles.filter((file) => file.startsWith("twitter-image.")).sort();
|
|
322
|
+
if (twitterImageFiles.length) {
|
|
323
|
+
headConfig.meta.push(
|
|
324
|
+
...(await Promise.all(twitterImageFiles.map(async (twitterImageFile) => {
|
|
325
|
+
const dimensions = await getImageDimensions(resolve(publicDirPath, twitterImageFile));
|
|
326
|
+
return unpackMeta({
|
|
327
|
+
twitterImage: {
|
|
328
|
+
url: twitterImageFile,
|
|
329
|
+
width: dimensions.width,
|
|
330
|
+
height: dimensions.height
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}))).flat()
|
|
334
|
+
);
|
|
335
|
+
hasTwitterImage = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (!hasMetaProperty(headConfig, "og:image")) {
|
|
339
|
+
const ogImageFiles = rootPublicFiles.filter((file) => file.startsWith("og-image.") || file.startsWith("og.")).sort();
|
|
340
|
+
if (ogImageFiles.length) {
|
|
341
|
+
headConfig.meta.push(
|
|
342
|
+
...(await Promise.all(ogImageFiles.map(async (src) => {
|
|
343
|
+
const meta = await getImageMeta(publicDirPath, src, false);
|
|
344
|
+
delete meta.sizes;
|
|
345
|
+
const seoMeta = {
|
|
346
|
+
ogImage: {
|
|
347
|
+
url: src,
|
|
348
|
+
...meta
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
if (!hasTwitterImage) {
|
|
352
|
+
seoMeta.twitterImage = {
|
|
353
|
+
url: src,
|
|
354
|
+
...meta
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return unpackMeta(seoMeta);
|
|
358
|
+
}))).flat()
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
nuxt.options.app.head = headConfig;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
|
|
366
|
+
const headConfig = nuxt.options.app.head;
|
|
367
|
+
headConfig.link = headConfig.link || [];
|
|
368
|
+
headConfig.htmlAttrs = headConfig.htmlAttrs || {};
|
|
369
|
+
headConfig.link = headConfig.link || [];
|
|
370
|
+
headConfig.meta = headConfig.meta || [];
|
|
371
|
+
if (!hasMetaProperty(headConfig, "og:type")) {
|
|
372
|
+
headConfig.meta.push({
|
|
373
|
+
property: "og:type",
|
|
374
|
+
content: "website"
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
nuxt.options.app.head = headConfig;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const module = defineNuxtModule({
|
|
381
|
+
meta: {
|
|
382
|
+
name: "nuxt-seo-utils",
|
|
383
|
+
configKey: "seo",
|
|
384
|
+
compatibility: {
|
|
385
|
+
nuxt: ">=3.16.0"
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
moduleDependencies: {
|
|
389
|
+
"@nuxtjs/i18n": {
|
|
390
|
+
version: ">=8",
|
|
391
|
+
optional: true
|
|
392
|
+
},
|
|
393
|
+
"nuxt-i18n-micro": {
|
|
394
|
+
version: ">=1",
|
|
395
|
+
optional: true
|
|
396
|
+
},
|
|
397
|
+
"nuxt-site-config": {
|
|
398
|
+
version: ">=3"
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
defaults: {
|
|
402
|
+
enabled: true,
|
|
403
|
+
debug: false,
|
|
404
|
+
redirectToCanonicalSiteUrl: false,
|
|
405
|
+
automaticDefaults: true,
|
|
406
|
+
fallbackTitle: true,
|
|
407
|
+
metaDataFiles: true,
|
|
408
|
+
mergeWithSiteConfig: true,
|
|
409
|
+
extendRouteRules: true,
|
|
410
|
+
fixRequiredAbsoluteMetaTagsLinks: true,
|
|
411
|
+
extendNuxtConfigAppHeadSeoMeta: true,
|
|
412
|
+
treeShakeUseSeoMeta: true,
|
|
413
|
+
extendNuxtConfigAppHeadTypes: true,
|
|
414
|
+
setupNuxtConfigAppHeadWithMoreDefaults: true,
|
|
415
|
+
automaticOgAndTwitterTags: true,
|
|
416
|
+
canonicalLowercase: true
|
|
417
|
+
},
|
|
418
|
+
async setup(config, nuxt) {
|
|
419
|
+
const logger = useLogger("nuxt-seo-utils");
|
|
420
|
+
logger.level = config.debug || nuxt.options.debug ? 4 : 3;
|
|
421
|
+
if (config.enabled === false) {
|
|
422
|
+
logger.debug("The module is disabled, skipping setup.");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const { resolve } = createResolver(import.meta.url);
|
|
426
|
+
await installNuxtSiteConfig();
|
|
427
|
+
const runtimeDir = resolve("./runtime");
|
|
428
|
+
if (config.metaDataFiles) {
|
|
429
|
+
await generateTagsFromPublicFiles();
|
|
430
|
+
await generateTagsFromPageDirImages();
|
|
431
|
+
if (nuxt.options.dev) {
|
|
432
|
+
addServerHandler({
|
|
433
|
+
middleware: true,
|
|
434
|
+
handler: resolve(runtimeDir, "server/middleware/resolveImagesInPagesDir")
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const hasI18n = hasNuxtModule("@nuxtjs/i18n") || hasNuxtModule("nuxt-i18n-micro");
|
|
439
|
+
if (config.automaticDefaults) {
|
|
440
|
+
if (hasI18n) {
|
|
441
|
+
addPlugin({
|
|
442
|
+
src: resolve(runtimeDir, `./app/plugins/defaultsWaitI18n`)
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
addPlugin({
|
|
446
|
+
src: resolve(runtimeDir, `./app/plugins/defaults`)
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (config.fallbackTitle) {
|
|
451
|
+
addPlugin({
|
|
452
|
+
src: resolve(runtimeDir, "./app/plugins/titles")
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (!hasI18n) {
|
|
456
|
+
addImports({
|
|
457
|
+
from: resolve(runtimeDir, `./app/composables/polyfills`),
|
|
458
|
+
name: "useI18n"
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
addImports({
|
|
462
|
+
from: resolve(runtimeDir, `./app/composables/useBreadcrumbItems`),
|
|
463
|
+
name: "useBreadcrumbItems"
|
|
464
|
+
});
|
|
465
|
+
const polyfills = [
|
|
466
|
+
// @ts-expect-error runtime type
|
|
467
|
+
...!hasNuxtModule("nuxt-schema-org") || nuxt.options.schemaOrg?.enable === false ? [
|
|
468
|
+
"useSchemaOrg",
|
|
469
|
+
"defineWebSite",
|
|
470
|
+
"defineWebPage",
|
|
471
|
+
"defineBreadcrumb"
|
|
472
|
+
] : []
|
|
473
|
+
];
|
|
474
|
+
polyfills.forEach((name) => {
|
|
475
|
+
addImports({
|
|
476
|
+
from: resolve(runtimeDir, "./app/composables/polyfills"),
|
|
477
|
+
name
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
nuxt.options.experimental.headNext = true;
|
|
481
|
+
if (!nuxt.options.dev && config.redirectToCanonicalSiteUrl) {
|
|
482
|
+
addServerHandler({
|
|
483
|
+
handler: resolve(runtimeDir, "./server/middleware/redirectCanonical"),
|
|
484
|
+
middleware: true
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
nuxt.options.alias["#seo-utils"] = runtimeDir;
|
|
488
|
+
nuxt.options.runtimeConfig.public["seo-utils"] = defu(nuxt.options.runtimeConfig.public["seo-utils"] || {}, {
|
|
489
|
+
canonicalQueryWhitelist: config.canonicalQueryWhitelist || [
|
|
490
|
+
"page",
|
|
491
|
+
"sort",
|
|
492
|
+
"filter",
|
|
493
|
+
"search",
|
|
494
|
+
"q",
|
|
495
|
+
"category",
|
|
496
|
+
"tag"
|
|
497
|
+
],
|
|
498
|
+
canonicalLowercase: config.canonicalLowercase
|
|
499
|
+
});
|
|
500
|
+
if (config.extendNuxtConfigAppHeadSeoMeta)
|
|
501
|
+
extendNuxtConfigAppHeadSeoMeta();
|
|
502
|
+
if (config.setupNuxtConfigAppHeadWithMoreDefaults)
|
|
503
|
+
setupNuxtConfigAppHeadWithMoreDefaults(nuxt);
|
|
504
|
+
if (config.extendNuxtConfigAppHeadTypes)
|
|
505
|
+
extendNuxtConfigAppHeadTypes();
|
|
506
|
+
addTypeTemplate({
|
|
507
|
+
filename: "module/nuxt-seo-utils.d.ts",
|
|
508
|
+
getContents: (data) => {
|
|
509
|
+
const typesPath = relative(resolve(data.nuxt.options.rootDir, data.nuxt.options.buildDir, "module"), resolve("runtime/types"));
|
|
510
|
+
const types = ` interface NitroRouteRules {
|
|
511
|
+
seoMeta?: import('${typesPath}').MetaFlatSerializable
|
|
512
|
+
head?: import('${typesPath}').Head
|
|
513
|
+
}
|
|
514
|
+
interface NitroRouteConfig {
|
|
515
|
+
seoMeta?: import('${typesPath}').MetaFlatSerializable
|
|
516
|
+
head?: import('${typesPath}').Head
|
|
517
|
+
}`;
|
|
518
|
+
return `// Generated by nuxt-seo-utils
|
|
519
|
+
|
|
520
|
+
import type { RobotsContext } from '#robots/types'
|
|
521
|
+
|
|
522
|
+
declare module 'nitropack/types' {
|
|
523
|
+
${types}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
declare module 'nitropack' {
|
|
527
|
+
${types}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
declare module '@nuxt/schema' {
|
|
532
|
+
interface AppHeadMetaObject { seoMeta?: import('${typesPath}').MetaFlatSerializable }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
declare module 'nuxt/schema' {
|
|
536
|
+
interface AppHeadMetaObject { seoMeta?: import('${typesPath}').MetaFlatSerializable }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export {}
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
}, {
|
|
543
|
+
nitro: true,
|
|
544
|
+
nuxt: true
|
|
545
|
+
});
|
|
546
|
+
const appRuntimeDir = resolve(runtimeDir, "./app");
|
|
547
|
+
if (config.treeShakeUseSeoMeta)
|
|
548
|
+
addVitePlugin(UnheadVite());
|
|
549
|
+
if (config.automaticOgAndTwitterTags)
|
|
550
|
+
addPlugin({ src: resolve(appRuntimeDir, "plugins", "inferSeoMetaPlugin") });
|
|
551
|
+
if (config.mergeWithSiteConfig)
|
|
552
|
+
addPlugin({ src: resolve(appRuntimeDir, "plugins", "siteConfig") });
|
|
553
|
+
if (config.extendRouteRules)
|
|
554
|
+
addPlugin({ src: resolve(appRuntimeDir, "plugins", "0.routeRules") });
|
|
555
|
+
if (config.fixRequiredAbsoluteMetaTagsLinks)
|
|
556
|
+
addPlugin({ src: resolve(appRuntimeDir, "plugins", "1.absoluteImageUrls.server"), mode: "server" });
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
export { module as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function useSchemaOrg(): void;
|
|
2
|
+
export declare function defineWebSite(): void;
|
|
3
|
+
export declare function defineWebPage(): void;
|
|
4
|
+
export declare function defineBreadcrumb(): void;
|
|
5
|
+
export declare function useI18n(): {
|
|
6
|
+
t: (_: string, fallback: string, options: any) => string;
|
|
7
|
+
te: (_: string) => boolean;
|
|
8
|
+
strategy: string;
|
|
9
|
+
defaultLocale: import("vue").ComputedRef<any>;
|
|
10
|
+
locale: import("vue").ComputedRef<any>;
|
|
11
|
+
};
|