@cavuno/board 3.1.0 → 4.0.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 +421 -0
- package/README.md +6 -2
- package/dist/{_spec-nkWAraS0.d.mts → _spec-GVe6WAfT.d.mts} +68 -59
- package/dist/{_spec-nkWAraS0.d.ts → _spec-GVe6WAfT.d.ts} +68 -59
- package/dist/bin.mjs +7 -2
- package/dist/blog-BsmKizpd.d.ts +29 -0
- package/dist/blog-Cf64mI8A.d.mts +29 -0
- package/dist/{salaries-BahBpxWT.d.ts → board-DUhgiacm.d.ts} +17 -29
- package/dist/{salaries-ChSqDBAM.d.mts → board-GGWJ4coe.d.mts} +17 -29
- package/dist/breadcrumbs-DFKr0HzU.d.mts +372 -0
- package/dist/breadcrumbs-DiccJhk4.d.ts +372 -0
- package/dist/chunk-2ETP5UH4.mjs +271 -0
- package/dist/chunk-BGKNXG4H.mjs +146 -0
- package/dist/chunk-DLSO6XXY.mjs +33 -0
- package/dist/chunk-T2KC6XGV.mjs +1060 -0
- package/dist/chunk-YEAQ3JEI.mjs +853 -0
- package/dist/doctor.js +1 -1
- package/dist/doctor.mjs +1 -1
- package/dist/filters.d.mts +25 -20
- package/dist/filters.d.ts +25 -20
- package/dist/filters.js +3 -692
- package/dist/filters.mjs +3 -690
- package/dist/format.d.mts +57 -230
- package/dist/format.d.ts +57 -230
- package/dist/format.js +355 -927
- package/dist/format.mjs +213 -931
- package/dist/go.js +11 -1
- package/dist/go.mjs +4 -29
- package/dist/index.d.mts +23 -75
- package/dist/index.d.ts +23 -75
- package/dist/index.js +11 -36
- package/dist/index.mjs +32 -313
- package/dist/{jobs-lVvKCBCU.d.mts → jobs-BuLpV8iy.d.mts} +4 -4
- package/dist/{jobs-BiEvL9d7.d.ts → jobs-QrbXOYB9.d.ts} +4 -4
- package/dist/paths.d.mts +89 -1
- package/dist/paths.d.ts +89 -1
- package/dist/paths.js +62 -14
- package/dist/paths.mjs +29 -78
- package/dist/route-contract.js +23 -13
- package/dist/route-contract.mjs +30 -1079
- package/dist/{search-CCyFWPi3.d.mts → search-Cr4noJJZ.d.mts} +14 -3
- package/dist/{search-CyRvVMUp.d.ts → search-DdsniXQv.d.ts} +14 -3
- package/dist/seo.d.mts +33 -189
- package/dist/seo.d.ts +33 -189
- package/dist/seo.js +197 -718
- package/dist/seo.mjs +44 -1417
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/server.mjs +5 -31
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/sitemap.js +25 -14
- package/dist/sitemap.mjs +22 -97
- package/dist/skills.js +1 -1
- package/dist/skills.mjs +1 -1
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/dist/suggest.js +17 -3
- package/dist/suggest.mjs +17 -3
- package/dist/well-known.js +23 -13
- package/dist/well-known.mjs +4 -421
- package/package.json +5 -4
- package/skills/cavuno-board-applications/SKILL.md +4 -1
- package/skills/cavuno-board-filters/SKILL.md +11 -12
- package/skills/cavuno-board-format/SKILL.md +68 -56
- package/skills/cavuno-board-i18n/SKILL.md +102 -21
- package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
- package/skills/cavuno-board-seo/BLOG.md +4 -1
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
- package/skills/cavuno-board-seo/LISTING.md +35 -12
- package/skills/cavuno-board-seo/SALARY.md +83 -18
- package/skills/cavuno-board-seo/SKILL.md +3 -2
- package/skills/manifest.json +3 -3
- package/dist/board-NE98PlwN.d.ts +0 -16
- package/dist/board-muu8BI8P.d.mts +0 -16
- package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
- package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOARD_PATHS,
|
|
3
|
+
jobsCategoryPath,
|
|
4
|
+
jobsLocationCategoryPath,
|
|
5
|
+
jobsLocationPath
|
|
6
|
+
} from "./chunk-BGKNXG4H.mjs";
|
|
7
|
+
|
|
8
|
+
// src/format/locale.ts
|
|
9
|
+
function normalizeLocale(language) {
|
|
10
|
+
const trimmed = language.trim();
|
|
11
|
+
if (!trimmed) return null;
|
|
12
|
+
try {
|
|
13
|
+
const candidate = trimmed.replace(/_/g, "-");
|
|
14
|
+
const [canonical] = Intl.getCanonicalLocales(candidate);
|
|
15
|
+
if (!canonical) return null;
|
|
16
|
+
const [supported] = Intl.NumberFormat.supportedLocalesOf([canonical]);
|
|
17
|
+
return supported ?? null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/seo/salary.ts
|
|
24
|
+
var currencyFormatterCache = /* @__PURE__ */ new Map();
|
|
25
|
+
function notationForMagnitude(...values) {
|
|
26
|
+
let anyFinite = false;
|
|
27
|
+
for (const v of values) {
|
|
28
|
+
if (v == null || !Number.isFinite(v)) continue;
|
|
29
|
+
anyFinite = true;
|
|
30
|
+
if (Math.abs(v) >= 1e3) return "compact";
|
|
31
|
+
}
|
|
32
|
+
return anyFinite ? "standard" : "compact";
|
|
33
|
+
}
|
|
34
|
+
function getCurrencyFormatter(language, currencyCode, notation) {
|
|
35
|
+
const key = `${language}:${currencyCode}:${notation}`;
|
|
36
|
+
const cached = currencyFormatterCache.get(key);
|
|
37
|
+
if (cached !== void 0) return cached;
|
|
38
|
+
let formatter;
|
|
39
|
+
try {
|
|
40
|
+
const options = {
|
|
41
|
+
style: "currency",
|
|
42
|
+
currency: currencyCode,
|
|
43
|
+
notation
|
|
44
|
+
};
|
|
45
|
+
if (notation === "compact") {
|
|
46
|
+
options.compactDisplay = "short";
|
|
47
|
+
options.minimumFractionDigits = 0;
|
|
48
|
+
options.maximumFractionDigits = 1;
|
|
49
|
+
}
|
|
50
|
+
formatter = new Intl.NumberFormat(language, options);
|
|
51
|
+
} catch {
|
|
52
|
+
formatter = null;
|
|
53
|
+
}
|
|
54
|
+
currencyFormatterCache.set(key, formatter);
|
|
55
|
+
return formatter;
|
|
56
|
+
}
|
|
57
|
+
function formatSalaryStat(locale, value, currency, notation) {
|
|
58
|
+
if (!Number.isFinite(value)) return null;
|
|
59
|
+
const currencyCode = currency?.trim().toUpperCase() || null;
|
|
60
|
+
if (!currencyCode) return null;
|
|
61
|
+
const normalized = normalizeLocale(locale);
|
|
62
|
+
if (!normalized) return null;
|
|
63
|
+
const resolved = notation ?? notationForMagnitude(value);
|
|
64
|
+
const formatter = getCurrencyFormatter(normalized, currencyCode, resolved);
|
|
65
|
+
if (!formatter) return null;
|
|
66
|
+
return formatter.format(value);
|
|
67
|
+
}
|
|
68
|
+
function formatSalaryStatRange(locale, min, max, currency, notation) {
|
|
69
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
|
|
70
|
+
const currencyCode = currency?.trim().toUpperCase() || null;
|
|
71
|
+
if (!currencyCode) return null;
|
|
72
|
+
const normalized = normalizeLocale(locale);
|
|
73
|
+
if (!normalized) return null;
|
|
74
|
+
const resolved = notation ?? notationForMagnitude(min, max);
|
|
75
|
+
const formatter = getCurrencyFormatter(normalized, currencyCode, resolved);
|
|
76
|
+
if (!formatter) return null;
|
|
77
|
+
const low = Math.min(min, max);
|
|
78
|
+
const high = Math.max(min, max);
|
|
79
|
+
if (low === high) return formatter.format(low);
|
|
80
|
+
return formatter.formatRange(low, high);
|
|
81
|
+
}
|
|
82
|
+
var SENIORITY_ORDER = [
|
|
83
|
+
"entry_level",
|
|
84
|
+
"associate",
|
|
85
|
+
"mid_level",
|
|
86
|
+
"senior",
|
|
87
|
+
"lead",
|
|
88
|
+
"principal",
|
|
89
|
+
"director",
|
|
90
|
+
"executive"
|
|
91
|
+
];
|
|
92
|
+
function sortBySeniority(items) {
|
|
93
|
+
return [...items].sort((a, b) => {
|
|
94
|
+
const aIdx = SENIORITY_ORDER.indexOf(
|
|
95
|
+
a.seniority
|
|
96
|
+
);
|
|
97
|
+
const bIdx = SENIORITY_ORDER.indexOf(
|
|
98
|
+
b.seniority
|
|
99
|
+
);
|
|
100
|
+
return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
var yearly = {
|
|
104
|
+
unitText: "YEAR",
|
|
105
|
+
duration: "P1Y"
|
|
106
|
+
};
|
|
107
|
+
function distribution(name, currency, minValue, maxValue, stats = {}) {
|
|
108
|
+
return {
|
|
109
|
+
"@type": "MonetaryAmountDistribution",
|
|
110
|
+
...name !== void 0 ? { name } : {},
|
|
111
|
+
currency,
|
|
112
|
+
minValue,
|
|
113
|
+
maxValue,
|
|
114
|
+
...stats,
|
|
115
|
+
...yearly
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function seniorityDistributions(rows, currency, entity, options) {
|
|
119
|
+
return rows.map((row) => {
|
|
120
|
+
const name = options?.seniorityName?.({
|
|
121
|
+
seniority: row.seniority,
|
|
122
|
+
entity
|
|
123
|
+
});
|
|
124
|
+
return distribution(
|
|
125
|
+
name,
|
|
126
|
+
currency,
|
|
127
|
+
row.avgSalaryMin,
|
|
128
|
+
row.avgSalaryMax
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function salaryEnvelope(type, name, extra, estimatedSalary) {
|
|
133
|
+
return {
|
|
134
|
+
"@context": "https://schema.org",
|
|
135
|
+
"@type": type,
|
|
136
|
+
name,
|
|
137
|
+
...extra,
|
|
138
|
+
estimatedSalary
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function buildSalaryFaq(locale, label, overall, currency) {
|
|
142
|
+
if (!overall) return [];
|
|
143
|
+
const range = formatSalaryStatRange(
|
|
144
|
+
locale,
|
|
145
|
+
overall.avgMin,
|
|
146
|
+
overall.avgMax,
|
|
147
|
+
currency
|
|
148
|
+
);
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
kind: "average",
|
|
152
|
+
label,
|
|
153
|
+
range,
|
|
154
|
+
avgMin: overall.avgMin,
|
|
155
|
+
avgMax: overall.avgMax,
|
|
156
|
+
currency,
|
|
157
|
+
jobCount: overall.jobCount
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
kind: "methodology",
|
|
161
|
+
label
|
|
162
|
+
}
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
function itemListJsonLd(items) {
|
|
166
|
+
if (items.length === 0) return null;
|
|
167
|
+
return {
|
|
168
|
+
"@context": "https://schema.org",
|
|
169
|
+
"@type": "ItemList",
|
|
170
|
+
numberOfItems: items.length,
|
|
171
|
+
itemListElement: items.map((it, i) => ({
|
|
172
|
+
"@type": "ListItem",
|
|
173
|
+
position: i + 1,
|
|
174
|
+
name: it.name,
|
|
175
|
+
url: it.url
|
|
176
|
+
}))
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function faqJsonLd(faqs) {
|
|
180
|
+
if (faqs.length === 0) return null;
|
|
181
|
+
return {
|
|
182
|
+
"@context": "https://schema.org",
|
|
183
|
+
"@type": "FAQPage",
|
|
184
|
+
mainEntity: faqs.map((f) => ({
|
|
185
|
+
"@type": "Question",
|
|
186
|
+
name: f.q,
|
|
187
|
+
acceptedAnswer: { "@type": "Answer", text: f.a }
|
|
188
|
+
}))
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function titleSalaryJsonLd(d, options) {
|
|
192
|
+
if (!d.overallSalary) return null;
|
|
193
|
+
return salaryEnvelope("Occupation", d.categoryName, {}, [
|
|
194
|
+
distribution(
|
|
195
|
+
d.categoryName,
|
|
196
|
+
d.currency,
|
|
197
|
+
d.overallSalary.avgMin,
|
|
198
|
+
d.overallSalary.avgMax,
|
|
199
|
+
{
|
|
200
|
+
...d.boardP25Min ? { percentile25: d.boardP25Min } : {},
|
|
201
|
+
...d.boardP75Max ? { percentile75: d.boardP75Max } : {}
|
|
202
|
+
}
|
|
203
|
+
),
|
|
204
|
+
...seniorityDistributions(
|
|
205
|
+
d.bySeniority,
|
|
206
|
+
d.currency,
|
|
207
|
+
d.categoryName,
|
|
208
|
+
options
|
|
209
|
+
)
|
|
210
|
+
]);
|
|
211
|
+
}
|
|
212
|
+
function skillSalaryJsonLd(d) {
|
|
213
|
+
if (!d.overallSalary) return null;
|
|
214
|
+
return salaryEnvelope("Occupation", d.skillName, {}, [
|
|
215
|
+
distribution(
|
|
216
|
+
d.skillName,
|
|
217
|
+
d.currency,
|
|
218
|
+
d.overallSalary.avgMin,
|
|
219
|
+
d.overallSalary.avgMax,
|
|
220
|
+
{
|
|
221
|
+
median: Math.round(
|
|
222
|
+
(d.overallSalary.medianMin + d.overallSalary.medianMax) / 2
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
function locationSalaryJsonLd(d, options) {
|
|
229
|
+
if (!options?.occupationUrl) return null;
|
|
230
|
+
const isCity = d.adminLevel === "city" || d.adminLevel === "locality";
|
|
231
|
+
if (!isCity) return null;
|
|
232
|
+
return itemListJsonLd(
|
|
233
|
+
d.topCategories.map((c) => ({
|
|
234
|
+
name: c.categoryName,
|
|
235
|
+
url: options.occupationUrl(c)
|
|
236
|
+
}))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
function crossAxisSalaryJsonLd(args, options) {
|
|
240
|
+
if (!args.overall) return null;
|
|
241
|
+
return salaryEnvelope(
|
|
242
|
+
"Occupation",
|
|
243
|
+
args.name,
|
|
244
|
+
{
|
|
245
|
+
occupationLocation: {
|
|
246
|
+
"@type": "AdministrativeArea",
|
|
247
|
+
name: args.placeName,
|
|
248
|
+
address: {
|
|
249
|
+
"@type": "PostalAddress",
|
|
250
|
+
addressCountry: args.countryCode
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[
|
|
255
|
+
distribution(
|
|
256
|
+
args.name,
|
|
257
|
+
args.currency,
|
|
258
|
+
args.overall.avgMin,
|
|
259
|
+
args.overall.avgMax,
|
|
260
|
+
{
|
|
261
|
+
...args.overall.p25Min ? { percentile25: args.overall.p25Min } : {},
|
|
262
|
+
...args.overall.p75Max ? { percentile75: args.overall.p75Max } : {}
|
|
263
|
+
}
|
|
264
|
+
),
|
|
265
|
+
...seniorityDistributions(
|
|
266
|
+
args.bySeniority,
|
|
267
|
+
args.currency,
|
|
268
|
+
args.name,
|
|
269
|
+
options
|
|
270
|
+
)
|
|
271
|
+
]
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
function companySalaryJsonLd(d, options) {
|
|
275
|
+
if (!options?.occupationUrl) return null;
|
|
276
|
+
return itemListJsonLd(
|
|
277
|
+
d.byCategory.map((c) => ({
|
|
278
|
+
name: c.categoryName,
|
|
279
|
+
url: options.occupationUrl(c)
|
|
280
|
+
}))
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function companyCategorySalaryJsonLd(d, options) {
|
|
284
|
+
if (!d.overallSalary) return null;
|
|
285
|
+
return salaryEnvelope(
|
|
286
|
+
"Occupation",
|
|
287
|
+
d.categoryName,
|
|
288
|
+
{
|
|
289
|
+
occupationalCategory: d.categoryName,
|
|
290
|
+
hiringOrganization: { "@type": "Organization", name: d.companyName }
|
|
291
|
+
},
|
|
292
|
+
[
|
|
293
|
+
distribution(
|
|
294
|
+
d.categoryName,
|
|
295
|
+
d.currency,
|
|
296
|
+
d.overallSalary.avgMin,
|
|
297
|
+
d.overallSalary.avgMax,
|
|
298
|
+
{
|
|
299
|
+
...d.boardCategoryP25Min ? { percentile25: d.boardCategoryP25Min } : {},
|
|
300
|
+
...d.boardCategoryP75Max ? { percentile75: d.boardCategoryP75Max } : {}
|
|
301
|
+
}
|
|
302
|
+
),
|
|
303
|
+
...seniorityDistributions(
|
|
304
|
+
d.bySeniority,
|
|
305
|
+
d.currency,
|
|
306
|
+
d.categoryName,
|
|
307
|
+
options
|
|
308
|
+
)
|
|
309
|
+
]
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/seo/job-posting.ts
|
|
314
|
+
var EDUCATION_TO_CREDENTIAL = {
|
|
315
|
+
high_school: "high school",
|
|
316
|
+
associate_degree: "associate degree",
|
|
317
|
+
bachelor_degree: "bachelor degree",
|
|
318
|
+
professional_certificate: "professional certificate",
|
|
319
|
+
postgraduate_degree: "postgraduate degree"
|
|
320
|
+
};
|
|
321
|
+
var EMPLOYMENT_TYPE_TO_GOOGLE = {
|
|
322
|
+
full_time: "FULL_TIME",
|
|
323
|
+
part_time: "PART_TIME",
|
|
324
|
+
contract: "CONTRACTOR",
|
|
325
|
+
internship: "INTERN",
|
|
326
|
+
temporary: "TEMPORARY",
|
|
327
|
+
volunteer: "VOLUNTEER",
|
|
328
|
+
other: "OTHER"
|
|
329
|
+
};
|
|
330
|
+
var SALARY_TIMEFRAME_TO_UNIT = {
|
|
331
|
+
per_year: "YEAR",
|
|
332
|
+
per_month: "MONTH",
|
|
333
|
+
per_week: "WEEK",
|
|
334
|
+
per_day: "DAY",
|
|
335
|
+
per_hour: "HOUR"
|
|
336
|
+
};
|
|
337
|
+
var ALL_COUNTRY_CODES = [
|
|
338
|
+
"AF",
|
|
339
|
+
"AX",
|
|
340
|
+
"AL",
|
|
341
|
+
"DZ",
|
|
342
|
+
"AS",
|
|
343
|
+
"AD",
|
|
344
|
+
"AO",
|
|
345
|
+
"AI",
|
|
346
|
+
"AQ",
|
|
347
|
+
"AG",
|
|
348
|
+
"AR",
|
|
349
|
+
"AM",
|
|
350
|
+
"AW",
|
|
351
|
+
"AU",
|
|
352
|
+
"AT",
|
|
353
|
+
"AZ",
|
|
354
|
+
"BS",
|
|
355
|
+
"BH",
|
|
356
|
+
"BD",
|
|
357
|
+
"BB",
|
|
358
|
+
"BY",
|
|
359
|
+
"BE",
|
|
360
|
+
"BZ",
|
|
361
|
+
"BJ",
|
|
362
|
+
"BM",
|
|
363
|
+
"BT",
|
|
364
|
+
"BO",
|
|
365
|
+
"BA",
|
|
366
|
+
"BW",
|
|
367
|
+
"BV",
|
|
368
|
+
"BR",
|
|
369
|
+
"IO",
|
|
370
|
+
"VG",
|
|
371
|
+
"BN",
|
|
372
|
+
"BG",
|
|
373
|
+
"BF",
|
|
374
|
+
"BI",
|
|
375
|
+
"KH",
|
|
376
|
+
"CM",
|
|
377
|
+
"CA",
|
|
378
|
+
"CV",
|
|
379
|
+
"BQ",
|
|
380
|
+
"KY",
|
|
381
|
+
"CF",
|
|
382
|
+
"TD",
|
|
383
|
+
"CL",
|
|
384
|
+
"CN",
|
|
385
|
+
"CX",
|
|
386
|
+
"CC",
|
|
387
|
+
"CO",
|
|
388
|
+
"KM",
|
|
389
|
+
"CK",
|
|
390
|
+
"CR",
|
|
391
|
+
"HR",
|
|
392
|
+
"CU",
|
|
393
|
+
"CW",
|
|
394
|
+
"CY",
|
|
395
|
+
"CZ",
|
|
396
|
+
"DK",
|
|
397
|
+
"DJ",
|
|
398
|
+
"DM",
|
|
399
|
+
"DO",
|
|
400
|
+
"CD",
|
|
401
|
+
"EC",
|
|
402
|
+
"EG",
|
|
403
|
+
"SV",
|
|
404
|
+
"GQ",
|
|
405
|
+
"ER",
|
|
406
|
+
"EE",
|
|
407
|
+
"SZ",
|
|
408
|
+
"ET",
|
|
409
|
+
"FK",
|
|
410
|
+
"FO",
|
|
411
|
+
"FJ",
|
|
412
|
+
"FI",
|
|
413
|
+
"FR",
|
|
414
|
+
"GF",
|
|
415
|
+
"PF",
|
|
416
|
+
"TF",
|
|
417
|
+
"GA",
|
|
418
|
+
"GM",
|
|
419
|
+
"GE",
|
|
420
|
+
"DE",
|
|
421
|
+
"GH",
|
|
422
|
+
"GI",
|
|
423
|
+
"GR",
|
|
424
|
+
"GL",
|
|
425
|
+
"GD",
|
|
426
|
+
"GP",
|
|
427
|
+
"GU",
|
|
428
|
+
"GT",
|
|
429
|
+
"GG",
|
|
430
|
+
"GN",
|
|
431
|
+
"GW",
|
|
432
|
+
"GY",
|
|
433
|
+
"HT",
|
|
434
|
+
"HM",
|
|
435
|
+
"HN",
|
|
436
|
+
"HK",
|
|
437
|
+
"HU",
|
|
438
|
+
"IS",
|
|
439
|
+
"IN",
|
|
440
|
+
"ID",
|
|
441
|
+
"IR",
|
|
442
|
+
"IQ",
|
|
443
|
+
"IE",
|
|
444
|
+
"IM",
|
|
445
|
+
"IL",
|
|
446
|
+
"IT",
|
|
447
|
+
"CI",
|
|
448
|
+
"JM",
|
|
449
|
+
"JP",
|
|
450
|
+
"JE",
|
|
451
|
+
"JO",
|
|
452
|
+
"KZ",
|
|
453
|
+
"KE",
|
|
454
|
+
"KI",
|
|
455
|
+
"XK",
|
|
456
|
+
"KW",
|
|
457
|
+
"KG",
|
|
458
|
+
"LA",
|
|
459
|
+
"LV",
|
|
460
|
+
"LB",
|
|
461
|
+
"LS",
|
|
462
|
+
"LR",
|
|
463
|
+
"LY",
|
|
464
|
+
"LI",
|
|
465
|
+
"LT",
|
|
466
|
+
"LU",
|
|
467
|
+
"MO",
|
|
468
|
+
"MG",
|
|
469
|
+
"MW",
|
|
470
|
+
"MY",
|
|
471
|
+
"MV",
|
|
472
|
+
"ML",
|
|
473
|
+
"MT",
|
|
474
|
+
"MH",
|
|
475
|
+
"MQ",
|
|
476
|
+
"MR",
|
|
477
|
+
"MU",
|
|
478
|
+
"YT",
|
|
479
|
+
"MX",
|
|
480
|
+
"FM",
|
|
481
|
+
"MD",
|
|
482
|
+
"MC",
|
|
483
|
+
"MN",
|
|
484
|
+
"ME",
|
|
485
|
+
"MS",
|
|
486
|
+
"MA",
|
|
487
|
+
"MZ",
|
|
488
|
+
"MM",
|
|
489
|
+
"NA",
|
|
490
|
+
"NR",
|
|
491
|
+
"NP",
|
|
492
|
+
"NL",
|
|
493
|
+
"NC",
|
|
494
|
+
"NZ",
|
|
495
|
+
"NI",
|
|
496
|
+
"NE",
|
|
497
|
+
"NG",
|
|
498
|
+
"NU",
|
|
499
|
+
"NF",
|
|
500
|
+
"MK",
|
|
501
|
+
"MP",
|
|
502
|
+
"NO",
|
|
503
|
+
"OM",
|
|
504
|
+
"PK",
|
|
505
|
+
"PW",
|
|
506
|
+
"PS",
|
|
507
|
+
"PA",
|
|
508
|
+
"PG",
|
|
509
|
+
"PY",
|
|
510
|
+
"PE",
|
|
511
|
+
"PH",
|
|
512
|
+
"PN",
|
|
513
|
+
"PL",
|
|
514
|
+
"PT",
|
|
515
|
+
"PR",
|
|
516
|
+
"QA",
|
|
517
|
+
"CG",
|
|
518
|
+
"RE",
|
|
519
|
+
"RO",
|
|
520
|
+
"RU",
|
|
521
|
+
"RW",
|
|
522
|
+
"BL",
|
|
523
|
+
"SH",
|
|
524
|
+
"KN",
|
|
525
|
+
"LC",
|
|
526
|
+
"MF",
|
|
527
|
+
"PM",
|
|
528
|
+
"VC",
|
|
529
|
+
"WS",
|
|
530
|
+
"SM",
|
|
531
|
+
"ST",
|
|
532
|
+
"SA",
|
|
533
|
+
"SN",
|
|
534
|
+
"RS",
|
|
535
|
+
"SC",
|
|
536
|
+
"SL",
|
|
537
|
+
"SG",
|
|
538
|
+
"SX",
|
|
539
|
+
"SK",
|
|
540
|
+
"SI",
|
|
541
|
+
"SB",
|
|
542
|
+
"SO",
|
|
543
|
+
"ZA",
|
|
544
|
+
"GS",
|
|
545
|
+
"KR",
|
|
546
|
+
"SS",
|
|
547
|
+
"ES",
|
|
548
|
+
"LK",
|
|
549
|
+
"SD",
|
|
550
|
+
"SR",
|
|
551
|
+
"SJ",
|
|
552
|
+
"SE",
|
|
553
|
+
"CH",
|
|
554
|
+
"SY",
|
|
555
|
+
"TW",
|
|
556
|
+
"TJ",
|
|
557
|
+
"TZ",
|
|
558
|
+
"TH",
|
|
559
|
+
"TL",
|
|
560
|
+
"TG",
|
|
561
|
+
"TK",
|
|
562
|
+
"TO",
|
|
563
|
+
"TT",
|
|
564
|
+
"TN",
|
|
565
|
+
"TR",
|
|
566
|
+
"TM",
|
|
567
|
+
"TC",
|
|
568
|
+
"TV",
|
|
569
|
+
"UG",
|
|
570
|
+
"UA",
|
|
571
|
+
"AE",
|
|
572
|
+
"GB",
|
|
573
|
+
"US",
|
|
574
|
+
"UM",
|
|
575
|
+
"VI",
|
|
576
|
+
"UY",
|
|
577
|
+
"UZ",
|
|
578
|
+
"VU",
|
|
579
|
+
"VA",
|
|
580
|
+
"VE",
|
|
581
|
+
"VN",
|
|
582
|
+
"WF",
|
|
583
|
+
"EH",
|
|
584
|
+
"YE",
|
|
585
|
+
"ZM",
|
|
586
|
+
"ZW"
|
|
587
|
+
];
|
|
588
|
+
function createJobPostingJsonLd({
|
|
589
|
+
job,
|
|
590
|
+
board,
|
|
591
|
+
shareUrl
|
|
592
|
+
}) {
|
|
593
|
+
const educationRequirements = createEducationRequirements(job);
|
|
594
|
+
const experienceRequirements = createExperienceRequirements(job);
|
|
595
|
+
const jsonLd = pruneJsonLd({
|
|
596
|
+
"@context": "https://schema.org/",
|
|
597
|
+
"@type": "JobPosting",
|
|
598
|
+
title: job.title,
|
|
599
|
+
description: job.description ?? "",
|
|
600
|
+
identifier: createIdentifier(job, board),
|
|
601
|
+
datePosted: toIsoDate(job.publishedAt),
|
|
602
|
+
validThrough: toIsoDate(job.expiresAt),
|
|
603
|
+
employmentType: job.employmentType ? EMPLOYMENT_TYPE_TO_GOOGLE[job.employmentType] ?? "OTHER" : "OTHER",
|
|
604
|
+
hiringOrganization: createHiringOrganization(job, board),
|
|
605
|
+
jobLocation: createJobLocation(job),
|
|
606
|
+
jobLocationType: job.remoteOption === "remote" ? "TELECOMMUTE" : void 0,
|
|
607
|
+
applicantLocationRequirements: job.remoteOption === "remote" ? createApplicantLocationRequirements(job) : null,
|
|
608
|
+
baseSalary: createBaseSalary(job),
|
|
609
|
+
educationRequirements,
|
|
610
|
+
experienceRequirements,
|
|
611
|
+
experienceInPlaceOfEducation: job.experienceInPlaceOfEducation === true && educationRequirements && experienceRequirements ? true : void 0,
|
|
612
|
+
url: shareUrl
|
|
613
|
+
});
|
|
614
|
+
if (!jsonLd || Array.isArray(jsonLd) || Object.keys(jsonLd).length === 0) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
return jsonLd;
|
|
618
|
+
}
|
|
619
|
+
function createIdentifier(job, board) {
|
|
620
|
+
const organizationName = job.company?.name ?? board.name ?? null;
|
|
621
|
+
const identifier = { "@type": "PropertyValue", value: job.id };
|
|
622
|
+
if (organizationName) identifier.name = organizationName;
|
|
623
|
+
return identifier;
|
|
624
|
+
}
|
|
625
|
+
function createHiringOrganization(job, board) {
|
|
626
|
+
const name = job.company?.name ?? board.name ?? null;
|
|
627
|
+
if (!name) return null;
|
|
628
|
+
const organization = { "@type": "Organization", name };
|
|
629
|
+
const website = job.company?.website ?? null;
|
|
630
|
+
const normalizedWebsite = website ? normalizeWebsiteUrl(website) : null;
|
|
631
|
+
if (normalizedWebsite) organization.sameAs = normalizedWebsite;
|
|
632
|
+
const logo = job.company?.logoUrl ?? board.logoUrl ?? null;
|
|
633
|
+
if (logo) organization.logo = logo;
|
|
634
|
+
return organization;
|
|
635
|
+
}
|
|
636
|
+
function normalizeWebsiteUrl(website) {
|
|
637
|
+
const trimmed = website.trim();
|
|
638
|
+
if (!trimmed) return null;
|
|
639
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
640
|
+
}
|
|
641
|
+
function createBaseSalary(job) {
|
|
642
|
+
const currency = job.salaryCurrency?.trim();
|
|
643
|
+
const hasMin = typeof job.salaryMin === "number";
|
|
644
|
+
const hasMax = typeof job.salaryMax === "number";
|
|
645
|
+
if (!currency || !hasMin && !hasMax) return null;
|
|
646
|
+
const value = { "@type": "QuantitativeValue" };
|
|
647
|
+
if (hasMin) value.minValue = job.salaryMin;
|
|
648
|
+
if (hasMax) value.maxValue = job.salaryMax;
|
|
649
|
+
const unitText = job.salaryTimeframe ? SALARY_TIMEFRAME_TO_UNIT[job.salaryTimeframe] : void 0;
|
|
650
|
+
if (unitText) value.unitText = unitText;
|
|
651
|
+
if (!hasMin && hasMax && typeof job.salaryMax === "number") {
|
|
652
|
+
value.value = job.salaryMax;
|
|
653
|
+
value.minValue = job.salaryMax;
|
|
654
|
+
} else if (!hasMax && hasMin && typeof job.salaryMin === "number") {
|
|
655
|
+
value.value = job.salaryMin;
|
|
656
|
+
value.maxValue = job.salaryMin;
|
|
657
|
+
}
|
|
658
|
+
return { "@type": "MonetaryAmount", currency: currency.toUpperCase(), value };
|
|
659
|
+
}
|
|
660
|
+
function createJobLocation(job) {
|
|
661
|
+
const places = job.officeLocations.map(
|
|
662
|
+
(office) => createPlaceJsonLd({
|
|
663
|
+
countryCode: office.countryCode,
|
|
664
|
+
region: office.region,
|
|
665
|
+
locality: office.city ?? office.locality,
|
|
666
|
+
postalCode: office.postalCode,
|
|
667
|
+
name: office.displayName
|
|
668
|
+
})
|
|
669
|
+
).filter((place) => place !== null);
|
|
670
|
+
if (places.length === 0) return null;
|
|
671
|
+
return places.length === 1 ? places[0] : places;
|
|
672
|
+
}
|
|
673
|
+
function createPlaceJsonLd({
|
|
674
|
+
countryCode,
|
|
675
|
+
region,
|
|
676
|
+
locality,
|
|
677
|
+
postalCode,
|
|
678
|
+
name
|
|
679
|
+
}) {
|
|
680
|
+
const address = { "@type": "PostalAddress" };
|
|
681
|
+
const country = countryCode?.trim();
|
|
682
|
+
const reg = region?.trim();
|
|
683
|
+
const loc = locality?.trim();
|
|
684
|
+
const postal = postalCode?.trim();
|
|
685
|
+
const placeName = name?.trim();
|
|
686
|
+
if (country) address.addressCountry = country.toUpperCase();
|
|
687
|
+
if (reg) address.addressRegion = reg;
|
|
688
|
+
if (loc) address.addressLocality = loc;
|
|
689
|
+
if (postal) address.postalCode = postal;
|
|
690
|
+
const hasAddressDetails = Object.keys(address).length > 1;
|
|
691
|
+
if (!hasAddressDetails && !placeName) return null;
|
|
692
|
+
const place = { "@type": "Place" };
|
|
693
|
+
if (placeName) place.name = placeName;
|
|
694
|
+
if (hasAddressDetails) place.address = address;
|
|
695
|
+
return place;
|
|
696
|
+
}
|
|
697
|
+
function createApplicantLocationRequirements(job) {
|
|
698
|
+
if (job.remoteWorldwide) {
|
|
699
|
+
return ALL_COUNTRY_CODES.map((code) => ({
|
|
700
|
+
"@type": "Country",
|
|
701
|
+
name: code
|
|
702
|
+
}));
|
|
703
|
+
}
|
|
704
|
+
const entries = [];
|
|
705
|
+
const seen = /* @__PURE__ */ new Set();
|
|
706
|
+
for (const code of job.remoteWorkPermitCountryCodes ?? []) {
|
|
707
|
+
const normalized = code?.trim().toUpperCase();
|
|
708
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
709
|
+
seen.add(normalized);
|
|
710
|
+
entries.push({ "@type": "Country", name: normalized });
|
|
711
|
+
}
|
|
712
|
+
for (const subdivision of job.remoteWorkPermitSubdivisionCodes ?? []) {
|
|
713
|
+
const countryCode = subdivision?.split("-")[0]?.trim().toUpperCase();
|
|
714
|
+
if (!countryCode || seen.has(countryCode)) continue;
|
|
715
|
+
seen.add(countryCode);
|
|
716
|
+
entries.push({ "@type": "Country", name: countryCode });
|
|
717
|
+
}
|
|
718
|
+
if (entries.length === 0) return null;
|
|
719
|
+
return entries.length === 1 ? entries[0] : entries;
|
|
720
|
+
}
|
|
721
|
+
function createEducationRequirements(job) {
|
|
722
|
+
const requirements = job.educationRequirements;
|
|
723
|
+
if (!requirements || requirements.length === 0) return null;
|
|
724
|
+
if (requirements.includes("no_requirements")) return "no requirements";
|
|
725
|
+
const credentials = requirements.filter((value) => value in EDUCATION_TO_CREDENTIAL).map((value) => ({
|
|
726
|
+
"@type": "EducationalOccupationalCredential",
|
|
727
|
+
credentialCategory: EDUCATION_TO_CREDENTIAL[value]
|
|
728
|
+
}));
|
|
729
|
+
return credentials.length > 0 ? credentials : null;
|
|
730
|
+
}
|
|
731
|
+
function createExperienceRequirements(job) {
|
|
732
|
+
const months = job.experienceMonths;
|
|
733
|
+
if (typeof months !== "number") return null;
|
|
734
|
+
if (months === 0) return "no requirements";
|
|
735
|
+
return {
|
|
736
|
+
"@type": "OccupationalExperienceRequirements",
|
|
737
|
+
monthsOfExperience: months
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function toIsoDate(value) {
|
|
741
|
+
if (!value) return null;
|
|
742
|
+
const date = new Date(value);
|
|
743
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
744
|
+
return date.toISOString();
|
|
745
|
+
}
|
|
746
|
+
function pruneJsonLd(value) {
|
|
747
|
+
const keep = (entry) => {
|
|
748
|
+
if (entry == null) return false;
|
|
749
|
+
if (Array.isArray(entry)) return entry.length > 0;
|
|
750
|
+
if (typeof entry === "object")
|
|
751
|
+
return Object.keys(entry).length > 0;
|
|
752
|
+
return true;
|
|
753
|
+
};
|
|
754
|
+
if (Array.isArray(value)) {
|
|
755
|
+
return value.map((entry) => pruneJsonLd(entry)).filter(keep);
|
|
756
|
+
}
|
|
757
|
+
if (value && typeof value === "object") {
|
|
758
|
+
const entries = Object.entries(value).map(([key, entry]) => [key, pruneJsonLd(entry)]).filter(([, entry]) => keep(entry));
|
|
759
|
+
return Object.fromEntries(entries);
|
|
760
|
+
}
|
|
761
|
+
return value;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/seo/breadcrumbs.ts
|
|
765
|
+
function createBreadcrumbJsonLd(items, options = {}) {
|
|
766
|
+
const normalizedItems = items.map((item) => {
|
|
767
|
+
const name = item.label?.trim();
|
|
768
|
+
return name ? {
|
|
769
|
+
name,
|
|
770
|
+
href: item.href?.trim() ?? null
|
|
771
|
+
} : null;
|
|
772
|
+
}).filter((item) => !!item);
|
|
773
|
+
if (normalizedItems.length < 2) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const listItems = normalizedItems.map((item, index) => {
|
|
777
|
+
const resolvedHref = item.href ? resolveBreadcrumbHref(item.href, options.origin ?? null) : null;
|
|
778
|
+
const listItem = {
|
|
779
|
+
"@type": "ListItem",
|
|
780
|
+
position: index + 1,
|
|
781
|
+
name: item.name
|
|
782
|
+
};
|
|
783
|
+
if (resolvedHref) {
|
|
784
|
+
listItem.item = resolvedHref;
|
|
785
|
+
}
|
|
786
|
+
return listItem;
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
"@context": "https://schema.org",
|
|
790
|
+
"@type": "BreadcrumbList",
|
|
791
|
+
itemListElement: listItems
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function resolveBreadcrumbHref(href, origin) {
|
|
795
|
+
if (isAbsoluteUrl(href)) {
|
|
796
|
+
return href;
|
|
797
|
+
}
|
|
798
|
+
if (!origin) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
const normalizedPath = href.startsWith("/") ? href : `/${href}`;
|
|
802
|
+
return `${origin}${normalizedPath}`;
|
|
803
|
+
}
|
|
804
|
+
function isAbsoluteUrl(href) {
|
|
805
|
+
return /^https?:\/\//i.test(href);
|
|
806
|
+
}
|
|
807
|
+
function buildJobBreadcrumbs(job) {
|
|
808
|
+
const crumbs = [
|
|
809
|
+
{ kind: "home", path: BOARD_PATHS.home },
|
|
810
|
+
{ kind: "jobs", path: BOARD_PATHS.jobs }
|
|
811
|
+
];
|
|
812
|
+
let lastPlaceSlug = null;
|
|
813
|
+
for (const place of job.placeHierarchy) {
|
|
814
|
+
lastPlaceSlug = place.slug;
|
|
815
|
+
crumbs.push({
|
|
816
|
+
kind: "place",
|
|
817
|
+
name: place.name,
|
|
818
|
+
path: jobsLocationPath(place.slug)
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const primaryCategory = job.categories[0];
|
|
822
|
+
if (primaryCategory) {
|
|
823
|
+
crumbs.push({
|
|
824
|
+
kind: "category",
|
|
825
|
+
name: primaryCategory.name,
|
|
826
|
+
path: lastPlaceSlug ? jobsLocationCategoryPath(lastPlaceSlug, primaryCategory.slug) : jobsCategoryPath(primaryCategory.slug)
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
crumbs.push({ kind: "job", name: job.title });
|
|
830
|
+
return crumbs;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export {
|
|
834
|
+
normalizeLocale,
|
|
835
|
+
formatSalaryStat,
|
|
836
|
+
formatSalaryStatRange,
|
|
837
|
+
SENIORITY_ORDER,
|
|
838
|
+
sortBySeniority,
|
|
839
|
+
buildSalaryFaq,
|
|
840
|
+
itemListJsonLd,
|
|
841
|
+
faqJsonLd,
|
|
842
|
+
titleSalaryJsonLd,
|
|
843
|
+
skillSalaryJsonLd,
|
|
844
|
+
locationSalaryJsonLd,
|
|
845
|
+
crossAxisSalaryJsonLd,
|
|
846
|
+
companySalaryJsonLd,
|
|
847
|
+
companyCategorySalaryJsonLd,
|
|
848
|
+
ALL_COUNTRY_CODES,
|
|
849
|
+
createJobPostingJsonLd,
|
|
850
|
+
normalizeWebsiteUrl,
|
|
851
|
+
createBreadcrumbJsonLd,
|
|
852
|
+
buildJobBreadcrumbs
|
|
853
|
+
};
|