@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/CHANGELOG.md CHANGED
@@ -3,6 +3,67 @@
3
3
  This changelog records changes that affect Board API compatibility, exported
4
4
  types, runtime behavior, or supported integration patterns.
5
5
 
6
+ ## 4.47.0 — 2026-10-14
7
+
8
+ - `board.taxonomy.places.list()` and `board.talent.places.list()` return each
9
+ place's board-language name and URL slug as `displayName` and
10
+ `canonicalSlug`. `name` and `slug` stay English. On an English board, or
11
+ for a place not yet translated, `displayName` is `name` and `canonicalSlug`
12
+ is `slug`. Link to location pages with a job place's `canonicalSlug`; pass a
13
+ talent place's `canonicalSlug` (or `slug`) only to `board.talent.list`'s
14
+ `place` filter.
15
+ - With `q`, both lists also match the board-language name and slug, so
16
+ "Londres" finds London on a French board.
17
+ - Deleting a company takes an optional `block=true`: the company is removed
18
+ and stays on the blocklist, so sourcing cannot add it back until it is
19
+ unblocked.
20
+ - Job alerts: `jobFunctions` are category and skill slugs. Display names are
21
+ still accepted and slugified when the alert is saved, and alerts read back
22
+ carry slugs.
23
+
24
+ ## 4.46.0 — 2026-10-14
25
+
26
+ - New `board.talent.places.list({ q, limit })`: the places a board's
27
+ directory candidates live in, each with `candidateCount`, the number of
28
+ candidates the viewer can see there. A candidate counts in their home place
29
+ and every place above it, so a country counts every candidate in its
30
+ cities. Signed-out viewers count public profiles; a signed-in board user
31
+ also counts profiles visible to signed-in users. Busiest first, up to
32
+ `limit` (1 to 500, default 100). With `q` (2 or more characters after
33
+ removing accents), it returns the best name and slug matches for a location
34
+ search field, at most 50. Pass a place's `slug` as `board.talent.list`'s
35
+ `place` filter. The list follows the directory's access rules.
36
+ - New exported types: `TalentPlace`, `TalentPlacesListEnvelope`, `TalentPlacesQuery`.
37
+ - Saved searches: a list has no size limit, so `talent_list_full` is no longer
38
+ raised. On a list linked to a job, `members.add` is how a sourced candidate
39
+ is shortlisted onto that job, and `members.remove` also takes them off the
40
+ job while they are still at Shortlisted. Linking a list shortlists everyone
41
+ saved to it and saves the job's sourced people to it; a job has at most one
42
+ list.
43
+ - Deprecated: `me.companies.sourcedCandidates`. Use `talentLists.members` on the
44
+ job's linked list; `add` and `remove` on the old namespace forward to it.
45
+
46
+ ## 4.45.0 — 2026-10-13
47
+
48
+ - `robotsBody()` and `robotsResponse()` from `@cavuno/board/seo-files` take
49
+ `listingPaths: { jobs, companies, talent }` for boards that rename or
50
+ translate their listing pages (for example `/vacatures` in place of
51
+ `/jobs`). The pagination `Disallow` rules then use those roots. A missing,
52
+ root (`/`) or unsafe value keeps the default root for that group, and
53
+ without the option the body is unchanged.
54
+ - They also take `localeListingPaths`, a list of the same shape, for
55
+ secondary interface languages served under a prefix (for example
56
+ `/fr/emplois`). Each root gets the same pagination rules after the
57
+ base-language rules, and roots already listed are skipped.
58
+ - New exported type: `RobotsListingPaths`.
59
+ - Doctor: a new `read.localizedUrls` check. On a board whose route manifest
60
+ uses translated words, each translated page's English path must answer a
61
+ 308 to the board's own path on the same origin, and that path must load.
62
+ English boards skip it with no extra requests.
63
+ - The `cavuno-board-i18n` skill explains how a renamed URL word keeps
64
+ working: list the old word in `src/url-words-previous.json`, and starters
65
+ that read the file redirect it to the current word with one 308.
66
+
6
67
  ## 4.44.0 — 2026-10-12
7
68
 
8
69
  - Saved searches: `me.companies.talentLists.members.list()` takes `stage`, a
@@ -2862,9 +2862,13 @@ interface components {
2862
2862
  id: string;
2863
2863
  /** @description Parent place's `id`; `null` for a root place. */
2864
2864
  parentId: string | null;
2865
- /** @description Public slug (links to `/jobs/locations/:slug`); `null` if unslugged. */
2865
+ /** @description English source slug: the key for the jobs `location` filter. `null` if unslugged. Build links with `canonicalSlug`: on a translated board, the location page at the English slug can 308-redirect. */
2866
2866
  slug: string | null;
2867
+ /** @description Board-language canonical URL slug where the board language has unique place slugs; otherwise, or for an untranslated place, `slug`. `null` if unslugged. Use it for links: it resolves as the jobs `location` filter, on `GET /boards/{identifier}/places/{slug}`, and as the location page slug (`/jobs/locations/:slug`). */
2868
+ canonicalSlug: string | null;
2867
2869
  name: string;
2870
+ /** @description Board-language name with source-name fallback. */
2871
+ displayName: string;
2868
2872
  placeType: string;
2869
2873
  countryCode: string | null;
2870
2874
  regionCode: string | null;
@@ -2940,6 +2944,26 @@ interface components {
2940
2944
  */
2941
2945
  offset?: number;
2942
2946
  };
2947
+ PublicTalentPlace: {
2948
+ /** @enum {string} */
2949
+ object: "talent_place";
2950
+ /** @description Stable place identity (the same id as `/places`). */
2951
+ id: string;
2952
+ /** @description Parent place's `id`; `null` for a root place. */
2953
+ parentId: string | null;
2954
+ /** @description English source place slug: pass it as the talent directory's `place` filter. */
2955
+ slug: string | null;
2956
+ /** @description Board-language place slug where the board language has unique place slugs; otherwise, or for an untranslated place, `slug`. `null` if unslugged. It resolves only as the talent directory's `place` filter: there are no talent location pages, and it is not a slug for `GET /boards/{identifier}/places/{slug}` or the job location pages. */
2957
+ canonicalSlug: string | null;
2958
+ name: string;
2959
+ /** @description Board-language name with source-name fallback. */
2960
+ displayName: string;
2961
+ placeType: string;
2962
+ countryCode: string | null;
2963
+ regionCode: string | null;
2964
+ /** @description Candidates the viewer can see in the directory whose home is this place or lies within it. Signed out: `public` profiles; signed in: `public` and `logged_in_only` profiles. */
2965
+ candidateCount: number;
2966
+ };
2943
2967
  PublicTaxonomyResolution: {
2944
2968
  /** @enum {string} */
2945
2969
  object: "taxonomy_resolution";
@@ -3607,7 +3631,7 @@ interface components {
3607
3631
  createdBy: string;
3608
3632
  createdAt: number;
3609
3633
  updatedAt: number;
3610
- /** @description People saved to this list (at most 500). Includes anyone the members list omits for the viewer (blocked, hidden or deactivated). */
3634
+ /** @description People saved to this list. Exact and immediate; a list has no size limit. Includes anyone the members list omits for the viewer (blocked, hidden or deactivated). */
3611
3635
  memberCount: number;
3612
3636
  };
3613
3637
  TalentListFilters: {
@@ -2862,9 +2862,13 @@ interface components {
2862
2862
  id: string;
2863
2863
  /** @description Parent place's `id`; `null` for a root place. */
2864
2864
  parentId: string | null;
2865
- /** @description Public slug (links to `/jobs/locations/:slug`); `null` if unslugged. */
2865
+ /** @description English source slug: the key for the jobs `location` filter. `null` if unslugged. Build links with `canonicalSlug`: on a translated board, the location page at the English slug can 308-redirect. */
2866
2866
  slug: string | null;
2867
+ /** @description Board-language canonical URL slug where the board language has unique place slugs; otherwise, or for an untranslated place, `slug`. `null` if unslugged. Use it for links: it resolves as the jobs `location` filter, on `GET /boards/{identifier}/places/{slug}`, and as the location page slug (`/jobs/locations/:slug`). */
2868
+ canonicalSlug: string | null;
2867
2869
  name: string;
2870
+ /** @description Board-language name with source-name fallback. */
2871
+ displayName: string;
2868
2872
  placeType: string;
2869
2873
  countryCode: string | null;
2870
2874
  regionCode: string | null;
@@ -2940,6 +2944,26 @@ interface components {
2940
2944
  */
2941
2945
  offset?: number;
2942
2946
  };
2947
+ PublicTalentPlace: {
2948
+ /** @enum {string} */
2949
+ object: "talent_place";
2950
+ /** @description Stable place identity (the same id as `/places`). */
2951
+ id: string;
2952
+ /** @description Parent place's `id`; `null` for a root place. */
2953
+ parentId: string | null;
2954
+ /** @description English source place slug: pass it as the talent directory's `place` filter. */
2955
+ slug: string | null;
2956
+ /** @description Board-language place slug where the board language has unique place slugs; otherwise, or for an untranslated place, `slug`. `null` if unslugged. It resolves only as the talent directory's `place` filter: there are no talent location pages, and it is not a slug for `GET /boards/{identifier}/places/{slug}` or the job location pages. */
2957
+ canonicalSlug: string | null;
2958
+ name: string;
2959
+ /** @description Board-language name with source-name fallback. */
2960
+ displayName: string;
2961
+ placeType: string;
2962
+ countryCode: string | null;
2963
+ regionCode: string | null;
2964
+ /** @description Candidates the viewer can see in the directory whose home is this place or lies within it. Signed out: `public` profiles; signed in: `public` and `logged_in_only` profiles. */
2965
+ candidateCount: number;
2966
+ };
2943
2967
  PublicTaxonomyResolution: {
2944
2968
  /** @enum {string} */
2945
2969
  object: "taxonomy_resolution";
@@ -3607,7 +3631,7 @@ interface components {
3607
3631
  createdBy: string;
3608
3632
  createdAt: number;
3609
3633
  updatedAt: number;
3610
- /** @description People saved to this list (at most 500). Includes anyone the members list omits for the viewer (blocked, hidden or deactivated). */
3634
+ /** @description People saved to this list. Exact and immediate; a list has no size limit. Includes anyone the members list omits for the viewer (blocked, hidden or deactivated). */
3611
3635
  memberCount: number;
3612
3636
  };
3613
3637
  TalentListFilters: {
package/dist/bin.mjs CHANGED
@@ -454,12 +454,13 @@ function extractJobDetailLink(html, templates) {
454
454
  var HREF_PATH_RE = /href="(\/[^"?#\s]+)"/gi;
455
455
  var SLUG_RE = /^[a-z0-9-]+$/i;
456
456
  var CANONICAL_JOB_DETAIL_TEMPLATE = CANONICAL_MANIFEST.roles.jobDetail;
457
- function jobRoutesFromManifest(manifest) {
457
+ function manifestRoleTemplate(manifest, name) {
458
458
  const roles = manifest !== null && typeof manifest === "object" && manifest.version === 1 ? manifest.roles : null;
459
- const role = (name) => {
460
- const template = roles !== null && typeof roles === "object" ? roles[name] : void 0;
461
- return typeof template === "string" && checkRoleTemplate(name, template).length === 0 ? template : null;
462
- };
459
+ const template = roles !== null && typeof roles === "object" ? roles[name] : void 0;
460
+ return typeof template === "string" && checkRoleTemplate(name, template).length === 0 ? template : null;
461
+ }
462
+ function jobRoutesFromManifest(manifest) {
463
+ const role = (name) => manifestRoleTemplate(manifest, name);
463
464
  const jobs = role("jobs");
464
465
  const company = role("company");
465
466
  const jobsWord = jobs?.split("/").filter((seg) => seg !== "" && !seg.includes(":")).at(-1);
@@ -777,6 +778,25 @@ async function probe(fetchImpl, url) {
777
778
  };
778
779
  }
779
780
  }
781
+ async function probeHop(fetchImpl, url) {
782
+ try {
783
+ const response = await fetchImpl(url, {
784
+ redirect: "manual",
785
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
786
+ });
787
+ await response.body?.cancel().catch(() => void 0);
788
+ return {
789
+ status: response.status,
790
+ location: response.headers.get("location")
791
+ };
792
+ } catch (error) {
793
+ return {
794
+ status: 0,
795
+ location: null,
796
+ error: error instanceof Error ? error.message : String(error)
797
+ };
798
+ }
799
+ }
780
800
 
781
801
  // node_modules/.pnpm/parse5@7.3.0/node_modules/parse5/dist/common/unicode.js
782
802
  var UNDEFINED_CODE_POINTS = /* @__PURE__ */ new Set([
@@ -8713,6 +8733,142 @@ function hasCrawlableCavunoBacklink(html) {
8713
8733
  return visit(root);
8714
8734
  }
8715
8735
 
8736
+ // src/doctor/localized-urls.ts
8737
+ var LOCALIZED_URLS = record("read.localizedUrls", 2);
8738
+ var REDIRECT_OK = /* @__PURE__ */ new Set([
8739
+ "employers",
8740
+ "alertsManage",
8741
+ "alertsConfirm"
8742
+ ]);
8743
+ var MAY_BE_UNPUBLISHED = /* @__PURE__ */ new Set([
8744
+ "blog",
8745
+ "about",
8746
+ "talent",
8747
+ "privacyPolicy",
8748
+ "termsOfService",
8749
+ "cookiePolicy",
8750
+ "impressum"
8751
+ ]);
8752
+ var GATE_SEGMENTS = /* @__PURE__ */ new Set([
8753
+ "password",
8754
+ "coming-soon-gate"
8755
+ ]);
8756
+ var CONCURRENCY = 1;
8757
+ var segments = (path) => path.split("/").filter(Boolean);
8758
+ function translatedStaticRoles(manifest) {
8759
+ const translated = [];
8760
+ for (const role of ALL_ROLES) {
8761
+ if (ROLE_PARAM_REGISTRY[role].length > 0 || role === "home") continue;
8762
+ const english = CANONICAL_MANIFEST.roles[role];
8763
+ const localized = manifestRoleTemplate(manifest, role);
8764
+ if (!english || !localized || localized === english) continue;
8765
+ if (segments(localized).length !== segments(english).length) continue;
8766
+ translated.push({ role, english, localized });
8767
+ }
8768
+ return translated;
8769
+ }
8770
+ function resolveLocation(location, requestUrl) {
8771
+ try {
8772
+ return new URL(location, requestUrl);
8773
+ } catch {
8774
+ return null;
8775
+ }
8776
+ }
8777
+ function pathUnder(base, url) {
8778
+ const prefix = base.pathname.replace(/\/$/, "");
8779
+ const path = prefix && url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) || "/" : url.pathname;
8780
+ return `${path}${url.search}`;
8781
+ }
8782
+ function isGate(base, url, requested) {
8783
+ const parts = segments(pathUnder(base, url).split("?")[0]);
8784
+ const last = parts.at(-1);
8785
+ if (last !== void 0 && GATE_SEGMENTS.has(last)) return true;
8786
+ return parts.length === 1 && url.searchParams.get("redirect") === requested;
8787
+ }
8788
+ async function checkRole(fetchImpl, baseUrl, { role, english, localized }) {
8789
+ const base = new URL(baseUrl);
8790
+ const problems = [];
8791
+ const englishUrl = `${baseUrl}${english}`;
8792
+ const hop = await probeHop(fetchImpl, englishUrl);
8793
+ const target = hop.location === null ? null : resolveLocation(hop.location, englishUrl);
8794
+ if (hop.status !== 308) {
8795
+ problems.push(
8796
+ `${english} \u2192 HTTP ${hop.status}${hop.error ? ` (${hop.error})` : ""}${target ? ` to ${target.href}` : ""}, expected 308 to ${localized}`
8797
+ );
8798
+ } else if (target === null) {
8799
+ problems.push(
8800
+ `${english} \u2192 308 without a usable Location, expected ${localized}`
8801
+ );
8802
+ } else if (target.origin !== base.origin) {
8803
+ problems.push(
8804
+ `${english} \u2192 308 off the board's origin to ${target.href}, expected ${localized}`
8805
+ );
8806
+ } else if (pathUnder(base, target) !== localized) {
8807
+ problems.push(
8808
+ `${english} \u2192 308 to ${pathUnder(base, target)}, expected ${localized}`
8809
+ );
8810
+ }
8811
+ const pageUrl = `${baseUrl}${localized}`;
8812
+ const page = await probeHop(fetchImpl, pageUrl);
8813
+ const pageTarget = page.location === null ? null : resolveLocation(page.location, pageUrl);
8814
+ const ok2xx = page.status >= 200 && page.status < 300;
8815
+ const redirected = page.status >= 300 && page.status < 400;
8816
+ if (ok2xx) {
8817
+ } else if (page.status === 404 && MAY_BE_UNPUBLISHED.has(role)) {
8818
+ } else if (redirected && pageTarget === null) {
8819
+ problems.push(
8820
+ `${localized} \u2192 HTTP ${page.status} without a usable Location, expected 2xx`
8821
+ );
8822
+ } else if (redirected && pageTarget.origin !== base.origin) {
8823
+ problems.push(
8824
+ `${localized} \u2192 HTTP ${page.status} off the board's origin to ${pageTarget.href}, expected 2xx`
8825
+ );
8826
+ } else if (redirected && isGate(base, pageTarget, localized)) {
8827
+ problems.push(
8828
+ `${localized} \u2192 HTTP ${page.status} to the board's access gate ${pathUnder(base, pageTarget)}, expected 2xx`
8829
+ );
8830
+ } else if (!(redirected && REDIRECT_OK.has(role))) {
8831
+ problems.push(
8832
+ `${localized} \u2192 HTTP ${page.status}${page.error ? ` (${page.error})` : ""}${pageTarget ? ` to ${pathUnder(base, pageTarget)}` : ""}, expected 2xx`
8833
+ );
8834
+ }
8835
+ return problems.length > 0 ? `${role}: ${problems.join("; ")}` : null;
8836
+ }
8837
+ async function probeLocalizedUrls(fetchImpl, base, manifest) {
8838
+ if (manifest === null) {
8839
+ return LOCALIZED_URLS(
8840
+ "skip",
8841
+ "not applicable \u2014 no readable route manifest at /.well-known/cavuno.json"
8842
+ );
8843
+ }
8844
+ const roles = translatedStaticRoles(manifest);
8845
+ if (roles.length === 0) {
8846
+ return LOCALIZED_URLS(
8847
+ "skip",
8848
+ "not applicable \u2014 the route manifest publishes no translated URL words"
8849
+ );
8850
+ }
8851
+ const failures = new Array(roles.length).fill(null);
8852
+ let next = 0;
8853
+ const worker = async () => {
8854
+ while (next < roles.length) {
8855
+ const index = next++;
8856
+ failures[index] = await checkRole(fetchImpl, base, roles[index]);
8857
+ }
8858
+ };
8859
+ await Promise.all(
8860
+ Array.from({ length: Math.min(CONCURRENCY, roles.length) }, worker)
8861
+ );
8862
+ const failed = failures.filter((f) => f !== null);
8863
+ if (failed.length > 0) {
8864
+ return LOCALIZED_URLS("fail", failed.join(" | "));
8865
+ }
8866
+ return LOCALIZED_URLS(
8867
+ "pass",
8868
+ `${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(", ")})`
8869
+ );
8870
+ }
8871
+
8716
8872
  // src/doctor/read.ts
8717
8873
  var READ = {
8718
8874
  home: record("read.home", 2),
@@ -8725,7 +8881,8 @@ var READ = {
8725
8881
  indexNow: record("read.indexNow", 2),
8726
8882
  googleVerification: record("read.googleVerification", 2),
8727
8883
  attribution: record("read.attribution", 2),
8728
- oauthCallback: record("read.oauthCallback", 2)
8884
+ oauthCallback: record("read.oauthCallback", 2),
8885
+ localizedUrls: LOCALIZED_URLS
8729
8886
  };
8730
8887
  function skipReadProbes(reason) {
8731
8888
  return Object.values(READ).map((make) => make("skip", reason));
@@ -8746,17 +8903,17 @@ async function confirmsNoPublishedJobs(fetchImpl, env) {
8746
8903
  return false;
8747
8904
  }
8748
8905
  }
8749
- async function readJobRoutes(fetchImpl, base) {
8906
+ async function readManifest(fetchImpl, base) {
8750
8907
  const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
8751
- if (!manifest.ok) return jobRoutesFromManifest(null);
8908
+ if (!manifest.ok) return null;
8752
8909
  try {
8753
- return jobRoutesFromManifest(JSON.parse(manifest.body));
8910
+ return JSON.parse(manifest.body);
8754
8911
  } catch {
8755
- return jobRoutesFromManifest(null);
8912
+ return null;
8756
8913
  }
8757
8914
  }
8758
- async function probeJobsAndJsonLd(fetchImpl, base, env) {
8759
- const { jobsPath, jobDetailTemplates } = await readJobRoutes(fetchImpl, base);
8915
+ async function probeJobsAndJsonLd(fetchImpl, base, manifest, env) {
8916
+ const { jobsPath, jobDetailTemplates } = jobRoutesFromManifest(manifest);
8760
8917
  const jobs = await probe(fetchImpl, `${base}${jobsPath}`);
8761
8918
  const jobLink = jobs.ok ? extractJobDetailLink(jobs.body, jobDetailTemplates) : null;
8762
8919
  if (jobs.status === 200 && !jobLink && /<(html|body|main)[\s>]/i.test(jobs.body) && await confirmsNoPublishedJobs(fetchImpl, env)) {
@@ -9005,7 +9162,13 @@ function scoreOAuthCallback(probed) {
9005
9162
  async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9006
9163
  const base = frontendUrl.replace(/\/$/, "");
9007
9164
  const home = await probe(fetchImpl, base);
9008
- const jobsAndJsonLd = await probeJobsAndJsonLd(fetchImpl, base, env);
9165
+ const manifest = await readManifest(fetchImpl, base);
9166
+ const jobsAndJsonLd = await probeJobsAndJsonLd(
9167
+ fetchImpl,
9168
+ base,
9169
+ manifest,
9170
+ env
9171
+ );
9009
9172
  const sitemap = await probeSitemap(fetchImpl, base);
9010
9173
  const robots = await probe(fetchImpl, `${base}/robots.txt`);
9011
9174
  const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
@@ -9020,6 +9183,7 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9020
9183
  `${base}/api/board-auth/oauth/linkedin/callback`
9021
9184
  )
9022
9185
  };
9186
+ const localizedUrls = await probeLocalizedUrls(fetchImpl, base, manifest);
9023
9187
  const board = env?.apiUrl && env.boardKey ? await probe(
9024
9188
  fetchImpl,
9025
9189
  `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}`
@@ -9058,7 +9222,8 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
9058
9222
  }),
9059
9223
  scoreGoogleVerification(home.body, seo),
9060
9224
  attribution,
9061
- scoreOAuthCallback(oauthCallback)
9225
+ scoreOAuthCallback(oauthCallback),
9226
+ localizedUrls
9062
9227
  ];
9063
9228
  }
9064
9229
 
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CLxrfYJb.mjs';
1
+ import { S as Schemas } from './_spec-DHa5r3B0.mjs';
2
2
 
3
3
  /** Author shape embedded on posts (no `object` discriminator). */
4
4
  type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CLxrfYJb.js';
1
+ import { S as Schemas } from './_spec-DHa5r3B0.js';
2
2
 
3
3
  /** Author shape embedded on posts (no `object` discriminator). */
4
4
  type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
@@ -1,5 +1,5 @@
1
- import { S as Schemas } from './_spec-CLxrfYJb.mjs';
2
- import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-CjQM8EPT.mjs';
1
+ import { S as Schemas } from './_spec-DHa5r3B0.mjs';
2
+ import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-CMK-DZg3.mjs';
3
3
 
4
4
  type PublicCompany = Schemas['CompanyPublic'];
5
5
  /** A market (sector) a company operates in — name + its source slug. */
@@ -1,5 +1,5 @@
1
- import { S as Schemas } from './_spec-CLxrfYJb.js';
2
- import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-DZVMzBOQ.js';
1
+ import { S as Schemas } from './_spec-DHa5r3B0.js';
2
+ import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-CIrTcMjw.js';
3
3
 
4
4
  type PublicCompany = Schemas['CompanyPublic'];
5
5
  /** A market (sector) a company operates in — name + its source slug. */
@@ -1,5 +1,5 @@
1
- import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-DWFDeICw.js';
2
- import { P as PublicJob } from './jobs-DZVMzBOQ.js';
1
+ import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-DHsd92Gy.js';
2
+ import { P as PublicJob } from './jobs-CIrTcMjw.js';
3
3
 
4
4
  /**
5
5
  * Wire enum for `salaryTimeframe`. Defined here (not re-exported from any
@@ -1,5 +1,5 @@
1
- import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-TOzb2D8c.mjs';
2
- import { P as PublicJob } from './jobs-CjQM8EPT.mjs';
1
+ import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-Bpze3b50.mjs';
2
+ import { P as PublicJob } from './jobs-CMK-DZg3.mjs';
3
3
 
4
4
  /**
5
5
  * Wire enum for `salaryTimeframe`. Defined here (not re-exported from any