svelte-vitals 0.12.0 → 0.13.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/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  knownRuleIds,
7
7
  readPackageVersion,
8
8
  run
9
- } from "./chunk-NBNJRN52.js";
9
+ } from "./chunk-Y4JY3ODE.js";
10
10
 
11
11
  // src/bin.ts
12
12
  import mri from "mri";
@@ -164,6 +164,11 @@ function valueFromNodes(nodes) {
164
164
  const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
165
165
  return text.trim().length > 0 ? "static" : "absent";
166
166
  }
167
+ function textFromNodes(nodes) {
168
+ if (!Array.isArray(nodes) || nodes.some((n) => n?.type === "ExpressionTag")) return void 0;
169
+ const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
170
+ return text.trim().length > 0 ? text : void 0;
171
+ }
167
172
  function attrText(attributes, name) {
168
173
  const attr = findAttr(attributes, name);
169
174
  if (!attr) return void 0;
@@ -210,37 +215,52 @@ function tagsFromHead(head) {
210
215
  const children = head?.fragment?.nodes ?? [];
211
216
  for (const node of children) {
212
217
  if (node?.type === "TitleElement") {
213
- tags.push({ kind: "title", value: valueFromNodes(node.fragment?.nodes ?? []) });
218
+ const titleNodes = node.fragment?.nodes ?? [];
219
+ const text = textFromNodes(titleNodes);
220
+ tags.push({ kind: "title", value: valueFromNodes(titleNodes), ...text !== void 0 ? { text } : {} });
214
221
  continue;
215
222
  }
216
223
  if (node?.type !== "RegularElement") continue;
217
224
  if (node.name === "meta") {
225
+ const charset = attrValue(node.attributes, "charset");
226
+ if (charset !== "absent") {
227
+ tags.push({ kind: "meta", name: "charset", value: charset });
228
+ continue;
229
+ }
218
230
  const name = attrText(node.attributes, "name");
219
231
  const property = attrText(node.attributes, "property");
220
232
  const content = name === "robots" ? attrText(node.attributes, "content") : void 0;
221
233
  const noindex = content !== void 0 && /(^|[\s,])(noindex|none)([\s,]|$)/i.test(content);
234
+ const contentValue = attrValue(node.attributes, "content");
235
+ const descText = name === "description" && contentValue === "static" ? attrText(node.attributes, "content") : void 0;
222
236
  tags.push({
223
237
  kind: "meta",
224
238
  ...name ? { name } : {},
225
239
  ...property ? { property } : {},
226
- value: attrValue(node.attributes, "content"),
227
- ...noindex ? { noindex: true } : {}
240
+ value: contentValue,
241
+ ...noindex ? { noindex: true } : {},
242
+ ...descText !== void 0 ? { text: descText } : {}
228
243
  });
229
244
  } else if (node.name === "link") {
230
245
  const rel = attrText(node.attributes, "rel");
231
246
  const hasAs = findAttr(node.attributes, "as") !== void 0;
232
247
  const asLiteral = attrText(node.attributes, "as");
233
248
  const hasCrossorigin = findAttr(node.attributes, "crossorigin") !== void 0;
249
+ const hreflang = attrText(node.attributes, "hreflang");
234
250
  tags.push({
235
251
  kind: "link",
236
252
  ...rel ? { rel } : {},
237
253
  value: attrValue(node.attributes, "href"),
238
254
  ...hasAs ? { hasAs: true } : {},
239
255
  ...asLiteral ? { as: asLiteral } : {},
240
- ...hasCrossorigin ? { hasCrossorigin: true } : {}
256
+ ...hasCrossorigin ? { hasCrossorigin: true } : {},
257
+ // Keep a literal empty hreflang="" (present-but-invalid) so SEO026 can flag it.
258
+ ...hreflang !== void 0 ? { hreflang } : {}
241
259
  });
242
260
  } else if (node.name === "script" && attrText(node.attributes, "type") === "application/ld+json") {
243
- tags.push({ kind: "jsonld", value: valueFromNodes(node.fragment?.nodes ?? []) });
261
+ const nodes = node.fragment?.nodes ?? [];
262
+ const raw = textFromNodes(nodes);
263
+ tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
244
264
  }
245
265
  }
246
266
  return tags;
@@ -252,6 +272,12 @@ function attrValueOf(attr) {
252
272
  if (v && v.type === "ExpressionTag") return "dynamic";
253
273
  return "absent";
254
274
  }
275
+ function attrTextOf(attr) {
276
+ const v = attr?.value;
277
+ if (!Array.isArray(v) || v.some((n) => n?.type === "ExpressionTag")) return void 0;
278
+ const text = v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
279
+ return text.trim().length > 0 ? text : void 0;
280
+ }
255
281
  function collectComponents(node, acc) {
256
282
  if (Array.isArray(node)) {
257
283
  for (const child of node) collectComponents(child, acc);
@@ -283,6 +309,7 @@ function collectImages(node, source, acc) {
283
309
  hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
284
310
  hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
285
311
  hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
312
+ hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
286
313
  line: lineOf(source, node.start)
287
314
  });
288
315
  }
@@ -290,6 +317,20 @@ function collectImages(node, source, acc) {
290
317
  if (key in node) collectImages(node[key], source, acc);
291
318
  }
292
319
  }
320
+ function collectHeadings(node, source, acc) {
321
+ if (Array.isArray(node)) {
322
+ for (const child of node) collectHeadings(child, source, acc);
323
+ return;
324
+ }
325
+ if (!node || typeof node !== "object") return;
326
+ if (node.type === "SvelteHead") return;
327
+ if (node.type === "RegularElement" && typeof node.name === "string" && /^h[1-6]$/.test(node.name)) {
328
+ acc.push({ level: Number(node.name[1]), line: lineOf(source, node.start) });
329
+ }
330
+ for (const key of CHILD_NODE_KEYS) {
331
+ if (key in node) collectHeadings(node[key], source, acc);
332
+ }
333
+ }
293
334
  function parseFile(source, filename) {
294
335
  const ast = parse(source, { modern: true, filename });
295
336
  const heads = [];
@@ -298,11 +339,14 @@ function parseFile(source, filename) {
298
339
  collectComponents(ast.fragment ?? ast, components);
299
340
  const images = [];
300
341
  collectImages(ast.fragment ?? ast, source, images);
342
+ const headings = [];
343
+ collectHeadings(ast.fragment ?? ast, source, headings);
301
344
  return {
302
345
  headTags: heads.flatMap(tagsFromHead),
303
346
  components,
304
347
  imports: collectImports(ast),
305
- images
348
+ images,
349
+ headings
306
350
  };
307
351
  }
308
352
 
@@ -319,10 +363,20 @@ var svelteMetaTagsAdapter = {
319
363
  resolve(use) {
320
364
  const tags = [];
321
365
  const attrs = use.attributes;
322
- const title = findAttr2(attrs, "title") ?? findAttr2(attrs, "titleTemplate");
323
- if (title) tags.push({ kind: "title", value: attrValueOf(title) });
366
+ const titleAttr = findAttr2(attrs, "title");
367
+ const templateAttr = findAttr2(attrs, "titleTemplate");
368
+ const title = titleAttr ?? templateAttr;
369
+ if (title) {
370
+ const value = attrValueOf(title);
371
+ const text = titleAttr && !templateAttr && value === "static" ? attrTextOf(titleAttr) : void 0;
372
+ tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
373
+ }
324
374
  const description = findAttr2(attrs, "description");
325
- if (description) tags.push({ kind: "meta", name: "description", value: attrValueOf(description) });
375
+ if (description) {
376
+ const value = attrValueOf(description);
377
+ const text = value === "static" ? attrTextOf(description) : void 0;
378
+ tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
379
+ }
326
380
  const canonical = findAttr2(attrs, "canonical");
327
381
  if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
328
382
  const robots = findAttr2(attrs, "robots");
@@ -345,9 +399,17 @@ var svelteSeoAdapter = {
345
399
  const tags = [];
346
400
  const attrs = use.attributes;
347
401
  const title = findAttr3(attrs, "title");
348
- if (title) tags.push({ kind: "title", value: attrValueOf(title) });
402
+ if (title) {
403
+ const value = attrValueOf(title);
404
+ const text = value === "static" ? attrTextOf(title) : void 0;
405
+ tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
406
+ }
349
407
  const description = findAttr3(attrs, "description");
350
- if (description) tags.push({ kind: "meta", name: "description", value: attrValueOf(description) });
408
+ if (description) {
409
+ const value = attrValueOf(description);
410
+ const text = value === "static" ? attrTextOf(description) : void 0;
411
+ tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
412
+ }
351
413
  const canonical = findAttr3(attrs, "canonical");
352
414
  if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
353
415
  const openGraph = findAttr3(attrs, "openGraph");
@@ -466,12 +528,16 @@ async function resolveRoute(rt, cwd, pageRel, config) {
466
528
  let broadOwn = false;
467
529
  let broadInherited = false;
468
530
  const images = [];
531
+ const headings = [];
469
532
  for (const { rel, isPage } of files) {
470
533
  const source = await rt.readFile(rt.join(cwd, rel));
471
534
  const parsed = parseFile(source, rel);
472
535
  for (const img of parsed.images) {
473
536
  images.push({ ...img, file: rel });
474
537
  }
538
+ for (const heading of parsed.headings) {
539
+ headings.push({ ...heading, file: rel });
540
+ }
475
541
  const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
476
542
  for (const tag of resolved.tags) {
477
543
  composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
@@ -491,7 +557,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
491
557
  const route = deriveRoute(pageRel);
492
558
  return {
493
559
  head: { route, source: "static", tags: [...composed.values()], file: pageRel },
494
- images: { route, images }
560
+ images: { route, images },
561
+ headings: { route, headings }
495
562
  };
496
563
  }
497
564
  async function collectRoutes(rt, cwd, config = defaultConfig) {
@@ -499,7 +566,8 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
499
566
  const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
500
567
  return {
501
568
  heads: facts.map((f) => f.head),
502
- images: facts.map((f) => f.images)
569
+ images: facts.map((f) => f.images),
570
+ headings: facts.map((f) => f.headings)
503
571
  };
504
572
  }
505
573
 
@@ -582,9 +650,10 @@ async function analyzeProject(opts = {}) {
582
650
  const collected = await collectRoutes(rt, cwd, config);
583
651
  const heads = collected.heads.filter((h) => matches(h.route));
584
652
  const images = collected.images.filter((i) => matches(i.route));
653
+ const headings = collected.headings.filter((h) => matches(h.route));
585
654
  const project = await collectProjectFacts(rt, cwd);
586
655
  const rules = selectRules(allRules2, config);
587
- const results = applyRuleSeverities(await runRules(rules, { heads, images, project, config }), config);
656
+ const results = applyRuleSeverities(await runRules(rules, { heads, images, headings, project, config }), config);
588
657
  return { results, config, version: readPackageVersion() };
589
658
  }
590
659
  async function run(opts = {}) {
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  knownRuleIds,
7
7
  routeMatcher,
8
8
  run
9
- } from "./chunk-NBNJRN52.js";
9
+ } from "./chunk-Y4JY3ODE.js";
10
10
  export {
11
11
  ProjectError,
12
12
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "mri": "^1.2.0",
43
43
  "svelte": "^5.56.3",
44
44
  "tinyglobby": "^0.2.17",
45
- "@svelte-vitals/core": "0.13.0"
45
+ "@svelte-vitals/core": "0.14.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^24.7.0"