@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.
- package/CHANGELOG.md +61 -0
- package/dist/{_spec-CLxrfYJb.d.mts → _spec-DHa5r3B0.d.mts} +26 -2
- package/dist/{_spec-CLxrfYJb.d.ts → _spec-DHa5r3B0.d.ts} +26 -2
- package/dist/bin.mjs +179 -14
- package/dist/{blog-CQ40QX7l.d.mts → blog-DylAbc0L.d.mts} +1 -1
- package/dist/{blog-mlhJ6UcD.d.ts → blog-wxhB8zAH.d.ts} +1 -1
- package/dist/{board-TOzb2D8c.d.mts → board-Bpze3b50.d.mts} +2 -2
- package/dist/{board-DWFDeICw.d.ts → board-DHsd92Gy.d.ts} +2 -2
- package/dist/{breadcrumbs-DJHyzBji.d.ts → breadcrumbs-15j5xvmN.d.ts} +2 -2
- package/dist/{breadcrumbs-DauM7O7_.d.mts → breadcrumbs-B6yhrHJV.d.mts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/doctor.d.mts +1 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +179 -14
- package/dist/doctor.mjs +179 -14
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +4 -4
- package/dist/format.d.ts +4 -4
- package/dist/index.d.mts +33 -12
- package/dist/index.d.ts +33 -12
- package/dist/index.js +78 -20
- package/dist/index.mjs +78 -20
- package/dist/{jobs-DZVMzBOQ.d.ts → jobs-CIrTcMjw.d.ts} +1 -1
- package/dist/{jobs-CjQM8EPT.d.mts → jobs-CMK-DZg3.d.mts} +1 -1
- package/dist/{search-CF_wa20h.d.ts → search-666oDd7H.d.ts} +1 -1
- package/dist/{search-BtBE6iJO.d.mts → search-DBQApXN1.d.mts} +1 -1
- package/dist/seo-files.d.mts +25 -3
- package/dist/seo-files.d.ts +25 -3
- package/dist/seo-files.js +44 -10
- package/dist/seo-files.mjs +44 -10
- package/dist/{seo-0bfk63qi.d.mts → seo-gY8y9RMw.d.mts} +1 -1
- package/dist/{seo-CU9LpWH7.d.ts → seo-ynqiQmby.d.ts} +1 -1
- package/dist/seo.d.mts +6 -6
- package/dist/seo.d.ts +6 -6
- package/dist/server.d.mts +6 -6
- package/dist/server.d.ts +6 -6
- package/dist/sitemap.d.mts +6 -6
- package/dist/sitemap.d.ts +6 -6
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +1 -1
- package/skills/cavuno-board-filters/SKILL.md +13 -0
- package/skills/cavuno-board-i18n/SKILL.md +10 -0
- package/skills/cavuno-board-job-alerts/SKILL.md +2 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
457
|
+
function manifestRoleTemplate(manifest, name) {
|
|
458
458
|
const roles = manifest !== null && typeof manifest === "object" && manifest.version === 1 ? manifest.roles : null;
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
8906
|
+
async function readManifest(fetchImpl, base) {
|
|
8750
8907
|
const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
|
|
8751
|
-
if (!manifest.ok) return
|
|
8908
|
+
if (!manifest.ok) return null;
|
|
8752
8909
|
try {
|
|
8753
|
-
return
|
|
8910
|
+
return JSON.parse(manifest.body);
|
|
8754
8911
|
} catch {
|
|
8755
|
-
return
|
|
8912
|
+
return null;
|
|
8756
8913
|
}
|
|
8757
8914
|
}
|
|
8758
|
-
async function probeJobsAndJsonLd(fetchImpl, base, env) {
|
|
8759
|
-
const { jobsPath, jobDetailTemplates } =
|
|
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
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import { S as Schemas } from './_spec-
|
|
2
|
-
import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-
|
|
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-
|
|
2
|
-
import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-
|
|
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-
|
|
2
|
-
import { P as PublicJob } from './jobs-
|
|
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-
|
|
2
|
-
import { P as PublicJob } from './jobs-
|
|
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
|