nuxt-seo-utils 8.3.3 → 8.4.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.
Files changed (29) hide show
  1. package/dist/cli.mjs +11 -32
  2. package/dist/devtools/lib/seo-utils/state.ts +1 -1
  3. package/dist/devtools/lib/seo-utils/tools.test.ts +31 -0
  4. package/dist/devtools/lib/seo-utils/tools.ts +42 -3
  5. package/dist/devtools/pages/seo-utils/identity.vue +17 -14
  6. package/dist/module.d.mts +1 -1
  7. package/dist/module.d.ts +1 -1
  8. package/dist/module.json +1 -1
  9. package/dist/module.mjs +181 -110
  10. package/dist/runtime/app/composables/useBreadcrumbItems.js +6 -3
  11. package/dist/runtime/app/logic/colorModeIcons.d.ts +3 -0
  12. package/dist/runtime/app/logic/colorModeIcons.js +10 -0
  13. package/dist/runtime/app/plugins/0.routeRules.js +11 -13
  14. package/dist/runtime/app/plugins/colorModeIcons.d.ts +2 -0
  15. package/dist/runtime/app/plugins/colorModeIcons.js +17 -0
  16. package/dist/runtime/server/middleware/redirectCanonical.d.ts +1 -1
  17. package/dist/runtime/server/middleware/redirectCanonical.js +1 -1
  18. package/dist/runtime/server/middleware/resolveImagesInPagesDir.d.ts +1 -1
  19. package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +1 -1
  20. package/dist/runtime/server/plugins/minifyHtml.d.ts +1 -1
  21. package/dist/runtime/server/plugins/minifyHtml.js +1 -1
  22. package/dist/runtime/server/routes/__nuxt-seo-utils/debug.d.ts +1 -17
  23. package/dist/runtime/server/routes/__nuxt-seo-utils/debug.js +3 -2
  24. package/dist/runtime/shared/breadcrumbs.d.ts +8 -0
  25. package/dist/runtime/shared/breadcrumbs.js +10 -0
  26. package/dist/runtime/shared/minify.js +47 -34
  27. package/dist/runtime/types.d.ts +5 -1
  28. package/dist/shared/nuxt-seo-utils.NAV2YNQn.mjs +94 -0
  29. package/package.json +20 -24
@@ -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,23 +1,32 @@
1
1
  export const JSON_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json"]);
2
2
  export const SKIP_JS_TYPES = /* @__PURE__ */ new Set(["application/json", "application/ld+json", "speculationrules", "importmap"]);
3
+ const JS_MINIFY_CANDIDATE_RE = /[\s/]/;
4
+ const CSS_MINIFY_CANDIDATE_RE = /[\s/]|;\}|0\.\d/;
3
5
  export function minifyJS(code) {
6
+ if (!JS_MINIFY_CANDIDATE_RE.test(code))
7
+ return code;
4
8
  let result = "";
9
+ let last = "";
5
10
  let i = 0;
6
11
  const len = code.length;
12
+ const append = (value) => {
13
+ result += value;
14
+ last = value;
15
+ };
7
16
  while (i < len) {
8
17
  const ch = code[i];
9
18
  if (ch === "'" || ch === '"' || ch === "`") {
10
19
  const quote = ch;
11
- result += ch;
20
+ append(ch);
12
21
  i++;
13
22
  while (i < len && code[i] !== quote) {
14
23
  if (code[i] === "\\") {
15
- result += code[i++];
24
+ append(code[i++]);
16
25
  }
17
- result += code[i++];
26
+ append(code[i++]);
18
27
  }
19
28
  if (i < len)
20
- result += code[i++];
29
+ append(code[i++]);
21
30
  } else if (ch === "/" && code[i + 1] === "/") {
22
31
  i += 2;
23
32
  while (i < len && code[i] !== "\n")
@@ -34,65 +43,70 @@ export function minifyJS(code) {
34
43
  hasNewline = true;
35
44
  i++;
36
45
  }
37
- const prev = result.at(-1);
38
46
  const next = code[i];
39
- if (hasNewline && prev && next && prev !== "{" && prev !== "}" && prev !== ";" && next !== "}" && next !== ";")
40
- result += "\n";
41
- else if (prev && next && isIdentChar(prev) && isIdentChar(next))
42
- result += " ";
43
- else if (prev && next && (prev === "+" && next === "+" || prev === "-" && next === "-"))
44
- result += " ";
47
+ if (hasNewline && last && next && last !== "{" && last !== "}" && last !== ";" && next !== "}" && next !== ";")
48
+ append("\n");
49
+ else if (last && next && isIdentChar(last) && isIdentChar(next))
50
+ append(" ");
51
+ else if (last && next && (last === "+" && next === "+" || last === "-" && next === "-"))
52
+ append(" ");
45
53
  } else {
46
- result += ch;
54
+ append(ch);
47
55
  i++;
48
56
  }
49
57
  }
50
58
  return result.trim();
51
59
  }
52
60
  export function minifyCSS(code) {
61
+ if (!CSS_MINIFY_CANDIDATE_RE.test(code))
62
+ return code;
53
63
  let result = "";
64
+ let last = "";
54
65
  let i = 0;
55
66
  const parenStack = [];
56
67
  const len = code.length;
68
+ const append = (value) => {
69
+ result += value;
70
+ last = value;
71
+ };
57
72
  while (i < len) {
58
73
  const ch = code[i];
59
74
  if (ch === "'" || ch === '"') {
60
75
  const quote = ch;
61
- result += ch;
76
+ append(ch);
62
77
  i++;
63
78
  while (i < len && code[i] !== quote) {
64
79
  if (code[i] === "\\")
65
- result += code[i++];
66
- result += code[i++];
80
+ append(code[i++]);
81
+ append(code[i++]);
67
82
  }
68
83
  if (i < len)
69
- result += code[i++];
84
+ append(code[i++]);
70
85
  } else if (ch === "/" && code[i + 1] === "*") {
71
86
  i += 2;
72
87
  while (i < len && !(code[i] === "*" && code[i + 1] === "/"))
73
88
  i++;
74
89
  i += 2;
75
90
  } else if (ch === "(") {
76
- parenStack.push(isSelectorFunctionParen(result));
77
- result += ch;
91
+ parenStack.push(isSelectorFunctionParen(code, i));
92
+ append(ch);
78
93
  i++;
79
94
  } else if (ch === ")") {
80
95
  parenStack.pop();
81
- result += ch;
96
+ append(ch);
82
97
  i++;
83
98
  } else if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
84
99
  while (i < len && (code[i] === " " || code[i] === " " || code[i] === "\n" || code[i] === "\r"))
85
100
  i++;
86
- const prev = result.at(-1);
87
101
  const next = code[i];
88
102
  if (next === "!")
89
103
  continue;
90
104
  if (parenStack.length > 0) {
91
105
  const isPunct = parenStack[parenStack.length - 1] ? isCSSPunctuation : isCSSCalcPunctuation;
92
- if (prev && next && !isPunct(prev) && !isPunct(next))
93
- result += " ";
94
- } else if (prev && next && !isCSSPunctuation(prev) && !isCSSPunctuation(next)) {
95
- result += " ";
106
+ if (last && next && !isPunct(last) && !isPunct(next))
107
+ append(" ");
108
+ } else if (last && next && !isCSSPunctuation(last) && !isCSSPunctuation(next)) {
109
+ append(" ");
96
110
  }
97
111
  } else if (ch === ";") {
98
112
  let j = i + 1;
@@ -101,19 +115,18 @@ export function minifyCSS(code) {
101
115
  if (code[j] === "}") {
102
116
  i++;
103
117
  } else {
104
- result += ch;
118
+ append(ch);
105
119
  i++;
106
120
  }
107
121
  } else if (ch === "0" && code[i + 1] === "." && (code[i + 2] ?? "") >= "0" && (code[i + 2] ?? "") <= "9") {
108
- const prev = result.at(-1);
109
- if (prev && prev >= "0" && prev <= "9") {
110
- result += ch;
122
+ if (last && last >= "0" && last <= "9") {
123
+ append(ch);
111
124
  i++;
112
125
  } else {
113
126
  i++;
114
127
  }
115
128
  } else {
116
- result += ch;
129
+ append(ch);
117
130
  i++;
118
131
  }
119
132
  }
@@ -134,13 +147,13 @@ function isCSSCalcPunctuation(ch) {
134
147
  function isCSSNameChar(ch) {
135
148
  return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "-";
136
149
  }
137
- function isSelectorFunctionParen(result) {
138
- let j = result.length - 1;
139
- while (j >= 0 && isCSSNameChar(result[j]))
150
+ function isSelectorFunctionParen(code, parenIndex) {
151
+ let j = parenIndex - 1;
152
+ while (j >= 0 && isCSSNameChar(code[j]))
140
153
  j--;
141
- if (result[j] !== ":")
154
+ if (code[j] !== ":")
142
155
  return false;
143
- const name = result.slice(j + 1).toLowerCase();
156
+ const name = code.slice(j + 1, parenIndex).toLowerCase();
144
157
  return name === "is" || name === "where" || name === "not" || name === "has" || name === "matches" || name === "host" || name === "host-context" || name === "slotted";
145
158
  }
146
159
  export function minifyJSON(code) {
@@ -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.3.3",
4
+ "version": "8.4.1",
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",
@@ -37,18 +37,14 @@
37
37
  ]
38
38
  },
39
39
  "peerDependencies": {
40
- "@unhead/bundler": "^3.2.1",
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
  },
48
47
  "peerDependenciesMeta": {
49
- "@unhead/bundler": {
50
- "optional": true
51
- },
52
48
  "@unhead/vue": {
53
49
  "optional": true
54
50
  },
@@ -66,15 +62,15 @@
66
62
  }
67
63
  },
68
64
  "dependencies": {
69
- "@nuxt/kit": "^4.5.1",
65
+ "@nuxt/kit": "^4.5.2",
70
66
  "citty": "^0.2.2",
71
67
  "consola": "^3.4.2",
72
68
  "defu": "^6.1.7",
73
69
  "escape-string-regexp": "^5.0.0",
74
70
  "exsolve": "^1.1.1",
75
71
  "image-size": "^2.0.2",
76
- "nuxt-site-config": "^4.1.2",
77
- "nuxtseo-shared": "^5.3.6",
72
+ "nuxt-site-config": "^4.2.1",
73
+ "nuxtseo-shared": "^5.3.12",
78
74
  "pathe": "^2.0.3",
79
75
  "pkg-types": "^2.3.1",
80
76
  "scule": "^1.3.0",
@@ -85,37 +81,36 @@
85
81
  "sharp": "^0.35.3"
86
82
  },
87
83
  "devDependencies": {
88
- "@antfu/eslint-config": "^9.2.0",
84
+ "@antfu/eslint-config": "^9.3.0",
89
85
  "@nuxt/devtools-kit": "4.0.0-alpha.9",
90
86
  "@nuxt/module-builder": "^1.0.3",
91
- "@nuxt/schema": "^4.5.1",
87
+ "@nuxt/schema": "^4.5.2",
92
88
  "@nuxt/test-utils": "^4.1.0",
93
89
  "@nuxt/ui": "^4.10.0",
94
- "@nuxtjs/i18n": "^10.5.0",
95
- "@types/node": "^26.1.2",
96
- "@unhead/bundler": "^3.2.3",
90
+ "@nuxtjs/i18n": "^10.6.0",
91
+ "@types/node": "^26.2.0",
97
92
  "@unhead/vue": "3.2.3",
98
93
  "@vue/test-utils": "^2.4.11",
99
- "bumpp": "^12.0.0",
94
+ "bumpp": "^12.2.0",
100
95
  "case-police": "^2.2.1",
101
96
  "cheerio": "^1.2.0",
102
- "eslint": "^10.8.0",
97
+ "eslint": "^10.8.1",
103
98
  "eslint-plugin-harlanzw": "^0.17.1",
104
- "happy-dom": "^20.11.1",
105
- "lint-staged": "^17.2.0",
99
+ "happy-dom": "^20.11.2",
100
+ "lint-staged": "^17.3.0",
106
101
  "markdownlint-cli": "^0.49.1",
107
- "nuxt": "^4.5.1",
102
+ "nuxt": "^4.5.2",
108
103
  "nuxt-security": "^2.6.0",
109
- "nuxtseo-layer-devtools": "^5.3.6",
110
- "playwright-core": "^1.62.0",
104
+ "nuxtseo-layer-devtools": "^5.3.12",
105
+ "playwright-core": "^1.62.1",
111
106
  "sass": "^1.102.0",
112
107
  "typescript": "6.0.3",
113
108
  "ultrahtml": "^1.7.0",
114
109
  "unbuild": "^3.6.1",
115
110
  "unhead": "3.2.3",
116
111
  "vitest": "^4.1.10",
117
- "vue": "^3.5.40",
118
- "vue-tsc": "^3.3.8"
112
+ "vue": "^3.5.41",
113
+ "vue-tsc": "^3.3.9"
119
114
  },
120
115
  "scripts": {
121
116
  "client:build": "node -e \"require('fs').cpSync('devtools','dist/devtools',{recursive:true})\"",
@@ -127,9 +122,10 @@
127
122
  "lint:fix": "eslint . --fix",
128
123
  "typecheck": "nuxt typecheck",
129
124
  "prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/i18n && nuxt prepare test/fixtures/csp",
130
- "test": "vitest",
125
+ "test": "vitest && pnpm test:nuxt5",
131
126
  "test:run": "vitest run --exclude '**/\\_\\_runtime\\_\\_/**'",
132
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",
133
129
  "release": "pnpm build && bumpp -x \"npx changelogen --output=CHANGELOG.md\"",
134
130
  "lint:docs": "markdownlint ./docs/content && case-police 'docs/content/**/*.md' *.md",
135
131
  "lint:docs:fix": "markdownlint ./docs/content --fix && case-police 'docs/content/**/*.md' *.md --fix",