@cavuno/board 4.41.1 → 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.
- package/CHANGELOG.md +55 -0
- package/dist/{_spec-DRUEt9M3.d.mts → _spec-CLxrfYJb.d.mts} +33 -0
- package/dist/{_spec-DRUEt9M3.d.ts → _spec-CLxrfYJb.d.ts} +33 -0
- package/dist/bin.mjs +401 -6
- package/dist/{blog-DZejMyq-.d.ts → blog-CQ40QX7l.d.mts} +6 -1
- package/dist/{blog-B0oLDN4M.d.mts → blog-mlhJ6UcD.d.ts} +6 -1
- package/dist/{board-C-Hz8MMP.d.ts → board-DWFDeICw.d.ts} +2 -2
- package/dist/{board-Eusn8R9i.d.mts → board-TOzb2D8c.d.mts} +2 -2
- package/dist/{breadcrumbs-nX2svS5_.d.ts → breadcrumbs-DJHyzBji.d.ts} +2 -2
- package/dist/{breadcrumbs-BTqDFpGD.d.mts → breadcrumbs-DauM7O7_.d.mts} +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.js +401 -6
- package/dist/doctor.mjs +401 -6
- 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 +99 -15
- package/dist/index.d.ts +99 -15
- package/dist/index.js +105 -8
- package/dist/index.mjs +104 -9
- package/dist/{jobs-B-imggAc.d.mts → jobs-CjQM8EPT.d.mts} +1 -1
- package/dist/{jobs-jQwzYyvr.d.ts → jobs-DZVMzBOQ.d.ts} +1 -1
- package/dist/{search-C8uy6k8-.d.mts → search-BtBE6iJO.d.mts} +1 -1
- package/dist/{search-vXX_s6f5.d.ts → search-CF_wa20h.d.ts} +1 -1
- package/dist/{seo-D3AF4q7d.d.mts → seo-0bfk63qi.d.mts} +1 -1
- package/dist/{seo-BdNi6Eif.d.ts → seo-CU9LpWH7.d.ts} +1 -1
- package/dist/seo-files.d.mts +2 -2
- package/dist/seo-files.d.ts +2 -2
- 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-blog/SKILL.md +7 -3
- package/skills/cavuno-board-post-a-job/SKILL.md +2 -1
- package/skills/manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,61 @@
|
|
|
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.44.0 — 2026-10-12
|
|
7
|
+
|
|
8
|
+
- Saved searches: `me.companies.talentLists.members.list()` takes `stage`, a
|
|
9
|
+
comma-separated list of pipeline stages, on a saved search linked to a job.
|
|
10
|
+
It keeps the saved people whose application on that job is at one of those
|
|
11
|
+
stages, so a stage view can be paged like any other list. Values match
|
|
12
|
+
stored stages exactly: to match a board column, pass every stage the column
|
|
13
|
+
shows (the Applied column is `applied,review`). `count` reflects the stage
|
|
14
|
+
and filters; `total` is still every saved person you can see. Passing
|
|
15
|
+
`stage` for a saved search with no linked job is a 400.
|
|
16
|
+
|
|
17
|
+
## 4.43.0 — 2026-10-12
|
|
18
|
+
|
|
19
|
+
- Saved searches can hold people. `me.companies.talentLists.members.list()`,
|
|
20
|
+
`.add()` and `.remove()` manage the people saved to a saved search, and
|
|
21
|
+
`me.companies.talentLists.memberships()` lists the saved searches one
|
|
22
|
+
candidate is on. `members.list()` takes the same filters as `talent.list()`,
|
|
23
|
+
including `radius`, and returns `total` (every saved person you can see)
|
|
24
|
+
next to `count` (those matching the filters). Each saved search reports
|
|
25
|
+
`memberCount`. A saved search holds up to 500 people; adding more returns
|
|
26
|
+
409 `talent_list_full`.
|
|
27
|
+
- Saving someone to a saved search that is linked to a job also shortlists
|
|
28
|
+
them on that job, and linking a job later shortlists everyone already
|
|
29
|
+
saved. Removing someone from a saved search never changes their place in a
|
|
30
|
+
job pipeline.
|
|
31
|
+
- Messaging: on boards that sell talent access, sending a message about a
|
|
32
|
+
shortlisted candidate who has not applied needs talent access, as a message
|
|
33
|
+
started from the talent directory does. Messages about people who applied
|
|
34
|
+
are unchanged.
|
|
35
|
+
- Talent directory: signed-in viewers also see profiles that are visible to
|
|
36
|
+
signed-in users only.
|
|
37
|
+
- Job postings: spending an existing credit or subscription through
|
|
38
|
+
`job-postings` requires `billingVerificationToken`, the token you pass to
|
|
39
|
+
`getBillingOptions`. Posts that buy a plan or are free are unaffected.
|
|
40
|
+
|
|
41
|
+
## 4.42.0 — 2026-10-12
|
|
42
|
+
|
|
43
|
+
- Talent search: `talent.list()` takes `radius`, in kilometres (1–250,
|
|
44
|
+
decimals allowed). With a city or locality `place`, it also returns
|
|
45
|
+
candidates whose home city or locality is within that distance. Region and
|
|
46
|
+
country places ignore it. `count` and paging include the nearby
|
|
47
|
+
candidates. Build mile choices with `searchRadiusOptions`,
|
|
48
|
+
`defaultSearchRadius` and `distanceUnitForCountry` from
|
|
49
|
+
`@cavuno/board/format`, as for job search.
|
|
50
|
+
- Job search: `count` is the full number of matches again. `hasMore` and
|
|
51
|
+
`nextCursor` stop at the deepest reachable result, and the new optional
|
|
52
|
+
`reachableCount` is sent when results are capped below `count`.
|
|
53
|
+
- Blog: `blog.posts.list()` takes `sort: 'featured'`, which lists featured posts
|
|
54
|
+
newest first, then the rest newest first, as one list. The default is
|
|
55
|
+
still `newest`.
|
|
56
|
+
- Job custom fields can be private to the board team. Private fields and
|
|
57
|
+
their values are left out of public job responses and the public job form.
|
|
58
|
+
- Coupons: a coupon `name` longer than 40 characters is rejected with a 400
|
|
59
|
+
that names the field, instead of a payment-provider error.
|
|
60
|
+
|
|
6
61
|
## 4.41.1 — 2026-10-08
|
|
7
62
|
|
|
8
63
|
- Analytics: `analytics.track()` now sends the visit's session id (the
|
|
@@ -64,6 +64,9 @@ interface components {
|
|
|
64
64
|
job: string;
|
|
65
65
|
candidateBoardUserId: string;
|
|
66
66
|
};
|
|
67
|
+
AddTalentListMemberBody: {
|
|
68
|
+
candidateBoardUserId: string;
|
|
69
|
+
};
|
|
67
70
|
Alert: {
|
|
68
71
|
/** @description Job-alert preference ID. Also the path key. */
|
|
69
72
|
id: string;
|
|
@@ -1082,6 +1085,8 @@ interface components {
|
|
|
1082
1085
|
id?: string;
|
|
1083
1086
|
planId: string;
|
|
1084
1087
|
};
|
|
1088
|
+
/** @description The `verificationToken` the billing options were listed with. Required when `selectedBilling.type` is `order` or `subscription`; the token's email must match `submission.contactEmail`. */
|
|
1089
|
+
billingVerificationToken?: string;
|
|
1085
1090
|
invoiceBilling?: {
|
|
1086
1091
|
billingName?: string;
|
|
1087
1092
|
address: {
|
|
@@ -1139,6 +1144,11 @@ interface components {
|
|
|
1139
1144
|
min?: number;
|
|
1140
1145
|
/** @description Inclusive maximum for `number` fields, when set. */
|
|
1141
1146
|
max?: number;
|
|
1147
|
+
/**
|
|
1148
|
+
* @description Who sees the field. `public` (or absent): the job page, employers, search and the Board API. `private`: only the board team, in the dashboard and this Operator API. Board API (`/v1/boards/...`) responses never include private fields or their values. Omitted on an existing field keeps its current visibility.
|
|
1149
|
+
* @enum {string}
|
|
1150
|
+
*/
|
|
1151
|
+
visibility?: "public" | "private";
|
|
1142
1152
|
};
|
|
1143
1153
|
CustomFieldOption: {
|
|
1144
1154
|
/** @description Stable option key — the value stored on a job, not the label. */
|
|
@@ -1739,6 +1749,11 @@ interface components {
|
|
|
1739
1749
|
allowOverrides?: boolean;
|
|
1740
1750
|
/** @description Key of the collection's short, long or rich text field whose value is a selected entry's default description. When that field is rich text, an entry's `description` is sanitised HTML. Absent means entries have no default description. */
|
|
1741
1751
|
descriptionFieldKey?: string;
|
|
1752
|
+
/**
|
|
1753
|
+
* @description Who sees the field. `public` (or absent): the job page, employers, search and the Board API. `private`: only the board team, in the dashboard and this Operator API. Board API (`/v1/boards/...`) responses never include private fields or their values. Omitted on an existing field keeps its current visibility.
|
|
1754
|
+
* @enum {string}
|
|
1755
|
+
*/
|
|
1756
|
+
visibility?: "public" | "private";
|
|
1742
1757
|
};
|
|
1743
1758
|
JobCollectionOverride: {
|
|
1744
1759
|
/** @description The collection field key used in `collectionValues`. */
|
|
@@ -3592,6 +3607,8 @@ interface components {
|
|
|
3592
3607
|
createdBy: string;
|
|
3593
3608
|
createdAt: number;
|
|
3594
3609
|
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). */
|
|
3611
|
+
memberCount: number;
|
|
3595
3612
|
};
|
|
3596
3613
|
TalentListFilters: {
|
|
3597
3614
|
q?: string;
|
|
@@ -3607,6 +3624,22 @@ interface components {
|
|
|
3607
3624
|
permitCountry?: string;
|
|
3608
3625
|
interestedRole?: string;
|
|
3609
3626
|
};
|
|
3627
|
+
TalentListMember: {
|
|
3628
|
+
id: string;
|
|
3629
|
+
/** @enum {string} */
|
|
3630
|
+
object: "talent_list_member";
|
|
3631
|
+
candidate: components["schemas"]["TalentDirectoryEntry"];
|
|
3632
|
+
addedAt: number;
|
|
3633
|
+
/** @description Board-user id of the employer who saved them. */
|
|
3634
|
+
addedBy: string;
|
|
3635
|
+
};
|
|
3636
|
+
TalentListMembership: {
|
|
3637
|
+
/** @enum {string} */
|
|
3638
|
+
object: "talent_list_membership";
|
|
3639
|
+
listId: string;
|
|
3640
|
+
candidateBoardUserId: string;
|
|
3641
|
+
addedAt: number;
|
|
3642
|
+
};
|
|
3610
3643
|
TalentProfile: {
|
|
3611
3644
|
/** @enum {string} */
|
|
3612
3645
|
object: "talent_profile";
|
|
@@ -64,6 +64,9 @@ interface components {
|
|
|
64
64
|
job: string;
|
|
65
65
|
candidateBoardUserId: string;
|
|
66
66
|
};
|
|
67
|
+
AddTalentListMemberBody: {
|
|
68
|
+
candidateBoardUserId: string;
|
|
69
|
+
};
|
|
67
70
|
Alert: {
|
|
68
71
|
/** @description Job-alert preference ID. Also the path key. */
|
|
69
72
|
id: string;
|
|
@@ -1082,6 +1085,8 @@ interface components {
|
|
|
1082
1085
|
id?: string;
|
|
1083
1086
|
planId: string;
|
|
1084
1087
|
};
|
|
1088
|
+
/** @description The `verificationToken` the billing options were listed with. Required when `selectedBilling.type` is `order` or `subscription`; the token's email must match `submission.contactEmail`. */
|
|
1089
|
+
billingVerificationToken?: string;
|
|
1085
1090
|
invoiceBilling?: {
|
|
1086
1091
|
billingName?: string;
|
|
1087
1092
|
address: {
|
|
@@ -1139,6 +1144,11 @@ interface components {
|
|
|
1139
1144
|
min?: number;
|
|
1140
1145
|
/** @description Inclusive maximum for `number` fields, when set. */
|
|
1141
1146
|
max?: number;
|
|
1147
|
+
/**
|
|
1148
|
+
* @description Who sees the field. `public` (or absent): the job page, employers, search and the Board API. `private`: only the board team, in the dashboard and this Operator API. Board API (`/v1/boards/...`) responses never include private fields or their values. Omitted on an existing field keeps its current visibility.
|
|
1149
|
+
* @enum {string}
|
|
1150
|
+
*/
|
|
1151
|
+
visibility?: "public" | "private";
|
|
1142
1152
|
};
|
|
1143
1153
|
CustomFieldOption: {
|
|
1144
1154
|
/** @description Stable option key — the value stored on a job, not the label. */
|
|
@@ -1739,6 +1749,11 @@ interface components {
|
|
|
1739
1749
|
allowOverrides?: boolean;
|
|
1740
1750
|
/** @description Key of the collection's short, long or rich text field whose value is a selected entry's default description. When that field is rich text, an entry's `description` is sanitised HTML. Absent means entries have no default description. */
|
|
1741
1751
|
descriptionFieldKey?: string;
|
|
1752
|
+
/**
|
|
1753
|
+
* @description Who sees the field. `public` (or absent): the job page, employers, search and the Board API. `private`: only the board team, in the dashboard and this Operator API. Board API (`/v1/boards/...`) responses never include private fields or their values. Omitted on an existing field keeps its current visibility.
|
|
1754
|
+
* @enum {string}
|
|
1755
|
+
*/
|
|
1756
|
+
visibility?: "public" | "private";
|
|
1742
1757
|
};
|
|
1743
1758
|
JobCollectionOverride: {
|
|
1744
1759
|
/** @description The collection field key used in `collectionValues`. */
|
|
@@ -3592,6 +3607,8 @@ interface components {
|
|
|
3592
3607
|
createdBy: string;
|
|
3593
3608
|
createdAt: number;
|
|
3594
3609
|
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). */
|
|
3611
|
+
memberCount: number;
|
|
3595
3612
|
};
|
|
3596
3613
|
TalentListFilters: {
|
|
3597
3614
|
q?: string;
|
|
@@ -3607,6 +3624,22 @@ interface components {
|
|
|
3607
3624
|
permitCountry?: string;
|
|
3608
3625
|
interestedRole?: string;
|
|
3609
3626
|
};
|
|
3627
|
+
TalentListMember: {
|
|
3628
|
+
id: string;
|
|
3629
|
+
/** @enum {string} */
|
|
3630
|
+
object: "talent_list_member";
|
|
3631
|
+
candidate: components["schemas"]["TalentDirectoryEntry"];
|
|
3632
|
+
addedAt: number;
|
|
3633
|
+
/** @description Board-user id of the employer who saved them. */
|
|
3634
|
+
addedBy: string;
|
|
3635
|
+
};
|
|
3636
|
+
TalentListMembership: {
|
|
3637
|
+
/** @enum {string} */
|
|
3638
|
+
object: "talent_list_membership";
|
|
3639
|
+
listId: string;
|
|
3640
|
+
candidateBoardUserId: string;
|
|
3641
|
+
addedAt: number;
|
|
3642
|
+
};
|
|
3610
3643
|
TalentProfile: {
|
|
3611
3644
|
/** @enum {string} */
|
|
3612
3645
|
object: "talent_profile";
|
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,41 @@ 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 jobRoutesFromManifest(manifest) {
|
|
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
|
+
};
|
|
463
|
+
const jobs = role("jobs");
|
|
464
|
+
const company = role("company");
|
|
465
|
+
const jobsWord = jobs?.split("/").filter((seg) => seg !== "" && !seg.includes(":")).at(-1);
|
|
466
|
+
const derived = company && jobsWord ? `${company}/${jobsWord}/:jobSlug` : null;
|
|
467
|
+
const detail = role("jobDetail") ?? (derived && checkRoleTemplate("jobDetail", derived).length === 0 ? derived : null);
|
|
468
|
+
return {
|
|
469
|
+
jobsPath: jobs ?? "/jobs",
|
|
470
|
+
jobDetailTemplates: detail && detail !== CANONICAL_JOB_DETAIL_TEMPLATE ? [detail, CANONICAL_JOB_DETAIL_TEMPLATE] : [CANONICAL_JOB_DETAIL_TEMPLATE]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function describeJobDetailTemplate(template) {
|
|
474
|
+
return template.replace(":companySlug", "{c}").replace(":jobSlug", "{s}").replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
|
|
90
475
|
}
|
|
91
476
|
var JSON_LD_RE = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
|
|
92
477
|
function extractJobPostingJsonLd(html) {
|
|
@@ -8361,9 +8746,19 @@ async function confirmsNoPublishedJobs(fetchImpl, env) {
|
|
|
8361
8746
|
return false;
|
|
8362
8747
|
}
|
|
8363
8748
|
}
|
|
8749
|
+
async function readJobRoutes(fetchImpl, base) {
|
|
8750
|
+
const manifest = await probe(fetchImpl, `${base}/.well-known/cavuno.json`);
|
|
8751
|
+
if (!manifest.ok) return jobRoutesFromManifest(null);
|
|
8752
|
+
try {
|
|
8753
|
+
return jobRoutesFromManifest(JSON.parse(manifest.body));
|
|
8754
|
+
} catch {
|
|
8755
|
+
return jobRoutesFromManifest(null);
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8364
8758
|
async function probeJobsAndJsonLd(fetchImpl, base, env) {
|
|
8365
|
-
const
|
|
8366
|
-
const
|
|
8759
|
+
const { jobsPath, jobDetailTemplates } = await readJobRoutes(fetchImpl, base);
|
|
8760
|
+
const jobs = await probe(fetchImpl, `${base}${jobsPath}`);
|
|
8761
|
+
const jobLink = jobs.ok ? extractJobDetailLink(jobs.body, jobDetailTemplates) : null;
|
|
8367
8762
|
if (jobs.status === 200 && !jobLink && /<(html|body|main)[\s>]/i.test(jobs.body) && await confirmsNoPublishedJobs(fetchImpl, env)) {
|
|
8368
8763
|
return [
|
|
8369
8764
|
READ.jobs("pass", "listing renders; API confirms zero published jobs"),
|
|
@@ -8372,14 +8767,14 @@ async function probeJobsAndJsonLd(fetchImpl, base, env) {
|
|
|
8372
8767
|
}
|
|
8373
8768
|
const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
|
|
8374
8769
|
"fail",
|
|
8375
|
-
jobs.ok ?
|
|
8770
|
+
jobs.ok ? `listing renders but contains no ${describeJobDetailTemplate(jobDetailTemplates[0])} detail links` : `listing HTTP ${jobs.status}`
|
|
8376
8771
|
);
|
|
8377
8772
|
if (!jobLink) {
|
|
8378
8773
|
return [
|
|
8379
8774
|
jobsResult,
|
|
8380
8775
|
READ.jsonld(
|
|
8381
8776
|
"skip",
|
|
8382
|
-
jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" :
|
|
8777
|
+
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
8778
|
)
|
|
8384
8779
|
];
|
|
8385
8780
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as Schemas } from './_spec-
|
|
1
|
+
import { S as Schemas } from './_spec-CLxrfYJb.mjs';
|
|
2
2
|
|
|
3
3
|
/** Author shape embedded on posts (no `object` discriminator). */
|
|
4
4
|
type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
|
|
@@ -19,6 +19,11 @@ type BlogPostsListQuery = {
|
|
|
19
19
|
authorSlug?: string;
|
|
20
20
|
/** Opt-in only: pass `'true'` to restrict to featured posts. */
|
|
21
21
|
featured?: 'true';
|
|
22
|
+
/**
|
|
23
|
+
* `'newest'` (default): most recently published first. `'featured'`:
|
|
24
|
+
* featured posts first (newest first), then the rest, as one list.
|
|
25
|
+
*/
|
|
26
|
+
sort?: 'newest' | 'featured';
|
|
22
27
|
};
|
|
23
28
|
type BlogSimilarQuery = {
|
|
24
29
|
/** 1–20; default 6. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as Schemas } from './_spec-
|
|
1
|
+
import { S as Schemas } from './_spec-CLxrfYJb.js';
|
|
2
2
|
|
|
3
3
|
/** Author shape embedded on posts (no `object` discriminator). */
|
|
4
4
|
type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
|
|
@@ -19,6 +19,11 @@ type BlogPostsListQuery = {
|
|
|
19
19
|
authorSlug?: string;
|
|
20
20
|
/** Opt-in only: pass `'true'` to restrict to featured posts. */
|
|
21
21
|
featured?: 'true';
|
|
22
|
+
/**
|
|
23
|
+
* `'newest'` (default): most recently published first. `'featured'`:
|
|
24
|
+
* featured posts first (newest first), then the rest, as one list.
|
|
25
|
+
*/
|
|
26
|
+
sort?: 'newest' | 'featured';
|
|
22
27
|
};
|
|
23
28
|
type BlogSimilarQuery = {
|
|
24
29
|
/** 1–20; default 6. */
|