@cavuno/board 4.42.0 → 4.45.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 +56 -0
- package/dist/{_spec-B2aizgxr.d.mts → _spec-CdNZDaaR.d.mts} +39 -0
- package/dist/{_spec-B2aizgxr.d.ts → _spec-CdNZDaaR.d.ts} +39 -0
- package/dist/bin.mjs +570 -10
- package/dist/{blog-BkACbXY0.d.ts → blog-ZvLJ6w1Q.d.ts} +1 -1
- package/dist/{blog-CYHSnNTZ.d.mts → blog-uV51b4Cg.d.mts} +1 -1
- package/dist/{board-DWeT9Jbl.d.ts → board-3JU14eF3.d.ts} +2 -2
- package/dist/{board-DK4HmSty.d.mts → board-CJw-MJpf.d.mts} +2 -2
- package/dist/{breadcrumbs-nTfIWzRh.d.mts → breadcrumbs-BY1UMCbL.d.mts} +2 -2
- package/dist/{breadcrumbs-CtKa2zPe.d.ts → breadcrumbs-CXvdv6Ry.d.ts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/{chunk-WSO6JN5C.mjs → chunk-DZD3DAEY.mjs} +2 -0
- package/dist/doctor.d.mts +1 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +570 -10
- package/dist/doctor.mjs +570 -10
- 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 +107 -14
- package/dist/index.d.ts +107 -14
- package/dist/index.js +129 -8
- package/dist/index.mjs +128 -9
- package/dist/{jobs-SAkNWgAB.d.ts → jobs-C7UvN_dW.d.ts} +1 -1
- package/dist/{jobs-C_eO-WfM.d.mts → jobs-D9ERWWda.d.mts} +1 -1
- package/dist/{search-Cb3Aoa4c.d.mts → search-BMSNK85o.d.mts} +1 -1
- package/dist/{search-CVcMFOuJ.d.ts → search-D0TFnn1x.d.ts} +1 -1
- package/dist/{seo-DPVud7SH.d.mts → seo-C3WmgifR.d.mts} +1 -1
- package/dist/{seo-DgFBQTIK.d.ts → seo-CYmVKHsu.d.ts} +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.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/server.mjs +1 -1
- package/dist/sitemap.d.mts +6 -6
- package/dist/sitemap.d.ts +6 -6
- package/dist/sitemap.mjs +1 -1
- 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-post-a-job/SKILL.md +2 -1
- package/skills/manifest.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -39,6 +39,358 @@ function loadSkillCorpus(baseDir) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// src/paths/index.ts
|
|
43
|
+
function encodePathSegment(segment) {
|
|
44
|
+
try {
|
|
45
|
+
return encodeURIComponent(decodeURIComponent(segment)).replace(
|
|
46
|
+
/%3A/gi,
|
|
47
|
+
":"
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
return encodeURIComponent(segment).replace(/%3A/gi, ":");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function jobDetailPath(companySlug, jobSlug) {
|
|
54
|
+
return `/companies/${encodePathSegment(companySlug)}/jobs/${encodePathSegment(jobSlug)}`;
|
|
55
|
+
}
|
|
56
|
+
function jobsCategoryPath(categorySlug) {
|
|
57
|
+
return `/jobs/${encodePathSegment(categorySlug)}`;
|
|
58
|
+
}
|
|
59
|
+
function jobsSkillPath(skillSlug) {
|
|
60
|
+
return `/jobs/skills/${encodePathSegment(skillSlug)}`;
|
|
61
|
+
}
|
|
62
|
+
function jobsLocationPath(placeSlug) {
|
|
63
|
+
return `/jobs/locations/${encodePathSegment(placeSlug)}`;
|
|
64
|
+
}
|
|
65
|
+
function companyPath(companySlug) {
|
|
66
|
+
return `/companies/${encodePathSegment(companySlug)}`;
|
|
67
|
+
}
|
|
68
|
+
function companyMarketPath(marketSlug) {
|
|
69
|
+
return `/companies/markets/${encodePathSegment(marketSlug)}`;
|
|
70
|
+
}
|
|
71
|
+
function companySalaryPath(companySlug) {
|
|
72
|
+
return `/companies/${encodePathSegment(companySlug)}/salaries`;
|
|
73
|
+
}
|
|
74
|
+
function salaryTitlePath(titleSlug) {
|
|
75
|
+
return `/salaries/titles/${encodePathSegment(titleSlug)}`;
|
|
76
|
+
}
|
|
77
|
+
function salarySkillPath(skillSlug) {
|
|
78
|
+
return `/salaries/skills/${encodePathSegment(skillSlug)}`;
|
|
79
|
+
}
|
|
80
|
+
function salaryLocationPath(placeSlug) {
|
|
81
|
+
return `/salaries/locations/${encodePathSegment(placeSlug)}`;
|
|
82
|
+
}
|
|
83
|
+
function blogPostPath(postSlug) {
|
|
84
|
+
return `/blog/${encodePathSegment(postSlug)}`;
|
|
85
|
+
}
|
|
86
|
+
function blogTagPath(tagSlug) {
|
|
87
|
+
return `/blog/tag/${encodePathSegment(tagSlug)}`;
|
|
88
|
+
}
|
|
89
|
+
function blogAuthorPath(authorSlug) {
|
|
90
|
+
return `/blog/author/${encodePathSegment(authorSlug)}`;
|
|
91
|
+
}
|
|
92
|
+
function talentProfilePath(handle) {
|
|
93
|
+
return `/p/${encodePathSegment(handle)}`;
|
|
94
|
+
}
|
|
95
|
+
var BOARD_PATHS = {
|
|
96
|
+
home: "/",
|
|
97
|
+
jobs: "/jobs",
|
|
98
|
+
companies: "/companies",
|
|
99
|
+
salaries: "/salaries",
|
|
100
|
+
salaryCompanies: "/salaries/companies",
|
|
101
|
+
salaryTitles: "/salaries/titles",
|
|
102
|
+
salarySkills: "/salaries/skills",
|
|
103
|
+
salaryLocations: "/salaries/locations",
|
|
104
|
+
blog: "/blog",
|
|
105
|
+
about: "/about",
|
|
106
|
+
privacyPolicy: "/privacy-policy",
|
|
107
|
+
termsOfService: "/terms-of-service",
|
|
108
|
+
cookiePolicy: "/cookie-policy",
|
|
109
|
+
impressum: "/impressum",
|
|
110
|
+
talent: "/talent",
|
|
111
|
+
employers: "/employers",
|
|
112
|
+
/**
|
|
113
|
+
* Alert-email surfaces. Promoted out of raw literals in
|
|
114
|
+
* the job-alerts composer so /go indirection and email composition share
|
|
115
|
+
* one definition.
|
|
116
|
+
*/
|
|
117
|
+
alertsManage: "/alerts/manage",
|
|
118
|
+
alertsConfirm: "/alerts/confirm"
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/route-contract/roles.ts
|
|
122
|
+
var ROLE_PARAM_REGISTRY = {
|
|
123
|
+
jobDetail: ["companySlug", "jobSlug"],
|
|
124
|
+
jobsCategory: ["categorySlug"],
|
|
125
|
+
jobsSkill: ["skillSlug"],
|
|
126
|
+
jobsLocation: ["placeSlug"],
|
|
127
|
+
company: ["companySlug"],
|
|
128
|
+
companyMarket: ["marketSlug"],
|
|
129
|
+
companySalary: ["companySlug"],
|
|
130
|
+
salaryTitle: ["titleSlug"],
|
|
131
|
+
salarySkill: ["skillSlug"],
|
|
132
|
+
salaryLocation: ["placeSlug"],
|
|
133
|
+
blogPost: ["postSlug"],
|
|
134
|
+
blogTag: ["tagSlug"],
|
|
135
|
+
blogAuthor: ["authorSlug"],
|
|
136
|
+
talentProfile: ["handle"],
|
|
137
|
+
home: [],
|
|
138
|
+
jobs: [],
|
|
139
|
+
companies: [],
|
|
140
|
+
salaries: [],
|
|
141
|
+
salaryCompanies: [],
|
|
142
|
+
salaryTitles: [],
|
|
143
|
+
salarySkills: [],
|
|
144
|
+
salaryLocations: [],
|
|
145
|
+
blog: [],
|
|
146
|
+
about: [],
|
|
147
|
+
privacyPolicy: [],
|
|
148
|
+
termsOfService: [],
|
|
149
|
+
cookiePolicy: [],
|
|
150
|
+
impressum: [],
|
|
151
|
+
talent: [],
|
|
152
|
+
employers: [],
|
|
153
|
+
alertsManage: [],
|
|
154
|
+
alertsConfirm: []
|
|
155
|
+
};
|
|
156
|
+
var ALL_ROLES = Object.keys(
|
|
157
|
+
ROLE_PARAM_REGISTRY
|
|
158
|
+
);
|
|
159
|
+
var ROLE_SET = new Set(ALL_ROLES);
|
|
160
|
+
var CANONICAL_MANIFEST = {
|
|
161
|
+
version: 1,
|
|
162
|
+
roles: {
|
|
163
|
+
jobDetail: jobDetailPath(":companySlug", ":jobSlug"),
|
|
164
|
+
jobsCategory: jobsCategoryPath(":categorySlug"),
|
|
165
|
+
jobsSkill: jobsSkillPath(":skillSlug"),
|
|
166
|
+
jobsLocation: jobsLocationPath(":placeSlug"),
|
|
167
|
+
company: companyPath(":companySlug"),
|
|
168
|
+
companyMarket: companyMarketPath(":marketSlug"),
|
|
169
|
+
companySalary: companySalaryPath(":companySlug"),
|
|
170
|
+
salaryTitle: salaryTitlePath(":titleSlug"),
|
|
171
|
+
salarySkill: salarySkillPath(":skillSlug"),
|
|
172
|
+
salaryLocation: salaryLocationPath(":placeSlug"),
|
|
173
|
+
blogPost: blogPostPath(":postSlug"),
|
|
174
|
+
blogTag: blogTagPath(":tagSlug"),
|
|
175
|
+
blogAuthor: blogAuthorPath(":authorSlug"),
|
|
176
|
+
talentProfile: talentProfilePath(":handle"),
|
|
177
|
+
home: BOARD_PATHS.home,
|
|
178
|
+
jobs: BOARD_PATHS.jobs,
|
|
179
|
+
companies: BOARD_PATHS.companies,
|
|
180
|
+
salaries: BOARD_PATHS.salaries,
|
|
181
|
+
salaryCompanies: BOARD_PATHS.salaryCompanies,
|
|
182
|
+
salaryTitles: BOARD_PATHS.salaryTitles,
|
|
183
|
+
salarySkills: BOARD_PATHS.salarySkills,
|
|
184
|
+
salaryLocations: BOARD_PATHS.salaryLocations,
|
|
185
|
+
blog: BOARD_PATHS.blog,
|
|
186
|
+
about: BOARD_PATHS.about,
|
|
187
|
+
privacyPolicy: BOARD_PATHS.privacyPolicy,
|
|
188
|
+
termsOfService: BOARD_PATHS.termsOfService,
|
|
189
|
+
cookiePolicy: BOARD_PATHS.cookiePolicy,
|
|
190
|
+
impressum: BOARD_PATHS.impressum,
|
|
191
|
+
talent: BOARD_PATHS.talent,
|
|
192
|
+
employers: BOARD_PATHS.employers,
|
|
193
|
+
alertsManage: "/alerts/manage",
|
|
194
|
+
alertsConfirm: "/alerts/confirm"
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/route-contract/match-history.ts
|
|
199
|
+
var ROLE_INDEX_PATHS = {
|
|
200
|
+
jobDetail: BOARD_PATHS.jobs,
|
|
201
|
+
jobsCategory: BOARD_PATHS.jobs,
|
|
202
|
+
jobsSkill: BOARD_PATHS.jobs,
|
|
203
|
+
jobsLocation: BOARD_PATHS.jobs,
|
|
204
|
+
company: BOARD_PATHS.companies,
|
|
205
|
+
companyMarket: BOARD_PATHS.companies,
|
|
206
|
+
companySalary: BOARD_PATHS.companies,
|
|
207
|
+
salaryTitle: BOARD_PATHS.salaries,
|
|
208
|
+
salarySkill: BOARD_PATHS.salaries,
|
|
209
|
+
salaryLocation: BOARD_PATHS.salaries,
|
|
210
|
+
salaryCompanies: BOARD_PATHS.salaries,
|
|
211
|
+
salaryTitles: BOARD_PATHS.salaries,
|
|
212
|
+
salarySkills: BOARD_PATHS.salaries,
|
|
213
|
+
salaryLocations: BOARD_PATHS.salaries,
|
|
214
|
+
salaries: BOARD_PATHS.salaries,
|
|
215
|
+
blogPost: BOARD_PATHS.blog,
|
|
216
|
+
blogTag: BOARD_PATHS.blog,
|
|
217
|
+
blogAuthor: BOARD_PATHS.blog,
|
|
218
|
+
blog: BOARD_PATHS.blog,
|
|
219
|
+
talentProfile: BOARD_PATHS.talent,
|
|
220
|
+
home: BOARD_PATHS.home,
|
|
221
|
+
jobs: BOARD_PATHS.jobs,
|
|
222
|
+
companies: BOARD_PATHS.companies,
|
|
223
|
+
about: BOARD_PATHS.about,
|
|
224
|
+
privacyPolicy: BOARD_PATHS.privacyPolicy,
|
|
225
|
+
termsOfService: BOARD_PATHS.termsOfService,
|
|
226
|
+
cookiePolicy: BOARD_PATHS.cookiePolicy,
|
|
227
|
+
impressum: BOARD_PATHS.impressum,
|
|
228
|
+
talent: BOARD_PATHS.talent,
|
|
229
|
+
employers: BOARD_PATHS.employers,
|
|
230
|
+
alertsManage: BOARD_PATHS.home,
|
|
231
|
+
alertsConfirm: BOARD_PATHS.home
|
|
232
|
+
};
|
|
233
|
+
function splitSegments(path) {
|
|
234
|
+
const withoutLeading = path.startsWith("/") ? path.slice(1) : path;
|
|
235
|
+
const withoutTrailing = withoutLeading.length > 1 && withoutLeading.endsWith("/") ? withoutLeading.slice(0, -1) : withoutLeading;
|
|
236
|
+
if (withoutTrailing === "") return [];
|
|
237
|
+
return withoutTrailing.split("/");
|
|
238
|
+
}
|
|
239
|
+
var PREFIXED_PARAM_RE = /^([^:]+):([A-Za-z_][A-Za-z0-9_]*)(.*)$/;
|
|
240
|
+
function matchPathToTemplate(template, pathname) {
|
|
241
|
+
const templateSegs = splitSegments(template);
|
|
242
|
+
const pathSegs = splitSegments(pathname);
|
|
243
|
+
if (templateSegs.length !== pathSegs.length) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const params = {};
|
|
247
|
+
for (let i = 0; i < templateSegs.length; i++) {
|
|
248
|
+
const tSeg = templateSegs[i];
|
|
249
|
+
const pSeg = pathSegs[i];
|
|
250
|
+
let name = null;
|
|
251
|
+
let value = pSeg;
|
|
252
|
+
if (tSeg.startsWith(":") && tSeg.length > 1) {
|
|
253
|
+
name = tSeg.slice(1);
|
|
254
|
+
} else {
|
|
255
|
+
const prefixed = PREFIXED_PARAM_RE.exec(tSeg);
|
|
256
|
+
if (prefixed) {
|
|
257
|
+
const [, prefix = "", param = "", suffix = ""] = prefixed;
|
|
258
|
+
if (!pSeg.startsWith(prefix) || !pSeg.endsWith(suffix)) return null;
|
|
259
|
+
name = param;
|
|
260
|
+
value = pSeg.slice(prefix.length, pSeg.length - suffix.length);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (name !== null) {
|
|
264
|
+
if (value.length === 0) return null;
|
|
265
|
+
if (value.includes("/") || value.includes("\\")) return null;
|
|
266
|
+
params[name] = value;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (tSeg !== pSeg) return null;
|
|
270
|
+
}
|
|
271
|
+
return params;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/route-contract/params.ts
|
|
275
|
+
function extractParamNames(template) {
|
|
276
|
+
const names = [];
|
|
277
|
+
const re = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
278
|
+
let match;
|
|
279
|
+
while ((match = re.exec(template)) !== null) {
|
|
280
|
+
const name = match[1];
|
|
281
|
+
if (name !== void 0) names.push(name);
|
|
282
|
+
}
|
|
283
|
+
return names;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/route-contract/validate.ts
|
|
287
|
+
var TEMPLATE_CHARSET_RE = /^[A-Za-z0-9/_.:@-]+$/;
|
|
288
|
+
var FIXED_TEXT_ROLES = /* @__PURE__ */ new Set(["talentProfile"]);
|
|
289
|
+
function checkRoleTemplate(role, template) {
|
|
290
|
+
const errors = [];
|
|
291
|
+
if (typeof template !== "string" || template.length === 0) {
|
|
292
|
+
errors.push({
|
|
293
|
+
code: "invalid_template",
|
|
294
|
+
message: `role '${role}' template must be a non-empty string`,
|
|
295
|
+
role
|
|
296
|
+
});
|
|
297
|
+
return errors;
|
|
298
|
+
}
|
|
299
|
+
if (!template.startsWith("/")) {
|
|
300
|
+
errors.push({
|
|
301
|
+
code: "invalid_template",
|
|
302
|
+
message: `role '${role}' template must start with '/', got ${JSON.stringify(template)}`,
|
|
303
|
+
role
|
|
304
|
+
});
|
|
305
|
+
return errors;
|
|
306
|
+
}
|
|
307
|
+
if (/\s/.test(template)) {
|
|
308
|
+
errors.push({
|
|
309
|
+
code: "invalid_template",
|
|
310
|
+
message: `role '${role}' template must not contain whitespace, got ${JSON.stringify(template)}`,
|
|
311
|
+
role
|
|
312
|
+
});
|
|
313
|
+
return errors;
|
|
314
|
+
}
|
|
315
|
+
if (template.includes("?") || template.includes("#")) {
|
|
316
|
+
errors.push({
|
|
317
|
+
code: "template_query_or_fragment",
|
|
318
|
+
message: `role '${role}' template must not contain '?' or '#', got ${JSON.stringify(template)}`,
|
|
319
|
+
role
|
|
320
|
+
});
|
|
321
|
+
return errors;
|
|
322
|
+
}
|
|
323
|
+
if (template.startsWith("//") || template.includes("//")) {
|
|
324
|
+
errors.push({
|
|
325
|
+
code: "template_double_slash",
|
|
326
|
+
message: `role '${role}' template must not contain '//', got ${JSON.stringify(template)}`,
|
|
327
|
+
role
|
|
328
|
+
});
|
|
329
|
+
return errors;
|
|
330
|
+
}
|
|
331
|
+
for (const seg of template.split("/")) {
|
|
332
|
+
if (seg === "." || seg === "..") {
|
|
333
|
+
errors.push({
|
|
334
|
+
code: "template_dot_segment",
|
|
335
|
+
message: `role '${role}' template must not contain '.' or '..' segments, got ${JSON.stringify(template)}`,
|
|
336
|
+
role
|
|
337
|
+
});
|
|
338
|
+
return errors;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!TEMPLATE_CHARSET_RE.test(template)) {
|
|
342
|
+
errors.push({
|
|
343
|
+
code: "template_illegal_charset",
|
|
344
|
+
message: `role '${role}' template contains illegal characters, got ${JSON.stringify(template)}`,
|
|
345
|
+
role
|
|
346
|
+
});
|
|
347
|
+
return errors;
|
|
348
|
+
}
|
|
349
|
+
const registry = ROLE_PARAM_REGISTRY[role];
|
|
350
|
+
const tokens = extractParamNames(template);
|
|
351
|
+
if (registry.length === 0) {
|
|
352
|
+
if (tokens.length > 0) {
|
|
353
|
+
errors.push({
|
|
354
|
+
code: "static_has_params",
|
|
355
|
+
message: `static role '${role}' template must not contain ':' tokens, got ${template}`,
|
|
356
|
+
role,
|
|
357
|
+
token: tokens[0]
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const registrySet = new Set(registry);
|
|
362
|
+
for (const token of tokens) {
|
|
363
|
+
if (!registrySet.has(token)) {
|
|
364
|
+
errors.push({
|
|
365
|
+
code: "unregistered_param",
|
|
366
|
+
message: `role '${role}' template references unregistered param ':${token}'`,
|
|
367
|
+
role,
|
|
368
|
+
token
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const retained = tokens.filter((t) => registrySet.has(t));
|
|
373
|
+
if (retained.length === 0) {
|
|
374
|
+
errors.push({
|
|
375
|
+
code: "missing_entity_param",
|
|
376
|
+
message: `role '${role}' template must contain at least one registry param (${registry.join(", ")})`,
|
|
377
|
+
role
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (FIXED_TEXT_ROLES.has(role) && isParamsOnly(template)) {
|
|
381
|
+
errors.push({
|
|
382
|
+
code: "invalid_template",
|
|
383
|
+
message: `role '${role}' template must carry fixed text around its params (e.g. '/p/:handle' or '/@:handle'), got ${JSON.stringify(template)}`,
|
|
384
|
+
role
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return errors;
|
|
389
|
+
}
|
|
390
|
+
function isParamsOnly(template) {
|
|
391
|
+
return template.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, "").replace(/\//g, "").length === 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
42
394
|
// src/doctor/checks.ts
|
|
43
395
|
function record(id, tier) {
|
|
44
396
|
return (status, detail) => ({
|
|
@@ -85,8 +437,42 @@ function checkEnv(env) {
|
|
|
85
437
|
return results;
|
|
86
438
|
}
|
|
87
439
|
var JOB_DETAIL_LINK_RE = /href="(\/companies\/[a-z0-9-]+\/jobs\/[a-z0-9-]+)"/i;
|
|
88
|
-
function extractJobDetailLink(html) {
|
|
89
|
-
return html.match(JOB_DETAIL_LINK_RE)?.[1] ?? null;
|
|
440
|
+
function extractJobDetailLink(html, templates) {
|
|
441
|
+
if (!templates) return html.match(JOB_DETAIL_LINK_RE)?.[1] ?? null;
|
|
442
|
+
for (const match of html.matchAll(HREF_PATH_RE)) {
|
|
443
|
+
const path = match[1];
|
|
444
|
+
if (path.endsWith("/")) continue;
|
|
445
|
+
for (const template of templates) {
|
|
446
|
+
const params = matchPathToTemplate(template, path);
|
|
447
|
+
if (params && Object.values(params).every((v) => SLUG_RE.test(v))) {
|
|
448
|
+
return path;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
var HREF_PATH_RE = /href="(\/[^"?#\s]+)"/gi;
|
|
455
|
+
var SLUG_RE = /^[a-z0-9-]+$/i;
|
|
456
|
+
var CANONICAL_JOB_DETAIL_TEMPLATE = CANONICAL_MANIFEST.roles.jobDetail;
|
|
457
|
+
function manifestRoleTemplate(manifest, name) {
|
|
458
|
+
const roles = manifest !== null && typeof manifest === "object" && manifest.version === 1 ? manifest.roles : null;
|
|
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);
|
|
464
|
+
const jobs = role("jobs");
|
|
465
|
+
const company = role("company");
|
|
466
|
+
const jobsWord = jobs?.split("/").filter((seg) => seg !== "" && !seg.includes(":")).at(-1);
|
|
467
|
+
const derived = company && jobsWord ? `${company}/${jobsWord}/:jobSlug` : null;
|
|
468
|
+
const detail = role("jobDetail") ?? (derived && checkRoleTemplate("jobDetail", derived).length === 0 ? derived : null);
|
|
469
|
+
return {
|
|
470
|
+
jobsPath: jobs ?? "/jobs",
|
|
471
|
+
jobDetailTemplates: detail && detail !== CANONICAL_JOB_DETAIL_TEMPLATE ? [detail, CANONICAL_JOB_DETAIL_TEMPLATE] : [CANONICAL_JOB_DETAIL_TEMPLATE]
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function describeJobDetailTemplate(template) {
|
|
475
|
+
return template.replace(":companySlug", "{c}").replace(":jobSlug", "{s}").replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
|
|
90
476
|
}
|
|
91
477
|
var JSON_LD_RE = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
|
|
92
478
|
function extractJobPostingJsonLd(html) {
|
|
@@ -392,6 +778,25 @@ async function probe(fetchImpl, url) {
|
|
|
392
778
|
};
|
|
393
779
|
}
|
|
394
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
|
+
}
|
|
395
800
|
|
|
396
801
|
// node_modules/.pnpm/parse5@7.3.0/node_modules/parse5/dist/common/unicode.js
|
|
397
802
|
var UNDEFINED_CODE_POINTS = /* @__PURE__ */ new Set([
|
|
@@ -8328,6 +8733,142 @@ function hasCrawlableCavunoBacklink(html) {
|
|
|
8328
8733
|
return visit(root);
|
|
8329
8734
|
}
|
|
8330
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
|
+
|
|
8331
8872
|
// src/doctor/read.ts
|
|
8332
8873
|
var READ = {
|
|
8333
8874
|
home: record("read.home", 2),
|
|
@@ -8340,7 +8881,8 @@ var READ = {
|
|
|
8340
8881
|
indexNow: record("read.indexNow", 2),
|
|
8341
8882
|
googleVerification: record("read.googleVerification", 2),
|
|
8342
8883
|
attribution: record("read.attribution", 2),
|
|
8343
|
-
oauthCallback: record("read.oauthCallback", 2)
|
|
8884
|
+
oauthCallback: record("read.oauthCallback", 2),
|
|
8885
|
+
localizedUrls: LOCALIZED_URLS
|
|
8344
8886
|
};
|
|
8345
8887
|
function skipReadProbes(reason) {
|
|
8346
8888
|
return Object.values(READ).map((make) => make("skip", reason));
|
|
@@ -8361,9 +8903,19 @@ async function confirmsNoPublishedJobs(fetchImpl, env) {
|
|
|
8361
8903
|
return false;
|
|
8362
8904
|
}
|
|
8363
8905
|
}
|
|
8364
|
-
async function
|
|
8365
|
-
const
|
|
8366
|
-
|
|
8906
|
+
async function readManifest(fetchImpl, base) {
|
|
8907
|
+
const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
|
|
8908
|
+
if (!manifest.ok) return null;
|
|
8909
|
+
try {
|
|
8910
|
+
return JSON.parse(manifest.body);
|
|
8911
|
+
} catch {
|
|
8912
|
+
return null;
|
|
8913
|
+
}
|
|
8914
|
+
}
|
|
8915
|
+
async function probeJobsAndJsonLd(fetchImpl, base, manifest, env) {
|
|
8916
|
+
const { jobsPath, jobDetailTemplates } = jobRoutesFromManifest(manifest);
|
|
8917
|
+
const jobs = await probe(fetchImpl, `${base}${jobsPath}`);
|
|
8918
|
+
const jobLink = jobs.ok ? extractJobDetailLink(jobs.body, jobDetailTemplates) : null;
|
|
8367
8919
|
if (jobs.status === 200 && !jobLink && /<(html|body|main)[\s>]/i.test(jobs.body) && await confirmsNoPublishedJobs(fetchImpl, env)) {
|
|
8368
8920
|
return [
|
|
8369
8921
|
READ.jobs("pass", "listing renders; API confirms zero published jobs"),
|
|
@@ -8372,14 +8924,14 @@ async function probeJobsAndJsonLd(fetchImpl, base, env) {
|
|
|
8372
8924
|
}
|
|
8373
8925
|
const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
|
|
8374
8926
|
"fail",
|
|
8375
|
-
jobs.ok ?
|
|
8927
|
+
jobs.ok ? `listing renders but contains no ${describeJobDetailTemplate(jobDetailTemplates[0])} detail links` : `listing HTTP ${jobs.status}`
|
|
8376
8928
|
);
|
|
8377
8929
|
if (!jobLink) {
|
|
8378
8930
|
return [
|
|
8379
8931
|
jobsResult,
|
|
8380
8932
|
READ.jsonld(
|
|
8381
8933
|
"skip",
|
|
8382
|
-
jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" :
|
|
8934
|
+
jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" : `not probed \u2014 the ${jobsPath} listing itself failed (see read.jobs failure)`
|
|
8383
8935
|
)
|
|
8384
8936
|
];
|
|
8385
8937
|
}
|
|
@@ -8610,7 +9162,13 @@ function scoreOAuthCallback(probed) {
|
|
|
8610
9162
|
async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
|
|
8611
9163
|
const base = frontendUrl.replace(/\/$/, "");
|
|
8612
9164
|
const home = await probe(fetchImpl, base);
|
|
8613
|
-
const
|
|
9165
|
+
const manifest = await readManifest(fetchImpl, base);
|
|
9166
|
+
const jobsAndJsonLd = await probeJobsAndJsonLd(
|
|
9167
|
+
fetchImpl,
|
|
9168
|
+
base,
|
|
9169
|
+
manifest,
|
|
9170
|
+
env
|
|
9171
|
+
);
|
|
8614
9172
|
const sitemap = await probeSitemap(fetchImpl, base);
|
|
8615
9173
|
const robots = await probe(fetchImpl, `${base}/robots.txt`);
|
|
8616
9174
|
const adsTxt = await probe(fetchImpl, `${base}/ads.txt`);
|
|
@@ -8625,6 +9183,7 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
|
|
|
8625
9183
|
`${base}/api/board-auth/oauth/linkedin/callback`
|
|
8626
9184
|
)
|
|
8627
9185
|
};
|
|
9186
|
+
const localizedUrls = await probeLocalizedUrls(fetchImpl, base, manifest);
|
|
8628
9187
|
const board = env?.apiUrl && env.boardKey ? await probe(
|
|
8629
9188
|
fetchImpl,
|
|
8630
9189
|
`${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}`
|
|
@@ -8663,7 +9222,8 @@ async function runReadProbes(fetchImpl, frontendUrl, seo, env) {
|
|
|
8663
9222
|
}),
|
|
8664
9223
|
scoreGoogleVerification(home.body, seo),
|
|
8665
9224
|
attribution,
|
|
8666
|
-
scoreOAuthCallback(oauthCallback)
|
|
9225
|
+
scoreOAuthCallback(oauthCallback),
|
|
9226
|
+
localizedUrls
|
|
8667
9227
|
];
|
|
8668
9228
|
}
|
|
8669
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-CdNZDaaR.js';
|
|
2
|
+
import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-C7UvN_dW.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 { 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-CdNZDaaR.mjs';
|
|
2
|
+
import { L as ListEnvelope, t as RelatedSearch, a as CustomFieldFilter, O as ObjectReferenceFilter } from './jobs-D9ERWWda.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 { 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-CJw-MJpf.mjs';
|
|
2
|
+
import { P as PublicJob } from './jobs-D9ERWWda.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Wire enum for `salaryTimeframe`. Defined here (not re-exported from any
|