nuxt-seo-utils 8.3.2 → 8.4.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/dist/cli.mjs +11 -32
- package/dist/devtools/lib/seo-utils/state.ts +5 -2
- package/dist/devtools/lib/seo-utils/tools.test.ts +31 -0
- package/dist/devtools/lib/seo-utils/tools.ts +42 -3
- package/dist/devtools/pages/seo-utils/identity.vue +17 -14
- package/dist/module.json +2 -2
- package/dist/module.mjs +188 -113
- package/dist/runtime/app/composables/useBreadcrumbItems.js +6 -3
- package/dist/runtime/app/logic/colorModeIcons.d.ts +3 -0
- package/dist/runtime/app/logic/colorModeIcons.js +10 -0
- package/dist/runtime/app/plugins/0.routeRules.js +11 -13
- package/dist/runtime/app/plugins/colorModeIcons.d.ts +2 -0
- package/dist/runtime/app/plugins/colorModeIcons.js +17 -0
- package/dist/runtime/app/plugins/minifyScripts.server.js +1 -3
- package/dist/runtime/server/middleware/redirectCanonical.d.ts +1 -1
- package/dist/runtime/server/middleware/redirectCanonical.js +1 -1
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.d.ts +1 -1
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +1 -1
- package/dist/runtime/server/plugins/minifyHtml.d.ts +1 -1
- package/dist/runtime/server/plugins/minifyHtml.js +2 -4
- package/dist/runtime/server/routes/__nuxt-seo-utils/debug.d.ts +1 -17
- package/dist/runtime/server/routes/__nuxt-seo-utils/debug.js +3 -2
- package/dist/runtime/shared/breadcrumbs.d.ts +8 -0
- package/dist/runtime/shared/breadcrumbs.js +10 -0
- package/dist/runtime/shared/minify.d.ts +2 -0
- package/dist/runtime/shared/minify.js +2 -0
- package/dist/runtime/types.d.ts +5 -1
- package/dist/shared/nuxt-seo-utils.NAV2YNQn.mjs +94 -0
- package/package.json +28 -31
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
1
|
import { existsSync } from 'node:fs';
|
|
3
2
|
import { writeFile } from 'node:fs/promises';
|
|
4
3
|
import process from 'node:process';
|
|
@@ -6,9 +5,11 @@ import { loadNuxtConfig } from '@nuxt/kit';
|
|
|
6
5
|
import { defineCommand, runMain } from 'citty';
|
|
7
6
|
import { colors } from 'consola/utils';
|
|
8
7
|
import { resolve } from 'pathe';
|
|
8
|
+
import { p as pngToIco } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
|
|
9
|
+
import 'node:buffer';
|
|
9
10
|
|
|
10
11
|
const name = "nuxt-seo-utils";
|
|
11
|
-
const version = "8.
|
|
12
|
+
const version = "8.4.0";
|
|
12
13
|
const description = "SEO utilities to improve your Nuxt sites discoverability and shareability";
|
|
13
14
|
|
|
14
15
|
const ICON_SIZES = [
|
|
@@ -27,33 +28,6 @@ const cwdArgs = {
|
|
|
27
28
|
default: "."
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
|
-
function pngToIco(pngBuffers) {
|
|
31
|
-
const count = pngBuffers.length;
|
|
32
|
-
const headerSize = 6;
|
|
33
|
-
const dirEntrySize = 16;
|
|
34
|
-
const dataOffset = headerSize + dirEntrySize * count;
|
|
35
|
-
const header = Buffer.alloc(headerSize);
|
|
36
|
-
header.writeUInt16LE(0, 0);
|
|
37
|
-
header.writeUInt16LE(1, 2);
|
|
38
|
-
header.writeUInt16LE(count, 4);
|
|
39
|
-
const dirEntries = [];
|
|
40
|
-
let offset = dataOffset;
|
|
41
|
-
for (const png of pngBuffers) {
|
|
42
|
-
const entry = Buffer.alloc(dirEntrySize);
|
|
43
|
-
const size = Math.min(256, 32);
|
|
44
|
-
entry.writeUInt8(size === 256 ? 0 : size, 0);
|
|
45
|
-
entry.writeUInt8(size === 256 ? 0 : size, 1);
|
|
46
|
-
entry.writeUInt8(0, 2);
|
|
47
|
-
entry.writeUInt8(0, 3);
|
|
48
|
-
entry.writeUInt16LE(1, 4);
|
|
49
|
-
entry.writeUInt16LE(32, 6);
|
|
50
|
-
entry.writeUInt32LE(png.length, 8);
|
|
51
|
-
entry.writeUInt32LE(offset, 12);
|
|
52
|
-
dirEntries.push(entry);
|
|
53
|
-
offset += png.length;
|
|
54
|
-
}
|
|
55
|
-
return Buffer.concat([header, ...dirEntries, ...pngBuffers]);
|
|
56
|
-
}
|
|
57
31
|
async function loadSharp() {
|
|
58
32
|
const mod = await import('sharp').catch(() => {
|
|
59
33
|
console.error(colors.red("sharp is required for icon generation. Install it with:"));
|
|
@@ -107,9 +81,14 @@ const main = defineCommand({
|
|
|
107
81
|
await sharp(sourcePath).resize(icon.width, icon.height, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toFile(outputPath);
|
|
108
82
|
console.log(colors.green(` ${icon.name} (${icon.width}x${icon.height})`));
|
|
109
83
|
}
|
|
110
|
-
const
|
|
111
|
-
await
|
|
112
|
-
|
|
84
|
+
const faviconSizes = [16, 32, 48];
|
|
85
|
+
const faviconPngs = await Promise.all(faviconSizes.map(async (size) => ({
|
|
86
|
+
buffer: await sharp(sourcePath).resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer(),
|
|
87
|
+
width: size,
|
|
88
|
+
height: size
|
|
89
|
+
})));
|
|
90
|
+
await writeFile(resolve(publicDir, "favicon.ico"), pngToIco(faviconPngs));
|
|
91
|
+
console.log(colors.green(" favicon.ico (16x16, 32x32, 48x48)"));
|
|
113
92
|
console.log(colors.bold(colors.green("\nDone! Generated all icon variants.")));
|
|
114
93
|
}
|
|
115
94
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ref, watch } from 'vue'
|
|
2
2
|
|
|
3
3
|
export interface DebugData {
|
|
4
|
-
runtimeConfig: { version: string, [key: string]: any }
|
|
4
|
+
runtimeConfig: { version: string, appBaseURL: string, [key: string]: any }
|
|
5
5
|
siteConfig: {
|
|
6
6
|
url: string
|
|
7
7
|
name: string
|
|
@@ -27,7 +27,10 @@ export async function refreshSources() {
|
|
|
27
27
|
return
|
|
28
28
|
data.value = await appFetch.value('/__nuxt-seo-utils/debug.json', {
|
|
29
29
|
query: { path: path.value },
|
|
30
|
-
}).catch(() =>
|
|
30
|
+
}).catch(() => {
|
|
31
|
+
// A failed devtools refresh means no debug data is available until the next poll.
|
|
32
|
+
return null
|
|
33
|
+
})
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
watch([path, appFetch, refreshTime], () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { getIconIssues, parseMetaTags, resolveAppUrl } from './tools'
|
|
5
|
+
|
|
6
|
+
describe('identity icon inspection', () => {
|
|
7
|
+
it('finds semantic icon rel tokens case-insensitively', () => {
|
|
8
|
+
const parsed = parseMetaTags('<link rel="alternate ICON" href="/favicon.ico" sizes="any">')
|
|
9
|
+
|
|
10
|
+
expect(parsed.iconLinks).toEqual([{
|
|
11
|
+
rel: 'icon',
|
|
12
|
+
href: '/favicon.ico',
|
|
13
|
+
type: undefined,
|
|
14
|
+
sizes: 'any',
|
|
15
|
+
media: undefined,
|
|
16
|
+
}])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('resolves inspected routes within the Nuxt baseURL', () => {
|
|
20
|
+
expect(resolveAppUrl('https://example.com', '/docs/', '/guide')).toBe('https://example.com/docs/guide')
|
|
21
|
+
expect(resolveAppUrl('https://example.com', '/docs/', '/docs/guide')).toBe('https://example.com/docs/guide')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('reports invalid runtime bitmap metadata', () => {
|
|
25
|
+
expect(getIconIssues([{
|
|
26
|
+
rel: 'icon',
|
|
27
|
+
href: 'https://example.com/favicon.ico',
|
|
28
|
+
sizes: 'any',
|
|
29
|
+
}])).toEqual(['https://example.com/favicon.ico uses sizes="any", which is reserved for scalable icons.'])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -7,10 +7,41 @@ export const SEO_LIMITS = {
|
|
|
7
7
|
DESC_MAX_PIXELS: 920,
|
|
8
8
|
} as const
|
|
9
9
|
|
|
10
|
+
export interface ParsedIconLink {
|
|
11
|
+
rel: string
|
|
12
|
+
href: string
|
|
13
|
+
type?: string
|
|
14
|
+
sizes?: string
|
|
15
|
+
media?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
export function estimatePixelWidth(text: string, fontSize: number = 16): number {
|
|
11
19
|
return Math.round(text.length * fontSize * 0.55)
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
export function resolveAppUrl(origin: string, appBaseURL: string, path: string): string {
|
|
23
|
+
const base = new URL(appBaseURL, origin)
|
|
24
|
+
if (!base.pathname.endsWith('/'))
|
|
25
|
+
base.pathname += '/'
|
|
26
|
+
|
|
27
|
+
const candidate = new URL(path, origin)
|
|
28
|
+
const basePath = base.pathname.replace(/\/$/, '')
|
|
29
|
+
if (candidate.pathname === basePath || candidate.pathname.startsWith(`${basePath}/`))
|
|
30
|
+
return candidate.href
|
|
31
|
+
return new URL(path.replace(/^\//, ''), base).href
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getIconIssues(icons: ParsedIconLink[]): string[] {
|
|
35
|
+
return icons.flatMap((icon) => {
|
|
36
|
+
const isSvg = icon.type === 'image/svg+xml' || /\.svg(?:[?#]|$)/i.test(icon.href)
|
|
37
|
+
if (!icon.sizes)
|
|
38
|
+
return [`${icon.href} has no sizes attribute.`]
|
|
39
|
+
if (icon.sizes.toLowerCase().split(/\s+/).includes('any') && !isSvg)
|
|
40
|
+
return [`${icon.href} uses sizes="any", which is reserved for scalable icons.`]
|
|
41
|
+
return []
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
14
45
|
export function parseMetaTags(html: string) {
|
|
15
46
|
const parser = new DOMParser()
|
|
16
47
|
const doc = parser.parseFromString(html, 'text/html')
|
|
@@ -64,13 +95,21 @@ export function parseMetaTags(html: string) {
|
|
|
64
95
|
}
|
|
65
96
|
})
|
|
66
97
|
|
|
67
|
-
const iconLinks:
|
|
68
|
-
doc.querySelectorAll('link[rel
|
|
98
|
+
const iconLinks: ParsedIconLink[] = []
|
|
99
|
+
doc.querySelectorAll('link[rel][href]').forEach((el) => {
|
|
100
|
+
const relTokens = (el.getAttribute('rel') || '').toLowerCase().split(/\s+/)
|
|
101
|
+
const rel = relTokens.includes('apple-touch-icon')
|
|
102
|
+
? 'apple-touch-icon'
|
|
103
|
+
: relTokens.includes('icon')
|
|
104
|
+
? 'icon'
|
|
105
|
+
: null
|
|
106
|
+
if (!rel)
|
|
107
|
+
return
|
|
69
108
|
const href = el.getAttribute('href')
|
|
70
109
|
if (!href)
|
|
71
110
|
return
|
|
72
111
|
iconLinks.push({
|
|
73
|
-
rel
|
|
112
|
+
rel,
|
|
74
113
|
href,
|
|
75
114
|
type: el.getAttribute('type') || undefined,
|
|
76
115
|
sizes: el.getAttribute('sizes') || undefined,
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { ParsedIconLink } from '../../lib/seo-utils/tools'
|
|
2
3
|
import { useClipboard } from '@vueuse/core'
|
|
3
4
|
import { computed, ref, watch } from 'vue'
|
|
4
5
|
import { data } from '../../lib/seo-utils/state'
|
|
5
|
-
import { parseMetaTags } from '../../lib/seo-utils/tools'
|
|
6
|
+
import { getIconIssues, parseMetaTags, resolveAppUrl } from '../../lib/seo-utils/tools'
|
|
6
7
|
|
|
7
8
|
const { copy } = useClipboard()
|
|
8
9
|
|
|
9
10
|
const loading = ref(false)
|
|
10
11
|
const error = ref<string | null>(null)
|
|
11
12
|
|
|
12
|
-
interface IconLink {
|
|
13
|
-
rel: string
|
|
14
|
-
href: string
|
|
15
|
-
type?: string
|
|
16
|
-
sizes?: string
|
|
17
|
-
media?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
interface ParsedIdentity {
|
|
21
|
-
icons:
|
|
14
|
+
icons: ParsedIconLink[]
|
|
22
15
|
ogImages: Array<{ url: string, width?: string, height?: string, alt?: string }>
|
|
23
16
|
twitterImages: Array<{ url: string, width?: string, height?: string }>
|
|
24
17
|
themeColors: Array<{ color: string, media?: string, context: 'light' | 'dark' | 'default' }>
|
|
@@ -30,9 +23,10 @@ const parsedPage = ref<ParsedIdentity | null>(null)
|
|
|
30
23
|
const siteConfig = computed(() => data.value?.siteConfig || null)
|
|
31
24
|
const pageIcons = computed(() => parsedPage.value?.icons || [])
|
|
32
25
|
const hasIcons = computed(() => pageIcons.value.length > 0)
|
|
26
|
+
const iconIssues = computed(() => getIconIssues(pageIcons.value))
|
|
33
27
|
|
|
34
28
|
const iconsByRel = computed(() => {
|
|
35
|
-
const groups: Record<string,
|
|
29
|
+
const groups: Record<string, ParsedIconLink[]> = {}
|
|
36
30
|
pageIcons.value.forEach((link) => {
|
|
37
31
|
const rel = link.rel || 'icon'
|
|
38
32
|
if (!groups[rel])
|
|
@@ -110,7 +104,8 @@ async function checkIdentity() {
|
|
|
110
104
|
|
|
111
105
|
try {
|
|
112
106
|
const baseUrl = window.parent?.location?.origin || window.location.origin
|
|
113
|
-
const
|
|
107
|
+
const pageUrl = resolveAppUrl(baseUrl, data.value?.runtimeConfig.appBaseURL || '/', path.value)
|
|
108
|
+
const response = await fetch(pageUrl, {
|
|
114
109
|
headers: { Accept: 'text/html' },
|
|
115
110
|
})
|
|
116
111
|
if (!response.ok)
|
|
@@ -155,7 +150,7 @@ async function checkIdentity() {
|
|
|
155
150
|
// Resolve relative icon URLs to absolute
|
|
156
151
|
const icons = parsed.iconLinks.map(icon => ({
|
|
157
152
|
...icon,
|
|
158
|
-
href:
|
|
153
|
+
href: new URL(icon.href, response.url).href,
|
|
159
154
|
}))
|
|
160
155
|
|
|
161
156
|
parsedPage.value = {
|
|
@@ -182,7 +177,7 @@ watch([() => data.value, path, refreshTime], () => {
|
|
|
182
177
|
const cliCommand = 'npx nuxt-seo-utils icons --source <your-logo.svg>'
|
|
183
178
|
|
|
184
179
|
const iconSizesGenerated = [
|
|
185
|
-
{ name: 'favicon.ico', size: '32x32', desc: 'Browser tab' },
|
|
180
|
+
{ name: 'favicon.ico', size: '16x16, 32x32, 48x48', desc: 'Browser tab' },
|
|
186
181
|
{ name: 'icon-16x16.png', size: '16x16', desc: 'Small favicon' },
|
|
187
182
|
{ name: 'icon-32x32.png', size: '32x32', desc: 'Standard favicon' },
|
|
188
183
|
{ name: 'apple-touch-icon.png', size: '180x180', desc: 'iOS home screen' },
|
|
@@ -258,6 +253,14 @@ const iconSizesGenerated = [
|
|
|
258
253
|
</template>
|
|
259
254
|
|
|
260
255
|
<template v-if="hasIcons">
|
|
256
|
+
<div v-if="iconIssues.length" class="hint-callout mb-4">
|
|
257
|
+
<UIcon name="carbon:warning" class="hint-callout-icon text-sm mt-0.5 shrink-0" />
|
|
258
|
+
<div class="text-xs text-[var(--color-text-muted)]">
|
|
259
|
+
<p v-for="issue in iconIssues" :key="issue">
|
|
260
|
+
{{ issue }}
|
|
261
|
+
</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
261
264
|
<div v-for="(icons, rel) in iconsByRel" :key="rel" class="mb-5 last:mb-0">
|
|
262
265
|
<div class="id-group-header">
|
|
263
266
|
<span>{{ iconRelLabels[rel]?.label || rel }}</span>
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -4,16 +4,19 @@ import { useNuxt, addTypeTemplate, useLogger, defineNuxtModule, createResolver,
|
|
|
4
4
|
import { defu } from 'defu';
|
|
5
5
|
import { resolveModulePath } from 'exsolve';
|
|
6
6
|
import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
7
|
-
import { resolveHostUnheadMajor } from 'nuxtseo-shared/kit';
|
|
7
|
+
import { useModuleLogger, setupNitroRuntimeCompatibility, renderNitroTypeAugmentations, resolveHostUnheadMajor } from 'nuxtseo-shared/kit';
|
|
8
8
|
import { resolve, extname, relative, dirname, basename } from 'pathe';
|
|
9
|
-
import { readPackageJSON } from 'pkg-types';
|
|
9
|
+
import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
|
|
10
10
|
import { unpackMeta } from '@unhead/vue/utils';
|
|
11
11
|
import { glob } from 'tinyglobby';
|
|
12
12
|
import { encodePath, joinURL } from 'ufo';
|
|
13
|
+
import { c as classifyIconFilename, g as getIconRel, n as normalizeIconSizes, a as partitionColorModeIconLinks } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
|
|
13
14
|
import escapeRE from 'escape-string-regexp';
|
|
14
15
|
import { readFile, readdir } from 'node:fs/promises';
|
|
15
16
|
import imageSize from 'image-size';
|
|
17
|
+
import { JSON_TYPES, SKIP_JS_TYPES } from '../dist/runtime/shared/minify.js';
|
|
16
18
|
import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
|
|
19
|
+
import 'node:buffer';
|
|
17
20
|
|
|
18
21
|
function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
|
|
19
22
|
const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || {};
|
|
@@ -73,20 +76,15 @@ export {}
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
const MetaTagFileDeepGlobs = [
|
|
76
|
-
"**/og-image.{png,jpg,jpeg,gif}",
|
|
77
|
-
"**/
|
|
78
|
-
"
|
|
79
|
-
"**/apple
|
|
80
|
-
"
|
|
81
|
-
"**/apple-touch-*.{jpg,jpeg,png}",
|
|
82
|
-
"**/apple-touch-icon.{jpg,jpeg,png}",
|
|
83
|
-
"**/apple-touch-icon-*.{jpg,jpeg,png}",
|
|
84
|
-
"**/icon.{ico,jpg,jpeg,png,svg}",
|
|
85
|
-
"**/icon-*.{ico,jpg,jpeg,png,svg}"
|
|
79
|
+
"**/{og-image,opengraph-image,twitter-image}.{png,jpg,jpeg,gif}",
|
|
80
|
+
"**/{favicon*,icon*}.{ico,jpg,jpeg,png,svg}",
|
|
81
|
+
"**/*.icon*.{ico,jpg,jpeg,png,svg}",
|
|
82
|
+
"**/apple-*.{jpg,jpeg,png}",
|
|
83
|
+
"**/*.apple-*.{jpg,jpeg,png}"
|
|
86
84
|
];
|
|
87
|
-
const
|
|
85
|
+
const SOCIAL_IMAGE_RE = /^(?:og-image|opengraph-image|twitter-image)\.(?:png|jpe?g|gif)$/;
|
|
88
86
|
function isMetaTagFile(filename) {
|
|
89
|
-
return
|
|
87
|
+
return SOCIAL_IMAGE_RE.test(filename) || classifyIconFilename(filename) !== void 0;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
const INDEX_SUFFIX_RE = /-index$/;
|
|
@@ -178,7 +176,7 @@ function parseSegment(segment) {
|
|
|
178
176
|
state = 0 /* initial */;
|
|
179
177
|
} else if (PARAM_CHAR_RE.test(c)) {
|
|
180
178
|
buffer += c;
|
|
181
|
-
}
|
|
179
|
+
}
|
|
182
180
|
break;
|
|
183
181
|
}
|
|
184
182
|
i++;
|
|
@@ -189,9 +187,6 @@ function parseSegment(segment) {
|
|
|
189
187
|
return tokens;
|
|
190
188
|
}
|
|
191
189
|
|
|
192
|
-
function hasLinkRel(input, rel) {
|
|
193
|
-
return input.link?.some((link) => link.rel === rel);
|
|
194
|
-
}
|
|
195
190
|
function hasMetaProperty(input, property) {
|
|
196
191
|
return input.meta?.some((meta) => meta.property === property);
|
|
197
192
|
}
|
|
@@ -200,12 +195,12 @@ async function getImageMeta(base, path, isIcon = false) {
|
|
|
200
195
|
const absolutePath = resolve(base, path);
|
|
201
196
|
const file = absolutePath.split("/").pop();
|
|
202
197
|
const keyword = file.split(".")[0];
|
|
203
|
-
let ext = absolutePath.split(".").pop();
|
|
198
|
+
let ext = absolutePath.split(".").pop()?.toLowerCase();
|
|
204
199
|
if (ext === "jpg")
|
|
205
200
|
ext = "jpeg";
|
|
206
|
-
const { width, height } = await getImageDimensions(absolutePath);
|
|
201
|
+
const { width, height, images } = await getImageDimensions(absolutePath);
|
|
207
202
|
const payload = {
|
|
208
|
-
type: ext === "svg" ?
|
|
203
|
+
type: ext === "svg" ? "image/svg+xml" : ext === "ico" ? "image/vnd.microsoft.icon" : `image/${ext}`
|
|
209
204
|
};
|
|
210
205
|
if (!isIcon) {
|
|
211
206
|
payload.width = width;
|
|
@@ -220,7 +215,9 @@ async function getImageMeta(base, path, isIcon = false) {
|
|
|
220
215
|
payload.media = "(prefers-color-scheme: dark)";
|
|
221
216
|
else if (path.includes(".light") || path.includes("-light"))
|
|
222
217
|
payload.media = "(prefers-color-scheme: light)";
|
|
223
|
-
if (ext
|
|
218
|
+
if (ext === "ico" && images?.length) {
|
|
219
|
+
payload.sizes = [...new Map(images.map((image) => [`${image.width}x${image.height}`, image])).values()].sort((a, b) => a.width * a.height - b.width * b.height).map((image) => `${image.width}x${image.height}`).join(" ");
|
|
220
|
+
} else if (ext !== "svg") {
|
|
224
221
|
payload.sizes = `${width}x${height}`;
|
|
225
222
|
} else {
|
|
226
223
|
payload.sizes = "any";
|
|
@@ -234,47 +231,64 @@ async function getImageDimensions(absolutePath) {
|
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
const DIR_SUFFIX_RE = /\/_dir$/;
|
|
234
|
+
function addGeneratedRouteRule(nuxt, route, generatedRule) {
|
|
235
|
+
nuxt.options.routeRules ||= {};
|
|
236
|
+
nuxt.options.nitro.routeRules ||= {};
|
|
237
|
+
const routeRules = nuxt.options.routeRules;
|
|
238
|
+
const rule = structuredClone(generatedRule);
|
|
239
|
+
routeRules[route] = defu(routeRules[route], rule);
|
|
240
|
+
const nitroRouteRules = nuxt.options.nitro.routeRules;
|
|
241
|
+
if (nitroRouteRules !== routeRules)
|
|
242
|
+
nitroRouteRules[route] = defu(nitroRouteRules[route], structuredClone(rule));
|
|
243
|
+
}
|
|
237
244
|
async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
238
245
|
const pagesDirs = nuxt.options._layers.map((layer) => resolve(layer.config.rootDir, layer.config.dir?.pages || "pages")).filter((dir) => fs.existsSync(dir));
|
|
239
246
|
const appendRouteRules = {};
|
|
247
|
+
const registeredFiles = /* @__PURE__ */ new Set();
|
|
240
248
|
const devMiddlewareMap = {};
|
|
241
249
|
const nitroOutputMap = [];
|
|
242
250
|
for (const pagesDir of pagesDirs) {
|
|
243
|
-
const files = await glob(MetaTagFileDeepGlobs, { cwd: pagesDir, onlyFiles: true });
|
|
251
|
+
const files = (await glob(MetaTagFileDeepGlobs, { cwd: pagesDir, onlyFiles: true })).filter((file) => isMetaTagFile(basename(file)));
|
|
244
252
|
for (const file of files) {
|
|
245
253
|
const fileName = basename(file);
|
|
246
254
|
const keyword = fileName.split(".")[0] || "";
|
|
247
255
|
let { path } = generateNuxtPageFromFile(resolve(pagesDir, dirname(file)), pagesDir);
|
|
248
|
-
const meta = await getImageMeta(pagesDir, file);
|
|
249
256
|
if (path.endsWith("/_dir"))
|
|
250
257
|
path = path.replace(DIR_SUFFIX_RE, "");
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
258
|
+
const registrationKey = `${path}:${fileName}`;
|
|
259
|
+
if (registeredFiles.has(registrationKey))
|
|
253
260
|
continue;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
261
|
+
registeredFiles.add(registrationKey);
|
|
262
|
+
const routeAssetPath = joinURL(path, fileName);
|
|
263
|
+
const href = joinURL(nuxt.options.app.baseURL, routeAssetPath);
|
|
264
|
+
const iconRel = classifyIconFilename(fileName);
|
|
265
|
+
const meta = await getImageMeta(pagesDir, file, iconRel !== void 0);
|
|
266
|
+
const routeRule = appendRouteRules[path] ||= { head: { link: [] }, seoMeta: {} };
|
|
267
|
+
if (iconRel) {
|
|
268
|
+
routeRule.head.link.push({
|
|
269
|
+
rel: iconRel,
|
|
270
|
+
type: String(meta.type),
|
|
271
|
+
href,
|
|
272
|
+
sizes: String(meta.sizes),
|
|
273
|
+
media: meta.media ? String(meta.media) : void 0
|
|
274
|
+
});
|
|
262
275
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
};
|
|
276
|
+
const property = ["opengraph-image", "og-image"].includes(keyword) ? "ogImage" : "twitterImage";
|
|
277
|
+
routeRule.seoMeta[property] ||= [];
|
|
278
|
+
routeRule.seoMeta[property].push({ url: href, ...meta, sizes: void 0 });
|
|
268
279
|
}
|
|
269
|
-
devMiddlewareMap[
|
|
280
|
+
devMiddlewareMap[routeAssetPath] = resolve(pagesDir, file);
|
|
270
281
|
nitroOutputMap.push({
|
|
271
282
|
src: resolve(pagesDir, file),
|
|
272
|
-
dest:
|
|
283
|
+
dest: routeAssetPath.slice(1)
|
|
273
284
|
});
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
for (const [route, rule] of Object.entries(appendRouteRules)) {
|
|
288
|
+
addGeneratedRouteRule(nuxt, route, rule);
|
|
289
|
+
if (route !== "/")
|
|
290
|
+
addGeneratedRouteRule(nuxt, `${route}/**`, rule);
|
|
291
|
+
}
|
|
278
292
|
if (nuxt.options.dev) {
|
|
279
293
|
nuxt.hooks.hook("nitro:config", async (nitroConfig) => {
|
|
280
294
|
nitroConfig.virtual["#seo-utils-virtual/pageDirImages"] = `export const fileMapping = ${JSON.stringify(devMiddlewareMap)}`;
|
|
@@ -294,9 +308,54 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
|
|
|
294
308
|
}
|
|
295
309
|
|
|
296
310
|
async function listMetaTagFiles(dir) {
|
|
297
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() =>
|
|
311
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch((error) => {
|
|
312
|
+
if (error.code === "ENOENT")
|
|
313
|
+
return [];
|
|
314
|
+
throw error;
|
|
315
|
+
});
|
|
298
316
|
return entries.filter((e) => e.isFile() && isMetaTagFile(e.name)).map((e) => e.name);
|
|
299
317
|
}
|
|
318
|
+
function findLocalIcon(href, baseURL, iconEntries) {
|
|
319
|
+
if (typeof href !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(href))
|
|
320
|
+
return;
|
|
321
|
+
const path = href.split(/[?#]/, 1)[0] || "";
|
|
322
|
+
const file = basename(path);
|
|
323
|
+
const entry = iconEntries.find((icon) => icon.file === file);
|
|
324
|
+
if (!entry)
|
|
325
|
+
return;
|
|
326
|
+
const validPaths = /* @__PURE__ */ new Set([entry.file, `/${entry.file}`, joinURL(baseURL, entry.file)]);
|
|
327
|
+
return validPaths.has(path) ? { entry, resolvedHref: `${joinURL(baseURL, entry.file)}${href.slice(path.length)}` } : void 0;
|
|
328
|
+
}
|
|
329
|
+
function enrichConfiguredIcon(link, baseURL, iconEntries, diagnostics) {
|
|
330
|
+
const iconLink = link;
|
|
331
|
+
const match = findLocalIcon(iconLink.href, baseURL, iconEntries);
|
|
332
|
+
if (!match)
|
|
333
|
+
return link;
|
|
334
|
+
const { entry, resolvedHref } = match;
|
|
335
|
+
if (iconLink.href !== resolvedHref) {
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
_tag: "IconHrefNormalized",
|
|
338
|
+
configured: String(iconLink.href),
|
|
339
|
+
resolved: resolvedHref
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const normalizedSizes = normalizeIconSizes(iconLink.sizes, entry.sizes);
|
|
343
|
+
if (normalizedSizes.normalized) {
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
_tag: "IconSizesNormalized",
|
|
346
|
+
href: String(iconLink.href),
|
|
347
|
+
configured: String(iconLink.sizes),
|
|
348
|
+
resolved: entry.sizes
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
...iconLink,
|
|
353
|
+
href: resolvedHref,
|
|
354
|
+
type: iconLink.type || entry.type,
|
|
355
|
+
sizes: normalizedSizes.sizes,
|
|
356
|
+
media: iconLink.media || entry.media
|
|
357
|
+
};
|
|
358
|
+
}
|
|
300
359
|
async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
301
360
|
const publicDirs = nuxt.options._layers.map((layer) => resolve(layer.config.rootDir, layer.config.dir?.public || "public"));
|
|
302
361
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -315,37 +374,33 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
|
315
374
|
link: [],
|
|
316
375
|
meta: []
|
|
317
376
|
});
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
};
|
|
346
|
-
})
|
|
347
|
-
])
|
|
348
|
-
);
|
|
377
|
+
const iconEntries = (await Promise.all(fileEntries.map(async ({ file, dir }) => {
|
|
378
|
+
const rel = classifyIconFilename(file);
|
|
379
|
+
if (!rel)
|
|
380
|
+
return;
|
|
381
|
+
const meta = await getImageMeta(dir, file, true);
|
|
382
|
+
return {
|
|
383
|
+
file,
|
|
384
|
+
dir,
|
|
385
|
+
rel,
|
|
386
|
+
type: String(meta.type),
|
|
387
|
+
sizes: String(meta.sizes),
|
|
388
|
+
media: meta.media ? String(meta.media) : void 0
|
|
389
|
+
};
|
|
390
|
+
}))).filter((entry) => entry !== void 0);
|
|
391
|
+
const diagnostics = [];
|
|
392
|
+
headConfig.link = headConfig.link.map((link) => enrichConfiguredIcon(link, nuxt.options.app.baseURL, iconEntries, diagnostics));
|
|
393
|
+
const configuredIconRels = new Set(headConfig.link.map((link) => getIconRel(link.rel)).filter((rel) => rel !== void 0));
|
|
394
|
+
for (const rel of ["icon", "apple-touch-icon"]) {
|
|
395
|
+
if (configuredIconRels.has(rel))
|
|
396
|
+
continue;
|
|
397
|
+
headConfig.link.push(...iconEntries.filter((entry) => entry.rel === rel).sort((a, b) => a.file.localeCompare(b.file)).map((entry) => ({
|
|
398
|
+
rel: entry.rel,
|
|
399
|
+
type: entry.type,
|
|
400
|
+
href: joinURL(nuxt.options.app.baseURL, entry.file),
|
|
401
|
+
sizes: entry.sizes,
|
|
402
|
+
media: entry.media
|
|
403
|
+
})));
|
|
349
404
|
}
|
|
350
405
|
let hasTwitterImage = hasMetaProperty(headConfig, "twitter:image");
|
|
351
406
|
if (!hasTwitterImage) {
|
|
@@ -391,12 +446,10 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
|
|
|
391
446
|
}
|
|
392
447
|
}
|
|
393
448
|
nuxt.options.app.head = headConfig;
|
|
394
|
-
const hasIcons =
|
|
395
|
-
return { hasIcons };
|
|
449
|
+
const hasIcons = iconEntries.length > 0 || configuredIconRels.size > 0;
|
|
450
|
+
return { hasIcons, diagnostics };
|
|
396
451
|
}
|
|
397
452
|
|
|
398
|
-
const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json"]);
|
|
399
|
-
const SKIP_JS_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
|
|
400
453
|
const logger = useLogger("nuxt-seo-utils");
|
|
401
454
|
async function minifyJSBuildTime(code) {
|
|
402
455
|
try {
|
|
@@ -491,11 +544,20 @@ function minifyStaticHead() {
|
|
|
491
544
|
});
|
|
492
545
|
}
|
|
493
546
|
|
|
547
|
+
function resolveUnheadVitePluginSource(input, resolvePath) {
|
|
548
|
+
if (input.unheadMajor < 3)
|
|
549
|
+
return { _tag: "unsupported-v2" };
|
|
550
|
+
const path = resolvePath("@unhead/vue/vite", {
|
|
551
|
+
try: true,
|
|
552
|
+
from: [...input.hostImportPaths, ...input.importPaths]
|
|
553
|
+
});
|
|
554
|
+
return path ? { _tag: "vue", url: pathToFileURL(path).href } : { _tag: "missing-vue" };
|
|
555
|
+
}
|
|
556
|
+
|
|
494
557
|
function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
|
|
495
558
|
const headConfig = nuxt.options.app.head;
|
|
496
559
|
headConfig.link = headConfig.link || [];
|
|
497
560
|
headConfig.htmlAttrs = headConfig.htmlAttrs || {};
|
|
498
|
-
headConfig.link = headConfig.link || [];
|
|
499
561
|
headConfig.meta = headConfig.meta || [];
|
|
500
562
|
if (!hasMetaProperty(headConfig, "og:type")) {
|
|
501
563
|
headConfig.meta.push({
|
|
@@ -556,20 +618,27 @@ const module$1 = defineNuxtModule({
|
|
|
556
618
|
minify: true
|
|
557
619
|
},
|
|
558
620
|
async setup(config, nuxt) {
|
|
559
|
-
const logger =
|
|
560
|
-
logger.level = config.debug || nuxt.options.debug ? 4 : 3;
|
|
621
|
+
const logger = useModuleLogger("nuxt-seo-utils", config, nuxt);
|
|
561
622
|
if (config.enabled === false) {
|
|
562
623
|
logger.debug("The module is disabled, skipping setup.");
|
|
563
624
|
return;
|
|
564
625
|
}
|
|
626
|
+
const nitroCompatibility = setupNitroRuntimeCompatibility(nuxt);
|
|
565
627
|
const { resolve } = createResolver(import.meta.url);
|
|
566
628
|
const { version } = await readPackageJSON(resolve("../package.json"));
|
|
567
629
|
await installNuxtSiteConfig();
|
|
568
630
|
const runtimeDir = resolve("./runtime");
|
|
569
631
|
if (config.metaDataFiles) {
|
|
570
|
-
const { hasIcons } = await generateTagsFromPublicFiles();
|
|
632
|
+
const { hasIcons, diagnostics } = await generateTagsFromPublicFiles();
|
|
571
633
|
await generateTagsFromPageDirImages();
|
|
572
634
|
if (nuxt.options.dev) {
|
|
635
|
+
diagnostics.forEach((diagnostic) => {
|
|
636
|
+
if (diagnostic._tag === "IconSizesNormalized") {
|
|
637
|
+
logger.warn(`Normalized icon sizes for ${diagnostic.href} from "${diagnostic.configured}" to "${diagnostic.resolved}" to match the local file.`);
|
|
638
|
+
} else if (diagnostic._tag === "IconHrefNormalized") {
|
|
639
|
+
logger.warn(`Normalized local icon href from "${diagnostic.configured}" to "${diagnostic.resolved}" to include app.baseURL.`);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
573
642
|
if (!hasIcons) {
|
|
574
643
|
const publicDir = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
|
|
575
644
|
const logoPatterns = ["logo.svg", "logo.png", "icon.svg", "icon.png", "logo.jpg", "logo.webp"];
|
|
@@ -675,25 +744,17 @@ const module$1 = defineNuxtModule({
|
|
|
675
744
|
filename: "module/nuxt-seo-utils.d.ts",
|
|
676
745
|
getContents: (data) => {
|
|
677
746
|
const typesPath = relative(resolve(data.nuxt.options.rootDir, data.nuxt.options.buildDir, "module"), resolve("runtime/types"));
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
head?: import('${typesPath}').Head
|
|
685
|
-
}`;
|
|
747
|
+
const routeRuleTypes = `seoMeta?: import('${typesPath}').MetaFlatSerializable
|
|
748
|
+
head?: import('${typesPath}').Head`;
|
|
749
|
+
const nitroTypes = renderNitroTypeAugmentations(nitroCompatibility, {
|
|
750
|
+
routeRules: routeRuleTypes,
|
|
751
|
+
routeConfig: routeRuleTypes
|
|
752
|
+
});
|
|
686
753
|
return `// Generated by nuxt-seo-utils
|
|
687
754
|
|
|
688
755
|
import type { RobotsContext } from '#robots/types'
|
|
689
756
|
|
|
690
|
-
|
|
691
|
-
${types}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
declare module 'nitropack' {
|
|
695
|
-
${types}
|
|
696
|
-
}
|
|
757
|
+
${nitroTypes}
|
|
697
758
|
|
|
698
759
|
|
|
699
760
|
declare module '@nuxt/schema' {
|
|
@@ -712,6 +773,18 @@ export {}
|
|
|
712
773
|
nuxt: true
|
|
713
774
|
});
|
|
714
775
|
const appRuntimeDir = resolve(runtimeDir, "./app");
|
|
776
|
+
nuxt.hook("modules:done", () => {
|
|
777
|
+
if (!hasNuxtModule("@nuxtjs/color-mode", nuxt))
|
|
778
|
+
return;
|
|
779
|
+
const headConfig = nuxt.options.app.head;
|
|
780
|
+
const result = partitionColorModeIconLinks(headConfig.link || []);
|
|
781
|
+
if (result._tag === "StaticIconLinks")
|
|
782
|
+
return;
|
|
783
|
+
headConfig.link = result.links;
|
|
784
|
+
const seoRuntimeConfig = nuxt.options.runtimeConfig.public["seo-utils"];
|
|
785
|
+
seoRuntimeConfig.colorModeIcons = result.icons;
|
|
786
|
+
addPlugin({ src: resolve(appRuntimeDir, "plugins", "colorModeIcons") });
|
|
787
|
+
});
|
|
715
788
|
nuxt.options.unhead = defu(
|
|
716
789
|
nuxt.options.unhead,
|
|
717
790
|
{ vite: config.treeShakeUseSeoMeta ? { validate: !nuxt.options.test } : false }
|
|
@@ -725,6 +798,12 @@ export {}
|
|
|
725
798
|
const lightningcssPath = resolveModulePath("lightningcss", { try: true, from: importPaths });
|
|
726
799
|
const rolldownURL = rolldownPath ? pathToFileURL(rolldownPath).href : void 0;
|
|
727
800
|
const lightningcssURL = lightningcssPath ? pathToFileURL(lightningcssPath).href : void 0;
|
|
801
|
+
const hostImportPaths = unheadMajor >= 3 ? [directoryToURL(dirname(await resolvePackageJSON("nuxt", { url: directoryToURL(nuxt.options.rootDir).href })))] : [];
|
|
802
|
+
const vitePluginSource = resolveUnheadVitePluginSource({
|
|
803
|
+
unheadMajor,
|
|
804
|
+
hostImportPaths,
|
|
805
|
+
importPaths
|
|
806
|
+
}, resolveModulePath);
|
|
728
807
|
const minify = {
|
|
729
808
|
js: rolldownURL ? async (code) => {
|
|
730
809
|
const { minify: minify2 } = await import(rolldownURL);
|
|
@@ -739,22 +818,18 @@ export {}
|
|
|
739
818
|
}).code).trim();
|
|
740
819
|
} : void 0
|
|
741
820
|
};
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
validate: false,
|
|
753
|
-
devtools: false
|
|
821
|
+
if (vitePluginSource._tag === "unsupported-v2") {
|
|
822
|
+
logger.warn("`treeShakeUseSeoMeta` requires Unhead v3; skipping the transform on Unhead v2.");
|
|
823
|
+
} else if (vitePluginSource._tag === "missing-vue") {
|
|
824
|
+
logger.warn("`treeShakeUseSeoMeta` requires `@unhead/vue` with the `/vite` export. Install Unhead v3 or disable `treeShakeUseSeoMeta`.");
|
|
825
|
+
} else {
|
|
826
|
+
addVitePlugin(async () => {
|
|
827
|
+
const { Unhead } = await import(vitePluginSource.url);
|
|
828
|
+
return Unhead({ minify, ...viteOpts });
|
|
829
|
+
}, {
|
|
830
|
+
prepend: true
|
|
754
831
|
});
|
|
755
|
-
}
|
|
756
|
-
prepend: true
|
|
757
|
-
});
|
|
832
|
+
}
|
|
758
833
|
}
|
|
759
834
|
if (config.automaticOgAndTwitterTags)
|
|
760
835
|
addPlugin({ src: resolve(appRuntimeDir, "plugins", "inferSeoMetaPlugin") });
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { defineBreadcrumb, useI18n, useSchemaOrg } from "#imports";
|
|
18
18
|
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
19
19
|
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
20
|
-
import { pathBreadcrumbSegments } from "../../shared/breadcrumbs.js";
|
|
20
|
+
import { pathBreadcrumbSegments, resolveBreadcrumbRoot } from "../../shared/breadcrumbs.js";
|
|
21
21
|
function withoutQuery(path) {
|
|
22
22
|
return path.split("?")[0] || "";
|
|
23
23
|
}
|
|
@@ -120,8 +120,11 @@ export function useBreadcrumbItems(_options = {}) {
|
|
|
120
120
|
}, {});
|
|
121
121
|
let rootNode = flatOptions.rootSegment || "/";
|
|
122
122
|
if (i18n) {
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
rootNode = resolveBreadcrumbRoot(rootNode, {
|
|
124
|
+
locale: toValue(i18n.locale),
|
|
125
|
+
defaultLocale: toValue(i18n.defaultLocale),
|
|
126
|
+
strategy: i18n.strategy
|
|
127
|
+
});
|
|
125
128
|
}
|
|
126
129
|
const current = withoutQuery(withoutTrailingSlash(flatOptions.path || toRaw(route)?.path || rootNode)) || "";
|
|
127
130
|
const segments = pathBreadcrumbSegments(current, rootNode).map((path, index) => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function selectColorModeIconLinks(icons, colorMode) {
|
|
2
|
+
const selected = colorMode === "dark" ? icons.dark : icons.light;
|
|
3
|
+
return selected.map((link, index) => ({
|
|
4
|
+
...link,
|
|
5
|
+
key: `nuxt-seo-utils:color-mode-icon:${getLinkKey(link, index)}`
|
|
6
|
+
}));
|
|
7
|
+
}
|
|
8
|
+
function getLinkKey(link, index) {
|
|
9
|
+
return `${link.rel}:${index}`;
|
|
10
|
+
}
|
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { defineNuxtPlugin, useHead, useRequestEvent, useRuntimeConfig, useSeoMeta, useState } from "#imports";
|
|
2
|
+
function parseRouteRuleState(context) {
|
|
3
|
+
const rules = context._nitro?.routeRules;
|
|
4
|
+
return {
|
|
5
|
+
head: rules?.head,
|
|
6
|
+
seoMeta: rules?.seoMeta
|
|
7
|
+
};
|
|
8
|
+
}
|
|
3
9
|
export default defineNuxtPlugin({
|
|
4
10
|
enforce: "post",
|
|
5
11
|
env: { islands: false },
|
|
6
|
-
|
|
7
|
-
const head = injectHead();
|
|
8
|
-
if (!head)
|
|
9
|
-
return;
|
|
12
|
+
setup() {
|
|
10
13
|
const { tagPriority } = useRuntimeConfig().public["seo-utils"];
|
|
11
14
|
const routeRuleState = useState("nuxt-seo-utils:routeRules", () => null);
|
|
12
15
|
if (import.meta.server) {
|
|
13
16
|
const event = useRequestEvent();
|
|
14
|
-
|
|
15
|
-
const rules = routeRules;
|
|
16
|
-
routeRuleState.value = {
|
|
17
|
-
head: rules.head,
|
|
18
|
-
seoMeta: rules.seoMeta
|
|
19
|
-
};
|
|
17
|
+
routeRuleState.value = parseRouteRuleState(event?.context);
|
|
20
18
|
}
|
|
21
19
|
if (routeRuleState.value) {
|
|
22
20
|
const { head: headInput, seoMeta } = routeRuleState.value;
|
|
23
21
|
if (headInput)
|
|
24
|
-
|
|
22
|
+
useHead(headInput);
|
|
25
23
|
if (seoMeta)
|
|
26
24
|
useSeoMeta(seoMeta, { tagPriority });
|
|
27
25
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useHead, useNuxtApp, useRuntimeConfig } from "#imports";
|
|
2
|
+
import { selectColorModeIconLinks } from "../logic/colorModeIcons.js";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
name: "nuxt-seo-utils:color-mode-icons",
|
|
5
|
+
enforce: "post",
|
|
6
|
+
setup() {
|
|
7
|
+
const runtimeConfig = useRuntimeConfig();
|
|
8
|
+
const seoConfig = runtimeConfig.public["seo-utils"];
|
|
9
|
+
const colorMode = useNuxtApp().$colorMode;
|
|
10
|
+
const icons = seoConfig.colorModeIcons;
|
|
11
|
+
if (!icons || !colorMode)
|
|
12
|
+
return;
|
|
13
|
+
useHead(() => ({
|
|
14
|
+
link: selectColorModeIconLinks(icons, colorMode.value)
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { injectHead } from "@unhead/vue";
|
|
2
2
|
import { defineNuxtPlugin } from "nuxt/app";
|
|
3
|
-
import { minifyCSS, minifyJS, minifyJSON } from "../../shared/minify.js";
|
|
4
|
-
const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json"]);
|
|
5
|
-
const SKIP_JS_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
|
|
3
|
+
import { JSON_TYPES, minifyCSS, minifyJS, minifyJSON, SKIP_JS_TYPES } from "../../shared/minify.js";
|
|
6
4
|
export default defineNuxtPlugin({
|
|
7
5
|
enforce: "post",
|
|
8
6
|
setup() {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineEventHandler, getRequestHost, sendRedirect, setHeader } from "h3";
|
|
2
1
|
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) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { defineEventHandler, setHeader } from "h3";
|
|
3
2
|
import { parseURL } from "ufo";
|
|
3
|
+
import { defineEventHandler, setHeader } from "#nuxtseo/h3";
|
|
4
4
|
import { fileMapping } from "#seo-utils-virtual/pageDirImages";
|
|
5
5
|
export default defineEventHandler(async (e) => {
|
|
6
6
|
const path = parseURL(e.path).pathname;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: any;
|
|
2
2
|
export default _default;
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { defineNitroPlugin } from "
|
|
2
|
-
import { minifyCSS, minifyJS, minifyJSON } from "../../shared/minify.js";
|
|
1
|
+
import { defineNitroPlugin } from "#nuxtseo/nitro";
|
|
2
|
+
import { JSON_TYPES, minifyCSS, minifyJS, minifyJSON, SKIP_JS_TYPES } 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;
|
|
6
|
-
const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json"]);
|
|
7
|
-
const SKIP_JS_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
|
|
8
6
|
function minifyChunk(chunk) {
|
|
9
7
|
let out = chunk.replace(INLINE_SCRIPT_RE, (full, attrs, content) => {
|
|
10
8
|
if (!content)
|
|
@@ -1,18 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
2
|
-
runtimeConfig: any;
|
|
3
|
-
siteConfig: {
|
|
4
|
-
url: string;
|
|
5
|
-
name: any;
|
|
6
|
-
description: any;
|
|
7
|
-
env: any;
|
|
8
|
-
indexable: any;
|
|
9
|
-
trailingSlash: any;
|
|
10
|
-
titleSeparator: any;
|
|
11
|
-
defaultLocale: any;
|
|
12
|
-
currentLocale: any;
|
|
13
|
-
twitter: any;
|
|
14
|
-
};
|
|
15
|
-
headConfig: any;
|
|
16
|
-
path: string | number | true | import("ufo").QueryValue[] | Record<string, any>;
|
|
17
|
-
}>;
|
|
1
|
+
declare const _default: any;
|
|
18
2
|
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { defineEventHandler, getQuery } from "h3";
|
|
2
|
-
import { useRuntimeConfig } from "
|
|
1
|
+
import { defineEventHandler, getQuery } from "#nuxtseo/h3";
|
|
2
|
+
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
3
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
4
|
export default defineEventHandler((e) => {
|
|
5
5
|
const siteConfig = getSiteConfig(e);
|
|
@@ -10,6 +10,7 @@ export default defineEventHandler((e) => {
|
|
|
10
10
|
return {
|
|
11
11
|
runtimeConfig: {
|
|
12
12
|
version: runtimeConfig.public?.["nuxt-seo-utils-version"] || "unknown",
|
|
13
|
+
appBaseURL: runtimeConfig.app.baseURL,
|
|
13
14
|
...seoUtils
|
|
14
15
|
},
|
|
15
16
|
siteConfig: {
|
|
@@ -1 +1,9 @@
|
|
|
1
|
+
import type { RuntimeI18nConfig } from 'nuxtseo-shared/i18n-runtime';
|
|
2
|
+
interface BreadcrumbLocaleContext {
|
|
3
|
+
locale?: string;
|
|
4
|
+
defaultLocale?: string;
|
|
5
|
+
strategy?: RuntimeI18nConfig['strategy'];
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveBreadcrumbRoot(rootNode: string, context: BreadcrumbLocaleContext): string;
|
|
1
8
|
export declare function pathBreadcrumbSegments(path: string, rootNode?: string): string[];
|
|
9
|
+
export {};
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { localePath } from "nuxtseo-shared/i18n-runtime";
|
|
1
2
|
import { hasTrailingSlash, parseURL, stringifyParsedURL, withTrailingSlash } from "ufo";
|
|
3
|
+
export function resolveBreadcrumbRoot(rootNode, context) {
|
|
4
|
+
if (!context.locale || !context.strategy)
|
|
5
|
+
return rootNode;
|
|
6
|
+
return localePath(rootNode, context.locale, {
|
|
7
|
+
defaultLocale: context.defaultLocale || context.locale,
|
|
8
|
+
strategy: context.strategy,
|
|
9
|
+
locales: []
|
|
10
|
+
});
|
|
11
|
+
}
|
|
2
12
|
export function pathBreadcrumbSegments(path, rootNode = "/") {
|
|
3
13
|
const startNode = parseURL(path);
|
|
4
14
|
const appendsTrailingSlash = hasTrailingSlash(startNode.pathname);
|
|
@@ -1,3 +1,5 @@
|
|
|
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"]);
|
|
1
3
|
export function minifyJS(code) {
|
|
2
4
|
let result = "";
|
|
3
5
|
let i = 0;
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { Head, MetaFlat, RawInput } from '@unhead/vue/types';
|
|
1
|
+
import type { Head, Link, MetaFlat, RawInput } from '@unhead/vue/types';
|
|
2
|
+
export interface ColorModeIconLinks {
|
|
3
|
+
dark: Link[];
|
|
4
|
+
light: Link[];
|
|
5
|
+
}
|
|
2
6
|
export type MetaFlatSerializable = MetaFlat & {
|
|
3
7
|
title?: RawInput<'title'>;
|
|
4
8
|
titleTemplate?: RawInput<'titleTemplate'>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
const APPLE_ICON_RE = /^(?:[^.]+\.)?apple-(?:icon|touch(?:-icon)?)(?:[.-][^.]+)?\.(?:jpe?g|png)$/i;
|
|
4
|
+
const ICON_RE = /^(?:favicon(?:[.-][^.]+)?\.(?:ico|jpe?g|png|svg)|(?:[^.]+\.)?icon(?:[.-][^.]+)?\.(?:ico|jpe?g|png|svg))$/i;
|
|
5
|
+
const SIZE_RE = /^\d+x\d+$/;
|
|
6
|
+
const COLOR_SCHEME_MEDIA_RE = /^\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)$/i;
|
|
7
|
+
function classifyIconFilename(filename) {
|
|
8
|
+
if (APPLE_ICON_RE.test(filename))
|
|
9
|
+
return "apple-touch-icon";
|
|
10
|
+
if (ICON_RE.test(filename))
|
|
11
|
+
return "icon";
|
|
12
|
+
}
|
|
13
|
+
function getIconRel(rel) {
|
|
14
|
+
if (typeof rel !== "string")
|
|
15
|
+
return;
|
|
16
|
+
const tokens = rel.toLowerCase().split(/\s+/);
|
|
17
|
+
if (tokens.includes("apple-touch-icon"))
|
|
18
|
+
return "apple-touch-icon";
|
|
19
|
+
if (tokens.includes("icon"))
|
|
20
|
+
return "icon";
|
|
21
|
+
}
|
|
22
|
+
function getIconColorMode(media) {
|
|
23
|
+
if (typeof media !== "string")
|
|
24
|
+
return;
|
|
25
|
+
return COLOR_SCHEME_MEDIA_RE.exec(media.trim())?.[1]?.toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
function partitionColorModeIconLinks(links) {
|
|
28
|
+
const candidates = links.map((link, index) => {
|
|
29
|
+
const iconLink = link;
|
|
30
|
+
return {
|
|
31
|
+
index,
|
|
32
|
+
link: iconLink,
|
|
33
|
+
mode: getIconColorMode(iconLink.media),
|
|
34
|
+
rel: getIconRel(iconLink.rel)
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const reactiveRels = /* @__PURE__ */ new Set();
|
|
38
|
+
for (const rel of ["icon", "apple-touch-icon"]) {
|
|
39
|
+
const modes = new Set(candidates.filter((candidate) => candidate.rel === rel).map((candidate) => candidate.mode));
|
|
40
|
+
if (modes.has("dark") && modes.has("light"))
|
|
41
|
+
reactiveRels.add(rel);
|
|
42
|
+
}
|
|
43
|
+
if (reactiveRels.size === 0)
|
|
44
|
+
return { _tag: "StaticIconLinks", links };
|
|
45
|
+
const icons = { dark: [], light: [] };
|
|
46
|
+
const reactiveIndexes = /* @__PURE__ */ new Set();
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (!candidate.rel || !candidate.mode || !reactiveRels.has(candidate.rel))
|
|
49
|
+
continue;
|
|
50
|
+
const { media: _, ...link } = candidate.link;
|
|
51
|
+
icons[candidate.mode].push(link);
|
|
52
|
+
reactiveIndexes.add(candidate.index);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
_tag: "ReactiveIconLinks",
|
|
56
|
+
links: links.filter((_, index) => !reactiveIndexes.has(index)),
|
|
57
|
+
icons
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function normalizeIconSizes(configured, resolved) {
|
|
61
|
+
if (typeof configured !== "string" || !configured.trim())
|
|
62
|
+
return { sizes: resolved, normalized: false };
|
|
63
|
+
if (resolved === "any")
|
|
64
|
+
return { sizes: configured, normalized: false };
|
|
65
|
+
const resolvedSizes = new Set(resolved.split(/\s+/));
|
|
66
|
+
const configuredSizes = configured.toLowerCase().trim().split(/\s+/);
|
|
67
|
+
const isValidSubset = configuredSizes.every((size) => SIZE_RE.test(size) && resolvedSizes.has(size));
|
|
68
|
+
return isValidSubset ? { sizes: configured, normalized: false } : { sizes: resolved, normalized: true };
|
|
69
|
+
}
|
|
70
|
+
function pngToIco(images) {
|
|
71
|
+
const headerSize = 6;
|
|
72
|
+
const directoryEntrySize = 16;
|
|
73
|
+
const header = Buffer.alloc(headerSize);
|
|
74
|
+
header.writeUInt16LE(0, 0);
|
|
75
|
+
header.writeUInt16LE(1, 2);
|
|
76
|
+
header.writeUInt16LE(images.length, 4);
|
|
77
|
+
let offset = headerSize + directoryEntrySize * images.length;
|
|
78
|
+
const directoryEntries = images.map(({ buffer, width, height }) => {
|
|
79
|
+
const entry = Buffer.alloc(directoryEntrySize);
|
|
80
|
+
entry.writeUInt8(width === 256 ? 0 : width, 0);
|
|
81
|
+
entry.writeUInt8(height === 256 ? 0 : height, 1);
|
|
82
|
+
entry.writeUInt8(0, 2);
|
|
83
|
+
entry.writeUInt8(0, 3);
|
|
84
|
+
entry.writeUInt16LE(1, 4);
|
|
85
|
+
entry.writeUInt16LE(32, 6);
|
|
86
|
+
entry.writeUInt32LE(buffer.length, 8);
|
|
87
|
+
entry.writeUInt32LE(offset, 12);
|
|
88
|
+
offset += buffer.length;
|
|
89
|
+
return entry;
|
|
90
|
+
});
|
|
91
|
+
return Buffer.concat([header, ...directoryEntries, ...images.map((image) => image.buffer)]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { partitionColorModeIconLinks as a, classifyIconFilename as c, getIconRel as g, normalizeIconSizes as n, pngToIco as p };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-seo-utils",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.4.0",
|
|
5
5
|
"description": "SEO utilities to improve your Nuxt sites discoverability and shareability",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://github.com/sponsors/harlan-zw",
|
|
@@ -33,15 +33,14 @@
|
|
|
33
33
|
"@unhead/vue",
|
|
34
34
|
"@unhead/vue/plugins",
|
|
35
35
|
"@unhead/vue/utils",
|
|
36
|
-
"@unhead/vue/vite"
|
|
37
|
-
"@unhead/bundler/vite"
|
|
36
|
+
"@unhead/vue/vite"
|
|
38
37
|
]
|
|
39
38
|
},
|
|
40
39
|
"peerDependencies": {
|
|
41
40
|
"@unhead/vue": "^2.0.7 || ^3.0.0",
|
|
42
41
|
"esbuild": ">=0.17.0",
|
|
43
42
|
"lightningcss": ">=1.20.0",
|
|
44
|
-
"nuxt": "^3.0.0 || ^4.0.0",
|
|
43
|
+
"nuxt": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
|
45
44
|
"rolldown": ">=1.0.0-beta.0",
|
|
46
45
|
"unhead": "^2.0.7 || ^3.0.0"
|
|
47
46
|
},
|
|
@@ -63,16 +62,15 @@
|
|
|
63
62
|
}
|
|
64
63
|
},
|
|
65
64
|
"dependencies": {
|
|
66
|
-
"@nuxt/kit": "^4.5.
|
|
67
|
-
"@unhead/bundler": "3.2.1",
|
|
65
|
+
"@nuxt/kit": "^4.5.1",
|
|
68
66
|
"citty": "^0.2.2",
|
|
69
67
|
"consola": "^3.4.2",
|
|
70
68
|
"defu": "^6.1.7",
|
|
71
69
|
"escape-string-regexp": "^5.0.0",
|
|
72
|
-
"exsolve": "^1.1.
|
|
70
|
+
"exsolve": "^1.1.1",
|
|
73
71
|
"image-size": "^2.0.2",
|
|
74
|
-
"nuxt-site-config": "^4.1.
|
|
75
|
-
"nuxtseo-shared": "^5.3.
|
|
72
|
+
"nuxt-site-config": "^4.1.4",
|
|
73
|
+
"nuxtseo-shared": "^5.3.11",
|
|
76
74
|
"pathe": "^2.0.3",
|
|
77
75
|
"pkg-types": "^2.3.1",
|
|
78
76
|
"scule": "^1.3.0",
|
|
@@ -83,38 +81,36 @@
|
|
|
83
81
|
"sharp": "^0.35.3"
|
|
84
82
|
},
|
|
85
83
|
"devDependencies": {
|
|
86
|
-
"@antfu/eslint-config": "^9.
|
|
87
|
-
"@nuxt/devtools-kit": "4.0.0-alpha.
|
|
88
|
-
"@nuxt/module-builder": "^1.0.
|
|
89
|
-
"@nuxt/schema": "^4.5.
|
|
90
|
-
"@nuxt/test-utils": "^4.0
|
|
84
|
+
"@antfu/eslint-config": "^9.2.0",
|
|
85
|
+
"@nuxt/devtools-kit": "4.0.0-alpha.9",
|
|
86
|
+
"@nuxt/module-builder": "^1.0.3",
|
|
87
|
+
"@nuxt/schema": "^4.5.1",
|
|
88
|
+
"@nuxt/test-utils": "^4.1.0",
|
|
91
89
|
"@nuxt/ui": "^4.10.0",
|
|
92
|
-
"@nuxtjs/i18n": "^10.
|
|
93
|
-
"@types/node": "^26.1.
|
|
94
|
-
"@unhead/vue": "3.2.
|
|
90
|
+
"@nuxtjs/i18n": "^10.5.0",
|
|
91
|
+
"@types/node": "^26.1.2",
|
|
92
|
+
"@unhead/vue": "3.2.3",
|
|
95
93
|
"@vue/test-utils": "^2.4.11",
|
|
96
|
-
"bumpp": "^
|
|
94
|
+
"bumpp": "^12.0.0",
|
|
97
95
|
"case-police": "^2.2.1",
|
|
98
96
|
"cheerio": "^1.2.0",
|
|
99
|
-
"eslint": "^10.
|
|
100
|
-
"eslint-plugin-harlanzw": "^0.17.
|
|
101
|
-
"happy-dom": "^20.
|
|
102
|
-
"lint-staged": "^17.0
|
|
97
|
+
"eslint": "^10.8.0",
|
|
98
|
+
"eslint-plugin-harlanzw": "^0.17.1",
|
|
99
|
+
"happy-dom": "^20.11.1",
|
|
100
|
+
"lint-staged": "^17.2.0",
|
|
103
101
|
"markdownlint-cli": "^0.49.1",
|
|
104
|
-
"nuxt": "^4.5.
|
|
102
|
+
"nuxt": "^4.5.1",
|
|
105
103
|
"nuxt-security": "^2.6.0",
|
|
106
|
-
"nuxtseo-layer-devtools": "^5.3.
|
|
107
|
-
"playwright-core": "^1.
|
|
108
|
-
"sass": "^1.
|
|
109
|
-
"semver": "^7.8.5",
|
|
110
|
-
"sirv": "^3.0.2",
|
|
104
|
+
"nuxtseo-layer-devtools": "^5.3.6",
|
|
105
|
+
"playwright-core": "^1.62.0",
|
|
106
|
+
"sass": "^1.102.0",
|
|
111
107
|
"typescript": "6.0.3",
|
|
112
108
|
"ultrahtml": "^1.7.0",
|
|
113
109
|
"unbuild": "^3.6.1",
|
|
114
|
-
"unhead": "3.2.
|
|
110
|
+
"unhead": "3.2.3",
|
|
115
111
|
"vitest": "^4.1.10",
|
|
116
112
|
"vue": "^3.5.40",
|
|
117
|
-
"vue-tsc": "^3.3.
|
|
113
|
+
"vue-tsc": "^3.3.8"
|
|
118
114
|
},
|
|
119
115
|
"scripts": {
|
|
120
116
|
"client:build": "node -e \"require('fs').cpSync('devtools','dist/devtools',{recursive:true})\"",
|
|
@@ -126,9 +122,10 @@
|
|
|
126
122
|
"lint:fix": "eslint . --fix",
|
|
127
123
|
"typecheck": "nuxt typecheck",
|
|
128
124
|
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/i18n && nuxt prepare test/fixtures/csp",
|
|
129
|
-
"test": "vitest",
|
|
125
|
+
"test": "vitest && pnpm test:nuxt5",
|
|
130
126
|
"test:run": "vitest run --exclude '**/\\_\\_runtime\\_\\_/**'",
|
|
131
127
|
"test:runtime": "cd test/fixtures/basic && vitest --run",
|
|
128
|
+
"test:nuxt5": "pnpm pack --out test/fixtures/nuxt5/module.tgz && node test/fixtures/nuxt5/run.mjs",
|
|
132
129
|
"release": "pnpm build && bumpp -x \"npx changelogen --output=CHANGELOG.md\"",
|
|
133
130
|
"lint:docs": "markdownlint ./docs/content && case-police 'docs/content/**/*.md' *.md",
|
|
134
131
|
"lint:docs:fix": "markdownlint ./docs/content --fix && case-police 'docs/content/**/*.md' *.md --fix",
|