@cavuno/board 3.0.0 → 3.2.0

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