@cavuno/board 4.44.0 → 4.47.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 (46) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/{_spec-CLxrfYJb.d.mts → _spec-DHa5r3B0.d.mts} +26 -2
  3. package/dist/{_spec-CLxrfYJb.d.ts → _spec-DHa5r3B0.d.ts} +26 -2
  4. package/dist/bin.mjs +179 -14
  5. package/dist/{blog-CQ40QX7l.d.mts → blog-DylAbc0L.d.mts} +1 -1
  6. package/dist/{blog-mlhJ6UcD.d.ts → blog-wxhB8zAH.d.ts} +1 -1
  7. package/dist/{board-TOzb2D8c.d.mts → board-Bpze3b50.d.mts} +2 -2
  8. package/dist/{board-DWFDeICw.d.ts → board-DHsd92Gy.d.ts} +2 -2
  9. package/dist/{breadcrumbs-DJHyzBji.d.ts → breadcrumbs-15j5xvmN.d.ts} +2 -2
  10. package/dist/{breadcrumbs-DauM7O7_.d.mts → breadcrumbs-B6yhrHJV.d.mts} +2 -2
  11. package/dist/browser/cavuno-board.global.min.js +1 -1
  12. package/dist/doctor.d.mts +1 -0
  13. package/dist/doctor.d.ts +1 -0
  14. package/dist/doctor.js +179 -14
  15. package/dist/doctor.mjs +179 -14
  16. package/dist/filters.d.mts +2 -2
  17. package/dist/filters.d.ts +2 -2
  18. package/dist/format.d.mts +4 -4
  19. package/dist/format.d.ts +4 -4
  20. package/dist/index.d.mts +33 -12
  21. package/dist/index.d.ts +33 -12
  22. package/dist/index.js +78 -20
  23. package/dist/index.mjs +78 -20
  24. package/dist/{jobs-DZVMzBOQ.d.ts → jobs-CIrTcMjw.d.ts} +1 -1
  25. package/dist/{jobs-CjQM8EPT.d.mts → jobs-CMK-DZg3.d.mts} +1 -1
  26. package/dist/{search-CF_wa20h.d.ts → search-666oDd7H.d.ts} +1 -1
  27. package/dist/{search-BtBE6iJO.d.mts → search-DBQApXN1.d.mts} +1 -1
  28. package/dist/seo-files.d.mts +25 -3
  29. package/dist/seo-files.d.ts +25 -3
  30. package/dist/seo-files.js +44 -10
  31. package/dist/seo-files.mjs +44 -10
  32. package/dist/{seo-0bfk63qi.d.mts → seo-gY8y9RMw.d.mts} +1 -1
  33. package/dist/{seo-CU9LpWH7.d.ts → seo-ynqiQmby.d.ts} +1 -1
  34. package/dist/seo.d.mts +6 -6
  35. package/dist/seo.d.ts +6 -6
  36. package/dist/server.d.mts +6 -6
  37. package/dist/server.d.ts +6 -6
  38. package/dist/sitemap.d.mts +6 -6
  39. package/dist/sitemap.d.ts +6 -6
  40. package/dist/suggest.d.mts +2 -2
  41. package/dist/suggest.d.ts +2 -2
  42. package/package.json +1 -1
  43. package/skills/cavuno-board-filters/SKILL.md +13 -0
  44. package/skills/cavuno-board-i18n/SKILL.md +10 -0
  45. package/skills/cavuno-board-job-alerts/SKILL.md +2 -0
  46. package/skills/manifest.json +1 -1
package/dist/doctor.js CHANGED
@@ -483,12 +483,13 @@ function extractJobDetailLink(html, templates) {
483
483
  var HREF_PATH_RE = /href="(\/[^"?#\s]+)"/gi;
484
484
  var SLUG_RE = /^[a-z0-9-]+$/i;
485
485
  var CANONICAL_JOB_DETAIL_TEMPLATE = CANONICAL_MANIFEST.roles.jobDetail;
486
- function jobRoutesFromManifest(manifest) {
486
+ function manifestRoleTemplate(manifest, name) {
487
487
  const roles = manifest !== null && typeof manifest === "object" && manifest.version === 1 ? manifest.roles : null;
488
- const role = (name) => {
489
- const template = roles !== null && typeof roles === "object" ? roles[name] : void 0;
490
- return typeof template === "string" && checkRoleTemplate(name, template).length === 0 ? template : null;
491
- };
488
+ const template = roles !== null && typeof roles === "object" ? roles[name] : void 0;
489
+ return typeof template === "string" && checkRoleTemplate(name, template).length === 0 ? template : null;
490
+ }
491
+ function jobRoutesFromManifest(manifest) {
492
+ const role = (name) => manifestRoleTemplate(manifest, name);
492
493
  const jobs = role("jobs");
493
494
  const company = role("company");
494
495
  const jobsWord = jobs?.split("/").filter((seg) => seg !== "" && !seg.includes(":")).at(-1);
@@ -831,6 +832,25 @@ async function probe(fetchImpl, url) {
831
832
  };
832
833
  }
833
834
  }
835
+ async function probeHop(fetchImpl, url) {
836
+ try {
837
+ const response = await fetchImpl(url, {
838
+ redirect: "manual",
839
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
840
+ });
841
+ await response.body?.cancel().catch(() => void 0);
842
+ return {
843
+ status: response.status,
844
+ location: response.headers.get("location")
845
+ };
846
+ } catch (error) {
847
+ return {
848
+ status: 0,
849
+ location: null,
850
+ error: error instanceof Error ? error.message : String(error)
851
+ };
852
+ }
853
+ }
834
854
 
835
855
  // node_modules/.pnpm/parse5@7.3.0/node_modules/parse5/dist/common/unicode.js
836
856
  var UNDEFINED_CODE_POINTS = /* @__PURE__ */ new Set([
@@ -8767,6 +8787,142 @@ function hasCrawlableCavunoBacklink(html) {
8767
8787
  return visit(root);
8768
8788
  }
8769
8789
 
8790
+ // src/doctor/localized-urls.ts
8791
+ var LOCALIZED_URLS = record("read.localizedUrls", 2);
8792
+ var REDIRECT_OK = /* @__PURE__ */ new Set([
8793
+ "employers",
8794
+ "alertsManage",
8795
+ "alertsConfirm"
8796
+ ]);
8797
+ var MAY_BE_UNPUBLISHED = /* @__PURE__ */ new Set([
8798
+ "blog",
8799
+ "about",
8800
+ "talent",
8801
+ "privacyPolicy",
8802
+ "termsOfService",
8803
+ "cookiePolicy",
8804
+ "impressum"
8805
+ ]);
8806
+ var GATE_SEGMENTS = /* @__PURE__ */ new Set([
8807
+ "password",
8808
+ "coming-soon-gate"
8809
+ ]);
8810
+ var CONCURRENCY = 1;
8811
+ var segments = (path) => path.split("/").filter(Boolean);
8812
+ function translatedStaticRoles(manifest) {
8813
+ const translated = [];
8814
+ for (const role of ALL_ROLES) {
8815
+ if (ROLE_PARAM_REGISTRY[role].length > 0 || role === "home") continue;
8816
+ const english = CANONICAL_MANIFEST.roles[role];
8817
+ const localized = manifestRoleTemplate(manifest, role);
8818
+ if (!english || !localized || localized === english) continue;
8819
+ if (segments(localized).length !== segments(english).length) continue;
8820
+ translated.push({ role, english, localized });
8821
+ }
8822
+ return translated;
8823
+ }
8824
+ function resolveLocation(location, requestUrl) {
8825
+ try {
8826
+ return new URL(location, requestUrl);
8827
+ } catch {
8828
+ return null;
8829
+ }
8830
+ }
8831
+ function pathUnder(base, url) {
8832
+ const prefix = base.pathname.replace(/\/$/, "");
8833
+ const path = prefix && url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) || "/" : url.pathname;
8834
+ return `${path}${url.search}`;
8835
+ }
8836
+ function isGate(base, url, requested) {
8837
+ const parts = segments(pathUnder(base, url).split("?")[0]);
8838
+ const last = parts.at(-1);
8839
+ if (last !== void 0 && GATE_SEGMENTS.has(last)) return true;
8840
+ return parts.length === 1 && url.searchParams.get("redirect") === requested;
8841
+ }
8842
+ async function checkRole(fetchImpl, baseUrl, { role, english, localized }) {
8843
+ const base = new URL(baseUrl);
8844
+ const problems = [];
8845
+ const englishUrl = `${baseUrl}${english}`;
8846
+ const hop = await probeHop(fetchImpl, englishUrl);
8847
+ const target = hop.location === null ? null : resolveLocation(hop.location, englishUrl);
8848
+ if (hop.status !== 308) {
8849
+ problems.push(
8850
+ `${english} \u2192 HTTP ${hop.status}${hop.error ? ` (${hop.error})` : ""}${target ? ` to ${target.href}` : ""}, expected 308 to ${localized}`
8851
+ );
8852
+ } else if (target === null) {
8853
+ problems.push(
8854
+ `${english} \u2192 308 without a usable Location, expected ${localized}`
8855
+ );
8856
+ } else if (target.origin !== base.origin) {
8857
+ problems.push(
8858
+ `${english} \u2192 308 off the board's origin to ${target.href}, expected ${localized}`
8859
+ );
8860
+ } else if (pathUnder(base, target) !== localized) {
8861
+ problems.push(
8862
+ `${english} \u2192 308 to ${pathUnder(base, target)}, expected ${localized}`
8863
+ );
8864
+ }
8865
+ const pageUrl = `${baseUrl}${localized}`;
8866
+ const page = await probeHop(fetchImpl, pageUrl);
8867
+ const pageTarget = page.location === null ? null : resolveLocation(page.location, pageUrl);
8868
+ const ok2xx = page.status >= 200 && page.status < 300;
8869
+ const redirected = page.status >= 300 && page.status < 400;
8870
+ if (ok2xx) {
8871
+ } else if (page.status === 404 && MAY_BE_UNPUBLISHED.has(role)) {
8872
+ } else if (redirected && pageTarget === null) {
8873
+ problems.push(
8874
+ `${localized} \u2192 HTTP ${page.status} without a usable Location, expected 2xx`
8875
+ );
8876
+ } else if (redirected && pageTarget.origin !== base.origin) {
8877
+ problems.push(
8878
+ `${localized} \u2192 HTTP ${page.status} off the board's origin to ${pageTarget.href}, expected 2xx`
8879
+ );
8880
+ } else if (redirected && isGate(base, pageTarget, localized)) {
8881
+ problems.push(
8882
+ `${localized} \u2192 HTTP ${page.status} to the board's access gate ${pathUnder(base, pageTarget)}, expected 2xx`
8883
+ );
8884
+ } else if (!(redirected && REDIRECT_OK.has(role))) {
8885
+ problems.push(
8886
+ `${localized} \u2192 HTTP ${page.status}${page.error ? ` (${page.error})` : ""}${pageTarget ? ` to ${pathUnder(base, pageTarget)}` : ""}, expected 2xx`
8887
+ );
8888
+ }
8889
+ return problems.length > 0 ? `${role}: ${problems.join("; ")}` : null;
8890
+ }
8891
+ async function probeLocalizedUrls(fetchImpl, base, manifest) {
8892
+ if (manifest === null) {
8893
+ return LOCALIZED_URLS(
8894
+ "skip",
8895
+ "not applicable \u2014 no readable route manifest at /.well-known/cavuno.json"
8896
+ );
8897
+ }
8898
+ const roles = translatedStaticRoles(manifest);
8899
+ if (roles.length === 0) {
8900
+ return LOCALIZED_URLS(
8901
+ "skip",
8902
+ "not applicable \u2014 the route manifest publishes no translated URL words"
8903
+ );
8904
+ }
8905
+ const failures = new Array(roles.length).fill(null);
8906
+ let next = 0;
8907
+ const worker = async () => {
8908
+ while (next < roles.length) {
8909
+ const index = next++;
8910
+ failures[index] = await checkRole(fetchImpl, base, roles[index]);
8911
+ }
8912
+ };
8913
+ await Promise.all(
8914
+ Array.from({ length: Math.min(CONCURRENCY, roles.length) }, worker)
8915
+ );
8916
+ const failed = failures.filter((f) => f !== null);
8917
+ if (failed.length > 0) {
8918
+ return LOCALIZED_URLS("fail", failed.join(" | "));
8919
+ }
8920
+ return LOCALIZED_URLS(
8921
+ "pass",
8922
+ `${roles.length} translated pages: each English path 308s to the board's word and the page answers (${roles.map((r) => `${r.english} \u2192 ${r.localized}`).join(", ")})`
8923
+ );
8924
+ }
8925
+
8770
8926
  // src/doctor/read.ts
8771
8927
  var READ = {
8772
8928
  home: record("read.home", 2),
@@ -8779,7 +8935,8 @@ var READ = {
8779
8935
  indexNow: record("read.indexNow", 2),
8780
8936
  googleVerification: record("read.googleVerification", 2),
8781
8937
  attribution: record("read.attribution", 2),
8782
- oauthCallback: record("read.oauthCallback", 2)
8938
+ oauthCallback: record("read.oauthCallback", 2),
8939
+ localizedUrls: LOCALIZED_URLS
8783
8940
  };
8784
8941
  function skipReadProbes(reason) {
8785
8942
  return Object.values(READ).map((make) => make("skip", reason));
@@ -8800,17 +8957,17 @@ async function confirmsNoPublishedJobs(fetchImpl, env) {
8800
8957
  return false;
8801
8958
  }
8802
8959
  }
8803
- async function readJobRoutes(fetchImpl, base) {
8960
+ async function readManifest(fetchImpl, base) {
8804
8961
  const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
8805
- if (!manifest.ok) return jobRoutesFromManifest(null);
8962
+ if (!manifest.ok) return null;
8806
8963
  try {
8807
- return jobRoutesFromManifest(JSON.parse(manifest.body));
8964
+ return JSON.parse(manifest.body);
8808
8965
  } catch {
8809
- return jobRoutesFromManifest(null);
8966
+ return null;
8810
8967
  }
8811
8968
  }
8812
- async function probeJobsAndJsonLd(fetchImpl, base, env) {
8813
- const { jobsPath, jobDetailTemplates } = await readJobRoutes(fetchImpl, base);
8969
+ async function probeJobsAndJsonLd(fetchImpl, base, manifest, env) {
8970
+ const { jobsPath, jobDetailTemplates } = jobRoutesFromManifest(manifest);
8814
8971
  const jobs = await probe(fetchImpl, `${base}${jobsPath}`);
8815
8972
  const jobLink = jobs.ok ? extractJobDetailLink(jobs.body, jobDetailTemplates) : null;
8816
8973
  if (jobs.status === 200 && !jobLink && /<(html|body|main)[\s>]/i.test(jobs.body) && await confirmsNoPublishedJobs(fetchImpl, env)) {
@@ -9059,7 +9216,13 @@ function scoreOAuthCallback(probed) {
9059
9216
  async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9060
9217
  const base = frontendUrl.replace(/\/$/, "");
9061
9218
  const home = await probe(fetchImpl, base);
9062
- const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base, env);
9219
+ const manifest = await readManifest(fetchImpl, base);
9220
+ const jobsAndJsonLd = await probeJobsAndJsonLd(
9221
+ fetchImpl,
9222
+ base,
9223
+ manifest,
9224
+ env
9225
+ );
9063
9226
  const sitemap = await probeSitemap(fetchImpl, base);
9064
9227
  const robots = await probe(fetchImpl, `${base}/robots.txt`);
9065
9228
  const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
@@ -9074,6 +9237,7 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9074
9237
  `${base}/api/board-auth/oauth/linkedin/callback`
9075
9238
  )
9076
9239
  };
9240
+ const localizedUrls = await probeLocalizedUrls(fetchImpl, base, manifest);
9077
9241
  const board = env?.apiUrl && env.boardKey ? await probe(
9078
9242
  fetchImpl,
9079
9243
  `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}`
@@ -9112,7 +9276,8 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9112
9276
  }),
9113
9277
  scoreGoogleVerification(home.body, seo),
9114
9278
  attribution,
9115
- scoreOAuthCallback(oauthCallback)
9279
+ scoreOAuthCallback(oauthCallback),
9280
+ localizedUrls
9116
9281
  ];
9117
9282
  }
9118
9283
 
package/dist/doctor.mjs CHANGED
@@ -452,12 +452,13 @@ function extractJobDetailLink(html, templates) {
452
452
  var HREF_PATH_RE = /href="(\/[^"?#\s]+)"/gi;
453
453
  var SLUG_RE = /^[a-z0-9-]+$/i;
454
454
  var CANONICAL_JOB_DETAIL_TEMPLATE = CANONICAL_MANIFEST.roles.jobDetail;
455
- function jobRoutesFromManifest(manifest) {
455
+ function manifestRoleTemplate(manifest, name) {
456
456
  const roles = manifest !== null && typeof manifest === "object" && manifest.version === 1 ? manifest.roles : null;
457
- const role = (name) => {
458
- const template = roles !== null && typeof roles === "object" ? roles[name] : void 0;
459
- return typeof template === "string" && checkRoleTemplate(name, template).length === 0 ? template : null;
460
- };
457
+ const template = roles !== null && typeof roles === "object" ? roles[name] : void 0;
458
+ return typeof template === "string" && checkRoleTemplate(name, template).length === 0 ? template : null;
459
+ }
460
+ function jobRoutesFromManifest(manifest) {
461
+ const role = (name) => manifestRoleTemplate(manifest, name);
461
462
  const jobs = role("jobs");
462
463
  const company = role("company");
463
464
  const jobsWord = jobs?.split("/").filter((seg) => seg !== "" && !seg.includes(":")).at(-1);
@@ -800,6 +801,25 @@ async function probe(fetchImpl, url) {
800
801
  };
801
802
  }
802
803
  }
804
+ async function probeHop(fetchImpl, url) {
805
+ try {
806
+ const response = await fetchImpl(url, {
807
+ redirect: "manual",
808
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
809
+ });
810
+ await response.body?.cancel().catch(() => void 0);
811
+ return {
812
+ status: response.status,
813
+ location: response.headers.get("location")
814
+ };
815
+ } catch (error) {
816
+ return {
817
+ status: 0,
818
+ location: null,
819
+ error: error instanceof Error ? error.message : String(error)
820
+ };
821
+ }
822
+ }
803
823
 
804
824
  // node_modules/.pnpm/parse5@7.3.0/node_modules/parse5/dist/common/unicode.js
805
825
  var UNDEFINED_CODE_POINTS = /* @__PURE__ */ new Set([
@@ -8736,6 +8756,142 @@ function hasCrawlableCavunoBacklink(html) {
8736
8756
  return visit(root);
8737
8757
  }
8738
8758
 
8759
+ // src/doctor/localized-urls.ts
8760
+ var LOCALIZED_URLS = record("read.localizedUrls", 2);
8761
+ var REDIRECT_OK = /* @__PURE__ */ new Set([
8762
+ "employers",
8763
+ "alertsManage",
8764
+ "alertsConfirm"
8765
+ ]);
8766
+ var MAY_BE_UNPUBLISHED = /* @__PURE__ */ new Set([
8767
+ "blog",
8768
+ "about",
8769
+ "talent",
8770
+ "privacyPolicy",
8771
+ "termsOfService",
8772
+ "cookiePolicy",
8773
+ "impressum"
8774
+ ]);
8775
+ var GATE_SEGMENTS = /* @__PURE__ */ new Set([
8776
+ "password",
8777
+ "coming-soon-gate"
8778
+ ]);
8779
+ var CONCURRENCY = 1;
8780
+ var segments = (path) => path.split("/").filter(Boolean);
8781
+ function translatedStaticRoles(manifest) {
8782
+ const translated = [];
8783
+ for (const role of ALL_ROLES) {
8784
+ if (ROLE_PARAM_REGISTRY[role].length > 0 || role === "home") continue;
8785
+ const english = CANONICAL_MANIFEST.roles[role];
8786
+ const localized = manifestRoleTemplate(manifest, role);
8787
+ if (!english || !localized || localized === english) continue;
8788
+ if (segments(localized).length !== segments(english).length) continue;
8789
+ translated.push({ role, english, localized });
8790
+ }
8791
+ return translated;
8792
+ }
8793
+ function resolveLocation(location, requestUrl) {
8794
+ try {
8795
+ return new URL(location, requestUrl);
8796
+ } catch {
8797
+ return null;
8798
+ }
8799
+ }
8800
+ function pathUnder(base, url) {
8801
+ const prefix = base.pathname.replace(/\/$/, "");
8802
+ const path = prefix && url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) || "/" : url.pathname;
8803
+ return `${path}${url.search}`;
8804
+ }
8805
+ function isGate(base, url, requested) {
8806
+ const parts = segments(pathUnder(base, url).split("?")[0]);
8807
+ const last = parts.at(-1);
8808
+ if (last !== void 0 && GATE_SEGMENTS.has(last)) return true;
8809
+ return parts.length === 1 && url.searchParams.get("redirect") === requested;
8810
+ }
8811
+ async function checkRole(fetchImpl, baseUrl, { role, english, localized }) {
8812
+ const base = new URL(baseUrl);
8813
+ const problems = [];
8814
+ const englishUrl = `${baseUrl}${english}`;
8815
+ const hop = await probeHop(fetchImpl, englishUrl);
8816
+ const target = hop.location === null ? null : resolveLocation(hop.location, englishUrl);
8817
+ if (hop.status !== 308) {
8818
+ problems.push(
8819
+ `${english} \u2192 HTTP ${hop.status}${hop.error ? ` (${hop.error})` : ""}${target ? ` to ${target.href}` : ""}, expected 308 to ${localized}`
8820
+ );
8821
+ } else if (target === null) {
8822
+ problems.push(
8823
+ `${english} \u2192 308 without a usable Location, expected ${localized}`
8824
+ );
8825
+ } else if (target.origin !== base.origin) {
8826
+ problems.push(
8827
+ `${english} \u2192 308 off the board's origin to ${target.href}, expected ${localized}`
8828
+ );
8829
+ } else if (pathUnder(base, target) !== localized) {
8830
+ problems.push(
8831
+ `${english} \u2192 308 to ${pathUnder(base, target)}, expected ${localized}`
8832
+ );
8833
+ }
8834
+ const pageUrl = `${baseUrl}${localized}`;
8835
+ const page = await probeHop(fetchImpl, pageUrl);
8836
+ const pageTarget = page.location === null ? null : resolveLocation(page.location, pageUrl);
8837
+ const ok2xx = page.status >= 200 && page.status < 300;
8838
+ const redirected = page.status >= 300 && page.status < 400;
8839
+ if (ok2xx) {
8840
+ } else if (page.status === 404 && MAY_BE_UNPUBLISHED.has(role)) {
8841
+ } else if (redirected && pageTarget === null) {
8842
+ problems.push(
8843
+ `${localized} \u2192 HTTP ${page.status} without a usable Location, expected 2xx`
8844
+ );
8845
+ } else if (redirected && pageTarget.origin !== base.origin) {
8846
+ problems.push(
8847
+ `${localized} \u2192 HTTP ${page.status} off the board's origin to ${pageTarget.href}, expected 2xx`
8848
+ );
8849
+ } else if (redirected && isGate(base, pageTarget, localized)) {
8850
+ problems.push(
8851
+ `${localized} \u2192 HTTP ${page.status} to the board's access gate ${pathUnder(base, pageTarget)}, expected 2xx`
8852
+ );
8853
+ } else if (!(redirected && REDIRECT_OK.has(role))) {
8854
+ problems.push(
8855
+ `${localized} \u2192 HTTP ${page.status}${page.error ? ` (${page.error})` : ""}${pageTarget ? ` to ${pathUnder(base, pageTarget)}` : ""}, expected 2xx`
8856
+ );
8857
+ }
8858
+ return problems.length > 0 ? `${role}: ${problems.join("; ")}` : null;
8859
+ }
8860
+ async function probeLocalizedUrls(fetchImpl, base, manifest) {
8861
+ if (manifest === null) {
8862
+ return LOCALIZED_URLS(
8863
+ "skip",
8864
+ "not applicable \u2014 no readable route manifest at /.well-known/cavuno.json"
8865
+ );
8866
+ }
8867
+ const roles = translatedStaticRoles(manifest);
8868
+ if (roles.length === 0) {
8869
+ return LOCALIZED_URLS(
8870
+ "skip",
8871
+ "not applicable \u2014 the route manifest publishes no translated URL words"
8872
+ );
8873
+ }
8874
+ const failures = new Array(roles.length).fill(null);
8875
+ let next = 0;
8876
+ const worker = async () => {
8877
+ while (next < roles.length) {
8878
+ const index = next++;
8879
+ failures[index] = await checkRole(fetchImpl, base, roles[index]);
8880
+ }
8881
+ };
8882
+ await Promise.all(
8883
+ Array.from({ length: Math.min(CONCURRENCY, roles.length) }, worker)
8884
+ );
8885
+ const failed = failures.filter((f) => f !== null);
8886
+ if (failed.length > 0) {
8887
+ return LOCALIZED_URLS("fail", failed.join(" | "));
8888
+ }
8889
+ return LOCALIZED_URLS(
8890
+ "pass",
8891
+ `${roles.length} translated pages: each English path 308s to the board's word and the page answers (${roles.map((r) => `${r.english} \u2192 ${r.localized}`).join(", ")})`
8892
+ );
8893
+ }
8894
+
8739
8895
  // src/doctor/read.ts
8740
8896
  var READ = {
8741
8897
  home: record("read.home", 2),
@@ -8748,7 +8904,8 @@ var READ = {
8748
8904
  indexNow: record("read.indexNow", 2),
8749
8905
  googleVerification: record("read.googleVerification", 2),
8750
8906
  attribution: record("read.attribution", 2),
8751
- oauthCallback: record("read.oauthCallback", 2)
8907
+ oauthCallback: record("read.oauthCallback", 2),
8908
+ localizedUrls: LOCALIZED_URLS
8752
8909
  };
8753
8910
  function skipReadProbes(reason) {
8754
8911
  return Object.values(READ).map((make) => make("skip", reason));
@@ -8769,17 +8926,17 @@ async function confirmsNoPublishedJobs(fetchImpl, env) {
8769
8926
  return false;
8770
8927
  }
8771
8928
  }
8772
- async function readJobRoutes(fetchImpl, base) {
8929
+ async function readManifest(fetchImpl, base) {
8773
8930
  const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
8774
- if (!manifest.ok) return jobRoutesFromManifest(null);
8931
+ if (!manifest.ok) return null;
8775
8932
  try {
8776
- return jobRoutesFromManifest(JSON.parse(manifest.body));
8933
+ return JSON.parse(manifest.body);
8777
8934
  } catch {
8778
- return jobRoutesFromManifest(null);
8935
+ return null;
8779
8936
  }
8780
8937
  }
8781
- async function probeJobsAndJsonLd(fetchImpl, base, env) {
8782
- const { jobsPath, jobDetailTemplates } = await readJobRoutes(fetchImpl, base);
8938
+ async function probeJobsAndJsonLd(fetchImpl, base, manifest, env) {
8939
+ const { jobsPath, jobDetailTemplates } = jobRoutesFromManifest(manifest);
8783
8940
  const jobs = await probe(fetchImpl, `${base}${jobsPath}`);
8784
8941
  const jobLink = jobs.ok ? extractJobDetailLink(jobs.body, jobDetailTemplates) : null;
8785
8942
  if (jobs.status === 200 && !jobLink && /<(html|body|main)[\s>]/i.test(jobs.body) && await confirmsNoPublishedJobs(fetchImpl, env)) {
@@ -9028,7 +9185,13 @@ function scoreOAuthCallback(probed) {
9028
9185
  async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9029
9186
  const base = frontendUrl.replace(/\/$/, "");
9030
9187
  const home = await probe(fetchImpl, base);
9031
- const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base, env);
9188
+ const manifest = await readManifest(fetchImpl, base);
9189
+ const jobsAndJsonLd = await probeJobsAndJsonLd(
9190
+ fetchImpl,
9191
+ base,
9192
+ manifest,
9193
+ env
9194
+ );
9032
9195
  const sitemap = await probeSitemap(fetchImpl, base);
9033
9196
  const robots = await probe(fetchImpl, `${base}/robots.txt`);
9034
9197
  const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
@@ -9043,6 +9206,7 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9043
9206
  `${base}/api/board-auth/oauth/linkedin/callback`
9044
9207
  )
9045
9208
  };
9209
+ const localizedUrls = await probeLocalizedUrls(fetchImpl, base, manifest);
9046
9210
  const board = env?.apiUrl && env.boardKey ? await probe(
9047
9211
  fetchImpl,
9048
9212
  `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}`
@@ -9081,7 +9245,8 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9081
9245
  }),
9082
9246
  scoreGoogleVerification(home.body, seo),
9083
9247
  attribution,
9084
- scoreOAuthCallback(oauthCallback)
9248
+ scoreOAuthCallback(oauthCallback),
9249
+ localizedUrls
9085
9250
  ];
9086
9251
  }
9087
9252
 
@@ -1,5 +1,5 @@
1
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-CjQM8EPT.mjs';
2
- import './_spec-CLxrfYJb.mjs';
1
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-CMK-DZg3.mjs';
2
+ import './_spec-DHa5r3B0.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-DZVMzBOQ.js';
2
- import './_spec-CLxrfYJb.js';
1
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-CIrTcMjw.js';
2
+ import './_spec-DHa5r3B0.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-DauM7O7_.mjs';
2
- import { C as CustomFieldDefinition } from './board-TOzb2D8c.mjs';
3
- import { C as CustomFieldValues } from './jobs-CjQM8EPT.mjs';
4
- import './_spec-CLxrfYJb.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-B6yhrHJV.mjs';
2
+ import { C as CustomFieldDefinition } from './board-Bpze3b50.mjs';
3
+ import { C as CustomFieldValues } from './jobs-CMK-DZg3.mjs';
4
+ import './_spec-DHa5r3B0.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-DJHyzBji.js';
2
- import { C as CustomFieldDefinition } from './board-DWFDeICw.js';
3
- import { C as CustomFieldValues } from './jobs-DZVMzBOQ.js';
4
- import './_spec-CLxrfYJb.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-15j5xvmN.js';
2
+ import { C as CustomFieldDefinition } from './board-DHsd92Gy.js';
3
+ import { C as CustomFieldValues } from './jobs-CIrTcMjw.js';
4
+ import './_spec-DHa5r3B0.js';
5
5
 
6
6
  /**
7
7
  * Date display helpers in the board language (required leading parameter,