nuxt-seo-utils 8.4.1 → 8.4.3

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 CHANGED
@@ -9,7 +9,7 @@ import { p as pngToIco } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
9
9
  import 'node:buffer';
10
10
 
11
11
  const name = "nuxt-seo-utils";
12
- const version = "8.4.1";
12
+ const version = "8.4.3";
13
13
  const description = "SEO utilities to improve your Nuxt sites discoverability and shareability";
14
14
 
15
15
  const ICON_SIZES = [
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "8.4.1",
7
+ "version": "8.4.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -13,10 +13,9 @@ import { encodePath, joinURL } from 'ufo';
13
13
  import { c as classifyIconFilename, g as getIconRel, n as normalizeIconSizes, a as partitionColorModeIconLinks } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
14
14
  import escapeRE from 'escape-string-regexp';
15
15
  import { readFile, readdir } from 'node:fs/promises';
16
- import imageSize from 'image-size';
16
+ import { Buffer } from 'node:buffer';
17
17
  import { JSON_TYPES, SKIP_JS_TYPES } from '../dist/runtime/shared/minify.js';
18
18
  import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
19
- import 'node:buffer';
20
19
 
21
20
  function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
22
21
  const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || {};
@@ -28,9 +27,10 @@ function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
28
27
  ...nuxt.options.app.head,
29
28
  meta: [
30
29
  ...nuxt.options.app.head.meta || [],
31
- // @ts-expect-error untyped
32
30
  ...unpackMeta(seoMeta),
33
31
  ...unpackMeta(configSeoMeta || {})
32
+ // Unhead accepts null and array content.
33
+ // Nuxt's static head type remains narrower.
34
34
  ]
35
35
  };
36
36
  delete nuxt.options.app.seoMeta;
@@ -187,6 +187,134 @@ function parseSegment(segment) {
187
187
  return tokens;
188
188
  }
189
189
 
190
+ const PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
191
+ const SVG_ROOT_RE = /<svg(?:\s|>)/i;
192
+ const SVG_LENGTH_RE = /^((?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)(cm|em|ex|in|m|mm|pc|pt|px)?$/i;
193
+ const SVG_UNIT_PIXELS = {
194
+ cm: 96 / 2.54,
195
+ em: 16,
196
+ ex: 8,
197
+ in: 96,
198
+ m: 96 / 2.54 * 100,
199
+ mm: 96 / 2.54 / 10,
200
+ pc: 96 / 72 / 12,
201
+ pt: 96 / 72,
202
+ px: 1
203
+ };
204
+ function validDimensions(width, height) {
205
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
206
+ return void 0;
207
+ return { width, height };
208
+ }
209
+ function parsePng(buffer) {
210
+ if (buffer.length < 24 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE))
211
+ return void 0;
212
+ if (buffer.readUInt32BE(8) !== 13 || buffer.toString("ascii", 12, 16) !== "IHDR")
213
+ return void 0;
214
+ return validDimensions(buffer.readUInt32BE(16), buffer.readUInt32BE(20));
215
+ }
216
+ function parseGif(buffer) {
217
+ if (buffer.length < 10 || !["GIF87a", "GIF89a"].includes(buffer.toString("ascii", 0, 6)))
218
+ return void 0;
219
+ return validDimensions(buffer.readUInt16LE(6), buffer.readUInt16LE(8));
220
+ }
221
+ function isJpegSizeMarker(marker) {
222
+ return marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker);
223
+ }
224
+ function parseJpeg(buffer) {
225
+ if (buffer.length < 4 || buffer[0] !== 255 || buffer[1] !== 216)
226
+ return void 0;
227
+ let offset = 2;
228
+ while (offset < buffer.length) {
229
+ while (offset < buffer.length && buffer[offset] !== 255)
230
+ offset++;
231
+ while (offset < buffer.length && buffer[offset] === 255)
232
+ offset++;
233
+ if (offset >= buffer.length)
234
+ return void 0;
235
+ const marker = buffer[offset++];
236
+ if (marker === 217 || marker === 218)
237
+ return void 0;
238
+ if (marker >= 208 && marker <= 215 || marker === 1)
239
+ continue;
240
+ if (offset + 2 > buffer.length)
241
+ return void 0;
242
+ const segmentLength = buffer.readUInt16BE(offset);
243
+ if (segmentLength < 2 || offset + segmentLength > buffer.length)
244
+ return void 0;
245
+ if (isJpegSizeMarker(marker)) {
246
+ if (segmentLength < 11)
247
+ return void 0;
248
+ const componentCount = buffer[offset + 7];
249
+ if (componentCount === 0 || segmentLength !== 8 + componentCount * 3)
250
+ return void 0;
251
+ return validDimensions(buffer.readUInt16BE(offset + 5), buffer.readUInt16BE(offset + 3));
252
+ }
253
+ offset += segmentLength;
254
+ }
255
+ return void 0;
256
+ }
257
+ function parseIco(buffer) {
258
+ if (buffer.length < 6 || buffer.readUInt16LE(0) !== 0 || buffer.readUInt16LE(2) !== 1)
259
+ return void 0;
260
+ const count = buffer.readUInt16LE(4);
261
+ const directoryEnd = 6 + count * 16;
262
+ if (count === 0 || directoryEnd > buffer.length)
263
+ return void 0;
264
+ const images = Array.from({ length: count }, (_, index) => {
265
+ const offset = 6 + index * 16;
266
+ const payloadSize = buffer.readUInt32LE(offset + 8);
267
+ const payloadOffset = buffer.readUInt32LE(offset + 12);
268
+ if (payloadSize === 0 || payloadOffset < directoryEnd || payloadOffset > buffer.length - payloadSize)
269
+ return void 0;
270
+ return {
271
+ width: buffer[offset] || 256,
272
+ height: buffer[offset + 1] || 256
273
+ };
274
+ });
275
+ if (images.includes(void 0))
276
+ return void 0;
277
+ const validImages = images;
278
+ const largestImage = validImages.reduce((largest, image) => image.width * image.height > largest.width * largest.height ? image : largest);
279
+ return { ...largestImage, images: validImages };
280
+ }
281
+ function parseSvgLength(root, name) {
282
+ const value = root.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*["']([^"']+)["']`, "i"))?.[1]?.trim();
283
+ if (!value)
284
+ return void 0;
285
+ const match = SVG_LENGTH_RE.exec(value);
286
+ if (!match)
287
+ return void 0;
288
+ const parsed = Number(match[1]) * (SVG_UNIT_PIXELS[match[2]?.toLowerCase() || "px"] || 1);
289
+ return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : void 0;
290
+ }
291
+ function parseSvg(buffer) {
292
+ const source = buffer.toString("utf8", 0, Math.min(buffer.length, 4096));
293
+ const rootStart = source.search(SVG_ROOT_RE);
294
+ const rootEnd = source.indexOf(">", rootStart);
295
+ if (rootStart < 0 || rootEnd < 0)
296
+ return void 0;
297
+ const root = source.slice(rootStart, rootEnd + 1);
298
+ const width = parseSvgLength(root, "width");
299
+ const height = parseSvgLength(root, "height");
300
+ if (width && height)
301
+ return { width, height };
302
+ const viewBox = root.match(/(?:^|\s)viewBox\s*=\s*["']([^"']+)["']/i)?.[1]?.trim().split(/[\s,]+/).map(Number);
303
+ if (!viewBox || viewBox.length !== 4 || !viewBox.every((value) => Number.isFinite(value)))
304
+ return void 0;
305
+ const viewBoxDimensions = validDimensions(Math.round(viewBox[2]), Math.round(viewBox[3]));
306
+ if (!viewBoxDimensions)
307
+ return void 0;
308
+ if (width)
309
+ return { width, height: Math.floor(width * viewBoxDimensions.height / viewBoxDimensions.width) };
310
+ if (height)
311
+ return { width: Math.floor(height * viewBoxDimensions.width / viewBoxDimensions.height), height };
312
+ return viewBoxDimensions;
313
+ }
314
+ function parseImageDimensions(buffer) {
315
+ return parsePng(buffer) || parseGif(buffer) || parseJpeg(buffer) || parseIco(buffer) || parseSvg(buffer);
316
+ }
317
+
190
318
  function hasMetaProperty(input, property) {
191
319
  return input.meta?.some((meta) => meta.property === property);
192
320
  }
@@ -227,7 +355,14 @@ async function getImageMeta(base, path, isIcon = false) {
227
355
  }
228
356
  async function getImageDimensions(absolutePath) {
229
357
  const buffer = await readFile(absolutePath);
230
- return imageSize(buffer);
358
+ const dimensions = parseImageDimensions(buffer);
359
+ if (!dimensions)
360
+ throw new TypeError(`Unsupported image format: ${absolutePath}`);
361
+ return {
362
+ width: dimensions.width,
363
+ height: dimensions.height,
364
+ images: dimensions.images
365
+ };
231
366
  }
232
367
 
233
368
  const DIR_SUFFIX_RE = /\/_dir$/;
@@ -16,7 +16,7 @@ export default defineNuxtPlugin({
16
16
  head.use({
17
17
  key: "absoluteImageUrls",
18
18
  hooks: {
19
- "tags:resolve": async ({ tags }) => {
19
+ "tags:resolve": ({ tags }) => {
20
20
  for (const tag of tags) {
21
21
  if (tag.tag !== "meta")
22
22
  continue;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-seo-utils",
3
3
  "type": "module",
4
- "version": "8.4.1",
4
+ "version": "8.4.3",
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",
@@ -68,8 +68,7 @@
68
68
  "defu": "^6.1.7",
69
69
  "escape-string-regexp": "^5.0.0",
70
70
  "exsolve": "^1.1.1",
71
- "image-size": "^2.0.2",
72
- "nuxt-site-config": "^4.2.1",
71
+ "nuxt-site-config": "^4.2.2",
73
72
  "nuxtseo-shared": "^5.3.12",
74
73
  "pathe": "^2.0.3",
75
74
  "pkg-types": "^2.3.1",
@@ -95,7 +94,7 @@
95
94
  "case-police": "^2.2.1",
96
95
  "cheerio": "^1.2.0",
97
96
  "eslint": "^10.8.1",
98
- "eslint-plugin-harlanzw": "^0.17.1",
97
+ "eslint-plugin-harlanzw": "^0.19.2",
99
98
  "happy-dom": "^20.11.2",
100
99
  "lint-staged": "^17.3.0",
101
100
  "markdownlint-cli": "^0.49.1",