@cavuno/board 4.8.0 → 4.10.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/{_spec-DePC7a05.d.mts → _spec-BwUP7Wni.d.mts} +100 -1
  3. package/dist/{_spec-DePC7a05.d.ts → _spec-BwUP7Wni.d.ts} +100 -1
  4. package/dist/bin.mjs +174 -19
  5. package/dist/{blog-DbFr4VPW.d.mts → blog-C_xXYOxc.d.mts} +1 -1
  6. package/dist/{blog-ummlt6Uq.d.ts → blog-DC4BfQqF.d.ts} +1 -1
  7. package/dist/{board-zWyHbyi4.d.ts → board-CG8rDGTY.d.mts} +5 -3
  8. package/dist/{board-DxTMBgIf.d.mts → board-zdQ875ck.d.ts} +5 -3
  9. package/dist/{breadcrumbs-P4qh0zML.d.mts → breadcrumbs-BK7AqNp2.d.mts} +2 -2
  10. package/dist/{breadcrumbs-kbmspmfk.d.ts → breadcrumbs-qs9DC0yh.d.ts} +2 -2
  11. package/dist/browser/cavuno-board.global.min.js +1 -1
  12. package/dist/{chunk-IWWPHFK2.mjs → chunk-XEJWXRLK.mjs} +10 -0
  13. package/dist/doctor.d.mts +3 -1
  14. package/dist/doctor.d.ts +3 -1
  15. package/dist/doctor.js +169 -17
  16. package/dist/doctor.mjs +169 -17
  17. package/dist/filters.d.mts +2 -2
  18. package/dist/filters.d.ts +2 -2
  19. package/dist/format.d.mts +4 -4
  20. package/dist/format.d.ts +4 -4
  21. package/dist/index.d.mts +118 -24
  22. package/dist/index.d.ts +118 -24
  23. package/dist/index.js +127 -9
  24. package/dist/index.mjs +118 -10
  25. package/dist/{jobs-twt5TO9l.d.ts → jobs-B-uO7ffG.d.ts} +1 -1
  26. package/dist/{jobs-DTLDt57G.d.mts → jobs-BToqhRuU.d.mts} +1 -1
  27. package/dist/{search-f2UNlTfv.d.mts → search-CvwKD0Va.d.mts} +1 -1
  28. package/dist/{search-CHD6peIl.d.ts → search-Sl9-xi1z.d.ts} +1 -1
  29. package/dist/seo-B9b2AkSE.d.mts +12 -0
  30. package/dist/seo-D1Vc_0HF.d.ts +12 -0
  31. package/dist/seo-files.d.mts +36 -0
  32. package/dist/seo-files.d.ts +36 -0
  33. package/dist/seo-files.js +102 -0
  34. package/dist/seo-files.mjs +81 -0
  35. package/dist/seo.d.mts +6 -6
  36. package/dist/seo.d.ts +6 -6
  37. package/dist/server.d.mts +6 -5
  38. package/dist/server.d.ts +6 -5
  39. package/dist/server.mjs +1 -1
  40. package/dist/sitemap.d.mts +6 -5
  41. package/dist/sitemap.d.ts +6 -5
  42. package/dist/suggest.d.mts +2 -2
  43. package/dist/suggest.d.ts +2 -2
  44. package/package.json +11 -1
  45. package/skills/cavuno-board-account/SKILL.md +3 -1
  46. package/skills/cavuno-board-filters/SKILL.md +17 -3
  47. package/skills/cavuno-board-setup/SKILL.md +1 -0
  48. package/skills/cavuno-board-talent-access/SKILL.md +149 -0
  49. package/skills/manifest.json +8 -1
package/dist/doctor.js CHANGED
@@ -137,7 +137,14 @@ function summarize(results) {
137
137
  const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
138
138
  const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
139
139
  const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
140
- return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
140
+ const warned = results.filter((r) => r.status === "warn").map((r) => r.id);
141
+ return {
142
+ exitCode: failed.length > 0 ? 1 : 0,
143
+ passed,
144
+ failed,
145
+ skipped,
146
+ warned
147
+ };
141
148
  }
142
149
 
143
150
  // src/doctor/cookie-conformance.ts
@@ -312,7 +319,7 @@ function checkCookieCodecConformance(projectRoot) {
312
319
  }
313
320
 
314
321
  // src/doctor/probe.ts
315
- var FETCH_TIMEOUT_MS = 1e4;
322
+ var FETCH_TIMEOUT_MS = 6e4;
316
323
  async function probe(fetchImpl, url) {
317
324
  try {
318
325
  const response = await fetchImpl(url, {
@@ -338,7 +345,11 @@ var READ = {
338
345
  jobs: record("read.jobs", 2),
339
346
  jsonld: record("read.jsonld", 2),
340
347
  sitemap: record("read.sitemap", 2),
341
- robots: record("read.robots", 2)
348
+ robots: record("read.robots", 2),
349
+ adsTxt: record("read.adsTxt", 2),
350
+ indexNow: record("read.indexNow", 2),
351
+ googleVerification: record("read.googleVerification", 2),
352
+ oauthCallback: record("read.oauthCallback", 2)
342
353
  };
343
354
  function skipReadProbes(reason) {
344
355
  return Object.values(READ).map((make) => make("skip", reason));
@@ -394,7 +405,11 @@ async function probeSitemap(fetchImpl, base) {
394
405
  }
395
406
  let urls = doc.urls;
396
407
  if (doc.kind === "index") {
397
- const child = await probe(fetchImpl, onFrontend(base, urls[0]));
408
+ const childUrl = onFrontend(base, urls[0]);
409
+ let child = await probe(fetchImpl, childUrl);
410
+ if (!child.ok && (child.status === 503 || child.status === 0)) {
411
+ child = await probe(fetchImpl, childUrl);
412
+ }
398
413
  const childDoc = child.ok ? parseSitemap(child.body) : null;
399
414
  if (!childDoc || childDoc.urls.length === 0) {
400
415
  return READ.sitemap(
@@ -408,14 +423,109 @@ async function probeSitemap(fetchImpl, base) {
408
423
  const sample = await probe(fetchImpl, sampleUrl);
409
424
  return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
410
425
  }
411
- async function runReadProbes(fetchImpl, frontendUrl) {
426
+ function scoreRobots(robots, seo) {
427
+ if (!robots.ok) {
428
+ return READ.robots("fail", `robots.txt HTTP ${robots.status}`);
429
+ }
430
+ const missing = [];
431
+ if (!/user-agent\s*:/i.test(robots.body)) missing.push("User-agent");
432
+ if (!robots.body.includes("Disallow: /go/")) missing.push("Disallow: /go/");
433
+ if (seo) {
434
+ const canonicalBase = seo.canonicalBase.replace(/\/+$/, "");
435
+ const sitemapLine = `Sitemap: ${canonicalBase}/sitemap.xml`;
436
+ if (!robots.body.includes(sitemapLine)) missing.push(sitemapLine);
437
+ }
438
+ if (missing.includes("User-agent")) {
439
+ return READ.robots(
440
+ "fail",
441
+ "robots.txt returned 200 but has no User-agent directive"
442
+ );
443
+ }
444
+ return missing.length === 0 ? READ.robots("pass", "present") : READ.robots(
445
+ "warn",
446
+ `robots.txt is missing ${missing.join(", ")} (hosted boards emit it)`
447
+ );
448
+ }
449
+ function configuredText(value) {
450
+ return typeof value === "string" && value.length > 0 ? value : null;
451
+ }
452
+ function scoreOptionalTextFile(options) {
453
+ const { make, label, path, seo, probed } = options;
454
+ if (seo == null) {
455
+ return make("skip", "board seo not fetched");
456
+ }
457
+ const expected = configuredText(options.configured);
458
+ if (expected !== null) {
459
+ if (!probed.ok) {
460
+ return make("fail", `${path} HTTP ${probed.status}`);
461
+ }
462
+ if (probed.body.trimEnd() === expected.trimEnd()) {
463
+ return make("pass", `${label} matches dashboard`);
464
+ }
465
+ return make("fail", `${path} body does not match dashboard ${label}`);
466
+ }
467
+ if (probed.status === 404) {
468
+ return make("pass", "unconfigured; 404 as expected");
469
+ }
470
+ if (probed.status === 200) {
471
+ return make(
472
+ "warn",
473
+ `${label} served but not configured in dashboard \u2014 clone leftover?`
474
+ );
475
+ }
476
+ return make("fail", `${path} HTTP ${probed.status}`);
477
+ }
478
+ function escapeRegExp(value) {
479
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
480
+ }
481
+ function hasGoogleSiteVerificationMeta(html, token) {
482
+ const escaped = escapeRegExp(token);
483
+ const quoted = `["']${escaped}["']`;
484
+ const name = `name\\s*=\\s*["']google-site-verification["']`;
485
+ const content = `content\\s*=\\s*${quoted}`;
486
+ return new RegExp(`<meta\\b[^>]*\\b${name}[^>]*\\b${content}`, "i").test(html) || new RegExp(`<meta\\b[^>]*\\b${content}[^>]*\\b${name}`, "i").test(html);
487
+ }
488
+ function scoreGoogleVerification(homeHtml, seo) {
489
+ if (seo == null) {
490
+ return READ.googleVerification("skip", "board seo not fetched");
491
+ }
492
+ const token = configuredText(seo.googleSiteVerification);
493
+ if (token === null) {
494
+ return READ.googleVerification("skip", "no token configured");
495
+ }
496
+ return hasGoogleSiteVerificationMeta(homeHtml, token) ? READ.googleVerification("pass", "google-site-verification meta present") : READ.googleVerification(
497
+ "fail",
498
+ "home HTML is missing the google-site-verification meta tag"
499
+ );
500
+ }
501
+ var OAUTH_PROVIDERS = ["google", "linkedin"];
502
+ function scoreOAuthCallback(probed) {
503
+ const broken = OAUTH_PROVIDERS.filter(
504
+ (provider) => !(probed[provider].status === 400 && /missing code or state/i.test(probed[provider].body))
505
+ );
506
+ return broken.length === 0 ? READ.oauthCallback("pass", "callback paths reach the hosted app") : READ.oauthCallback(
507
+ "warn",
508
+ `${broken.map((p) => `/api/board-auth/oauth/${p}/callback \u2192 HTTP ${probed[p].status}`).join(", ")} \u2014 only matters if you registered this domain's callback with the provider: forward the path to cavuno.com, or register https://cavuno.com/api/board-auth/oauth/{provider}/callback instead`
509
+ );
510
+ }
511
+ async function runReadProbes(fetchImpl, frontendUrl, seo) {
412
512
  const base = frontendUrl.replace(/\/$/, "");
413
- const [home, jobsAndJsonLd, sitemap, robots] = await Promise.all([
414
- probe(fetchImpl, base),
415
- probeJobsAndJsonLd(fetchImpl, base),
416
- probeSitemap(fetchImpl, base),
417
- probe(fetchImpl, `${base}/robots.txt`)
418
- ]);
513
+ const home = await probe(fetchImpl, base);
514
+ const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base);
515
+ const sitemap = await probeSitemap(fetchImpl, base);
516
+ const robots = await probe(fetchImpl, `${base}/robots.txt`);
517
+ const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
518
+ const indexNow = await probe(fetchImpl, `${base}/indexnow-key.txt`);
519
+ const oauthCallback = {
520
+ google: await probe(
521
+ fetchImpl,
522
+ `${base}/api/board-auth/oauth/google/callback`
523
+ ),
524
+ linkedin: await probe(
525
+ fetchImpl,
526
+ `${base}/api/board-auth/oauth/linkedin/callback`
527
+ )
528
+ };
419
529
  return [
420
530
  home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
421
531
  "fail",
@@ -423,10 +533,25 @@ async function runReadProbes(fetchImpl, frontendUrl) {
423
533
  ),
424
534
  ...jobsAndJsonLd,
425
535
  sitemap,
426
- robots.ok && /user-agent\s*:/i.test(robots.body) ? READ.robots("pass", "present") : READ.robots(
427
- "fail",
428
- robots.ok ? "robots.txt returned 200 but has no User-agent directive" : `robots.txt HTTP ${robots.status}`
429
- )
536
+ scoreRobots(robots, seo),
537
+ scoreOptionalTextFile({
538
+ make: READ.adsTxt,
539
+ label: "ads.txt",
540
+ path: "/ads.txt",
541
+ seo,
542
+ configured: seo?.adsTxt,
543
+ probed: adsTxt
544
+ }),
545
+ scoreOptionalTextFile({
546
+ make: READ.indexNow,
547
+ label: "indexnow-key.txt",
548
+ path: "/indexnow-key.txt",
549
+ seo,
550
+ configured: seo?.indexNowKey,
551
+ probed: indexNow
552
+ }),
553
+ scoreGoogleVerification(home.body, seo),
554
+ scoreOAuthCallback(oauthCallback)
430
555
  ];
431
556
  }
432
557
 
@@ -482,6 +607,28 @@ async function checkBoardResolves(fetchImpl, env) {
482
607
  }
483
608
  return STATIC_BOARD("pass", "publishable key resolves the board");
484
609
  }
610
+ async function fetchBoardSeo(fetchImpl, env) {
611
+ const result = await probe(
612
+ fetchImpl,
613
+ `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/seo`
614
+ );
615
+ if (!result.ok) return null;
616
+ try {
617
+ const parsed = JSON.parse(result.body);
618
+ if (typeof parsed !== "object" || parsed === null) return null;
619
+ const rec = parsed;
620
+ if (typeof rec.canonicalBase !== "string") return null;
621
+ const asNullableString = (value) => value === null || typeof value === "string" ? value : null;
622
+ return {
623
+ canonicalBase: rec.canonicalBase,
624
+ adsTxt: asNullableString(rec.adsTxt),
625
+ indexNowKey: asNullableString(rec.indexNowKey),
626
+ googleSiteVerification: asNullableString(rec.googleSiteVerification)
627
+ };
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
485
632
  var SKILL_ROOTS = [
486
633
  ".claude/skills",
487
634
  ".agents/skills",
@@ -543,8 +690,13 @@ async function runDoctor(options) {
543
690
  } else {
544
691
  results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
545
692
  }
693
+ let seo = null;
546
694
  if (envOk) {
547
- results.push(await checkBoardResolves(fetchImpl, env));
695
+ const boardResult = await checkBoardResolves(fetchImpl, env);
696
+ results.push(boardResult);
697
+ if (boardResult.status === "pass") {
698
+ seo = await fetchBoardSeo(fetchImpl, env);
699
+ }
548
700
  } else {
549
701
  results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
550
702
  }
@@ -553,7 +705,7 @@ async function runDoctor(options) {
553
705
  ...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
554
706
  );
555
707
  results.push(
556
- ...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
708
+ ...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl, seo) : skipReadProbes("no --frontend <url> given")
557
709
  );
558
710
  return { results, summary: summarize(results) };
559
711
  }
package/dist/doctor.mjs CHANGED
@@ -107,7 +107,14 @@ function summarize(results) {
107
107
  const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
108
108
  const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
109
109
  const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
110
- return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
110
+ const warned = results.filter((r) => r.status === "warn").map((r) => r.id);
111
+ return {
112
+ exitCode: failed.length > 0 ? 1 : 0,
113
+ passed,
114
+ failed,
115
+ skipped,
116
+ warned
117
+ };
111
118
  }
112
119
 
113
120
  // src/doctor/cookie-conformance.ts
@@ -282,7 +289,7 @@ function checkCookieCodecConformance(projectRoot) {
282
289
  }
283
290
 
284
291
  // src/doctor/probe.ts
285
- var FETCH_TIMEOUT_MS = 1e4;
292
+ var FETCH_TIMEOUT_MS = 6e4;
286
293
  async function probe(fetchImpl, url) {
287
294
  try {
288
295
  const response = await fetchImpl(url, {
@@ -308,7 +315,11 @@ var READ = {
308
315
  jobs: record("read.jobs", 2),
309
316
  jsonld: record("read.jsonld", 2),
310
317
  sitemap: record("read.sitemap", 2),
311
- robots: record("read.robots", 2)
318
+ robots: record("read.robots", 2),
319
+ adsTxt: record("read.adsTxt", 2),
320
+ indexNow: record("read.indexNow", 2),
321
+ googleVerification: record("read.googleVerification", 2),
322
+ oauthCallback: record("read.oauthCallback", 2)
312
323
  };
313
324
  function skipReadProbes(reason) {
314
325
  return Object.values(READ).map((make) => make("skip", reason));
@@ -364,7 +375,11 @@ async function probeSitemap(fetchImpl, base) {
364
375
  }
365
376
  let urls = doc.urls;
366
377
  if (doc.kind === "index") {
367
- const child = await probe(fetchImpl, onFrontend(base, urls[0]));
378
+ const childUrl = onFrontend(base, urls[0]);
379
+ let child = await probe(fetchImpl, childUrl);
380
+ if (!child.ok && (child.status === 503 || child.status === 0)) {
381
+ child = await probe(fetchImpl, childUrl);
382
+ }
368
383
  const childDoc = child.ok ? parseSitemap(child.body) : null;
369
384
  if (!childDoc || childDoc.urls.length === 0) {
370
385
  return READ.sitemap(
@@ -378,14 +393,109 @@ async function probeSitemap(fetchImpl, base) {
378
393
  const sample = await probe(fetchImpl, sampleUrl);
379
394
  return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
380
395
  }
381
- async function runReadProbes(fetchImpl, frontendUrl) {
396
+ function scoreRobots(robots, seo) {
397
+ if (!robots.ok) {
398
+ return READ.robots("fail", `robots.txt HTTP ${robots.status}`);
399
+ }
400
+ const missing = [];
401
+ if (!/user-agent\s*:/i.test(robots.body)) missing.push("User-agent");
402
+ if (!robots.body.includes("Disallow: /go/")) missing.push("Disallow: /go/");
403
+ if (seo) {
404
+ const canonicalBase = seo.canonicalBase.replace(/\/+$/, "");
405
+ const sitemapLine = `Sitemap: ${canonicalBase}/sitemap.xml`;
406
+ if (!robots.body.includes(sitemapLine)) missing.push(sitemapLine);
407
+ }
408
+ if (missing.includes("User-agent")) {
409
+ return READ.robots(
410
+ "fail",
411
+ "robots.txt returned 200 but has no User-agent directive"
412
+ );
413
+ }
414
+ return missing.length === 0 ? READ.robots("pass", "present") : READ.robots(
415
+ "warn",
416
+ `robots.txt is missing ${missing.join(", ")} (hosted boards emit it)`
417
+ );
418
+ }
419
+ function configuredText(value) {
420
+ return typeof value === "string" && value.length > 0 ? value : null;
421
+ }
422
+ function scoreOptionalTextFile(options) {
423
+ const { make, label, path, seo, probed } = options;
424
+ if (seo == null) {
425
+ return make("skip", "board seo not fetched");
426
+ }
427
+ const expected = configuredText(options.configured);
428
+ if (expected !== null) {
429
+ if (!probed.ok) {
430
+ return make("fail", `${path} HTTP ${probed.status}`);
431
+ }
432
+ if (probed.body.trimEnd() === expected.trimEnd()) {
433
+ return make("pass", `${label} matches dashboard`);
434
+ }
435
+ return make("fail", `${path} body does not match dashboard ${label}`);
436
+ }
437
+ if (probed.status === 404) {
438
+ return make("pass", "unconfigured; 404 as expected");
439
+ }
440
+ if (probed.status === 200) {
441
+ return make(
442
+ "warn",
443
+ `${label} served but not configured in dashboard \u2014 clone leftover?`
444
+ );
445
+ }
446
+ return make("fail", `${path} HTTP ${probed.status}`);
447
+ }
448
+ function escapeRegExp(value) {
449
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
450
+ }
451
+ function hasGoogleSiteVerificationMeta(html, token) {
452
+ const escaped = escapeRegExp(token);
453
+ const quoted = `["']${escaped}["']`;
454
+ const name = `name\\s*=\\s*["']google-site-verification["']`;
455
+ const content = `content\\s*=\\s*${quoted}`;
456
+ return new RegExp(`<meta\\b[^>]*\\b${name}[^>]*\\b${content}`, "i").test(html) || new RegExp(`<meta\\b[^>]*\\b${content}[^>]*\\b${name}`, "i").test(html);
457
+ }
458
+ function scoreGoogleVerification(homeHtml, seo) {
459
+ if (seo == null) {
460
+ return READ.googleVerification("skip", "board seo not fetched");
461
+ }
462
+ const token = configuredText(seo.googleSiteVerification);
463
+ if (token === null) {
464
+ return READ.googleVerification("skip", "no token configured");
465
+ }
466
+ return hasGoogleSiteVerificationMeta(homeHtml, token) ? READ.googleVerification("pass", "google-site-verification meta present") : READ.googleVerification(
467
+ "fail",
468
+ "home HTML is missing the google-site-verification meta tag"
469
+ );
470
+ }
471
+ var OAUTH_PROVIDERS = ["google", "linkedin"];
472
+ function scoreOAuthCallback(probed) {
473
+ const broken = OAUTH_PROVIDERS.filter(
474
+ (provider) => !(probed[provider].status === 400 && /missing code or state/i.test(probed[provider].body))
475
+ );
476
+ return broken.length === 0 ? READ.oauthCallback("pass", "callback paths reach the hosted app") : READ.oauthCallback(
477
+ "warn",
478
+ `${broken.map((p) => `/api/board-auth/oauth/${p}/callback \u2192 HTTP ${probed[p].status}`).join(", ")} \u2014 only matters if you registered this domain's callback with the provider: forward the path to cavuno.com, or register https://cavuno.com/api/board-auth/oauth/{provider}/callback instead`
479
+ );
480
+ }
481
+ async function runReadProbes(fetchImpl, frontendUrl, seo) {
382
482
  const base = frontendUrl.replace(/\/$/, "");
383
- const [home, jobsAndJsonLd, sitemap, robots] = await Promise.all([
384
- probe(fetchImpl, base),
385
- probeJobsAndJsonLd(fetchImpl, base),
386
- probeSitemap(fetchImpl, base),
387
- probe(fetchImpl, `${base}/robots.txt`)
388
- ]);
483
+ const home = await probe(fetchImpl, base);
484
+ const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base);
485
+ const sitemap = await probeSitemap(fetchImpl, base);
486
+ const robots = await probe(fetchImpl, `${base}/robots.txt`);
487
+ const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
488
+ const indexNow = await probe(fetchImpl, `${base}/indexnow-key.txt`);
489
+ const oauthCallback = {
490
+ google: await probe(
491
+ fetchImpl,
492
+ `${base}/api/board-auth/oauth/google/callback`
493
+ ),
494
+ linkedin: await probe(
495
+ fetchImpl,
496
+ `${base}/api/board-auth/oauth/linkedin/callback`
497
+ )
498
+ };
389
499
  return [
390
500
  home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
391
501
  "fail",
@@ -393,10 +503,25 @@ async function runReadProbes(fetchImpl, frontendUrl) {
393
503
  ),
394
504
  ...jobsAndJsonLd,
395
505
  sitemap,
396
- robots.ok && /user-agent\s*:/i.test(robots.body) ? READ.robots("pass", "present") : READ.robots(
397
- "fail",
398
- robots.ok ? "robots.txt returned 200 but has no User-agent directive" : `robots.txt HTTP ${robots.status}`
399
- )
506
+ scoreRobots(robots, seo),
507
+ scoreOptionalTextFile({
508
+ make: READ.adsTxt,
509
+ label: "ads.txt",
510
+ path: "/ads.txt",
511
+ seo,
512
+ configured: seo?.adsTxt,
513
+ probed: adsTxt
514
+ }),
515
+ scoreOptionalTextFile({
516
+ make: READ.indexNow,
517
+ label: "indexnow-key.txt",
518
+ path: "/indexnow-key.txt",
519
+ seo,
520
+ configured: seo?.indexNowKey,
521
+ probed: indexNow
522
+ }),
523
+ scoreGoogleVerification(home.body, seo),
524
+ scoreOAuthCallback(oauthCallback)
400
525
  ];
401
526
  }
402
527
 
@@ -452,6 +577,28 @@ async function checkBoardResolves(fetchImpl, env) {
452
577
  }
453
578
  return STATIC_BOARD("pass", "publishable key resolves the board");
454
579
  }
580
+ async function fetchBoardSeo(fetchImpl, env) {
581
+ const result = await probe(
582
+ fetchImpl,
583
+ `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}/seo`
584
+ );
585
+ if (!result.ok) return null;
586
+ try {
587
+ const parsed = JSON.parse(result.body);
588
+ if (typeof parsed !== "object" || parsed === null) return null;
589
+ const rec = parsed;
590
+ if (typeof rec.canonicalBase !== "string") return null;
591
+ const asNullableString = (value) => value === null || typeof value === "string" ? value : null;
592
+ return {
593
+ canonicalBase: rec.canonicalBase,
594
+ adsTxt: asNullableString(rec.adsTxt),
595
+ indexNowKey: asNullableString(rec.indexNowKey),
596
+ googleSiteVerification: asNullableString(rec.googleSiteVerification)
597
+ };
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
455
602
  var SKILL_ROOTS = [
456
603
  ".claude/skills",
457
604
  ".agents/skills",
@@ -513,8 +660,13 @@ async function runDoctor(options) {
513
660
  } else {
514
661
  results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
515
662
  }
663
+ let seo = null;
516
664
  if (envOk) {
517
- results.push(await checkBoardResolves(fetchImpl, env));
665
+ const boardResult = await checkBoardResolves(fetchImpl, env);
666
+ results.push(boardResult);
667
+ if (boardResult.status === "pass") {
668
+ seo = await fetchBoardSeo(fetchImpl, env);
669
+ }
518
670
  } else {
519
671
  results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
520
672
  }
@@ -523,7 +675,7 @@ async function runDoctor(options) {
523
675
  ...checkCookieCodecConformance(options.projectRoot ?? process.cwd())
524
676
  );
525
677
  results.push(
526
- ...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
678
+ ...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl, seo) : skipReadProbes("no --frontend <url> given")
527
679
  );
528
680
  return { results, summary: summarize(results) };
529
681
  }
@@ -1,5 +1,5 @@
1
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-DTLDt57G.mjs';
2
- import './_spec-DePC7a05.mjs';
1
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-BToqhRuU.mjs';
2
+ import './_spec-BwUP7Wni.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/filters` — the canonical listing-filter vocabulary and URL
package/dist/filters.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-twt5TO9l.js';
2
- import './_spec-DePC7a05.js';
1
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-B-uO7ffG.js';
2
+ import './_spec-BwUP7Wni.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/filters` — the canonical listing-filter vocabulary and URL
package/dist/format.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-P4qh0zML.mjs';
2
- import { C as CustomFieldDefinition } from './board-DxTMBgIf.mjs';
3
- import { C as CustomFieldValues } from './jobs-DTLDt57G.mjs';
4
- import './_spec-DePC7a05.mjs';
1
+ export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-BK7AqNp2.mjs';
2
+ import { C as CustomFieldDefinition } from './board-CG8rDGTY.mjs';
3
+ import { C as CustomFieldValues } from './jobs-BToqhRuU.mjs';
4
+ import './_spec-BwUP7Wni.mjs';
5
5
 
6
6
  /**
7
7
  * Date display helpers in the board language (required leading parameter,
package/dist/format.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-kbmspmfk.js';
2
- import { C as CustomFieldDefinition } from './board-zWyHbyi4.js';
3
- import { C as CustomFieldValues } from './jobs-twt5TO9l.js';
4
- import './_spec-DePC7a05.js';
1
+ export { C as CurrencyDisplay, F as FormattedSalaryRange, J as JobBreadcrumb, N as NumberNotation, S as SalaryTimeframeInput, a as SalaryTimeframeValue, b as buildJobBreadcrumbs, f as formatSalaryRange, c as formatSalaryStat, d as formatSalaryStatRange, n as normalizeWebsiteUrl } from './breadcrumbs-qs9DC0yh.js';
2
+ import { C as CustomFieldDefinition } from './board-zdQ875ck.js';
3
+ import { C as CustomFieldValues } from './jobs-B-uO7ffG.js';
4
+ import './_spec-BwUP7Wni.js';
5
5
 
6
6
  /**
7
7
  * Date display helpers in the board language (required leading parameter,