@cavuno/board 4.42.0 → 4.44.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 (43) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/{_spec-B2aizgxr.d.mts → _spec-CLxrfYJb.d.mts} +23 -0
  3. package/dist/{_spec-B2aizgxr.d.ts → _spec-CLxrfYJb.d.ts} +23 -0
  4. package/dist/bin.mjs +401 -6
  5. package/dist/{blog-CYHSnNTZ.d.mts → blog-CQ40QX7l.d.mts} +1 -1
  6. package/dist/{blog-BkACbXY0.d.ts → blog-mlhJ6UcD.d.ts} +1 -1
  7. package/dist/{board-DWeT9Jbl.d.ts → board-DWFDeICw.d.ts} +2 -2
  8. package/dist/{board-DK4HmSty.d.mts → board-TOzb2D8c.d.mts} +2 -2
  9. package/dist/{breadcrumbs-CtKa2zPe.d.ts → breadcrumbs-DJHyzBji.d.ts} +2 -2
  10. package/dist/{breadcrumbs-nTfIWzRh.d.mts → breadcrumbs-DauM7O7_.d.mts} +2 -2
  11. package/dist/browser/cavuno-board.global.min.js +1 -1
  12. package/dist/{chunk-WSO6JN5C.mjs → chunk-DZD3DAEY.mjs} +2 -0
  13. package/dist/doctor.js +401 -6
  14. package/dist/doctor.mjs +401 -6
  15. package/dist/filters.d.mts +2 -2
  16. package/dist/filters.d.ts +2 -2
  17. package/dist/format.d.mts +4 -4
  18. package/dist/format.d.ts +4 -4
  19. package/dist/index.d.mts +88 -14
  20. package/dist/index.d.ts +88 -14
  21. package/dist/index.js +105 -8
  22. package/dist/index.mjs +104 -9
  23. package/dist/{jobs-C_eO-WfM.d.mts → jobs-CjQM8EPT.d.mts} +1 -1
  24. package/dist/{jobs-SAkNWgAB.d.ts → jobs-DZVMzBOQ.d.ts} +1 -1
  25. package/dist/{search-Cb3Aoa4c.d.mts → search-BtBE6iJO.d.mts} +1 -1
  26. package/dist/{search-CVcMFOuJ.d.ts → search-CF_wa20h.d.ts} +1 -1
  27. package/dist/{seo-DPVud7SH.d.mts → seo-0bfk63qi.d.mts} +1 -1
  28. package/dist/{seo-DgFBQTIK.d.ts → seo-CU9LpWH7.d.ts} +1 -1
  29. package/dist/seo-files.d.mts +2 -2
  30. package/dist/seo-files.d.ts +2 -2
  31. package/dist/seo.d.mts +6 -6
  32. package/dist/seo.d.ts +6 -6
  33. package/dist/server.d.mts +6 -6
  34. package/dist/server.d.ts +6 -6
  35. package/dist/server.mjs +1 -1
  36. package/dist/sitemap.d.mts +6 -6
  37. package/dist/sitemap.d.ts +6 -6
  38. package/dist/sitemap.mjs +1 -1
  39. package/dist/suggest.d.mts +2 -2
  40. package/dist/suggest.d.ts +2 -2
  41. package/package.json +1 -1
  42. package/skills/cavuno-board-post-a-job/SKILL.md +2 -1
  43. package/skills/manifest.json +1 -1
package/dist/doctor.js CHANGED
@@ -68,6 +68,358 @@ function loadSkillCorpus(baseDir) {
68
68
  };
69
69
  }
70
70
 
71
+ // src/paths/index.ts
72
+ function encodePathSegment(segment) {
73
+ try {
74
+ return encodeURIComponent(decodeURIComponent(segment)).replace(
75
+ /%3A/gi,
76
+ ":"
77
+ );
78
+ } catch {
79
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
80
+ }
81
+ }
82
+ function jobDetailPath(companySlug, jobSlug) {
83
+ return `/companies/${encodePathSegment(companySlug)}/jobs/${encodePathSegment(jobSlug)}`;
84
+ }
85
+ function jobsCategoryPath(categorySlug) {
86
+ return `/jobs/${encodePathSegment(categorySlug)}`;
87
+ }
88
+ function jobsSkillPath(skillSlug) {
89
+ return `/jobs/skills/${encodePathSegment(skillSlug)}`;
90
+ }
91
+ function jobsLocationPath(placeSlug) {
92
+ return `/jobs/locations/${encodePathSegment(placeSlug)}`;
93
+ }
94
+ function companyPath(companySlug) {
95
+ return `/companies/${encodePathSegment(companySlug)}`;
96
+ }
97
+ function companyMarketPath(marketSlug) {
98
+ return `/companies/markets/${encodePathSegment(marketSlug)}`;
99
+ }
100
+ function companySalaryPath(companySlug) {
101
+ return `/companies/${encodePathSegment(companySlug)}/salaries`;
102
+ }
103
+ function salaryTitlePath(titleSlug) {
104
+ return `/salaries/titles/${encodePathSegment(titleSlug)}`;
105
+ }
106
+ function salarySkillPath(skillSlug) {
107
+ return `/salaries/skills/${encodePathSegment(skillSlug)}`;
108
+ }
109
+ function salaryLocationPath(placeSlug) {
110
+ return `/salaries/locations/${encodePathSegment(placeSlug)}`;
111
+ }
112
+ function blogPostPath(postSlug) {
113
+ return `/blog/${encodePathSegment(postSlug)}`;
114
+ }
115
+ function blogTagPath(tagSlug) {
116
+ return `/blog/tag/${encodePathSegment(tagSlug)}`;
117
+ }
118
+ function blogAuthorPath(authorSlug) {
119
+ return `/blog/author/${encodePathSegment(authorSlug)}`;
120
+ }
121
+ function talentProfilePath(handle) {
122
+ return `/p/${encodePathSegment(handle)}`;
123
+ }
124
+ var BOARD_PATHS = {
125
+ home: "/",
126
+ jobs: "/jobs",
127
+ companies: "/companies",
128
+ salaries: "/salaries",
129
+ salaryCompanies: "/salaries/companies",
130
+ salaryTitles: "/salaries/titles",
131
+ salarySkills: "/salaries/skills",
132
+ salaryLocations: "/salaries/locations",
133
+ blog: "/blog",
134
+ about: "/about",
135
+ privacyPolicy: "/privacy-policy",
136
+ termsOfService: "/terms-of-service",
137
+ cookiePolicy: "/cookie-policy",
138
+ impressum: "/impressum",
139
+ talent: "/talent",
140
+ employers: "/employers",
141
+ /**
142
+ * Alert-email surfaces. Promoted out of raw literals in
143
+ * the job-alerts composer so /go indirection and email composition share
144
+ * one definition.
145
+ */
146
+ alertsManage: "/alerts/manage",
147
+ alertsConfirm: "/alerts/confirm"
148
+ };
149
+
150
+ // src/route-contract/roles.ts
151
+ var ROLE_PARAM_REGISTRY = {
152
+ jobDetail: ["companySlug", "jobSlug"],
153
+ jobsCategory: ["categorySlug"],
154
+ jobsSkill: ["skillSlug"],
155
+ jobsLocation: ["placeSlug"],
156
+ company: ["companySlug"],
157
+ companyMarket: ["marketSlug"],
158
+ companySalary: ["companySlug"],
159
+ salaryTitle: ["titleSlug"],
160
+ salarySkill: ["skillSlug"],
161
+ salaryLocation: ["placeSlug"],
162
+ blogPost: ["postSlug"],
163
+ blogTag: ["tagSlug"],
164
+ blogAuthor: ["authorSlug"],
165
+ talentProfile: ["handle"],
166
+ home: [],
167
+ jobs: [],
168
+ companies: [],
169
+ salaries: [],
170
+ salaryCompanies: [],
171
+ salaryTitles: [],
172
+ salarySkills: [],
173
+ salaryLocations: [],
174
+ blog: [],
175
+ about: [],
176
+ privacyPolicy: [],
177
+ termsOfService: [],
178
+ cookiePolicy: [],
179
+ impressum: [],
180
+ talent: [],
181
+ employers: [],
182
+ alertsManage: [],
183
+ alertsConfirm: []
184
+ };
185
+ var ALL_ROLES = Object.keys(
186
+ ROLE_PARAM_REGISTRY
187
+ );
188
+ var ROLE_SET = new Set(ALL_ROLES);
189
+ var CANONICAL_MANIFEST = {
190
+ version: 1,
191
+ roles: {
192
+ jobDetail: jobDetailPath(":companySlug", ":jobSlug"),
193
+ jobsCategory: jobsCategoryPath(":categorySlug"),
194
+ jobsSkill: jobsSkillPath(":skillSlug"),
195
+ jobsLocation: jobsLocationPath(":placeSlug"),
196
+ company: companyPath(":companySlug"),
197
+ companyMarket: companyMarketPath(":marketSlug"),
198
+ companySalary: companySalaryPath(":companySlug"),
199
+ salaryTitle: salaryTitlePath(":titleSlug"),
200
+ salarySkill: salarySkillPath(":skillSlug"),
201
+ salaryLocation: salaryLocationPath(":placeSlug"),
202
+ blogPost: blogPostPath(":postSlug"),
203
+ blogTag: blogTagPath(":tagSlug"),
204
+ blogAuthor: blogAuthorPath(":authorSlug"),
205
+ talentProfile: talentProfilePath(":handle"),
206
+ home: BOARD_PATHS.home,
207
+ jobs: BOARD_PATHS.jobs,
208
+ companies: BOARD_PATHS.companies,
209
+ salaries: BOARD_PATHS.salaries,
210
+ salaryCompanies: BOARD_PATHS.salaryCompanies,
211
+ salaryTitles: BOARD_PATHS.salaryTitles,
212
+ salarySkills: BOARD_PATHS.salarySkills,
213
+ salaryLocations: BOARD_PATHS.salaryLocations,
214
+ blog: BOARD_PATHS.blog,
215
+ about: BOARD_PATHS.about,
216
+ privacyPolicy: BOARD_PATHS.privacyPolicy,
217
+ termsOfService: BOARD_PATHS.termsOfService,
218
+ cookiePolicy: BOARD_PATHS.cookiePolicy,
219
+ impressum: BOARD_PATHS.impressum,
220
+ talent: BOARD_PATHS.talent,
221
+ employers: BOARD_PATHS.employers,
222
+ alertsManage: "/alerts/manage",
223
+ alertsConfirm: "/alerts/confirm"
224
+ }
225
+ };
226
+
227
+ // src/route-contract/match-history.ts
228
+ var ROLE_INDEX_PATHS = {
229
+ jobDetail: BOARD_PATHS.jobs,
230
+ jobsCategory: BOARD_PATHS.jobs,
231
+ jobsSkill: BOARD_PATHS.jobs,
232
+ jobsLocation: BOARD_PATHS.jobs,
233
+ company: BOARD_PATHS.companies,
234
+ companyMarket: BOARD_PATHS.companies,
235
+ companySalary: BOARD_PATHS.companies,
236
+ salaryTitle: BOARD_PATHS.salaries,
237
+ salarySkill: BOARD_PATHS.salaries,
238
+ salaryLocation: BOARD_PATHS.salaries,
239
+ salaryCompanies: BOARD_PATHS.salaries,
240
+ salaryTitles: BOARD_PATHS.salaries,
241
+ salarySkills: BOARD_PATHS.salaries,
242
+ salaryLocations: BOARD_PATHS.salaries,
243
+ salaries: BOARD_PATHS.salaries,
244
+ blogPost: BOARD_PATHS.blog,
245
+ blogTag: BOARD_PATHS.blog,
246
+ blogAuthor: BOARD_PATHS.blog,
247
+ blog: BOARD_PATHS.blog,
248
+ talentProfile: BOARD_PATHS.talent,
249
+ home: BOARD_PATHS.home,
250
+ jobs: BOARD_PATHS.jobs,
251
+ companies: BOARD_PATHS.companies,
252
+ about: BOARD_PATHS.about,
253
+ privacyPolicy: BOARD_PATHS.privacyPolicy,
254
+ termsOfService: BOARD_PATHS.termsOfService,
255
+ cookiePolicy: BOARD_PATHS.cookiePolicy,
256
+ impressum: BOARD_PATHS.impressum,
257
+ talent: BOARD_PATHS.talent,
258
+ employers: BOARD_PATHS.employers,
259
+ alertsManage: BOARD_PATHS.home,
260
+ alertsConfirm: BOARD_PATHS.home
261
+ };
262
+ function splitSegments(path) {
263
+ const withoutLeading = path.startsWith("/") ? path.slice(1) : path;
264
+ const withoutTrailing = withoutLeading.length > 1 && withoutLeading.endsWith("/") ? withoutLeading.slice(0, -1) : withoutLeading;
265
+ if (withoutTrailing === "") return [];
266
+ return withoutTrailing.split("/");
267
+ }
268
+ var PREFIXED_PARAM_RE = /^([^:]+):([A-Za-z_][A-Za-z0-9_]*)(.*)$/;
269
+ function matchPathToTemplate(template, pathname) {
270
+ const templateSegs = splitSegments(template);
271
+ const pathSegs = splitSegments(pathname);
272
+ if (templateSegs.length !== pathSegs.length) {
273
+ return null;
274
+ }
275
+ const params = {};
276
+ for (let i = 0; i < templateSegs.length; i++) {
277
+ const tSeg = templateSegs[i];
278
+ const pSeg = pathSegs[i];
279
+ let name = null;
280
+ let value = pSeg;
281
+ if (tSeg.startsWith(":") && tSeg.length > 1) {
282
+ name = tSeg.slice(1);
283
+ } else {
284
+ const prefixed = PREFIXED_PARAM_RE.exec(tSeg);
285
+ if (prefixed) {
286
+ const [, prefix = "", param = "", suffix = ""] = prefixed;
287
+ if (!pSeg.startsWith(prefix) || !pSeg.endsWith(suffix)) return null;
288
+ name = param;
289
+ value = pSeg.slice(prefix.length, pSeg.length - suffix.length);
290
+ }
291
+ }
292
+ if (name !== null) {
293
+ if (value.length === 0) return null;
294
+ if (value.includes("/") || value.includes("\\")) return null;
295
+ params[name] = value;
296
+ continue;
297
+ }
298
+ if (tSeg !== pSeg) return null;
299
+ }
300
+ return params;
301
+ }
302
+
303
+ // src/route-contract/params.ts
304
+ function extractParamNames(template) {
305
+ const names = [];
306
+ const re = /:([A-Za-z_][A-Za-z0-9_]*)/g;
307
+ let match;
308
+ while ((match = re.exec(template)) !== null) {
309
+ const name = match[1];
310
+ if (name !== void 0) names.push(name);
311
+ }
312
+ return names;
313
+ }
314
+
315
+ // src/route-contract/validate.ts
316
+ var TEMPLATE_CHARSET_RE = /^[A-Za-z0-9/_.:@-]+$/;
317
+ var FIXED_TEXT_ROLES = /* @__PURE__ */ new Set(["talentProfile"]);
318
+ function checkRoleTemplate(role, template) {
319
+ const errors = [];
320
+ if (typeof template !== "string" || template.length === 0) {
321
+ errors.push({
322
+ code: "invalid_template",
323
+ message: `role '${role}' template must be a non-empty string`,
324
+ role
325
+ });
326
+ return errors;
327
+ }
328
+ if (!template.startsWith("/")) {
329
+ errors.push({
330
+ code: "invalid_template",
331
+ message: `role '${role}' template must start with '/', got ${JSON.stringify(template)}`,
332
+ role
333
+ });
334
+ return errors;
335
+ }
336
+ if (/\s/.test(template)) {
337
+ errors.push({
338
+ code: "invalid_template",
339
+ message: `role '${role}' template must not contain whitespace, got ${JSON.stringify(template)}`,
340
+ role
341
+ });
342
+ return errors;
343
+ }
344
+ if (template.includes("?") || template.includes("#")) {
345
+ errors.push({
346
+ code: "template_query_or_fragment",
347
+ message: `role '${role}' template must not contain '?' or '#', got ${JSON.stringify(template)}`,
348
+ role
349
+ });
350
+ return errors;
351
+ }
352
+ if (template.startsWith("//") || template.includes("//")) {
353
+ errors.push({
354
+ code: "template_double_slash",
355
+ message: `role '${role}' template must not contain '//', got ${JSON.stringify(template)}`,
356
+ role
357
+ });
358
+ return errors;
359
+ }
360
+ for (const seg of template.split("/")) {
361
+ if (seg === "." || seg === "..") {
362
+ errors.push({
363
+ code: "template_dot_segment",
364
+ message: `role '${role}' template must not contain '.' or '..' segments, got ${JSON.stringify(template)}`,
365
+ role
366
+ });
367
+ return errors;
368
+ }
369
+ }
370
+ if (!TEMPLATE_CHARSET_RE.test(template)) {
371
+ errors.push({
372
+ code: "template_illegal_charset",
373
+ message: `role '${role}' template contains illegal characters, got ${JSON.stringify(template)}`,
374
+ role
375
+ });
376
+ return errors;
377
+ }
378
+ const registry = ROLE_PARAM_REGISTRY[role];
379
+ const tokens = extractParamNames(template);
380
+ if (registry.length === 0) {
381
+ if (tokens.length > 0) {
382
+ errors.push({
383
+ code: "static_has_params",
384
+ message: `static role '${role}' template must not contain ':' tokens, got ${template}`,
385
+ role,
386
+ token: tokens[0]
387
+ });
388
+ }
389
+ } else {
390
+ const registrySet = new Set(registry);
391
+ for (const token of tokens) {
392
+ if (!registrySet.has(token)) {
393
+ errors.push({
394
+ code: "unregistered_param",
395
+ message: `role '${role}' template references unregistered param ':${token}'`,
396
+ role,
397
+ token
398
+ });
399
+ }
400
+ }
401
+ const retained = tokens.filter((t) => registrySet.has(t));
402
+ if (retained.length === 0) {
403
+ errors.push({
404
+ code: "missing_entity_param",
405
+ message: `role '${role}' template must contain at least one registry param (${registry.join(", ")})`,
406
+ role
407
+ });
408
+ }
409
+ if (FIXED_TEXT_ROLES.has(role) && isParamsOnly(template)) {
410
+ errors.push({
411
+ code: "invalid_template",
412
+ message: `role '${role}' template must carry fixed text around its params (e.g. '/p/:handle' or '/@:handle'), got ${JSON.stringify(template)}`,
413
+ role
414
+ });
415
+ }
416
+ }
417
+ return errors;
418
+ }
419
+ function isParamsOnly(template) {
420
+ return template.replace(/:[A-Za-z_][A-Za-z0-9_]*/g, "").replace(/\//g, "").length === 0;
421
+ }
422
+
71
423
  // src/doctor/checks.ts
72
424
  function record(id, tier) {
73
425
  return (status, detail) => ({
@@ -114,8 +466,41 @@ function checkEnv(env) {
114
466
  return results;
115
467
  }
116
468
  var JOB_DETAIL_LINK_RE = /href="(\/companies\/[a-z0-9-]+\/jobs\/[a-z0-9-]+)"/i;
117
- function extractJobDetailLink(html) {
118
- return html.match(JOB_DETAIL_LINK_RE)?.[1] ?? null;
469
+ function extractJobDetailLink(html, templates) {
470
+ if (!templates) return html.match(JOB_DETAIL_LINK_RE)?.[1] ?? null;
471
+ for (const match of html.matchAll(HREF_PATH_RE)) {
472
+ const path = match[1];
473
+ if (path.endsWith("/")) continue;
474
+ for (const template of templates) {
475
+ const params = matchPathToTemplate(template, path);
476
+ if (params && Object.values(params).every((v) => SLUG_RE.test(v))) {
477
+ return path;
478
+ }
479
+ }
480
+ }
481
+ return null;
482
+ }
483
+ var HREF_PATH_RE = /href="(\/[^"?#\s]+)"/gi;
484
+ var SLUG_RE = /^[a-z0-9-]+$/i;
485
+ var CANONICAL_JOB_DETAIL_TEMPLATE = CANONICAL_MANIFEST.roles.jobDetail;
486
+ function jobRoutesFromManifest(manifest) {
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
+ };
492
+ const jobs = role("jobs");
493
+ const company = role("company");
494
+ const jobsWord = jobs?.split("/").filter((seg) => seg !== "" && !seg.includes(":")).at(-1);
495
+ const derived = company && jobsWord ? `${company}/${jobsWord}/:jobSlug` : null;
496
+ const detail = role("jobDetail") ?? (derived && checkRoleTemplate("jobDetail", derived).length === 0 ? derived : null);
497
+ return {
498
+ jobsPath: jobs ?? "/jobs",
499
+ jobDetailTemplates: detail && detail !== CANONICAL_JOB_DETAIL_TEMPLATE ? [detail, CANONICAL_JOB_DETAIL_TEMPLATE] : [CANONICAL_JOB_DETAIL_TEMPLATE]
500
+ };
501
+ }
502
+ function describeJobDetailTemplate(template) {
503
+ return template.replace(":companySlug", "{c}").replace(":jobSlug", "{s}").replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
119
504
  }
120
505
  var JSON_LD_RE = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
121
506
  function extractJobPostingJsonLd(html) {
@@ -8415,9 +8800,19 @@ async function confirmsNoPublishedJobs(fetchImpl, env) {
8415
8800
  return false;
8416
8801
  }
8417
8802
  }
8803
+ async function readJobRoutes(fetchImpl, base) {
8804
+ const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
8805
+ if (!manifest.ok) return jobRoutesFromManifest(null);
8806
+ try {
8807
+ return jobRoutesFromManifest(JSON.parse(manifest.body));
8808
+ } catch {
8809
+ return jobRoutesFromManifest(null);
8810
+ }
8811
+ }
8418
8812
  async function probeJobsAndJsonLd(fetchImpl, base, env) {
8419
- const jobs = await probe(fetchImpl, `${base}/jobs`);
8420
- const jobLink = jobs.ok ? extractJobDetailLink(jobs.body) : null;
8813
+ const { jobsPath, jobDetailTemplates } = await readJobRoutes(fetchImpl, base);
8814
+ const jobs = await probe(fetchImpl, `${base}${jobsPath}`);
8815
+ const jobLink = jobs.ok ? extractJobDetailLink(jobs.body, jobDetailTemplates) : null;
8421
8816
  if (jobs.status === 200 && !jobLink && /<(html|body|main)[\s>]/i.test(jobs.body) && await confirmsNoPublishedJobs(fetchImpl, env)) {
8422
8817
  return [
8423
8818
  READ.jobs("pass", "listing renders; API confirms zero published jobs"),
@@ -8426,14 +8821,14 @@ async function probeJobsAndJsonLd(fetchImpl, base, env) {
8426
8821
  }
8427
8822
  const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
8428
8823
  "fail",
8429
- jobs.ok ? "listing renders but contains no /companies/{c}/jobs/{s} detail links" : `listing HTTP ${jobs.status}`
8824
+ jobs.ok ? `listing renders but contains no ${describeJobDetailTemplate(jobDetailTemplates[0])} detail links` : `listing HTTP ${jobs.status}`
8430
8825
  );
8431
8826
  if (!jobLink) {
8432
8827
  return [
8433
8828
  jobsResult,
8434
8829
  READ.jsonld(
8435
8830
  "skip",
8436
- jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" : "not probed \u2014 the /jobs listing itself failed (see read.jobs failure)"
8831
+ 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)`
8437
8832
  )
8438
8833
  ];
8439
8834
  }