@cavuno/board 1.26.0 → 1.27.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/dist/filters.d.mts +1 -1
- package/dist/filters.d.ts +1 -1
- package/dist/format.d.mts +1 -1
- package/dist/format.d.ts +1 -1
- package/dist/index.d.mts +6 -131
- package/dist/index.d.ts +6 -131
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{jobs-CM67_J6H.d.mts → jobs-DK5mPBgq.d.mts} +1 -1
- package/dist/{jobs-CM67_J6H.d.ts → jobs-DK5mPBgq.d.ts} +1 -1
- package/dist/salaries-CXt6Vkrp.d.ts +130 -0
- package/dist/salaries-CrJsaZe6.d.mts +130 -0
- package/dist/seo.d.mts +288 -0
- package/dist/seo.d.ts +288 -0
- package/dist/seo.js +1102 -0
- package/dist/seo.mjs +1079 -0
- package/dist/sitemap.d.mts +63 -0
- package/dist/sitemap.d.ts +63 -0
- package/dist/sitemap.js +353 -0
- package/dist/sitemap.mjs +330 -0
- package/package.json +21 -1
- package/skills/cavuno-board-seo/SKILL.md +180 -0
- package/skills/cavuno-board-sitemap/SKILL.md +147 -0
- package/skills/manifest.json +15 -1
package/dist/seo.js
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/seo/index.ts
|
|
21
|
+
var seo_exports = {};
|
|
22
|
+
__export(seo_exports, {
|
|
23
|
+
ALL_COUNTRY_CODES: () => ALL_COUNTRY_CODES,
|
|
24
|
+
SENIORITY_ORDER: () => SENIORITY_ORDER,
|
|
25
|
+
buildSalaryFaq: () => buildSalaryFaq,
|
|
26
|
+
companyCategorySalaryJsonLd: () => companyCategorySalaryJsonLd,
|
|
27
|
+
companySalaryJsonLd: () => companySalaryJsonLd,
|
|
28
|
+
createAuthorProfileJsonLd: () => createAuthorProfileJsonLd,
|
|
29
|
+
createBlogArticleJsonLd: () => createBlogArticleJsonLd,
|
|
30
|
+
createBreadcrumbJsonLd: () => createBreadcrumbJsonLd,
|
|
31
|
+
createJobPostingJsonLd: () => createJobPostingJsonLd,
|
|
32
|
+
crossAxisSalaryJsonLd: () => crossAxisSalaryJsonLd,
|
|
33
|
+
faqJsonLd: () => faqJsonLd,
|
|
34
|
+
formatRange: () => formatRange,
|
|
35
|
+
formatSeniority: () => formatSeniority,
|
|
36
|
+
formatUsd: () => formatUsd,
|
|
37
|
+
itemListJsonLd: () => itemListJsonLd,
|
|
38
|
+
listingHead: () => listingHead,
|
|
39
|
+
listingJsonLd: () => listingJsonLd,
|
|
40
|
+
locationSalaryJsonLd: () => locationSalaryJsonLd,
|
|
41
|
+
normalizeWebsiteUrl: () => normalizeWebsiteUrl,
|
|
42
|
+
skillSalaryJsonLd: () => skillSalaryJsonLd,
|
|
43
|
+
sortBySeniority: () => sortBySeniority,
|
|
44
|
+
titleSalaryJsonLd: () => titleSalaryJsonLd
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(seo_exports);
|
|
47
|
+
|
|
48
|
+
// src/seo/blog.ts
|
|
49
|
+
function createBlogArticleJsonLd({
|
|
50
|
+
post,
|
|
51
|
+
boardName,
|
|
52
|
+
permalink,
|
|
53
|
+
ogImageUrl
|
|
54
|
+
}) {
|
|
55
|
+
const article = {
|
|
56
|
+
"@context": "https://schema.org",
|
|
57
|
+
"@type": "Article",
|
|
58
|
+
headline: post.title,
|
|
59
|
+
url: permalink,
|
|
60
|
+
mainEntityOfPage: { "@type": "WebPage", "@id": permalink },
|
|
61
|
+
publisher: { "@type": "Organization", name: boardName }
|
|
62
|
+
};
|
|
63
|
+
if (post.customExcerpt) article.description = post.customExcerpt;
|
|
64
|
+
if (post.publishedAt) article.datePublished = post.publishedAt;
|
|
65
|
+
const image = post.coverUrl ?? ogImageUrl;
|
|
66
|
+
if (image) article.image = image;
|
|
67
|
+
const author = post.authors[0];
|
|
68
|
+
if (author) {
|
|
69
|
+
article.author = {
|
|
70
|
+
"@type": "Person",
|
|
71
|
+
name: author.name,
|
|
72
|
+
...author.websiteUrl ? { url: author.websiteUrl } : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return article;
|
|
76
|
+
}
|
|
77
|
+
function createAuthorProfileJsonLd({
|
|
78
|
+
author,
|
|
79
|
+
canonical,
|
|
80
|
+
description,
|
|
81
|
+
origin,
|
|
82
|
+
posts,
|
|
83
|
+
totalPosts
|
|
84
|
+
}) {
|
|
85
|
+
if (!author.name) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const normalizedDescription = description.trim();
|
|
89
|
+
const sameAs = [
|
|
90
|
+
normalizeUrl(author.websiteUrl),
|
|
91
|
+
normalizeUrl(author.twitterUrl),
|
|
92
|
+
normalizeUrl(author.linkedinUrl),
|
|
93
|
+
normalizeUrl(author.githubUrl)
|
|
94
|
+
].filter((value) => Boolean(value));
|
|
95
|
+
const authorId = `${canonical}#profile`;
|
|
96
|
+
const hasPart = posts.slice(0, 5).map((post) => {
|
|
97
|
+
const postUrl = buildAbsoluteUrl(origin, `/blog/${post.slug}`);
|
|
98
|
+
if (!postUrl) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
"@type": "Article",
|
|
103
|
+
headline: post.title,
|
|
104
|
+
url: postUrl,
|
|
105
|
+
datePublished: post.publishedAt,
|
|
106
|
+
author: { "@id": authorId }
|
|
107
|
+
};
|
|
108
|
+
}).filter((value) => value !== null);
|
|
109
|
+
const agentInteractionStatistic = totalPosts > 0 ? {
|
|
110
|
+
"@type": "InteractionCounter",
|
|
111
|
+
interactionType: "https://schema.org/WriteAction",
|
|
112
|
+
userInteractionCount: totalPosts
|
|
113
|
+
} : void 0;
|
|
114
|
+
const profilePage = {
|
|
115
|
+
"@context": "https://schema.org",
|
|
116
|
+
"@type": "ProfilePage",
|
|
117
|
+
url: canonical,
|
|
118
|
+
mainEntity: {
|
|
119
|
+
"@type": "Person",
|
|
120
|
+
"@id": authorId,
|
|
121
|
+
name: author.name,
|
|
122
|
+
alternateName: author.slug ?? void 0,
|
|
123
|
+
identifier: author.id ?? void 0,
|
|
124
|
+
description: normalizedDescription || void 0,
|
|
125
|
+
image: author.avatarUrl ?? void 0,
|
|
126
|
+
sameAs: sameAs.length > 0 ? sameAs : void 0,
|
|
127
|
+
url: canonical,
|
|
128
|
+
mainEntityOfPage: canonical,
|
|
129
|
+
agentInteractionStatistic
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (hasPart.length > 0) {
|
|
133
|
+
profilePage.hasPart = hasPart;
|
|
134
|
+
}
|
|
135
|
+
return profilePage;
|
|
136
|
+
}
|
|
137
|
+
function normalizeUrl(value) {
|
|
138
|
+
if (!value) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const trimmed = value.trim();
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const candidate = trimmed.startsWith("http://") || trimmed.startsWith("https://") ? trimmed : `https://${trimmed}`;
|
|
147
|
+
return new URL(candidate).toString();
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function buildAbsoluteUrl(origin, path) {
|
|
153
|
+
if (!origin || !path) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return new URL(path, origin).toString();
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/seo/breadcrumbs.ts
|
|
164
|
+
function createBreadcrumbJsonLd(items, options = {}) {
|
|
165
|
+
const normalizedItems = items.map((item) => {
|
|
166
|
+
const name = item.label?.trim();
|
|
167
|
+
return name ? {
|
|
168
|
+
name,
|
|
169
|
+
href: item.href?.trim() ?? null
|
|
170
|
+
} : null;
|
|
171
|
+
}).filter((item) => !!item);
|
|
172
|
+
if (normalizedItems.length < 2) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const listItems = normalizedItems.map((item, index) => {
|
|
176
|
+
const resolvedHref = item.href ? resolveBreadcrumbHref(item.href, options.origin ?? null) : null;
|
|
177
|
+
const listItem = {
|
|
178
|
+
"@type": "ListItem",
|
|
179
|
+
position: index + 1,
|
|
180
|
+
name: item.name
|
|
181
|
+
};
|
|
182
|
+
if (resolvedHref) {
|
|
183
|
+
listItem.item = resolvedHref;
|
|
184
|
+
}
|
|
185
|
+
return listItem;
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
"@context": "https://schema.org",
|
|
189
|
+
"@type": "BreadcrumbList",
|
|
190
|
+
itemListElement: listItems
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function resolveBreadcrumbHref(href, origin) {
|
|
194
|
+
if (isAbsoluteUrl(href)) {
|
|
195
|
+
return href;
|
|
196
|
+
}
|
|
197
|
+
if (!origin) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const normalizedPath = href.startsWith("/") ? href : `/${href}`;
|
|
201
|
+
return `${origin}${normalizedPath}`;
|
|
202
|
+
}
|
|
203
|
+
function isAbsoluteUrl(href) {
|
|
204
|
+
return /^https?:\/\//i.test(href);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/seo/job-posting.ts
|
|
208
|
+
var EDUCATION_TO_CREDENTIAL = {
|
|
209
|
+
high_school: "high school",
|
|
210
|
+
associate_degree: "associate degree",
|
|
211
|
+
bachelor_degree: "bachelor degree",
|
|
212
|
+
professional_certificate: "professional certificate",
|
|
213
|
+
postgraduate_degree: "postgraduate degree"
|
|
214
|
+
};
|
|
215
|
+
var EMPLOYMENT_TYPE_TO_GOOGLE = {
|
|
216
|
+
full_time: "FULL_TIME",
|
|
217
|
+
part_time: "PART_TIME",
|
|
218
|
+
contract: "CONTRACTOR",
|
|
219
|
+
internship: "INTERN",
|
|
220
|
+
temporary: "TEMPORARY",
|
|
221
|
+
volunteer: "VOLUNTEER",
|
|
222
|
+
other: "OTHER"
|
|
223
|
+
};
|
|
224
|
+
var SALARY_TIMEFRAME_TO_UNIT = {
|
|
225
|
+
per_year: "YEAR",
|
|
226
|
+
per_month: "MONTH",
|
|
227
|
+
per_week: "WEEK",
|
|
228
|
+
per_day: "DAY",
|
|
229
|
+
per_hour: "HOUR"
|
|
230
|
+
};
|
|
231
|
+
var ALL_COUNTRY_CODES = [
|
|
232
|
+
"AF",
|
|
233
|
+
"AX",
|
|
234
|
+
"AL",
|
|
235
|
+
"DZ",
|
|
236
|
+
"AS",
|
|
237
|
+
"AD",
|
|
238
|
+
"AO",
|
|
239
|
+
"AI",
|
|
240
|
+
"AQ",
|
|
241
|
+
"AG",
|
|
242
|
+
"AR",
|
|
243
|
+
"AM",
|
|
244
|
+
"AW",
|
|
245
|
+
"AU",
|
|
246
|
+
"AT",
|
|
247
|
+
"AZ",
|
|
248
|
+
"BS",
|
|
249
|
+
"BH",
|
|
250
|
+
"BD",
|
|
251
|
+
"BB",
|
|
252
|
+
"BY",
|
|
253
|
+
"BE",
|
|
254
|
+
"BZ",
|
|
255
|
+
"BJ",
|
|
256
|
+
"BM",
|
|
257
|
+
"BT",
|
|
258
|
+
"BO",
|
|
259
|
+
"BA",
|
|
260
|
+
"BW",
|
|
261
|
+
"BV",
|
|
262
|
+
"BR",
|
|
263
|
+
"IO",
|
|
264
|
+
"VG",
|
|
265
|
+
"BN",
|
|
266
|
+
"BG",
|
|
267
|
+
"BF",
|
|
268
|
+
"BI",
|
|
269
|
+
"KH",
|
|
270
|
+
"CM",
|
|
271
|
+
"CA",
|
|
272
|
+
"CV",
|
|
273
|
+
"BQ",
|
|
274
|
+
"KY",
|
|
275
|
+
"CF",
|
|
276
|
+
"TD",
|
|
277
|
+
"CL",
|
|
278
|
+
"CN",
|
|
279
|
+
"CX",
|
|
280
|
+
"CC",
|
|
281
|
+
"CO",
|
|
282
|
+
"KM",
|
|
283
|
+
"CK",
|
|
284
|
+
"CR",
|
|
285
|
+
"HR",
|
|
286
|
+
"CU",
|
|
287
|
+
"CW",
|
|
288
|
+
"CY",
|
|
289
|
+
"CZ",
|
|
290
|
+
"DK",
|
|
291
|
+
"DJ",
|
|
292
|
+
"DM",
|
|
293
|
+
"DO",
|
|
294
|
+
"CD",
|
|
295
|
+
"EC",
|
|
296
|
+
"EG",
|
|
297
|
+
"SV",
|
|
298
|
+
"GQ",
|
|
299
|
+
"ER",
|
|
300
|
+
"EE",
|
|
301
|
+
"SZ",
|
|
302
|
+
"ET",
|
|
303
|
+
"FK",
|
|
304
|
+
"FO",
|
|
305
|
+
"FJ",
|
|
306
|
+
"FI",
|
|
307
|
+
"FR",
|
|
308
|
+
"GF",
|
|
309
|
+
"PF",
|
|
310
|
+
"TF",
|
|
311
|
+
"GA",
|
|
312
|
+
"GM",
|
|
313
|
+
"GE",
|
|
314
|
+
"DE",
|
|
315
|
+
"GH",
|
|
316
|
+
"GI",
|
|
317
|
+
"GR",
|
|
318
|
+
"GL",
|
|
319
|
+
"GD",
|
|
320
|
+
"GP",
|
|
321
|
+
"GU",
|
|
322
|
+
"GT",
|
|
323
|
+
"GG",
|
|
324
|
+
"GN",
|
|
325
|
+
"GW",
|
|
326
|
+
"GY",
|
|
327
|
+
"HT",
|
|
328
|
+
"HM",
|
|
329
|
+
"HN",
|
|
330
|
+
"HK",
|
|
331
|
+
"HU",
|
|
332
|
+
"IS",
|
|
333
|
+
"IN",
|
|
334
|
+
"ID",
|
|
335
|
+
"IR",
|
|
336
|
+
"IQ",
|
|
337
|
+
"IE",
|
|
338
|
+
"IM",
|
|
339
|
+
"IL",
|
|
340
|
+
"IT",
|
|
341
|
+
"CI",
|
|
342
|
+
"JM",
|
|
343
|
+
"JP",
|
|
344
|
+
"JE",
|
|
345
|
+
"JO",
|
|
346
|
+
"KZ",
|
|
347
|
+
"KE",
|
|
348
|
+
"KI",
|
|
349
|
+
"XK",
|
|
350
|
+
"KW",
|
|
351
|
+
"KG",
|
|
352
|
+
"LA",
|
|
353
|
+
"LV",
|
|
354
|
+
"LB",
|
|
355
|
+
"LS",
|
|
356
|
+
"LR",
|
|
357
|
+
"LY",
|
|
358
|
+
"LI",
|
|
359
|
+
"LT",
|
|
360
|
+
"LU",
|
|
361
|
+
"MO",
|
|
362
|
+
"MG",
|
|
363
|
+
"MW",
|
|
364
|
+
"MY",
|
|
365
|
+
"MV",
|
|
366
|
+
"ML",
|
|
367
|
+
"MT",
|
|
368
|
+
"MH",
|
|
369
|
+
"MQ",
|
|
370
|
+
"MR",
|
|
371
|
+
"MU",
|
|
372
|
+
"YT",
|
|
373
|
+
"MX",
|
|
374
|
+
"FM",
|
|
375
|
+
"MD",
|
|
376
|
+
"MC",
|
|
377
|
+
"MN",
|
|
378
|
+
"ME",
|
|
379
|
+
"MS",
|
|
380
|
+
"MA",
|
|
381
|
+
"MZ",
|
|
382
|
+
"MM",
|
|
383
|
+
"NA",
|
|
384
|
+
"NR",
|
|
385
|
+
"NP",
|
|
386
|
+
"NL",
|
|
387
|
+
"NC",
|
|
388
|
+
"NZ",
|
|
389
|
+
"NI",
|
|
390
|
+
"NE",
|
|
391
|
+
"NG",
|
|
392
|
+
"NU",
|
|
393
|
+
"NF",
|
|
394
|
+
"MK",
|
|
395
|
+
"MP",
|
|
396
|
+
"NO",
|
|
397
|
+
"OM",
|
|
398
|
+
"PK",
|
|
399
|
+
"PW",
|
|
400
|
+
"PS",
|
|
401
|
+
"PA",
|
|
402
|
+
"PG",
|
|
403
|
+
"PY",
|
|
404
|
+
"PE",
|
|
405
|
+
"PH",
|
|
406
|
+
"PN",
|
|
407
|
+
"PL",
|
|
408
|
+
"PT",
|
|
409
|
+
"PR",
|
|
410
|
+
"QA",
|
|
411
|
+
"CG",
|
|
412
|
+
"RE",
|
|
413
|
+
"RO",
|
|
414
|
+
"RU",
|
|
415
|
+
"RW",
|
|
416
|
+
"BL",
|
|
417
|
+
"SH",
|
|
418
|
+
"KN",
|
|
419
|
+
"LC",
|
|
420
|
+
"MF",
|
|
421
|
+
"PM",
|
|
422
|
+
"VC",
|
|
423
|
+
"WS",
|
|
424
|
+
"SM",
|
|
425
|
+
"ST",
|
|
426
|
+
"SA",
|
|
427
|
+
"SN",
|
|
428
|
+
"RS",
|
|
429
|
+
"SC",
|
|
430
|
+
"SL",
|
|
431
|
+
"SG",
|
|
432
|
+
"SX",
|
|
433
|
+
"SK",
|
|
434
|
+
"SI",
|
|
435
|
+
"SB",
|
|
436
|
+
"SO",
|
|
437
|
+
"ZA",
|
|
438
|
+
"GS",
|
|
439
|
+
"KR",
|
|
440
|
+
"SS",
|
|
441
|
+
"ES",
|
|
442
|
+
"LK",
|
|
443
|
+
"SD",
|
|
444
|
+
"SR",
|
|
445
|
+
"SJ",
|
|
446
|
+
"SE",
|
|
447
|
+
"CH",
|
|
448
|
+
"SY",
|
|
449
|
+
"TW",
|
|
450
|
+
"TJ",
|
|
451
|
+
"TZ",
|
|
452
|
+
"TH",
|
|
453
|
+
"TL",
|
|
454
|
+
"TG",
|
|
455
|
+
"TK",
|
|
456
|
+
"TO",
|
|
457
|
+
"TT",
|
|
458
|
+
"TN",
|
|
459
|
+
"TR",
|
|
460
|
+
"TM",
|
|
461
|
+
"TC",
|
|
462
|
+
"TV",
|
|
463
|
+
"UG",
|
|
464
|
+
"UA",
|
|
465
|
+
"AE",
|
|
466
|
+
"GB",
|
|
467
|
+
"US",
|
|
468
|
+
"UM",
|
|
469
|
+
"VI",
|
|
470
|
+
"UY",
|
|
471
|
+
"UZ",
|
|
472
|
+
"VU",
|
|
473
|
+
"VA",
|
|
474
|
+
"VE",
|
|
475
|
+
"VN",
|
|
476
|
+
"WF",
|
|
477
|
+
"EH",
|
|
478
|
+
"YE",
|
|
479
|
+
"ZM",
|
|
480
|
+
"ZW"
|
|
481
|
+
];
|
|
482
|
+
function createJobPostingJsonLd({
|
|
483
|
+
job,
|
|
484
|
+
board,
|
|
485
|
+
shareUrl
|
|
486
|
+
}) {
|
|
487
|
+
const educationRequirements = createEducationRequirements(job);
|
|
488
|
+
const experienceRequirements = createExperienceRequirements(job);
|
|
489
|
+
const jsonLd = pruneJsonLd({
|
|
490
|
+
"@context": "https://schema.org/",
|
|
491
|
+
"@type": "JobPosting",
|
|
492
|
+
title: job.title,
|
|
493
|
+
description: job.description ?? "",
|
|
494
|
+
identifier: createIdentifier(job, board),
|
|
495
|
+
datePosted: toIsoDate(job.publishedAt),
|
|
496
|
+
validThrough: toIsoDate(job.expiresAt),
|
|
497
|
+
employmentType: job.employmentType ? EMPLOYMENT_TYPE_TO_GOOGLE[job.employmentType] ?? "OTHER" : "OTHER",
|
|
498
|
+
hiringOrganization: createHiringOrganization(job, board),
|
|
499
|
+
jobLocation: createJobLocation(job),
|
|
500
|
+
jobLocationType: job.remoteOption === "remote" ? "TELECOMMUTE" : void 0,
|
|
501
|
+
applicantLocationRequirements: job.remoteOption === "remote" ? createApplicantLocationRequirements(job) : null,
|
|
502
|
+
baseSalary: createBaseSalary(job),
|
|
503
|
+
educationRequirements,
|
|
504
|
+
experienceRequirements,
|
|
505
|
+
experienceInPlaceOfEducation: job.experienceInPlaceOfEducation === true && educationRequirements && experienceRequirements ? true : void 0,
|
|
506
|
+
url: shareUrl
|
|
507
|
+
});
|
|
508
|
+
if (!jsonLd || Array.isArray(jsonLd) || Object.keys(jsonLd).length === 0) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
return jsonLd;
|
|
512
|
+
}
|
|
513
|
+
function createIdentifier(job, board) {
|
|
514
|
+
const organizationName = job.company?.name ?? board.name ?? null;
|
|
515
|
+
const identifier = { "@type": "PropertyValue", value: job.id };
|
|
516
|
+
if (organizationName) identifier.name = organizationName;
|
|
517
|
+
return identifier;
|
|
518
|
+
}
|
|
519
|
+
function createHiringOrganization(job, board) {
|
|
520
|
+
const name = job.company?.name ?? board.name ?? null;
|
|
521
|
+
if (!name) return null;
|
|
522
|
+
const organization = { "@type": "Organization", name };
|
|
523
|
+
const website = job.company?.website ?? null;
|
|
524
|
+
const normalizedWebsite = website ? normalizeWebsiteUrl(website) : null;
|
|
525
|
+
if (normalizedWebsite) organization.sameAs = normalizedWebsite;
|
|
526
|
+
const logo = job.company?.logoUrl ?? board.logoUrl ?? null;
|
|
527
|
+
if (logo) organization.logo = logo;
|
|
528
|
+
return organization;
|
|
529
|
+
}
|
|
530
|
+
function normalizeWebsiteUrl(website) {
|
|
531
|
+
const trimmed = website.trim();
|
|
532
|
+
if (!trimmed) return null;
|
|
533
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
534
|
+
}
|
|
535
|
+
function createBaseSalary(job) {
|
|
536
|
+
const currency = job.salaryCurrency?.trim();
|
|
537
|
+
const hasMin = typeof job.salaryMin === "number";
|
|
538
|
+
const hasMax = typeof job.salaryMax === "number";
|
|
539
|
+
if (!currency || !hasMin && !hasMax) return null;
|
|
540
|
+
const value = { "@type": "QuantitativeValue" };
|
|
541
|
+
if (hasMin) value.minValue = job.salaryMin;
|
|
542
|
+
if (hasMax) value.maxValue = job.salaryMax;
|
|
543
|
+
const unitText = job.salaryTimeframe ? SALARY_TIMEFRAME_TO_UNIT[job.salaryTimeframe] : void 0;
|
|
544
|
+
if (unitText) value.unitText = unitText;
|
|
545
|
+
if (!hasMin && hasMax && typeof job.salaryMax === "number") {
|
|
546
|
+
value.value = job.salaryMax;
|
|
547
|
+
value.minValue = job.salaryMax;
|
|
548
|
+
} else if (!hasMax && hasMin && typeof job.salaryMin === "number") {
|
|
549
|
+
value.value = job.salaryMin;
|
|
550
|
+
value.maxValue = job.salaryMin;
|
|
551
|
+
}
|
|
552
|
+
return { "@type": "MonetaryAmount", currency: currency.toUpperCase(), value };
|
|
553
|
+
}
|
|
554
|
+
function createJobLocation(job) {
|
|
555
|
+
const places = job.officeLocations.map(
|
|
556
|
+
(office) => createPlaceJsonLd({
|
|
557
|
+
countryCode: office.countryCode,
|
|
558
|
+
region: office.region,
|
|
559
|
+
locality: office.city ?? office.locality,
|
|
560
|
+
postalCode: office.postalCode,
|
|
561
|
+
name: office.displayName
|
|
562
|
+
})
|
|
563
|
+
).filter((place) => place !== null);
|
|
564
|
+
if (places.length === 0) return null;
|
|
565
|
+
return places.length === 1 ? places[0] : places;
|
|
566
|
+
}
|
|
567
|
+
function createPlaceJsonLd({
|
|
568
|
+
countryCode,
|
|
569
|
+
region,
|
|
570
|
+
locality,
|
|
571
|
+
postalCode,
|
|
572
|
+
name
|
|
573
|
+
}) {
|
|
574
|
+
const address = { "@type": "PostalAddress" };
|
|
575
|
+
const country = countryCode?.trim();
|
|
576
|
+
const reg = region?.trim();
|
|
577
|
+
const loc = locality?.trim();
|
|
578
|
+
const postal = postalCode?.trim();
|
|
579
|
+
const placeName = name?.trim();
|
|
580
|
+
if (country) address.addressCountry = country.toUpperCase();
|
|
581
|
+
if (reg) address.addressRegion = reg;
|
|
582
|
+
if (loc) address.addressLocality = loc;
|
|
583
|
+
if (postal) address.postalCode = postal;
|
|
584
|
+
const hasAddressDetails = Object.keys(address).length > 1;
|
|
585
|
+
if (!hasAddressDetails && !placeName) return null;
|
|
586
|
+
const place = { "@type": "Place" };
|
|
587
|
+
if (placeName) place.name = placeName;
|
|
588
|
+
if (hasAddressDetails) place.address = address;
|
|
589
|
+
return place;
|
|
590
|
+
}
|
|
591
|
+
function createApplicantLocationRequirements(job) {
|
|
592
|
+
if (job.remoteWorldwide) {
|
|
593
|
+
return ALL_COUNTRY_CODES.map((code) => ({
|
|
594
|
+
"@type": "Country",
|
|
595
|
+
name: code
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
const entries = [];
|
|
599
|
+
const seen = /* @__PURE__ */ new Set();
|
|
600
|
+
for (const code of job.remoteWorkPermitCountryCodes ?? []) {
|
|
601
|
+
const normalized = code?.trim().toUpperCase();
|
|
602
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
603
|
+
seen.add(normalized);
|
|
604
|
+
entries.push({ "@type": "Country", name: normalized });
|
|
605
|
+
}
|
|
606
|
+
for (const subdivision of job.remoteWorkPermitSubdivisionCodes ?? []) {
|
|
607
|
+
const countryCode = subdivision?.split("-")[0]?.trim().toUpperCase();
|
|
608
|
+
if (!countryCode || seen.has(countryCode)) continue;
|
|
609
|
+
seen.add(countryCode);
|
|
610
|
+
entries.push({ "@type": "Country", name: countryCode });
|
|
611
|
+
}
|
|
612
|
+
if (entries.length === 0) return null;
|
|
613
|
+
return entries.length === 1 ? entries[0] : entries;
|
|
614
|
+
}
|
|
615
|
+
function createEducationRequirements(job) {
|
|
616
|
+
const requirements = job.educationRequirements;
|
|
617
|
+
if (!requirements || requirements.length === 0) return null;
|
|
618
|
+
if (requirements.includes("no_requirements")) return "no requirements";
|
|
619
|
+
const credentials = requirements.filter((value) => value in EDUCATION_TO_CREDENTIAL).map((value) => ({
|
|
620
|
+
"@type": "EducationalOccupationalCredential",
|
|
621
|
+
credentialCategory: EDUCATION_TO_CREDENTIAL[value]
|
|
622
|
+
}));
|
|
623
|
+
return credentials.length > 0 ? credentials : null;
|
|
624
|
+
}
|
|
625
|
+
function createExperienceRequirements(job) {
|
|
626
|
+
const months = job.experienceMonths;
|
|
627
|
+
if (typeof months !== "number") return null;
|
|
628
|
+
if (months === 0) return "no requirements";
|
|
629
|
+
return {
|
|
630
|
+
"@type": "OccupationalExperienceRequirements",
|
|
631
|
+
monthsOfExperience: months
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function toIsoDate(value) {
|
|
635
|
+
if (!value) return null;
|
|
636
|
+
const date = new Date(value);
|
|
637
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
638
|
+
return date.toISOString();
|
|
639
|
+
}
|
|
640
|
+
function pruneJsonLd(value) {
|
|
641
|
+
const keep = (entry) => {
|
|
642
|
+
if (entry == null) return false;
|
|
643
|
+
if (Array.isArray(entry)) return entry.length > 0;
|
|
644
|
+
if (typeof entry === "object")
|
|
645
|
+
return Object.keys(entry).length > 0;
|
|
646
|
+
return true;
|
|
647
|
+
};
|
|
648
|
+
if (Array.isArray(value)) {
|
|
649
|
+
return value.map((entry) => pruneJsonLd(entry)).filter(keep);
|
|
650
|
+
}
|
|
651
|
+
if (value && typeof value === "object") {
|
|
652
|
+
const entries = Object.entries(value).map(([key, entry]) => [key, pruneJsonLd(entry)]).filter(([, entry]) => keep(entry));
|
|
653
|
+
return Object.fromEntries(entries);
|
|
654
|
+
}
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/seo/listing.ts
|
|
659
|
+
function listingHead(options) {
|
|
660
|
+
const countPrefix = typeof options.count === "number" ? `${options.count} ` : "";
|
|
661
|
+
const title = `${countPrefix}${options.heading} | ${options.boardName}`;
|
|
662
|
+
const description = `Browse ${countPrefix}${options.heading.toLowerCase()} on ${options.boardName}.`;
|
|
663
|
+
const url = `${options.origin}${options.path}`;
|
|
664
|
+
return {
|
|
665
|
+
meta: [
|
|
666
|
+
{ title },
|
|
667
|
+
{ name: "description", content: description },
|
|
668
|
+
{ property: "og:title", content: title },
|
|
669
|
+
{ property: "og:description", content: description },
|
|
670
|
+
{ property: "og:type", content: "website" },
|
|
671
|
+
{ property: "og:url", content: url }
|
|
672
|
+
],
|
|
673
|
+
links: [{ rel: "canonical", href: url }]
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function listingJsonLd(options) {
|
|
677
|
+
const jobUrl = (job) => job.company ? `${options.origin}/companies/${job.company.slug}/jobs/${job.slug}` : `${options.origin}/jobs/${job.slug}`;
|
|
678
|
+
const objects = [
|
|
679
|
+
{
|
|
680
|
+
"@context": "https://schema.org",
|
|
681
|
+
// Deliberately NOT createBreadcrumbJsonLd (breadcrumbs.ts): listing trails
|
|
682
|
+
// are code-constructed (never user copy), so the hosted listing page skips
|
|
683
|
+
// the trim/drop-blank/min-2 gate that module transcribes. Same schema.org
|
|
684
|
+
// fragment, different (intentional) semantics — do not unify blindly.
|
|
685
|
+
"@type": "BreadcrumbList",
|
|
686
|
+
itemListElement: options.breadcrumbs.map((crumb, index) => ({
|
|
687
|
+
"@type": "ListItem",
|
|
688
|
+
position: index + 1,
|
|
689
|
+
name: crumb.name,
|
|
690
|
+
...crumb.path ? { item: `${options.origin}${crumb.path}` } : {}
|
|
691
|
+
}))
|
|
692
|
+
}
|
|
693
|
+
];
|
|
694
|
+
if (options.jobs && options.jobs.length > 0) {
|
|
695
|
+
objects.push({
|
|
696
|
+
"@context": "https://schema.org",
|
|
697
|
+
"@type": "ItemList",
|
|
698
|
+
itemListElement: options.jobs.map((job, index) => ({
|
|
699
|
+
"@type": "ListItem",
|
|
700
|
+
position: index + 1,
|
|
701
|
+
url: jobUrl(job)
|
|
702
|
+
}))
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return objects;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/format/salary-lexicon.ts
|
|
709
|
+
var EN = {
|
|
710
|
+
timeframe: {
|
|
711
|
+
per_year: "Yearly",
|
|
712
|
+
per_month: "Monthly",
|
|
713
|
+
per_week: "Weekly",
|
|
714
|
+
per_day: "Daily",
|
|
715
|
+
per_hour: "Hourly"
|
|
716
|
+
},
|
|
717
|
+
rangePrefix: { from: "From", upTo: "Up to" },
|
|
718
|
+
seniority: {
|
|
719
|
+
entry_level: "Entry level",
|
|
720
|
+
associate: "Associate",
|
|
721
|
+
mid_level: "Mid-level",
|
|
722
|
+
senior: "Senior",
|
|
723
|
+
lead: "Lead",
|
|
724
|
+
principal: "Principal",
|
|
725
|
+
director: "Director",
|
|
726
|
+
executive: "Executive"
|
|
727
|
+
},
|
|
728
|
+
frames: {
|
|
729
|
+
entitySalariesTitle: ({ entity, range }) => `${entity} Salaries (${range}/yr)`,
|
|
730
|
+
salariesInPlaceTitle: ({ place, range }) => `Salaries in ${place} (${range}/yr)`,
|
|
731
|
+
salariesInPlaceTitleNoRange: ({ place }) => `Salaries in ${place}`,
|
|
732
|
+
entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Salaries in ${place} (${range}/yr)`,
|
|
733
|
+
companySalariesTitle: ({ company, range }) => `${company} Salaries${range ? ` (${range}/yr)` : ""}`,
|
|
734
|
+
companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Salaries${range ? ` (${range}/yr)` : ""}`,
|
|
735
|
+
professionalsEarn: ({ entity, range, count }) => `${entity} professionals earn ${range}/yr on average across ${count} job postings.`,
|
|
736
|
+
rolesPay: ({ entity, range, count }) => `${entity} roles pay ${range}/yr on average across ${count} job postings.`,
|
|
737
|
+
professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity} professionals in ${place} earn ${range}/yr on average across ${count} job postings.`,
|
|
738
|
+
seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} roles start at ${lowAmount}, ${highLabel} roles reach ${highAmount}.`,
|
|
739
|
+
seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} salaries start at ${lowAmount}, while ${highLabel} roles reach ${highAmount}.`,
|
|
740
|
+
compareAcrossTopPaying: ({ count }) => `Compare across ${count} top-paying ${count === 1 ? "company" : "companies"}.`,
|
|
741
|
+
compareAcrossTop: ({ count }) => `Compare across ${count} top ${count === 1 ? "company" : "companies"}.`,
|
|
742
|
+
boardWideAverage: ({ entity, range }) => `Board-wide ${entity} average: ${range}/yr.`,
|
|
743
|
+
averageSalaryInCity: ({ place, range, count }) => `Average salary in ${place} is ${range}/yr across ${count} jobs. Browse salary data by title and skill.`,
|
|
744
|
+
browseLocationsInRegion: ({ count, place }) => `Browse salary data across ${count} locations in ${place}. Compare salaries by title, skill, and company.`,
|
|
745
|
+
companyPays: ({ company, range, count }) => `${company} pays ${range}/yr on average across ${count} job postings.`,
|
|
746
|
+
breakdownAcrossCategories: ({ count }) => `Breakdown across ${count} job ${count === 1 ? "category" : "categories"}.`,
|
|
747
|
+
compareWithSimilar: ({ count }) => `Compare with ${count} similar ${count === 1 ? "company" : "companies"}.`,
|
|
748
|
+
salaryInfoFor: ({ company, board }) => `Salary information for ${company} on ${board}.`,
|
|
749
|
+
categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category} roles at ${company} pay ${range}/yr based on ${count} job postings.`,
|
|
750
|
+
categorySalaryDataFor: ({ category, company, board }) => `${category} salary data for ${company} on ${board}.`
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
var DE = {
|
|
754
|
+
timeframe: {
|
|
755
|
+
per_year: "pro Jahr",
|
|
756
|
+
per_month: "pro Monat",
|
|
757
|
+
per_week: "pro Woche",
|
|
758
|
+
per_day: "pro Tag",
|
|
759
|
+
per_hour: "pro Stunde"
|
|
760
|
+
},
|
|
761
|
+
rangePrefix: { from: "ab", upTo: "bis zu" },
|
|
762
|
+
seniority: {
|
|
763
|
+
entry_level: "Entry-Level",
|
|
764
|
+
associate: "Associate",
|
|
765
|
+
mid_level: "Mid-Level",
|
|
766
|
+
senior: "Senior",
|
|
767
|
+
lead: "Lead",
|
|
768
|
+
principal: "Principal",
|
|
769
|
+
director: "Director",
|
|
770
|
+
executive: "F\xFChrungskraft"
|
|
771
|
+
},
|
|
772
|
+
frames: {
|
|
773
|
+
entitySalariesTitle: ({ entity, range }) => `${entity} Geh\xE4lter (${range}/Jahr)`,
|
|
774
|
+
salariesInPlaceTitle: ({ place, range }) => `Geh\xE4lter in ${place} (${range}/Jahr)`,
|
|
775
|
+
salariesInPlaceTitleNoRange: ({ place }) => `Geh\xE4lter in ${place}`,
|
|
776
|
+
entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Geh\xE4lter in ${place} (${range}/Jahr)`,
|
|
777
|
+
companySalariesTitle: ({ company, range }) => `${company} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
|
|
778
|
+
companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
|
|
779
|
+
professionalsEarn: ({ entity, range, count }) => `${entity}-Fachkr\xE4fte verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
|
|
780
|
+
rolesPay: ({ entity, range, count }) => `${entity}-Positionen werden mit durchschnittlich ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
|
|
781
|
+
professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity}-Fachkr\xE4fte in ${place} verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
|
|
782
|
+
seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Positionen beginnen bei ${lowAmount}, ${highLabel}-Positionen erreichen ${highAmount}.`,
|
|
783
|
+
seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Geh\xE4lter beginnen bei ${lowAmount}, w\xE4hrend ${highLabel}-Positionen ${highAmount} erreichen.`,
|
|
784
|
+
compareAcrossTopPaying: ({ count }) => `Vergleichen Sie ${count} ${count === 1 ? "bestzahlendes" : "bestzahlende"} Unternehmen.`,
|
|
785
|
+
compareAcrossTop: ({ count }) => `Vergleichen Sie ${count} Top-Unternehmen.`,
|
|
786
|
+
boardWideAverage: ({ entity, range }) => `Gesamtdurchschnitt ${entity}: ${range}/Jahr.`,
|
|
787
|
+
averageSalaryInCity: ({ place, range, count }) => `Das Durchschnittsgehalt in ${place} betr\xE4gt ${range}/Jahr, basierend auf ${count} Stellen. Gehaltsdaten nach Titel und F\xE4higkeiten durchsuchen.`,
|
|
788
|
+
browseLocationsInRegion: ({ count, place }) => `Gehaltsdaten f\xFCr ${count} Standorte in ${place} durchsuchen. Geh\xE4lter nach Titel, F\xE4higkeiten und Unternehmen vergleichen.`,
|
|
789
|
+
companyPays: ({ company, range, count }) => `${company} zahlt durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
|
|
790
|
+
breakdownAcrossCategories: ({ count }) => `Aufschl\xFCsselung \xFCber ${count} Job-${count === 1 ? "Kategorie" : "Kategorien"}.`,
|
|
791
|
+
compareWithSimilar: ({ count }) => `Vergleichen Sie mit ${count} \xE4hnlichen Unternehmen.`,
|
|
792
|
+
salaryInfoFor: ({ company, board }) => `Gehaltsinformationen f\xFCr ${company} auf ${board}.`,
|
|
793
|
+
categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category}-Positionen bei ${company} werden mit ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
|
|
794
|
+
categorySalaryDataFor: ({ category, company, board }) => `${category}-Gehaltsdaten f\xFCr ${company} auf ${board}.`
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
var LEXICONS = { en: EN, de: DE };
|
|
798
|
+
function getSalaryLexicon(locale) {
|
|
799
|
+
return locale && LEXICONS[locale] || EN;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/seo/salary.ts
|
|
803
|
+
var compactCurrencyCache = /* @__PURE__ */ new Map();
|
|
804
|
+
function getCompactCurrencyFormatter(language) {
|
|
805
|
+
const cached = compactCurrencyCache.get(language);
|
|
806
|
+
if (cached) return cached;
|
|
807
|
+
const formatter = new Intl.NumberFormat(language, {
|
|
808
|
+
style: "currency",
|
|
809
|
+
currency: "USD",
|
|
810
|
+
notation: "compact",
|
|
811
|
+
compactDisplay: "short",
|
|
812
|
+
minimumFractionDigits: 0,
|
|
813
|
+
maximumFractionDigits: 1
|
|
814
|
+
});
|
|
815
|
+
compactCurrencyCache.set(language, formatter);
|
|
816
|
+
return formatter;
|
|
817
|
+
}
|
|
818
|
+
function formatUsd(locale, value) {
|
|
819
|
+
try {
|
|
820
|
+
return getCompactCurrencyFormatter(locale).format(value);
|
|
821
|
+
} catch {
|
|
822
|
+
if (value >= 1e6) {
|
|
823
|
+
const millions = value / 1e6;
|
|
824
|
+
return `$${millions % 1 === 0 ? millions : millions.toFixed(1)}M`;
|
|
825
|
+
}
|
|
826
|
+
if (value >= 1e3) {
|
|
827
|
+
const thousands = value / 1e3;
|
|
828
|
+
return `$${thousands % 1 === 0 ? thousands : thousands.toFixed(1)}k`;
|
|
829
|
+
}
|
|
830
|
+
return `$${value}`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function formatRange(locale, min, max) {
|
|
834
|
+
return `${formatUsd(locale, min)} \u2013 ${formatUsd(locale, max)}`;
|
|
835
|
+
}
|
|
836
|
+
function formatSeniority(locale, value, overrides) {
|
|
837
|
+
if (overrides?.[value]) {
|
|
838
|
+
return overrides[value];
|
|
839
|
+
}
|
|
840
|
+
const lexiconLabel = getSalaryLexicon(locale).seniority[value];
|
|
841
|
+
if (lexiconLabel) {
|
|
842
|
+
return lexiconLabel;
|
|
843
|
+
}
|
|
844
|
+
return value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
845
|
+
}
|
|
846
|
+
var SENIORITY_ORDER = [
|
|
847
|
+
"entry_level",
|
|
848
|
+
"associate",
|
|
849
|
+
"mid_level",
|
|
850
|
+
"senior",
|
|
851
|
+
"lead",
|
|
852
|
+
"principal",
|
|
853
|
+
"director",
|
|
854
|
+
"executive"
|
|
855
|
+
];
|
|
856
|
+
function sortBySeniority(items) {
|
|
857
|
+
return [...items].sort((a, b) => {
|
|
858
|
+
const aIdx = SENIORITY_ORDER.indexOf(
|
|
859
|
+
a.seniority
|
|
860
|
+
);
|
|
861
|
+
const bIdx = SENIORITY_ORDER.indexOf(
|
|
862
|
+
b.seniority
|
|
863
|
+
);
|
|
864
|
+
return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
var yearly = {
|
|
868
|
+
unitText: "YEAR",
|
|
869
|
+
duration: "P1Y"
|
|
870
|
+
};
|
|
871
|
+
function distribution(name, currency, minValue, maxValue, stats = {}) {
|
|
872
|
+
return {
|
|
873
|
+
"@type": "MonetaryAmountDistribution",
|
|
874
|
+
name,
|
|
875
|
+
currency,
|
|
876
|
+
minValue,
|
|
877
|
+
maxValue,
|
|
878
|
+
...stats,
|
|
879
|
+
...yearly
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function seniorityDistributions(locale, rows, currency, nameFor) {
|
|
883
|
+
return rows.map(
|
|
884
|
+
(row) => distribution(
|
|
885
|
+
nameFor(formatSeniority(locale, row.seniority)),
|
|
886
|
+
currency,
|
|
887
|
+
row.avgSalaryMin,
|
|
888
|
+
row.avgSalaryMax
|
|
889
|
+
)
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
function salaryEnvelope(type, name, extra, estimatedSalary) {
|
|
893
|
+
return {
|
|
894
|
+
"@context": "https://schema.org",
|
|
895
|
+
"@type": type,
|
|
896
|
+
name,
|
|
897
|
+
...extra,
|
|
898
|
+
estimatedSalary
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function buildSalaryFaq(locale, label, overall) {
|
|
902
|
+
if (!overall) return [];
|
|
903
|
+
const range = formatRange(locale, overall.avgMin, overall.avgMax);
|
|
904
|
+
return [
|
|
905
|
+
{
|
|
906
|
+
q: `What is the average salary for ${label}?`,
|
|
907
|
+
a: `The average salary for ${label} is ${range} per year, based on ${overall.jobCount} job ${overall.jobCount === 1 ? "posting" : "postings"} on this board that disclose pay.`
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
q: `How is the salary figure for ${label} calculated?`,
|
|
911
|
+
a: `These figures are aggregated from current and recent job postings on this board that disclose pay, then averaged across roles. They are estimates, not guarantees of compensation.`
|
|
912
|
+
}
|
|
913
|
+
];
|
|
914
|
+
}
|
|
915
|
+
function itemListJsonLd(items) {
|
|
916
|
+
if (items.length === 0) return null;
|
|
917
|
+
return {
|
|
918
|
+
"@context": "https://schema.org",
|
|
919
|
+
"@type": "ItemList",
|
|
920
|
+
numberOfItems: items.length,
|
|
921
|
+
itemListElement: items.map((it, i) => ({
|
|
922
|
+
"@type": "ListItem",
|
|
923
|
+
position: i + 1,
|
|
924
|
+
name: it.name,
|
|
925
|
+
url: it.url
|
|
926
|
+
}))
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function faqJsonLd(faqs) {
|
|
930
|
+
if (faqs.length === 0) return null;
|
|
931
|
+
return {
|
|
932
|
+
"@context": "https://schema.org",
|
|
933
|
+
"@type": "FAQPage",
|
|
934
|
+
mainEntity: faqs.map((f) => ({
|
|
935
|
+
"@type": "Question",
|
|
936
|
+
name: f.q,
|
|
937
|
+
acceptedAnswer: { "@type": "Answer", text: f.a }
|
|
938
|
+
}))
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
function titleSalaryJsonLd(locale, d) {
|
|
942
|
+
if (!d.overallSalary) return null;
|
|
943
|
+
return salaryEnvelope("Occupation", d.categoryName, {}, [
|
|
944
|
+
distribution(
|
|
945
|
+
`${d.categoryName} salary (all levels)`,
|
|
946
|
+
d.currency,
|
|
947
|
+
d.overallSalary.avgMin,
|
|
948
|
+
d.overallSalary.avgMax,
|
|
949
|
+
{
|
|
950
|
+
...d.boardP25Min ? { percentile25: d.boardP25Min } : {},
|
|
951
|
+
...d.boardP75Max ? { percentile75: d.boardP75Max } : {}
|
|
952
|
+
}
|
|
953
|
+
),
|
|
954
|
+
...seniorityDistributions(
|
|
955
|
+
locale,
|
|
956
|
+
d.bySeniority,
|
|
957
|
+
d.currency,
|
|
958
|
+
(label) => `${label} ${d.categoryName}`
|
|
959
|
+
)
|
|
960
|
+
]);
|
|
961
|
+
}
|
|
962
|
+
function skillSalaryJsonLd(d) {
|
|
963
|
+
if (!d.overallSalary) return null;
|
|
964
|
+
return salaryEnvelope("Occupation", d.skillName, {}, [
|
|
965
|
+
distribution(
|
|
966
|
+
`${d.skillName} salary (all levels)`,
|
|
967
|
+
d.currency,
|
|
968
|
+
d.overallSalary.avgMin,
|
|
969
|
+
d.overallSalary.avgMax,
|
|
970
|
+
{
|
|
971
|
+
median: Math.round(
|
|
972
|
+
(d.overallSalary.medianMin + d.overallSalary.medianMax) / 2
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
)
|
|
976
|
+
]);
|
|
977
|
+
}
|
|
978
|
+
function locationSalaryJsonLd(d) {
|
|
979
|
+
const isCity = d.adminLevel === "city" || d.adminLevel === "locality";
|
|
980
|
+
if (!isCity || !d.overallSalary) return null;
|
|
981
|
+
return salaryEnvelope(
|
|
982
|
+
"Occupation",
|
|
983
|
+
`Average Salary in ${d.placeName}`,
|
|
984
|
+
{
|
|
985
|
+
occupationLocation: {
|
|
986
|
+
"@type": "City",
|
|
987
|
+
name: d.placeName,
|
|
988
|
+
address: { "@type": "PostalAddress", addressCountry: d.countryCode }
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
[
|
|
992
|
+
distribution(
|
|
993
|
+
`Average salary in ${d.placeName}`,
|
|
994
|
+
d.currency,
|
|
995
|
+
d.overallSalary.avgMin,
|
|
996
|
+
d.overallSalary.avgMax,
|
|
997
|
+
{
|
|
998
|
+
median: Math.round(
|
|
999
|
+
(d.overallSalary.medianMin + d.overallSalary.medianMax) / 2
|
|
1000
|
+
),
|
|
1001
|
+
// Hosted includes these unconditionally, nulls and all.
|
|
1002
|
+
percentile25: d.overallSalary.p25Min,
|
|
1003
|
+
percentile75: d.overallSalary.p75Max
|
|
1004
|
+
}
|
|
1005
|
+
)
|
|
1006
|
+
]
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
function crossAxisSalaryJsonLd(locale, args) {
|
|
1010
|
+
if (!args.overall) return null;
|
|
1011
|
+
return salaryEnvelope(
|
|
1012
|
+
"Occupation",
|
|
1013
|
+
`${args.name} salary in ${args.placeName}`,
|
|
1014
|
+
{
|
|
1015
|
+
occupationLocation: {
|
|
1016
|
+
"@type": "AdministrativeArea",
|
|
1017
|
+
name: args.placeName,
|
|
1018
|
+
address: {
|
|
1019
|
+
"@type": "PostalAddress",
|
|
1020
|
+
addressCountry: args.countryCode
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
[
|
|
1025
|
+
distribution(
|
|
1026
|
+
`${args.name} salary in ${args.placeName} (all levels)`,
|
|
1027
|
+
args.currency,
|
|
1028
|
+
args.overall.avgMin,
|
|
1029
|
+
args.overall.avgMax,
|
|
1030
|
+
{
|
|
1031
|
+
...args.overall.p25Min ? { percentile25: args.overall.p25Min } : {},
|
|
1032
|
+
...args.overall.p75Max ? { percentile75: args.overall.p75Max } : {}
|
|
1033
|
+
}
|
|
1034
|
+
),
|
|
1035
|
+
...seniorityDistributions(
|
|
1036
|
+
locale,
|
|
1037
|
+
args.bySeniority,
|
|
1038
|
+
args.currency,
|
|
1039
|
+
(label) => `${label} ${args.name} in ${args.placeName}`
|
|
1040
|
+
)
|
|
1041
|
+
]
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
function companySalaryJsonLd(locale, d) {
|
|
1045
|
+
if (!d.overallSalary) return null;
|
|
1046
|
+
return salaryEnvelope(
|
|
1047
|
+
"OccupationAggregationByEmployer",
|
|
1048
|
+
`Jobs at ${d.companyName}`,
|
|
1049
|
+
{
|
|
1050
|
+
sampleSize: d.overallSalary.jobCount,
|
|
1051
|
+
hiringOrganization: { "@type": "Organization", name: d.companyName }
|
|
1052
|
+
},
|
|
1053
|
+
[
|
|
1054
|
+
distribution(
|
|
1055
|
+
"base",
|
|
1056
|
+
d.currency,
|
|
1057
|
+
d.overallSalary.avgMin,
|
|
1058
|
+
d.overallSalary.avgMax,
|
|
1059
|
+
{
|
|
1060
|
+
...d.boardMedianMin && d.boardMedianMax ? { median: Math.round((d.boardMedianMin + d.boardMedianMax) / 2) } : {},
|
|
1061
|
+
...d.boardP25Min ? { percentile25: d.boardP25Min } : {},
|
|
1062
|
+
...d.boardP75Max ? { percentile75: d.boardP75Max } : {}
|
|
1063
|
+
}
|
|
1064
|
+
),
|
|
1065
|
+
...seniorityDistributions(
|
|
1066
|
+
locale,
|
|
1067
|
+
d.bySeniority,
|
|
1068
|
+
d.currency,
|
|
1069
|
+
(label) => `${label} salary`
|
|
1070
|
+
)
|
|
1071
|
+
]
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
function companyCategorySalaryJsonLd(locale, d) {
|
|
1075
|
+
if (!d.overallSalary) return null;
|
|
1076
|
+
return salaryEnvelope(
|
|
1077
|
+
"Occupation",
|
|
1078
|
+
`${d.categoryName} at ${d.companyName}`,
|
|
1079
|
+
{
|
|
1080
|
+
occupationalCategory: d.categoryName,
|
|
1081
|
+
hiringOrganization: { "@type": "Organization", name: d.companyName }
|
|
1082
|
+
},
|
|
1083
|
+
[
|
|
1084
|
+
distribution(
|
|
1085
|
+
`${d.categoryName} salary (all levels)`,
|
|
1086
|
+
d.currency,
|
|
1087
|
+
d.overallSalary.avgMin,
|
|
1088
|
+
d.overallSalary.avgMax,
|
|
1089
|
+
{
|
|
1090
|
+
...d.boardCategoryP25Min ? { percentile25: d.boardCategoryP25Min } : {},
|
|
1091
|
+
...d.boardCategoryP75Max ? { percentile75: d.boardCategoryP75Max } : {}
|
|
1092
|
+
}
|
|
1093
|
+
),
|
|
1094
|
+
...seniorityDistributions(
|
|
1095
|
+
locale,
|
|
1096
|
+
d.bySeniority,
|
|
1097
|
+
d.currency,
|
|
1098
|
+
(label) => `${label} ${d.categoryName}`
|
|
1099
|
+
)
|
|
1100
|
+
]
|
|
1101
|
+
);
|
|
1102
|
+
}
|