svelte-vitals 0.11.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-QI6NLWDD.js";
9
+ } from "./chunk-Y4JY3ODE.js";
10
10
 
11
11
  // src/bin.ts
12
12
  import mri from "mri";
@@ -95,13 +95,28 @@ async function detectAppHtmlLang(rt, cwd) {
95
95
  if (!await rt.exists(appHtmlPath)) return { presence: "none", value: "absent" };
96
96
  return detectHtmlLang(await rt.readFile(appHtmlPath));
97
97
  }
98
+ async function robotsRefsSitemap(rt, cwd) {
99
+ const p = rt.join(cwd, "static/robots.txt");
100
+ if (!await rt.exists(p)) return void 0;
101
+ try {
102
+ return /^\s*sitemap:/im.test(await rt.readFile(p));
103
+ } catch {
104
+ return void 0;
105
+ }
106
+ }
98
107
  async function collectProjectFacts(rt, cwd) {
99
108
  const [hasRobotsTxt, hasSitemap, htmlLang] = await Promise.all([
100
109
  existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
101
110
  existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
102
111
  detectAppHtmlLang(rt, cwd)
103
112
  ]);
104
- return { hasRobotsTxt, hasSitemap, htmlLang };
113
+ const robotsReferencesSitemap = await robotsRefsSitemap(rt, cwd);
114
+ return {
115
+ hasRobotsTxt,
116
+ hasSitemap,
117
+ htmlLang,
118
+ ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {}
119
+ };
105
120
  }
106
121
 
107
122
  // src/providers/source/parse.ts
@@ -149,6 +164,11 @@ function valueFromNodes(nodes) {
149
164
  const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
150
165
  return text.trim().length > 0 ? "static" : "absent";
151
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
+ }
152
172
  function attrText(attributes, name) {
153
173
  const attr = findAttr(attributes, name);
154
174
  if (!attr) return void 0;
@@ -195,34 +215,52 @@ function tagsFromHead(head) {
195
215
  const children = head?.fragment?.nodes ?? [];
196
216
  for (const node of children) {
197
217
  if (node?.type === "TitleElement") {
198
- 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 } : {} });
199
221
  continue;
200
222
  }
201
223
  if (node?.type !== "RegularElement") continue;
202
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
+ }
203
230
  const name = attrText(node.attributes, "name");
204
231
  const property = attrText(node.attributes, "property");
232
+ const content = name === "robots" ? attrText(node.attributes, "content") : void 0;
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;
205
236
  tags.push({
206
237
  kind: "meta",
207
238
  ...name ? { name } : {},
208
239
  ...property ? { property } : {},
209
- value: attrValue(node.attributes, "content")
240
+ value: contentValue,
241
+ ...noindex ? { noindex: true } : {},
242
+ ...descText !== void 0 ? { text: descText } : {}
210
243
  });
211
244
  } else if (node.name === "link") {
212
245
  const rel = attrText(node.attributes, "rel");
213
246
  const hasAs = findAttr(node.attributes, "as") !== void 0;
214
247
  const asLiteral = attrText(node.attributes, "as");
215
248
  const hasCrossorigin = findAttr(node.attributes, "crossorigin") !== void 0;
249
+ const hreflang = attrText(node.attributes, "hreflang");
216
250
  tags.push({
217
251
  kind: "link",
218
252
  ...rel ? { rel } : {},
219
253
  value: attrValue(node.attributes, "href"),
220
254
  ...hasAs ? { hasAs: true } : {},
221
255
  ...asLiteral ? { as: asLiteral } : {},
222
- ...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 } : {}
223
259
  });
224
260
  } else if (node.name === "script" && attrText(node.attributes, "type") === "application/ld+json") {
225
- 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 } : {} });
226
264
  }
227
265
  }
228
266
  return tags;
@@ -234,6 +272,12 @@ function attrValueOf(attr) {
234
272
  if (v && v.type === "ExpressionTag") return "dynamic";
235
273
  return "absent";
236
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
+ }
237
281
  function collectComponents(node, acc) {
238
282
  if (Array.isArray(node)) {
239
283
  for (const child of node) collectComponents(child, acc);
@@ -265,6 +309,7 @@ function collectImages(node, source, acc) {
265
309
  hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
266
310
  hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
267
311
  hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
312
+ hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
268
313
  line: lineOf(source, node.start)
269
314
  });
270
315
  }
@@ -272,6 +317,20 @@ function collectImages(node, source, acc) {
272
317
  if (key in node) collectImages(node[key], source, acc);
273
318
  }
274
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
+ }
275
334
  function parseFile(source, filename) {
276
335
  const ast = parse(source, { modern: true, filename });
277
336
  const heads = [];
@@ -280,11 +339,14 @@ function parseFile(source, filename) {
280
339
  collectComponents(ast.fragment ?? ast, components);
281
340
  const images = [];
282
341
  collectImages(ast.fragment ?? ast, source, images);
342
+ const headings = [];
343
+ collectHeadings(ast.fragment ?? ast, source, headings);
283
344
  return {
284
345
  headTags: heads.flatMap(tagsFromHead),
285
346
  components,
286
347
  imports: collectImports(ast),
287
- images
348
+ images,
349
+ headings
288
350
  };
289
351
  }
290
352
 
@@ -301,10 +363,20 @@ var svelteMetaTagsAdapter = {
301
363
  resolve(use) {
302
364
  const tags = [];
303
365
  const attrs = use.attributes;
304
- const title = findAttr2(attrs, "title") ?? findAttr2(attrs, "titleTemplate");
305
- 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
+ }
306
374
  const description = findAttr2(attrs, "description");
307
- 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
+ }
308
380
  const canonical = findAttr2(attrs, "canonical");
309
381
  if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
310
382
  const robots = findAttr2(attrs, "robots");
@@ -327,9 +399,17 @@ var svelteSeoAdapter = {
327
399
  const tags = [];
328
400
  const attrs = use.attributes;
329
401
  const title = findAttr3(attrs, "title");
330
- 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
+ }
331
407
  const description = findAttr3(attrs, "description");
332
- 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
+ }
333
413
  const canonical = findAttr3(attrs, "canonical");
334
414
  if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
335
415
  const openGraph = findAttr3(attrs, "openGraph");
@@ -448,12 +528,16 @@ async function resolveRoute(rt, cwd, pageRel, config) {
448
528
  let broadOwn = false;
449
529
  let broadInherited = false;
450
530
  const images = [];
531
+ const headings = [];
451
532
  for (const { rel, isPage } of files) {
452
533
  const source = await rt.readFile(rt.join(cwd, rel));
453
534
  const parsed = parseFile(source, rel);
454
535
  for (const img of parsed.images) {
455
536
  images.push({ ...img, file: rel });
456
537
  }
538
+ for (const heading of parsed.headings) {
539
+ headings.push({ ...heading, file: rel });
540
+ }
457
541
  const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
458
542
  for (const tag of resolved.tags) {
459
543
  composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
@@ -473,7 +557,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
473
557
  const route = deriveRoute(pageRel);
474
558
  return {
475
559
  head: { route, source: "static", tags: [...composed.values()], file: pageRel },
476
- images: { route, images }
560
+ images: { route, images },
561
+ headings: { route, headings }
477
562
  };
478
563
  }
479
564
  async function collectRoutes(rt, cwd, config = defaultConfig) {
@@ -481,7 +566,8 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
481
566
  const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
482
567
  return {
483
568
  heads: facts.map((f) => f.head),
484
- images: facts.map((f) => f.images)
569
+ images: facts.map((f) => f.images),
570
+ headings: facts.map((f) => f.headings)
485
571
  };
486
572
  }
487
573
 
@@ -564,9 +650,10 @@ async function analyzeProject(opts = {}) {
564
650
  const collected = await collectRoutes(rt, cwd, config);
565
651
  const heads = collected.heads.filter((h) => matches(h.route));
566
652
  const images = collected.images.filter((i) => matches(i.route));
653
+ const headings = collected.headings.filter((h) => matches(h.route));
567
654
  const project = await collectProjectFacts(rt, cwd);
568
655
  const rules = selectRules(allRules2, config);
569
- const results = applyRuleSeverities(await runRules(rules, { heads, images, project, config }), config);
656
+ const results = applyRuleSeverities(await runRules(rules, { heads, images, headings, project, config }), config);
570
657
  return { results, config, version: readPackageVersion() };
571
658
  }
572
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-QI6NLWDD.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.11.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.12.0"
45
+ "@svelte-vitals/core": "0.14.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^24.7.0"