nuxt-seo-utils 7.0.19 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/bin/nuxt-seo-utils.mjs +2 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +118 -0
- package/dist/module.d.mts +25 -0
- package/dist/module.d.ts +140 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +229 -18
- package/dist/runtime/app/composables/polyfills.d.ts +4 -3
- package/dist/runtime/app/composables/polyfills.js +1 -2
- package/dist/runtime/app/composables/useBreadcrumbItems.d.ts +2 -2
- package/dist/runtime/app/composables/useBreadcrumbItems.js +7 -7
- package/dist/runtime/app/composables/useFallbackTitle.d.ts +1 -0
- package/dist/runtime/app/composables/useFallbackTitle.js +31 -0
- package/dist/runtime/app/composables/useShareLinks.d.ts +78 -0
- package/dist/runtime/app/composables/useShareLinks.js +94 -0
- package/dist/runtime/app/logic/applyDefaults.js +10 -6
- package/dist/runtime/app/plugins/0.routeRules.js +10 -5
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.js +1 -1
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.js +1 -1
- package/dist/runtime/app/plugins/minifyScripts.server.d.ts +2 -0
- package/dist/runtime/app/plugins/minifyScripts.server.js +52 -0
- package/dist/runtime/app/plugins/siteConfig.js +4 -3
- package/dist/runtime/app/plugins/titles.js +6 -17
- package/dist/runtime/server/middleware/redirectCanonical.js +2 -2
- package/dist/runtime/server/routes/__nuxt-seo-utils/debug.d.ts +6 -0
- package/dist/runtime/server/routes/__nuxt-seo-utils/debug.js +10 -0
- package/dist/runtime/shared/minify.d.ts +14 -0
- package/dist/runtime/shared/minify.js +133 -0
- package/package.json +75 -40
- package/dist/runtime/server/tsconfig.json +0 -3
package/dist/module.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs, { existsSync } from 'node:fs';
|
|
2
|
+
import { useNuxt, addTypeTemplate, useLogger, defineNuxtModule, createResolver, addServerHandler, hasNuxtModule, addPlugin, addImports, addVitePlugin } from '@nuxt/kit';
|
|
2
3
|
import UnheadVite from '@unhead/addons/vite';
|
|
3
4
|
import { defu } from 'defu';
|
|
4
5
|
import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
5
|
-
import { resolve,
|
|
6
|
+
import { resolve, extname, relative, dirname, basename } from 'pathe';
|
|
6
7
|
import { unpackMeta } from '@unhead/vue/utils';
|
|
7
|
-
import
|
|
8
|
-
import fs from 'node:fs';
|
|
8
|
+
import { glob } from 'tinyglobby';
|
|
9
9
|
import { encodePath, joinURL } from 'ufo';
|
|
10
10
|
import escapeRE from 'escape-string-regexp';
|
|
11
11
|
import { readFile } from 'node:fs/promises';
|
|
12
12
|
import imageSize from 'image-size';
|
|
13
|
+
import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
|
|
13
14
|
|
|
14
15
|
function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
|
|
15
16
|
const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || {};
|
|
@@ -34,7 +35,7 @@ function extendNuxtConfigAppHeadTypes(nuxt = useNuxt()) {
|
|
|
34
35
|
addTypeTemplate({
|
|
35
36
|
filename: "module/nuxt-seo-utils.assets.d.ts",
|
|
36
37
|
getContents: async () => {
|
|
37
|
-
const paths = (await
|
|
38
|
+
const paths = (await glob(["**/*"], { cwd: resolve(nuxt.options.srcDir, "public") })).map((p) => `/${p}`);
|
|
38
39
|
const jsPaths = paths.filter((p) => p.endsWith(".js") || p.endsWith(".mjs"));
|
|
39
40
|
return `// Generated by nuxt-seo-utils
|
|
40
41
|
declare module '#app' {
|
|
@@ -81,8 +82,10 @@ const MetaTagFileGlobs = [
|
|
|
81
82
|
"**/*icon-*.{ico,jpg,jpeg,png,svg}"
|
|
82
83
|
];
|
|
83
84
|
|
|
85
|
+
const INDEX_SUFFIX_RE = /-index$/;
|
|
84
86
|
function generateNuxtPageFromFile(file, pagesDir) {
|
|
85
|
-
const
|
|
87
|
+
const extPattern = new RegExp(`${escapeRE(extname(file))}$`);
|
|
88
|
+
const segments = relative(pagesDir, file).replace(extPattern, "").split("/");
|
|
86
89
|
const route = {
|
|
87
90
|
name: "",
|
|
88
91
|
path: "",
|
|
@@ -106,7 +109,7 @@ function generateNuxtPageFromFile(file, pagesDir) {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
if (route.name)
|
|
109
|
-
route.name = route.name.replace(
|
|
112
|
+
route.name = route.name.replace(INDEX_SUFFIX_RE, "");
|
|
110
113
|
return route;
|
|
111
114
|
}
|
|
112
115
|
function getRoutePath(tokens) {
|
|
@@ -160,7 +163,7 @@ function parseSegment(segment) {
|
|
|
160
163
|
}
|
|
161
164
|
if (c === "[" && state === 2 /* dynamic */)
|
|
162
165
|
state = 3 /* optional */;
|
|
163
|
-
if (c === "]" && (state !== 3 /* optional */ || buffer
|
|
166
|
+
if (c === "]" && (state !== 3 /* optional */ || buffer.at(-1) === "]")) {
|
|
164
167
|
if (!buffer)
|
|
165
168
|
throw new Error("Empty param");
|
|
166
169
|
else
|
|
@@ -185,6 +188,7 @@ function hasLinkRel(input, rel) {
|
|
|
185
188
|
function hasMetaProperty(input, property) {
|
|
186
189
|
return input.meta?.some((meta) => meta.property === property);
|
|
187
190
|
}
|
|
191
|
+
const ALT_NEWLINE_RE = /\n/g;
|
|
188
192
|
async function getImageMeta(base, path, isIcon = false) {
|
|
189
193
|
const absolutePath = resolve(base, path);
|
|
190
194
|
const file = absolutePath.split("/").pop();
|
|
@@ -202,7 +206,7 @@ async function getImageMeta(base, path, isIcon = false) {
|
|
|
202
206
|
const altTextPath = resolve(dirname(absolutePath), `${keyword}.alt.txt`);
|
|
203
207
|
if (fs.existsSync(altTextPath)) {
|
|
204
208
|
payload.alt = fs.readFileSync(altTextPath, "utf8");
|
|
205
|
-
payload.alt = String(payload.alt).replace(
|
|
209
|
+
payload.alt = String(payload.alt).replace(ALT_NEWLINE_RE, " ").trim();
|
|
206
210
|
}
|
|
207
211
|
} else {
|
|
208
212
|
if (path.includes(".dark") || path.includes("-dark"))
|
|
@@ -222,9 +226,10 @@ async function getImageDimensions(absolutePath) {
|
|
|
222
226
|
return imageSize(buffer);
|
|
223
227
|
}
|
|
224
228
|
|
|
229
|
+
const DIR_SUFFIX_RE = /\/_dir$/;
|
|
225
230
|
async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
226
231
|
const pagesDir = resolve(nuxt.options.rootDir, nuxt.options.dir.pages);
|
|
227
|
-
const files = await
|
|
232
|
+
const files = await glob(MetaTagFileGlobs, { cwd: pagesDir, onlyFiles: true });
|
|
228
233
|
const appendRouteRules = {};
|
|
229
234
|
const devMiddlewareMap = {};
|
|
230
235
|
const nitroOutputMap = [];
|
|
@@ -234,7 +239,7 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
|
234
239
|
let { path } = generateNuxtPageFromFile(resolve(pagesDir, dirname(file)), pagesDir);
|
|
235
240
|
const meta = await getImageMeta(pagesDir, file);
|
|
236
241
|
if (path.endsWith("/_dir"))
|
|
237
|
-
path = path.replace(
|
|
242
|
+
path = path.replace(DIR_SUFFIX_RE, "");
|
|
238
243
|
const src = joinURL(path, fileName);
|
|
239
244
|
if (["icon", "apple-touch-icon", "apple-icon"].includes(keyword) || keyword.startsWith("icon-")) {
|
|
240
245
|
appendRouteRules[`${path}/**`] = {
|
|
@@ -280,7 +285,7 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
|
280
285
|
|
|
281
286
|
async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
282
287
|
const publicDirPath = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
|
|
283
|
-
const rootPublicFiles = (await
|
|
288
|
+
const rootPublicFiles = (await glob(MetaTagFileGlobs, { cwd: publicDirPath, onlyFiles: true, deep: 1 })).map((file) => basename(file));
|
|
284
289
|
const headConfig = defu(nuxt.options.app.head, {
|
|
285
290
|
link: [],
|
|
286
291
|
meta: []
|
|
@@ -360,6 +365,163 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
|
360
365
|
}
|
|
361
366
|
}
|
|
362
367
|
nuxt.options.app.head = headConfig;
|
|
368
|
+
const hasIcons = rootPublicFiles.some((f) => f.includes("icon") || f === "favicon.ico");
|
|
369
|
+
return { hasIcons };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const INLINE_SCRIPT_RE = /<script(?![^>]+\bsrc\b)([^>]*)>([\s\S]*?)<\/script\s*>/gi;
|
|
373
|
+
const INLINE_STYLE_RE = /<style([^>]*)>([\s\S]*?)<\/style\s*>/gi;
|
|
374
|
+
const TYPE_ATTR_RE = /\btype\s*=\s*["']([^"']*)["']/i;
|
|
375
|
+
const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json"]);
|
|
376
|
+
const SKIP_JS_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
|
|
377
|
+
const logger = useLogger("nuxt-seo-utils");
|
|
378
|
+
async function minifyJSBuildTime(code) {
|
|
379
|
+
try {
|
|
380
|
+
const rolldownPath = "rolldown/experimental";
|
|
381
|
+
const { minify } = await import(
|
|
382
|
+
/* @vite-ignore */
|
|
383
|
+
rolldownPath
|
|
384
|
+
);
|
|
385
|
+
const result = await minify("inline.js", code);
|
|
386
|
+
return result.code.trim();
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const esbuild = await import('esbuild');
|
|
391
|
+
const result = await esbuild.transform(code, {
|
|
392
|
+
minify: true,
|
|
393
|
+
loader: "js"
|
|
394
|
+
});
|
|
395
|
+
return result.code.trim();
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
logger.debug("Build-time JS minification skipped: neither rolldown nor esbuild is available. Install one as a dependency to enable it.");
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
async function minifyCSSWithLightningCSS(code) {
|
|
402
|
+
try {
|
|
403
|
+
const lightningcss = await import('lightningcss');
|
|
404
|
+
const result = lightningcss.transform({
|
|
405
|
+
filename: "inline.css",
|
|
406
|
+
code: new TextEncoder().encode(code),
|
|
407
|
+
minify: true
|
|
408
|
+
});
|
|
409
|
+
return new TextDecoder().decode(result.code).trim();
|
|
410
|
+
} catch {
|
|
411
|
+
logger.debug("Build-time CSS minification skipped: lightningcss is not available. Install it as a dependency to enable it.");
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function minifyPrerender() {
|
|
416
|
+
const nuxt = useNuxt();
|
|
417
|
+
nuxt.hooks.hook("app:resolve", async () => {
|
|
418
|
+
const head = nuxt.options.app.head;
|
|
419
|
+
const promises = [];
|
|
420
|
+
if (head.script?.length) {
|
|
421
|
+
for (const script of head.script) {
|
|
422
|
+
if (typeof script === "string")
|
|
423
|
+
continue;
|
|
424
|
+
const content = String(script.innerHTML || script.textContent || "");
|
|
425
|
+
if (!content)
|
|
426
|
+
continue;
|
|
427
|
+
const setContent = (val) => {
|
|
428
|
+
if (script.innerHTML)
|
|
429
|
+
script.innerHTML = val;
|
|
430
|
+
else if (script.textContent)
|
|
431
|
+
script.textContent = val;
|
|
432
|
+
};
|
|
433
|
+
if (script.type && JSON_TYPES.has(script.type)) {
|
|
434
|
+
try {
|
|
435
|
+
const minified = JSON.stringify(JSON.parse(content));
|
|
436
|
+
if (minified.length < content.length)
|
|
437
|
+
setContent(minified);
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (script.type && SKIP_JS_TYPES.has(script.type))
|
|
443
|
+
continue;
|
|
444
|
+
promises.push(minifyJSBuildTime(content).then((minified) => {
|
|
445
|
+
if (minified && minified.length < content.length)
|
|
446
|
+
setContent(minified);
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (head.style?.length) {
|
|
451
|
+
for (const style of head.style) {
|
|
452
|
+
if (typeof style === "string")
|
|
453
|
+
continue;
|
|
454
|
+
const content = String(style.innerHTML || style.textContent || "");
|
|
455
|
+
if (!content)
|
|
456
|
+
continue;
|
|
457
|
+
promises.push(minifyCSSWithLightningCSS(content).then((minified) => {
|
|
458
|
+
if (minified && minified.length < content.length) {
|
|
459
|
+
if (style.innerHTML)
|
|
460
|
+
style.innerHTML = minified;
|
|
461
|
+
else if (style.textContent)
|
|
462
|
+
style.textContent = minified;
|
|
463
|
+
}
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
await Promise.all(promises);
|
|
468
|
+
});
|
|
469
|
+
nuxt.hooks.hook("nitro:init", (nitro) => {
|
|
470
|
+
nitro.hooks.hook("prerender:generate", async (route) => {
|
|
471
|
+
if (!route.contents || !route.contentType?.includes("html"))
|
|
472
|
+
return;
|
|
473
|
+
route.contents = await replaceAsync(
|
|
474
|
+
route.contents,
|
|
475
|
+
new RegExp(INLINE_SCRIPT_RE.source, INLINE_SCRIPT_RE.flags),
|
|
476
|
+
async (fullMatch, attrs, content) => {
|
|
477
|
+
if (!content)
|
|
478
|
+
return fullMatch;
|
|
479
|
+
const typeMatch = TYPE_ATTR_RE.exec(attrs);
|
|
480
|
+
const type = typeMatch?.[1];
|
|
481
|
+
if (type && JSON_TYPES.has(type)) {
|
|
482
|
+
try {
|
|
483
|
+
const minified2 = JSON.stringify(JSON.parse(content));
|
|
484
|
+
if (minified2.length < content.length)
|
|
485
|
+
return `<script${attrs}>${minified2}<\/script>`;
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
return fullMatch;
|
|
489
|
+
}
|
|
490
|
+
if (type && SKIP_JS_TYPES.has(type))
|
|
491
|
+
return fullMatch;
|
|
492
|
+
const minified = await minifyJSBuildTime(content);
|
|
493
|
+
if (minified && minified.length < content.length)
|
|
494
|
+
return `<script${attrs}>${minified}<\/script>`;
|
|
495
|
+
return fullMatch;
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
route.contents = await replaceAsync(
|
|
499
|
+
route.contents,
|
|
500
|
+
new RegExp(INLINE_STYLE_RE.source, INLINE_STYLE_RE.flags),
|
|
501
|
+
async (fullMatch, attrs, content) => {
|
|
502
|
+
if (!content)
|
|
503
|
+
return fullMatch;
|
|
504
|
+
const minified = await minifyCSSWithLightningCSS(content);
|
|
505
|
+
if (minified && minified.length < content.length)
|
|
506
|
+
return `<style${attrs}>${minified}</style>`;
|
|
507
|
+
return fullMatch;
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async function replaceAsync(str, regex, asyncFn) {
|
|
514
|
+
const matches = [];
|
|
515
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
516
|
+
for (let m = re.exec(str); m !== null; m = re.exec(str))
|
|
517
|
+
matches.push({ match: m, replacement: asyncFn(...m) });
|
|
518
|
+
const replacements = await Promise.all(matches.map((m) => m.replacement));
|
|
519
|
+
let result = str;
|
|
520
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
521
|
+
const { match } = matches[i];
|
|
522
|
+
result = result.slice(0, match.index) + replacements[i] + result.slice(match.index + match[0].length);
|
|
523
|
+
}
|
|
524
|
+
return result;
|
|
363
525
|
}
|
|
364
526
|
|
|
365
527
|
function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
|
|
@@ -377,7 +539,16 @@ function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
|
|
|
377
539
|
nuxt.options.app.head = headConfig;
|
|
378
540
|
}
|
|
379
541
|
|
|
380
|
-
|
|
542
|
+
function setupDevToolsUI(resolve, nuxt = useNuxt()) {
|
|
543
|
+
setupDevToolsUI$1({
|
|
544
|
+
route: "/__nuxt-seo-utils",
|
|
545
|
+
name: "nuxt-seo-utils",
|
|
546
|
+
title: "SEO Utils",
|
|
547
|
+
icon: "carbon:search-locate"
|
|
548
|
+
}, resolve, nuxt);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const module$1 = defineNuxtModule({
|
|
381
552
|
meta: {
|
|
382
553
|
name: "nuxt-seo-utils",
|
|
383
554
|
configKey: "seo",
|
|
@@ -395,7 +566,7 @@ const module = defineNuxtModule({
|
|
|
395
566
|
optional: true
|
|
396
567
|
},
|
|
397
568
|
"nuxt-site-config": {
|
|
398
|
-
version: ">=3"
|
|
569
|
+
version: ">=3.2"
|
|
399
570
|
}
|
|
400
571
|
},
|
|
401
572
|
defaults: {
|
|
@@ -413,7 +584,9 @@ const module = defineNuxtModule({
|
|
|
413
584
|
extendNuxtConfigAppHeadTypes: true,
|
|
414
585
|
setupNuxtConfigAppHeadWithMoreDefaults: true,
|
|
415
586
|
automaticOgAndTwitterTags: true,
|
|
416
|
-
canonicalLowercase: true
|
|
587
|
+
canonicalLowercase: true,
|
|
588
|
+
tagPriority: 30,
|
|
589
|
+
minify: true
|
|
417
590
|
},
|
|
418
591
|
async setup(config, nuxt) {
|
|
419
592
|
const logger = useLogger("nuxt-seo-utils");
|
|
@@ -426,9 +599,17 @@ const module = defineNuxtModule({
|
|
|
426
599
|
await installNuxtSiteConfig();
|
|
427
600
|
const runtimeDir = resolve("./runtime");
|
|
428
601
|
if (config.metaDataFiles) {
|
|
429
|
-
await generateTagsFromPublicFiles();
|
|
602
|
+
const { hasIcons } = await generateTagsFromPublicFiles();
|
|
430
603
|
await generateTagsFromPageDirImages();
|
|
431
604
|
if (nuxt.options.dev) {
|
|
605
|
+
if (!hasIcons) {
|
|
606
|
+
const publicDir = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
|
|
607
|
+
const logoPatterns = ["logo.svg", "logo.png", "icon.svg", "icon.png", "logo.jpg", "logo.webp"];
|
|
608
|
+
const foundSource = logoPatterns.find((p) => existsSync(resolve(publicDir, p))) || logoPatterns.find((p) => existsSync(resolve(nuxt.options.rootDir, p)));
|
|
609
|
+
const sourceArg = foundSource || "<your-logo.svg>";
|
|
610
|
+
logger.info(`No favicon detected. Generate icons with:
|
|
611
|
+
npx nuxt-seo-utils icons --source ${sourceArg}`);
|
|
612
|
+
}
|
|
432
613
|
addServerHandler({
|
|
433
614
|
middleware: true,
|
|
434
615
|
handler: resolve(runtimeDir, "server/middleware/resolveImagesInPagesDir")
|
|
@@ -462,6 +643,14 @@ const module = defineNuxtModule({
|
|
|
462
643
|
from: resolve(runtimeDir, `./app/composables/useBreadcrumbItems`),
|
|
463
644
|
name: "useBreadcrumbItems"
|
|
464
645
|
});
|
|
646
|
+
addImports({
|
|
647
|
+
from: resolve(runtimeDir, `./app/composables/useFallbackTitle`),
|
|
648
|
+
name: "useFallbackTitle"
|
|
649
|
+
});
|
|
650
|
+
addImports({
|
|
651
|
+
from: resolve(runtimeDir, `./app/composables/useShareLinks`),
|
|
652
|
+
name: "useShareLinks"
|
|
653
|
+
});
|
|
465
654
|
const polyfills = [
|
|
466
655
|
// @ts-expect-error runtime type
|
|
467
656
|
...!hasNuxtModule("nuxt-schema-org") || nuxt.options.schemaOrg?.enable === false ? [
|
|
@@ -495,7 +684,8 @@ const module = defineNuxtModule({
|
|
|
495
684
|
"category",
|
|
496
685
|
"tag"
|
|
497
686
|
],
|
|
498
|
-
canonicalLowercase: config.canonicalLowercase
|
|
687
|
+
canonicalLowercase: config.canonicalLowercase,
|
|
688
|
+
tagPriority: config.tagPriority
|
|
499
689
|
});
|
|
500
690
|
if (config.extendNuxtConfigAppHeadSeoMeta)
|
|
501
691
|
extendNuxtConfigAppHeadSeoMeta();
|
|
@@ -557,7 +747,28 @@ export {}
|
|
|
557
747
|
addPlugin({ src: resolve(appRuntimeDir, "plugins", "0.routeRules") });
|
|
558
748
|
if (config.fixRequiredAbsoluteMetaTagsLinks)
|
|
559
749
|
addPlugin({ src: resolve(appRuntimeDir, "plugins", "1.absoluteImageUrls.server"), mode: "server" });
|
|
750
|
+
if (!nuxt.options.dev && config.minify) {
|
|
751
|
+
const minifyOpts = config.minify === true ? { build: true, runtime: true } : config.minify;
|
|
752
|
+
if (minifyOpts.runtime !== false) {
|
|
753
|
+
addPlugin({
|
|
754
|
+
src: resolve(appRuntimeDir, "plugins", "minifyScripts.server"),
|
|
755
|
+
mode: "server"
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (minifyOpts.build !== false) {
|
|
759
|
+
minifyPrerender();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (config.debug || nuxt.options.dev) {
|
|
763
|
+
addServerHandler({
|
|
764
|
+
route: "/__nuxt-seo-utils/debug.json",
|
|
765
|
+
handler: resolve("./runtime/server/routes/__nuxt-seo-utils/debug")
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (nuxt.options.dev) {
|
|
769
|
+
setupDevToolsUI(resolve, nuxt);
|
|
770
|
+
}
|
|
560
771
|
}
|
|
561
772
|
});
|
|
562
773
|
|
|
563
|
-
export { module as default };
|
|
774
|
+
export { module$1 as default };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue';
|
|
1
2
|
export declare function useSchemaOrg(): void;
|
|
2
3
|
export declare function defineWebSite(): void;
|
|
3
4
|
export declare function defineWebPage(): void;
|
|
4
5
|
export declare function defineBreadcrumb(): void;
|
|
5
6
|
export declare function useI18n(): {
|
|
6
|
-
t: (_: string, fallback: string,
|
|
7
|
+
t: (_: string, fallback: string, _options: any) => string;
|
|
7
8
|
te: (_: string) => boolean;
|
|
8
9
|
strategy: string;
|
|
9
|
-
defaultLocale:
|
|
10
|
-
locale:
|
|
10
|
+
defaultLocale: ComputedRef<string>;
|
|
11
|
+
locale: ComputedRef<string>;
|
|
11
12
|
};
|
|
@@ -13,8 +13,7 @@ export function useI18n() {
|
|
|
13
13
|
resolveRefs: false
|
|
14
14
|
});
|
|
15
15
|
return {
|
|
16
|
-
|
|
17
|
-
t: (_, fallback, options) => fallback,
|
|
16
|
+
t: (_, fallback, _options) => fallback,
|
|
18
17
|
te: (_) => false,
|
|
19
18
|
strategy: "no_prefix",
|
|
20
19
|
defaultLocale: computed(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NuxtLinkProps } from 'nuxt/app';
|
|
2
|
-
import type { MaybeRefOrGetter } from 'vue';
|
|
2
|
+
import type { MaybeRefOrGetter, Ref } from 'vue';
|
|
3
3
|
interface NuxtUIBreadcrumbItem extends NuxtLinkProps {
|
|
4
4
|
label: string;
|
|
5
5
|
labelClass?: string;
|
|
@@ -108,5 +108,5 @@ export interface BreadcrumbItemProps extends NuxtUIBreadcrumbItem {
|
|
|
108
108
|
* @see https://github.com/harlan-zw/nuxt-seo/blob/main/src/runtime/nuxt/composables/useBreadcrumbItems.ts
|
|
109
109
|
* @docs https://nuxtseo.com/nuxt-seo/api/breadcrumbs
|
|
110
110
|
*/
|
|
111
|
-
export declare function useBreadcrumbItems(_options?: BreadcrumbProps):
|
|
111
|
+
export declare function useBreadcrumbItems(_options?: BreadcrumbProps): Ref<BreadcrumbItemProps[]>;
|
|
112
112
|
export {};
|
|
@@ -19,10 +19,11 @@ import {
|
|
|
19
19
|
} from "vue";
|
|
20
20
|
import { pathBreadcrumbSegments } from "../../shared/breadcrumbs.js";
|
|
21
21
|
function withoutQuery(path) {
|
|
22
|
-
return path.split("?")[0];
|
|
22
|
+
return path.split("?")[0] || "";
|
|
23
23
|
}
|
|
24
|
+
const TITLE_CASE_RE = /\w\S*/g;
|
|
24
25
|
function titleCase(s) {
|
|
25
|
-
return s.replaceAll("-", " ").replace(
|
|
26
|
+
return s.replaceAll("-", " ").replace(TITLE_CASE_RE, (w) => w.charAt(0).toUpperCase() + w.substr(1).toLowerCase());
|
|
26
27
|
}
|
|
27
28
|
const BreadcrumbCtx = Symbol("BreadcrumbCtx");
|
|
28
29
|
export function useBreadcrumbItems(_options = {}) {
|
|
@@ -171,18 +172,17 @@ export function useBreadcrumbItems(_options = {}) {
|
|
|
171
172
|
return m;
|
|
172
173
|
}).filter(Boolean);
|
|
173
174
|
});
|
|
174
|
-
watch(items, (newItems) => {
|
|
175
|
+
watch(() => items.value, (newItems) => {
|
|
175
176
|
if (!pauseUpdates.value) {
|
|
176
177
|
lastBreadcrumbs.value = newItems;
|
|
177
178
|
}
|
|
178
179
|
}, { immediate: true, flush: "sync" });
|
|
179
180
|
const schemaOrgEnabled = typeof _options.schemaOrg === "undefined" ? true : _options.schemaOrg;
|
|
180
|
-
if ((import.meta.
|
|
181
|
+
if ((import.meta.server || import.meta.env?.NODE_ENV === "test") && schemaOrgEnabled) {
|
|
181
182
|
useSchemaOrg([
|
|
182
|
-
// @ts-expect-error untyped
|
|
183
183
|
defineBreadcrumb({
|
|
184
|
-
id: `#${id}`,
|
|
185
|
-
itemListElement: computed(() => items.value.map((item) => ({
|
|
184
|
+
"@id": `#${id}`,
|
|
185
|
+
"itemListElement": computed(() => items.value.map((item) => ({
|
|
186
186
|
name: item.label || item.ariaLabel,
|
|
187
187
|
item: item.to ? siteResolver(item.to) : void 0
|
|
188
188
|
})))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useFallbackTitle(): import("vue").ComputedRef<string | null>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useI18n } from "#imports";
|
|
2
|
+
import { useError, useRoute } from "nuxt/app";
|
|
3
|
+
import { titleCase } from "scule";
|
|
4
|
+
import { withoutTrailingSlash } from "ufo";
|
|
5
|
+
import { computed } from "vue";
|
|
6
|
+
export function useFallbackTitle() {
|
|
7
|
+
const route = useRoute();
|
|
8
|
+
const err = useError();
|
|
9
|
+
let i18n;
|
|
10
|
+
try {
|
|
11
|
+
i18n = useI18n();
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
return computed(() => {
|
|
15
|
+
if (err.value?.statusCode && [404, 500].includes(err.value.statusCode)) {
|
|
16
|
+
return `${err.value.statusCode} - ${err.value.message}`;
|
|
17
|
+
}
|
|
18
|
+
if (typeof route.meta?.title === "string")
|
|
19
|
+
return route.meta?.title;
|
|
20
|
+
const path = withoutTrailingSlash(route.path || "/");
|
|
21
|
+
const lastSegment = path.split("/").pop();
|
|
22
|
+
let fallback = lastSegment ? titleCase(lastSegment) : null;
|
|
23
|
+
const matched = route.matched?.at(-1);
|
|
24
|
+
if (matched) {
|
|
25
|
+
const routeName = String(matched.name).split("___")?.[0];
|
|
26
|
+
if (routeName && i18n)
|
|
27
|
+
fallback = i18n.t(`pages.${routeName}.title`, fallback || "", { missingWarn: false }) || fallback;
|
|
28
|
+
}
|
|
29
|
+
return fallback;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
|
2
|
+
export type SharePlatform = 'twitter' | 'facebook' | 'linkedin' | 'whatsapp' | 'telegram' | 'reddit' | 'pinterest' | 'email';
|
|
3
|
+
export interface ShareLinkUtmParams {
|
|
4
|
+
/**
|
|
5
|
+
* Set to 'auto' to use the platform name as utm_source per link.
|
|
6
|
+
* When a string (other than 'auto'), use that value for all platforms.
|
|
7
|
+
* @default undefined
|
|
8
|
+
*/
|
|
9
|
+
source?: 'auto' | (string & {});
|
|
10
|
+
medium?: string;
|
|
11
|
+
campaign?: string;
|
|
12
|
+
term?: string;
|
|
13
|
+
content?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ShareLinksOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Override the URL to share. Defaults to the canonical URL of the current page.
|
|
18
|
+
*/
|
|
19
|
+
url?: MaybeRefOrGetter<string | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* The title/text to share. Defaults to the site name from site config.
|
|
22
|
+
*/
|
|
23
|
+
title?: MaybeRefOrGetter<string | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* UTM tracking parameters. Defaults to `true` for automatic per-platform tracking
|
|
26
|
+
* (utm_source = platform name, utm_medium = 'social'/'email'). Pass `false` to disable,
|
|
27
|
+
* or an object for manual control.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Auto: utm_source=twitter&utm_medium=social, utm_source=facebook&utm_medium=email, etc.
|
|
31
|
+
* utm: true
|
|
32
|
+
*
|
|
33
|
+
* // Manual: same utm params on all platforms
|
|
34
|
+
* utm: { source: 'newsletter', medium: 'email', campaign: 'launch' }
|
|
35
|
+
*
|
|
36
|
+
* // Hybrid: auto source + medium per platform, with custom campaign
|
|
37
|
+
* utm: { source: 'auto', campaign: 'launch' }
|
|
38
|
+
*/
|
|
39
|
+
utm?: MaybeRefOrGetter<boolean | ShareLinkUtmParams | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Twitter/X specific options.
|
|
42
|
+
*/
|
|
43
|
+
twitter?: {
|
|
44
|
+
/** Twitter username to attribute (without @). */
|
|
45
|
+
via?: string;
|
|
46
|
+
/** Hashtags to include (without #). */
|
|
47
|
+
hashtags?: string[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Facebook specific options.
|
|
51
|
+
*/
|
|
52
|
+
facebook?: {
|
|
53
|
+
/** Pre-filled text to accompany the share. */
|
|
54
|
+
quote?: string;
|
|
55
|
+
/** A single hashtag (without #, e.g. "nuxt"). */
|
|
56
|
+
hashtag?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Pinterest specific options.
|
|
60
|
+
*/
|
|
61
|
+
pinterest?: {
|
|
62
|
+
/** Direct image URL to pin. */
|
|
63
|
+
media?: MaybeRefOrGetter<string | undefined>;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export type ShareLinks = Record<SharePlatform, string> & {
|
|
67
|
+
/** The resolved canonical URL (without UTM params), useful for "copy link" buttons. */
|
|
68
|
+
canonicalUrl: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Generate social share URLs for the current page across multiple platforms.
|
|
72
|
+
*
|
|
73
|
+
* Uses the site config base URL and canonical URL logic to build share links
|
|
74
|
+
* with optional UTM parameter tracking.
|
|
75
|
+
*
|
|
76
|
+
* @docs https://nuxtseo.com/nuxt-seo/api/share-links
|
|
77
|
+
*/
|
|
78
|
+
export declare function useShareLinks(options?: ShareLinksOptions): ComputedRef<ShareLinks>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
2
|
+
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
3
|
+
import { useRoute, useRuntimeConfig } from "nuxt/app";
|
|
4
|
+
import { stringifyQuery, withQuery } from "ufo";
|
|
5
|
+
import { computed, toValue } from "vue";
|
|
6
|
+
function buildShareUrl(platform, url, title, options) {
|
|
7
|
+
const encodedUrl = encodeURIComponent(url);
|
|
8
|
+
const encodedTitle = encodeURIComponent(title);
|
|
9
|
+
switch (platform) {
|
|
10
|
+
case "twitter": {
|
|
11
|
+
let twitterUrl = `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
|
|
12
|
+
if (options.twitter?.via)
|
|
13
|
+
twitterUrl += `&via=${encodeURIComponent(options.twitter.via)}`;
|
|
14
|
+
if (options.twitter?.hashtags?.length)
|
|
15
|
+
twitterUrl += `&hashtags=${encodeURIComponent(options.twitter.hashtags.join(","))}`;
|
|
16
|
+
return twitterUrl;
|
|
17
|
+
}
|
|
18
|
+
case "facebook": {
|
|
19
|
+
let fbUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`;
|
|
20
|
+
if (options.facebook?.quote)
|
|
21
|
+
fbUrl += `"e=${encodeURIComponent(options.facebook.quote)}`;
|
|
22
|
+
if (options.facebook?.hashtag)
|
|
23
|
+
fbUrl += `&hashtag=${encodeURIComponent(`#${options.facebook.hashtag}`)}`;
|
|
24
|
+
return fbUrl;
|
|
25
|
+
}
|
|
26
|
+
case "linkedin":
|
|
27
|
+
return `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
|
|
28
|
+
case "whatsapp":
|
|
29
|
+
return `https://wa.me/?text=${encodedTitle}%20${encodedUrl}`;
|
|
30
|
+
case "telegram":
|
|
31
|
+
return `https://t.me/share/url?url=${encodedUrl}&text=${encodedTitle}`;
|
|
32
|
+
case "reddit":
|
|
33
|
+
return `https://www.reddit.com/submit?url=${encodedUrl}&title=${encodedTitle}`;
|
|
34
|
+
case "pinterest": {
|
|
35
|
+
let pinUrl = `https://pinterest.com/pin/create/button/?url=${encodedUrl}&description=${encodedTitle}`;
|
|
36
|
+
const media = toValue(options.pinterest?.media);
|
|
37
|
+
if (media)
|
|
38
|
+
pinUrl += `&media=${encodeURIComponent(media)}`;
|
|
39
|
+
return pinUrl;
|
|
40
|
+
}
|
|
41
|
+
case "email":
|
|
42
|
+
return `mailto:?subject=${encodedTitle}&body=${encodedUrl}`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function useShareLinks(options = {}) {
|
|
46
|
+
const route = useRoute();
|
|
47
|
+
const siteConfig = useSiteConfig();
|
|
48
|
+
const { canonicalQueryWhitelist, canonicalLowercase } = useRuntimeConfig().public["seo-utils"];
|
|
49
|
+
const resolveUrl = createSitePathResolver({ withBase: true, absolute: true });
|
|
50
|
+
return computed(() => {
|
|
51
|
+
const customUrl = toValue(options.url);
|
|
52
|
+
let canonicalUrl;
|
|
53
|
+
if (customUrl) {
|
|
54
|
+
canonicalUrl = customUrl;
|
|
55
|
+
} else {
|
|
56
|
+
let url = resolveUrl(route.path || "/").value || route.path;
|
|
57
|
+
if (canonicalLowercase) {
|
|
58
|
+
url = url.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
const filteredQuery = Object.fromEntries(
|
|
61
|
+
Object.entries(route.query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
|
|
62
|
+
);
|
|
63
|
+
canonicalUrl = Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
|
|
64
|
+
}
|
|
65
|
+
const utm = toValue(options.utm) ?? true;
|
|
66
|
+
const title = toValue(options.title) || siteConfig.name || "";
|
|
67
|
+
const platforms = ["twitter", "facebook", "linkedin", "whatsapp", "telegram", "reddit", "pinterest", "email"];
|
|
68
|
+
const result = { canonicalUrl };
|
|
69
|
+
for (const platform of platforms) {
|
|
70
|
+
const query = {};
|
|
71
|
+
if (utm === false) {
|
|
72
|
+
} else if (utm === true || typeof utm === "object" && utm.source === "auto") {
|
|
73
|
+
query.utm_source = platform;
|
|
74
|
+
query.utm_medium = utm !== true && utm?.medium || (platform === "email" ? "email" : "social");
|
|
75
|
+
} else if (utm) {
|
|
76
|
+
if (utm.source)
|
|
77
|
+
query.utm_source = utm.source;
|
|
78
|
+
if (utm.medium)
|
|
79
|
+
query.utm_medium = utm.medium;
|
|
80
|
+
}
|
|
81
|
+
if (typeof utm === "object") {
|
|
82
|
+
if (utm.campaign)
|
|
83
|
+
query.utm_campaign = utm.campaign;
|
|
84
|
+
if (utm.term)
|
|
85
|
+
query.utm_term = utm.term;
|
|
86
|
+
if (utm.content)
|
|
87
|
+
query.utm_content = utm.content;
|
|
88
|
+
}
|
|
89
|
+
const url = Object.keys(query).length > 0 ? withQuery(canonicalUrl, query) : canonicalUrl;
|
|
90
|
+
result[platform] = buildShareUrl(platform, url, title, options);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
});
|
|
94
|
+
}
|