@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +421 -0
  2. package/README.md +6 -2
  3. package/dist/{_spec-nkWAraS0.d.mts → _spec-GVe6WAfT.d.mts} +68 -59
  4. package/dist/{_spec-nkWAraS0.d.ts → _spec-GVe6WAfT.d.ts} +68 -59
  5. package/dist/bin.mjs +7 -2
  6. package/dist/blog-BsmKizpd.d.ts +29 -0
  7. package/dist/blog-Cf64mI8A.d.mts +29 -0
  8. package/dist/{salaries-BahBpxWT.d.ts → board-DUhgiacm.d.ts} +17 -29
  9. package/dist/{salaries-ChSqDBAM.d.mts → board-GGWJ4coe.d.mts} +17 -29
  10. package/dist/breadcrumbs-DFKr0HzU.d.mts +372 -0
  11. package/dist/breadcrumbs-DiccJhk4.d.ts +372 -0
  12. package/dist/chunk-2ETP5UH4.mjs +271 -0
  13. package/dist/chunk-BGKNXG4H.mjs +146 -0
  14. package/dist/chunk-DLSO6XXY.mjs +33 -0
  15. package/dist/chunk-T2KC6XGV.mjs +1060 -0
  16. package/dist/chunk-YEAQ3JEI.mjs +853 -0
  17. package/dist/doctor.js +1 -1
  18. package/dist/doctor.mjs +1 -1
  19. package/dist/filters.d.mts +25 -20
  20. package/dist/filters.d.ts +25 -20
  21. package/dist/filters.js +3 -692
  22. package/dist/filters.mjs +3 -690
  23. package/dist/format.d.mts +57 -230
  24. package/dist/format.d.ts +57 -230
  25. package/dist/format.js +355 -927
  26. package/dist/format.mjs +213 -931
  27. package/dist/go.js +11 -1
  28. package/dist/go.mjs +4 -29
  29. package/dist/index.d.mts +23 -75
  30. package/dist/index.d.ts +23 -75
  31. package/dist/index.js +11 -36
  32. package/dist/index.mjs +32 -313
  33. package/dist/{jobs-lVvKCBCU.d.mts → jobs-BuLpV8iy.d.mts} +4 -4
  34. package/dist/{jobs-BiEvL9d7.d.ts → jobs-QrbXOYB9.d.ts} +4 -4
  35. package/dist/paths.d.mts +89 -1
  36. package/dist/paths.d.ts +89 -1
  37. package/dist/paths.js +62 -14
  38. package/dist/paths.mjs +29 -78
  39. package/dist/route-contract.js +23 -13
  40. package/dist/route-contract.mjs +30 -1079
  41. package/dist/{search-CCyFWPi3.d.mts → search-Cr4noJJZ.d.mts} +14 -3
  42. package/dist/{search-CyRvVMUp.d.ts → search-DdsniXQv.d.ts} +14 -3
  43. package/dist/seo.d.mts +33 -189
  44. package/dist/seo.d.ts +33 -189
  45. package/dist/seo.js +197 -718
  46. package/dist/seo.mjs +44 -1417
  47. package/dist/server.d.mts +5 -5
  48. package/dist/server.d.ts +5 -5
  49. package/dist/server.mjs +5 -31
  50. package/dist/sitemap.d.mts +5 -5
  51. package/dist/sitemap.d.ts +5 -5
  52. package/dist/sitemap.js +25 -14
  53. package/dist/sitemap.mjs +22 -97
  54. package/dist/skills.js +1 -1
  55. package/dist/skills.mjs +1 -1
  56. package/dist/suggest.d.mts +2 -2
  57. package/dist/suggest.d.ts +2 -2
  58. package/dist/suggest.js +17 -3
  59. package/dist/suggest.mjs +17 -3
  60. package/dist/well-known.js +23 -13
  61. package/dist/well-known.mjs +4 -421
  62. package/package.json +5 -4
  63. package/skills/cavuno-board-applications/SKILL.md +4 -1
  64. package/skills/cavuno-board-filters/SKILL.md +11 -12
  65. package/skills/cavuno-board-format/SKILL.md +68 -56
  66. package/skills/cavuno-board-i18n/SKILL.md +102 -21
  67. package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
  68. package/skills/cavuno-board-seo/BLOG.md +4 -1
  69. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
  70. package/skills/cavuno-board-seo/LISTING.md +35 -12
  71. package/skills/cavuno-board-seo/SALARY.md +83 -18
  72. package/skills/cavuno-board-seo/SKILL.md +3 -2
  73. package/skills/manifest.json +3 -3
  74. package/dist/board-NE98PlwN.d.ts +0 -16
  75. package/dist/board-muu8BI8P.d.mts +0 -16
  76. package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
  77. package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
package/dist/seo.js CHANGED
@@ -32,9 +32,8 @@ __export(seo_exports, {
32
32
  createJobPostingJsonLd: () => createJobPostingJsonLd,
33
33
  crossAxisSalaryJsonLd: () => crossAxisSalaryJsonLd,
34
34
  faqJsonLd: () => faqJsonLd,
35
- formatRange: () => formatRange,
36
- formatSeniority: () => formatSeniority,
37
- formatUsd: () => formatUsd,
35
+ formatSalaryStat: () => formatSalaryStat,
36
+ formatSalaryStatRange: () => formatSalaryStatRange,
38
37
  itemListJsonLd: () => itemListJsonLd,
39
38
  listingHead: () => listingHead,
40
39
  listingJsonLd: () => listingJsonLd,
@@ -161,490 +160,56 @@ function buildAbsoluteUrl(origin, path) {
161
160
  }
162
161
  }
163
162
 
164
- // src/format/ui-copy.ts
165
- var EN = {
166
- jobCard: {
167
- featuredLabel: "Featured",
168
- aiRankedLabel: "AI-ranked",
169
- sortNewestLabel: "Most recent",
170
- sortSalaryHighLabel: "Salary: high to low"
171
- },
172
- jobSearch: {
173
- headingJobs: "Jobs",
174
- keywordLabel: "Keyword",
175
- keywordPlaceholder: "Search jobs\u2026",
176
- locationLabel: "Location",
177
- locationPlaceholder: "City or region",
178
- workplacePlaceholder: "Workplace",
179
- anyWorkplaceLabel: "Any workplace",
180
- typePlaceholder: "Type",
181
- anyTypeLabel: "Any type",
182
- sortPlaceholder: "Sort",
183
- loadMoreLabel: "Load more",
184
- noJobsMatchText: "No jobs match \u2014 try clearing a filter."
185
- },
186
- jobDetail: {
187
- categoriesHeading: "Categories",
188
- skillsHeading: "Skills",
189
- additionalDetailsHeading: "Additional details",
190
- customFieldYesLabel: "Yes",
191
- customFieldNoLabel: "No",
192
- locationsLabel: "Locations",
193
- workPermitsLabel: "Work permits",
194
- timezonesLabel: "Timezones",
195
- educationLabel: "Education",
196
- experienceLabel: "Experience",
197
- worldwideLabel: "Worldwide",
198
- noExperienceRequiredLabel: "No experience required",
199
- experienceYears: (years) => `${years}+ years`,
200
- posted: (published) => `Posted ${published}`,
201
- noDescriptionText: "No description provided.",
202
- viewCompanyProfileLabel: "View company profile",
203
- similarJobsHeading: "Similar jobs",
204
- breadcrumbAriaLabel: "Breadcrumb"
205
- },
206
- apply: {
207
- applyButtonText: "Apply for this job",
208
- applyingLabel: "Applying\u2026",
209
- applyOnEmployerSiteLabel: "Apply on employer site",
210
- signInToApplyLabel: "Sign in to apply",
211
- verifyEmailToApplyLabel: "Verify email to apply",
212
- appliedViewApplicationsLabel: "Applied \u2014 view applications",
213
- applicationSubmitError: "Something went wrong. Please try again."
214
- },
215
- alerts: {
216
- jobAlertTitle: "Get job alerts",
217
- jobAlertEmailPlaceholder: "you@example.com",
218
- jobAlertButtonText: "Get alerts",
219
- subscribingLabel: "Subscribing\u2026",
220
- jobAlertSuccessToast: "If this email isn't already subscribed, we've sent a confirmation link \u2014 check your inbox.",
221
- jobAlertErrorToast: "Something went wrong. Please try again.",
222
- sectionAriaLabel: "Job alerts",
223
- emailAriaLabel: "email",
224
- submitAriaLabel: "get job alerts"
225
- },
226
- copyLink: {
227
- copyLinkLabel: "Copy link",
228
- copiedLabel: "Copied",
229
- ariaLabel: "copy link"
230
- },
231
- salary: {
232
- comparisonHeadlineAverage: "Average salary",
233
- perYearSuffix: "/ year",
234
- comparisonPercentile25Label: "25th percentile",
235
- medianLabel: "Median",
236
- comparisonPercentile75Label: "75th percentile",
237
- basedOnLabel: "Based on",
238
- seniorityTableHeaderLevel: "Level",
239
- seniorityTableHeaderAvg: "Average",
240
- boardBaselineLabel: "Board baseline",
241
- seniorityTableHeaderDiff: "vs. board",
242
- faqHeading: "Frequently asked questions"
243
- },
244
- nav: {
245
- home: "Jobs",
246
- companies: "Companies",
247
- pricing: "Pricing",
248
- talent: "Talent",
249
- post: "Post a job",
250
- blog: "Blog"
251
- },
252
- footer: {
253
- forCandidatesHeading: "For Candidates",
254
- forCompaniesHeading: "For Companies",
255
- resourcesHeading: "Resources",
256
- aboutHeading: "About",
257
- websiteLabel: "Website",
258
- aboutLabel: "About",
259
- contactLabel: "Contact us",
260
- locationsLabel: "Locations",
261
- salariesLabel: "Salaries",
262
- sitemapLabel: "Sitemap",
263
- termsOfServiceLabel: "Terms of Service",
264
- privacyPolicyLabel: "Privacy Policy",
265
- cookiePolicyLabel: "Cookie Policy",
266
- impressumLabel: "Impressum",
267
- allRightsReservedText: "All rights reserved.",
268
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
269
- poweredByText: "Powered by",
270
- defaultDescription: "Discover the latest roles from {{board_name}}."
271
- },
272
- breadcrumbs: {
273
- home: "Home",
274
- jobs: "Jobs",
275
- locations: "Locations",
276
- salaries: "Salaries",
277
- companies: "Companies",
278
- skills: "Skills",
279
- titles: "Titles",
280
- blog: "Blog",
281
- post: "Post a Job",
282
- pricing: "Pricing",
283
- about: "About",
284
- impressum: "Impressum",
285
- termsOfService: "Terms of Service",
286
- privacyPolicy: "Privacy Policy",
287
- cookiePolicy: "Cookie Policy",
288
- talent: "Talent"
289
- },
290
- pagination: {
291
- ariaLabel: "Pagination",
292
- previousLabel: "Previous",
293
- nextLabel: "Next"
294
- },
295
- blog: {
296
- bylineLabel: "By",
297
- readingTimeLabel: "min read",
298
- searchLabel: "Search",
299
- searchPlaceholder: "Search posts...",
300
- clearButtonLabel: "Clear search",
301
- tagFilterLabel: "Topics",
302
- tagFilterAllLabel: "All",
303
- emptyTitle: "No matching posts",
304
- emptyDescription: "Try adjusting your search or clearing your filters.",
305
- emptyResetLabel: "Clear filters"
306
- },
307
- entity: {
308
- jobSingular: "job",
309
- jobPlural: "jobs",
310
- companySingular: "company",
311
- companyPlural: "companies"
312
- }
313
- };
314
- var DE = {
315
- jobCard: {
316
- featuredLabel: "Hervorgehoben",
317
- aiRankedLabel: "KI-sortiert",
318
- sortNewestLabel: "Neueste zuerst",
319
- sortSalaryHighLabel: "Gehalt: absteigend"
320
- },
321
- jobSearch: {
322
- headingJobs: "Jobs",
323
- keywordLabel: "Stichwort",
324
- keywordPlaceholder: "Jobtitel oder -beschreibungen suchen",
325
- locationLabel: "Standort",
326
- locationPlaceholder: "Stadt, Land oder Region",
327
- workplacePlaceholder: "Arbeitsmodell",
328
- anyWorkplaceLabel: "Alle Arbeitsmodelle",
329
- typePlaceholder: "Anstellungsart",
330
- anyTypeLabel: "Alle Anstellungsarten",
331
- sortPlaceholder: "Sortieren",
332
- loadMoreLabel: "Mehr laden",
333
- noJobsMatchText: "Keine passenden Jobs \u2014 setze einen Filter zur\xFCck."
334
- },
335
- jobDetail: {
336
- categoriesHeading: "Kategorien",
337
- skillsHeading: "F\xE4higkeiten",
338
- additionalDetailsHeading: "Weitere Details",
339
- customFieldYesLabel: "Ja",
340
- customFieldNoLabel: "Nein",
341
- locationsLabel: "Standorte",
342
- workPermitsLabel: "Arbeitserlaubnis",
343
- timezonesLabel: "Zeitzonen",
344
- educationLabel: "Ausbildung",
345
- experienceLabel: "Berufserfahrung",
346
- worldwideLabel: "Weltweit",
347
- noExperienceRequiredLabel: "Keine Berufserfahrung erforderlich",
348
- experienceYears: (years) => `${years}+ Jahre`,
349
- posted: (published) => `Ver\xF6ffentlicht ${published}`,
350
- noDescriptionText: "Keine Beschreibung vorhanden.",
351
- viewCompanyProfileLabel: "Unternehmensprofil ansehen",
352
- similarJobsHeading: "\xC4hnliche Jobs",
353
- breadcrumbAriaLabel: "Seitenpfad"
354
- },
355
- apply: {
356
- applyButtonText: "Jetzt bewerben",
357
- applyingLabel: "Wird gesendet \u2026",
358
- applyOnEmployerSiteLabel: "Beim Unternehmen bewerben",
359
- signInToApplyLabel: "Zum Bewerben anmelden",
360
- verifyEmailToApplyLabel: "E-Mail best\xE4tigen, um dich zu bewerben",
361
- appliedViewApplicationsLabel: "Beworben \u2014 Bewerbungen ansehen",
362
- applicationSubmitError: "Etwas ist schiefgelaufen. Bitte versuche es erneut."
363
- },
364
- alerts: {
365
- jobAlertTitle: "Job-Benachrichtigungen erhalten",
366
- jobAlertEmailPlaceholder: "name@beispiel.de",
367
- jobAlertButtonText: "Abonnieren",
368
- subscribingLabel: "Wird abonniert \u2026",
369
- jobAlertSuccessToast: "Falls diese E-Mail noch nicht abonniert ist, haben wir einen Best\xE4tigungslink gesendet \u2014 pr\xFCfe deinen Posteingang.",
370
- jobAlertErrorToast: "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
371
- sectionAriaLabel: "Job-Benachrichtigungen",
372
- emailAriaLabel: "E-Mail",
373
- submitAriaLabel: "Job-Benachrichtigungen erhalten"
374
- },
375
- copyLink: {
376
- copyLinkLabel: "Link kopieren",
377
- copiedLabel: "Kopiert",
378
- ariaLabel: "Link kopieren"
379
- },
380
- salary: {
381
- comparisonHeadlineAverage: "Durchschnittsgehalt",
382
- perYearSuffix: "/ Jahr",
383
- comparisonPercentile25Label: "25. Perzentil",
384
- medianLabel: "Median",
385
- comparisonPercentile75Label: "75. Perzentil",
386
- basedOnLabel: "Basierend auf",
387
- seniorityTableHeaderLevel: "Erfahrungslevel",
388
- seniorityTableHeaderAvg: "Durchschnitt",
389
- boardBaselineLabel: "Board-Durchschnitt",
390
- seniorityTableHeaderDiff: "vs. Board",
391
- faqHeading: "H\xE4ufig gestellte Fragen"
392
- },
393
- nav: {
394
- home: "Jobs",
395
- companies: "Unternehmen",
396
- pricing: "Preise",
397
- talent: "Talente",
398
- post: "Job ver\xF6ffentlichen",
399
- blog: "Blog"
400
- },
401
- footer: {
402
- forCandidatesHeading: "F\xFCr Kandidaten",
403
- forCompaniesHeading: "F\xFCr Unternehmen",
404
- resourcesHeading: "Ressourcen",
405
- aboutHeading: "\xDCber uns",
406
- websiteLabel: "Website",
407
- aboutLabel: "\xDCber uns",
408
- contactLabel: "Kontakt",
409
- locationsLabel: "Standorte",
410
- salariesLabel: "Geh\xE4lter",
411
- sitemapLabel: "Sitemap",
412
- termsOfServiceLabel: "Nutzungsbedingungen",
413
- privacyPolicyLabel: "Datenschutzerkl\xE4rung",
414
- cookiePolicyLabel: "Cookie-Richtlinie",
415
- impressumLabel: "Impressum",
416
- allRightsReservedText: "Alle Rechte vorbehalten.",
417
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
418
- poweredByText: "Bereitgestellt von",
419
- defaultDescription: "Entdecke die neuesten Stellen von {{board_name}}."
420
- },
421
- breadcrumbs: {
422
- home: "Startseite",
423
- jobs: "Jobs",
424
- locations: "Standorte",
425
- salaries: "Geh\xE4lter",
426
- companies: "Unternehmen",
427
- skills: "F\xE4higkeiten",
428
- titles: "Berufsbezeichnungen",
429
- blog: "Blog",
430
- post: "Job ver\xF6ffentlichen",
431
- pricing: "Preise",
432
- about: "\xDCber uns",
433
- impressum: "Impressum",
434
- termsOfService: "Nutzungsbedingungen",
435
- privacyPolicy: "Datenschutzerkl\xE4rung",
436
- cookiePolicy: "Cookie-Richtlinie",
437
- talent: "Talente"
438
- },
439
- pagination: {
440
- ariaLabel: "Seitennummerierung",
441
- previousLabel: "Zur\xFCck",
442
- nextLabel: "Weiter"
443
- },
444
- blog: {
445
- bylineLabel: "Von",
446
- readingTimeLabel: "Minuten Lesezeit",
447
- searchLabel: "Suche",
448
- searchPlaceholder: "Artikel suchen...",
449
- clearButtonLabel: "Suche leeren",
450
- tagFilterLabel: "Themen",
451
- tagFilterAllLabel: "Alle",
452
- emptyTitle: "Keine passenden Artikel gefunden",
453
- emptyDescription: "Versuche, deine Suche anzupassen oder die Filter zur\xFCckzusetzen.",
454
- emptyResetLabel: "Filter zur\xFCcksetzen"
455
- },
456
- entity: {
457
- jobSingular: "Job",
458
- jobPlural: "Jobs",
459
- companySingular: "Unternehmen",
460
- companyPlural: "Unternehmen"
461
- }
462
- };
463
- var FR = {
464
- jobCard: {
465
- featuredLabel: "\xC0 la une",
466
- aiRankedLabel: "Classement IA",
467
- sortNewestLabel: "Plus r\xE9centes",
468
- sortSalaryHighLabel: "Salaire : d\xE9croissant"
469
- },
470
- jobSearch: {
471
- headingJobs: "Offres d'emploi",
472
- keywordLabel: "Mot-cl\xE9",
473
- keywordPlaceholder: "Rechercher une offre\u2026",
474
- locationLabel: "Lieu",
475
- locationPlaceholder: "Ville ou r\xE9gion",
476
- workplacePlaceholder: "Mode de travail",
477
- anyWorkplaceLabel: "Tous les modes de travail",
478
- typePlaceholder: "Type de contrat",
479
- anyTypeLabel: "Tous les types",
480
- sortPlaceholder: "Trier",
481
- loadMoreLabel: "Voir plus",
482
- noJobsMatchText: "Aucune offre ne correspond \u2014 essayez de retirer un filtre."
483
- },
484
- jobDetail: {
485
- categoriesHeading: "Cat\xE9gories",
486
- skillsHeading: "Comp\xE9tences",
487
- additionalDetailsHeading: "Informations compl\xE9mentaires",
488
- customFieldYesLabel: "Oui",
489
- customFieldNoLabel: "Non",
490
- locationsLabel: "Lieux",
491
- workPermitsLabel: "Permis de travail",
492
- timezonesLabel: "Fuseaux horaires",
493
- educationLabel: "Formation",
494
- experienceLabel: "Exp\xE9rience",
495
- worldwideLabel: "Monde entier",
496
- noExperienceRequiredLabel: "Aucune exp\xE9rience requise",
497
- experienceYears: (years) => `${years}+ ans`,
498
- posted: (published) => `Publi\xE9e ${published}`,
499
- noDescriptionText: "Aucune description fournie.",
500
- viewCompanyProfileLabel: "Voir le profil de l'entreprise",
501
- similarJobsHeading: "Offres similaires",
502
- breadcrumbAriaLabel: "Fil d'Ariane"
503
- },
504
- apply: {
505
- applyButtonText: "Postuler \xE0 cette offre",
506
- applyingLabel: "Envoi en cours\u2026",
507
- applyOnEmployerSiteLabel: "Postuler sur le site de l'employeur",
508
- signInToApplyLabel: "Connectez-vous pour postuler",
509
- verifyEmailToApplyLabel: "V\xE9rifiez votre e-mail pour postuler",
510
- appliedViewApplicationsLabel: "Candidature envoy\xE9e \u2014 voir mes candidatures",
511
- applicationSubmitError: "Une erreur est survenue. Veuillez r\xE9essayer."
512
- },
513
- alerts: {
514
- jobAlertTitle: "Recevoir des alertes emploi",
515
- jobAlertEmailPlaceholder: "vous@exemple.fr",
516
- jobAlertButtonText: "Activer les alertes",
517
- subscribingLabel: "Abonnement en cours\u2026",
518
- jobAlertSuccessToast: "Si cet e-mail n'est pas d\xE9j\xE0 abonn\xE9, nous avons envoy\xE9 un lien de confirmation \u2014 v\xE9rifiez votre bo\xEEte de r\xE9ception.",
519
- jobAlertErrorToast: "Une erreur est survenue. Veuillez r\xE9essayer.",
520
- sectionAriaLabel: "Alertes emploi",
521
- emailAriaLabel: "e-mail",
522
- submitAriaLabel: "recevoir des alertes emploi"
523
- },
524
- copyLink: {
525
- copyLinkLabel: "Copier le lien",
526
- copiedLabel: "Copi\xE9",
527
- ariaLabel: "copier le lien"
528
- },
529
- salary: {
530
- comparisonHeadlineAverage: "Salaire moyen",
531
- perYearSuffix: "/ an",
532
- comparisonPercentile25Label: "25e percentile",
533
- medianLabel: "M\xE9diane",
534
- comparisonPercentile75Label: "75e percentile",
535
- basedOnLabel: "Bas\xE9 sur",
536
- seniorityTableHeaderLevel: "Niveau",
537
- seniorityTableHeaderAvg: "Moyenne",
538
- boardBaselineLabel: "R\xE9f\xE9rence du site",
539
- seniorityTableHeaderDiff: "vs site",
540
- faqHeading: "Questions fr\xE9quentes"
541
- },
542
- nav: {
543
- home: "Offres",
544
- companies: "Entreprises",
545
- pricing: "Tarifs",
546
- talent: "Talents",
547
- post: "Publier une offre",
548
- blog: "Blog"
549
- },
550
- footer: {
551
- forCandidatesHeading: "Pour les candidats",
552
- forCompaniesHeading: "Pour les entreprises",
553
- resourcesHeading: "Ressources",
554
- aboutHeading: "\xC0 propos",
555
- websiteLabel: "Site web",
556
- aboutLabel: "\xC0 propos",
557
- contactLabel: "Nous contacter",
558
- locationsLabel: "Lieux",
559
- salariesLabel: "Salaires",
560
- sitemapLabel: "Plan du site",
561
- termsOfServiceLabel: "Conditions d'utilisation",
562
- privacyPolicyLabel: "Politique de confidentialit\xE9",
563
- cookiePolicyLabel: "Politique de cookies",
564
- impressumLabel: "Mentions l\xE9gales",
565
- allRightsReservedText: "Tous droits r\xE9serv\xE9s.",
566
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
567
- poweredByText: "Propuls\xE9 par",
568
- defaultDescription: "D\xE9couvrez les derni\xE8res offres de {{board_name}}."
569
- },
570
- breadcrumbs: {
571
- home: "Accueil",
572
- jobs: "Offres",
573
- locations: "Lieux",
574
- salaries: "Salaires",
575
- companies: "Entreprises",
576
- skills: "Comp\xE9tences",
577
- titles: "Intitul\xE9s de poste",
578
- blog: "Blog",
579
- post: "Publier une offre",
580
- pricing: "Tarifs",
581
- about: "\xC0 propos",
582
- impressum: "Mentions l\xE9gales",
583
- termsOfService: "Conditions d'utilisation",
584
- privacyPolicy: "Politique de confidentialit\xE9",
585
- cookiePolicy: "Politique de cookies",
586
- talent: "Talents"
587
- },
588
- pagination: {
589
- ariaLabel: "Pagination",
590
- previousLabel: "Pr\xE9c\xE9dent",
591
- nextLabel: "Suivant"
592
- },
593
- blog: {
594
- bylineLabel: "Par",
595
- readingTimeLabel: "min de lecture",
596
- searchLabel: "Recherche",
597
- searchPlaceholder: "Rechercher un article...",
598
- clearButtonLabel: "Effacer la recherche",
599
- tagFilterLabel: "Th\xE8mes",
600
- tagFilterAllLabel: "Tous",
601
- emptyTitle: "Aucun article correspondant",
602
- emptyDescription: "Essayez de modifier votre recherche ou de r\xE9initialiser les filtres.",
603
- emptyResetLabel: "R\xE9initialiser les filtres"
604
- },
605
- entity: {
606
- jobSingular: "offre",
607
- jobPlural: "offres",
608
- companySingular: "entreprise",
609
- companyPlural: "entreprises"
610
- }
611
- };
612
- var CATALOGS = { en: EN, de: DE, fr: FR };
613
- function mergeGroup(group, overrides) {
614
- if (!overrides) return group;
615
- const out = { ...group };
616
- for (const [key, value] of Object.entries(group)) {
617
- if (typeof value !== "string") continue;
618
- const override = overrides[key];
619
- if (typeof override === "string" && override.trim() !== "") {
620
- out[key] = override;
621
- }
163
+ // src/paths/index.ts
164
+ function encodePathSegment(segment) {
165
+ try {
166
+ return encodeURIComponent(decodeURIComponent(segment)).replace(
167
+ /%3A/gi,
168
+ ":"
169
+ );
170
+ } catch {
171
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
622
172
  }
623
- return out;
624
173
  }
625
- var GROUP_OVERRIDE_SOURCE = {
626
- jobCard: "jobCardLabels",
627
- jobSearch: "jobSearchLabels",
628
- jobDetail: "jobCardLabels",
629
- apply: "jobCardLabels",
630
- alerts: "jobCardLabels",
631
- copyLink: "jobCardLabels",
632
- salary: "salaryLabels",
633
- nav: "navLabels",
634
- footer: "footerLabels",
635
- breadcrumbs: "breadcrumbsLabels",
636
- pagination: "globalPaginationLabels",
637
- blog: "blogSharedLabels",
638
- entity: "entityLabels"
174
+ function jobDetailPath(companySlug, jobSlug) {
175
+ return `/companies/${encodePathSegment(companySlug)}/jobs/${encodePathSegment(jobSlug)}`;
176
+ }
177
+ function jobsCategoryPath(categorySlug) {
178
+ return `/jobs/${encodePathSegment(categorySlug)}`;
179
+ }
180
+ function jobsLocationPath(placeSlug) {
181
+ return `/jobs/locations/${encodePathSegment(placeSlug)}`;
182
+ }
183
+ function jobsLocationCategoryPath(placeSlug, categorySlug) {
184
+ return `${jobsLocationPath(placeSlug)}/${encodePathSegment(categorySlug)}`;
185
+ }
186
+ var BOARD_PATHS = {
187
+ home: "/",
188
+ jobs: "/jobs",
189
+ companies: "/companies",
190
+ salaries: "/salaries",
191
+ salaryCompanies: "/salaries/companies",
192
+ salaryTitles: "/salaries/titles",
193
+ salarySkills: "/salaries/skills",
194
+ salaryLocations: "/salaries/locations",
195
+ blog: "/blog",
196
+ about: "/about",
197
+ privacyPolicy: "/privacy-policy",
198
+ termsOfService: "/terms-of-service",
199
+ cookiePolicy: "/cookie-policy",
200
+ impressum: "/impressum",
201
+ talent: "/talent",
202
+ employers: "/employers",
203
+ /**
204
+ * Alert-email surfaces. Promoted out of raw literals in
205
+ * the job-alerts composer so /go indirection and email composition share
206
+ * one definition.
207
+ */
208
+ alertsManage: "/alerts/manage",
209
+ alertsConfirm: "/alerts/confirm"
639
210
  };
640
- function uiCopy(language, labels) {
641
- const catalog = language && CATALOGS[language] || EN;
642
- if (!labels) return catalog;
643
- const resolved = {};
644
- for (const [group, source] of Object.entries(GROUP_OVERRIDE_SOURCE)) {
645
- resolved[group] = mergeGroup(catalog[group], labels[source]);
646
- }
647
- return resolved;
211
+ function boardUrl(origin, path) {
212
+ return `${origin.replace(/\/+$/, "")}${path}`;
648
213
  }
649
214
 
650
215
  // src/seo/breadcrumbs.ts
@@ -690,25 +255,29 @@ function resolveBreadcrumbHref(href, origin) {
690
255
  function isAbsoluteUrl(href) {
691
256
  return /^https?:\/\//i.test(href);
692
257
  }
693
- function buildJobBreadcrumbs(job, language, labels) {
694
- const copy = uiCopy(language, labels).breadcrumbs;
258
+ function buildJobBreadcrumbs(job) {
695
259
  const crumbs = [
696
- { name: copy.home, path: "/" },
697
- { name: copy.jobs, path: "/jobs" }
260
+ { kind: "home", path: BOARD_PATHS.home },
261
+ { kind: "jobs", path: BOARD_PATHS.jobs }
698
262
  ];
699
263
  let lastPlaceSlug = null;
700
264
  for (const place of job.placeHierarchy) {
701
265
  lastPlaceSlug = place.slug;
702
- crumbs.push({ name: place.name, path: `/jobs/locations/${place.slug}` });
266
+ crumbs.push({
267
+ kind: "place",
268
+ name: place.name,
269
+ path: jobsLocationPath(place.slug)
270
+ });
703
271
  }
704
272
  const primaryCategory = job.categories[0];
705
273
  if (primaryCategory) {
706
274
  crumbs.push({
275
+ kind: "category",
707
276
  name: primaryCategory.name,
708
- path: lastPlaceSlug ? `/jobs/locations/${lastPlaceSlug}/${primaryCategory.slug}` : `/jobs/${primaryCategory.slug}`
277
+ path: lastPlaceSlug ? jobsLocationCategoryPath(lastPlaceSlug, primaryCategory.slug) : jobsCategoryPath(primaryCategory.slug)
709
278
  });
710
279
  }
711
- crumbs.push({ name: job.title });
280
+ crumbs.push({ kind: "job", name: job.title });
712
281
  return crumbs;
713
282
  }
714
283
 
@@ -1165,9 +734,8 @@ function pruneJsonLd(value) {
1165
734
 
1166
735
  // src/seo/listing.ts
1167
736
  function listingHead(options) {
1168
- const countPrefix = typeof options.count === "number" ? `${options.count} ` : "";
1169
- const title = `${countPrefix}${options.heading} | ${options.boardName}`;
1170
- const description = `Browse ${countPrefix}${options.heading.toLowerCase()} on ${options.boardName}.`;
737
+ const title = options.title;
738
+ const description = options.description;
1171
739
  const url = `${options.origin}${options.path}`;
1172
740
  return {
1173
741
  meta: [
@@ -1182,174 +750,112 @@ function listingHead(options) {
1182
750
  };
1183
751
  }
1184
752
  function listingJsonLd(options) {
1185
- const jobUrl = (job) => job.company ? `${options.origin}/companies/${job.company.slug}/jobs/${job.slug}` : `${options.origin}/jobs/${job.slug}`;
1186
- const objects = [
1187
- {
753
+ const objects = [];
754
+ if (options.breadcrumbs.length >= 2) {
755
+ objects.push({
1188
756
  "@context": "https://schema.org",
1189
- // Deliberately NOT createBreadcrumbJsonLd (breadcrumbs.ts): listing trails
1190
- // are code-constructed (never user copy), so the hosted listing page skips
1191
- // the trim/drop-blank/min-2 gate that module transcribes. Same schema.org
1192
- // fragment, different (intentional) semantics — do not unify blindly.
1193
757
  "@type": "BreadcrumbList",
1194
758
  itemListElement: options.breadcrumbs.map((crumb, index) => ({
1195
759
  "@type": "ListItem",
1196
760
  position: index + 1,
1197
761
  name: crumb.name,
1198
- ...crumb.path ? { item: `${options.origin}${crumb.path}` } : {}
762
+ ...crumb.path ? { item: boardUrl(options.origin, crumb.path) } : {}
1199
763
  }))
1200
- }
1201
- ];
1202
- if (options.jobs && options.jobs.length > 0) {
764
+ });
765
+ }
766
+ const detailJobs = (options.jobs ?? []).filter(
767
+ (job) => job.company != null && Boolean(job.company.slug)
768
+ );
769
+ if (detailJobs.length > 0) {
1203
770
  objects.push({
1204
771
  "@context": "https://schema.org",
1205
772
  "@type": "ItemList",
1206
- itemListElement: options.jobs.map((job, index) => ({
773
+ itemListElement: detailJobs.map((job, index) => ({
1207
774
  "@type": "ListItem",
1208
775
  position: index + 1,
1209
- url: jobUrl(job)
776
+ url: boardUrl(
777
+ options.origin,
778
+ jobDetailPath(job.company.slug, job.slug)
779
+ )
1210
780
  }))
1211
781
  });
1212
782
  }
1213
783
  return objects;
1214
784
  }
1215
785
 
1216
- // src/format/salary-lexicon.ts
1217
- var EN2 = {
1218
- timeframe: {
1219
- per_year: "Yearly",
1220
- per_month: "Monthly",
1221
- per_week: "Weekly",
1222
- per_day: "Daily",
1223
- per_hour: "Hourly"
1224
- },
1225
- rangePrefix: { from: "From", upTo: "Up to" },
1226
- seniority: {
1227
- entry_level: "Entry level",
1228
- associate: "Associate",
1229
- mid_level: "Mid-level",
1230
- senior: "Senior",
1231
- lead: "Lead",
1232
- principal: "Principal",
1233
- director: "Director",
1234
- executive: "Executive"
1235
- },
1236
- frames: {
1237
- entitySalariesTitle: ({ entity, range }) => `${entity} Salaries (${range}/yr)`,
1238
- salariesInPlaceTitle: ({ place, range }) => `Salaries in ${place} (${range}/yr)`,
1239
- salariesInPlaceTitleNoRange: ({ place }) => `Salaries in ${place}`,
1240
- entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Salaries in ${place} (${range}/yr)`,
1241
- companySalariesTitle: ({ company, range }) => `${company} Salaries${range ? ` (${range}/yr)` : ""}`,
1242
- companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Salaries${range ? ` (${range}/yr)` : ""}`,
1243
- professionalsEarn: ({ entity, range, count }) => `${entity} professionals earn ${range}/yr on average across ${count} job postings.`,
1244
- rolesPay: ({ entity, range, count }) => `${entity} roles pay ${range}/yr on average across ${count} job postings.`,
1245
- professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity} professionals in ${place} earn ${range}/yr on average across ${count} job postings.`,
1246
- seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} roles start at ${lowAmount}, ${highLabel} roles reach ${highAmount}.`,
1247
- seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} salaries start at ${lowAmount}, while ${highLabel} roles reach ${highAmount}.`,
1248
- compareAcrossTopPaying: ({ count }) => `Compare across ${count} top-paying ${count === 1 ? "company" : "companies"}.`,
1249
- compareAcrossTop: ({ count }) => `Compare across ${count} top ${count === 1 ? "company" : "companies"}.`,
1250
- boardWideAverage: ({ entity, range }) => `Board-wide ${entity} average: ${range}/yr.`,
1251
- averageSalaryInCity: ({ place, range, count }) => `Average salary in ${place} is ${range}/yr across ${count} jobs. Browse salary data by title and skill.`,
1252
- browseLocationsInRegion: ({ count, place }) => `Browse salary data across ${count} locations in ${place}. Compare salaries by title, skill, and company.`,
1253
- companyPays: ({ company, range, count }) => `${company} pays ${range}/yr on average across ${count} job postings.`,
1254
- breakdownAcrossCategories: ({ count }) => `Breakdown across ${count} job ${count === 1 ? "category" : "categories"}.`,
1255
- compareWithSimilar: ({ count }) => `Compare with ${count} similar ${count === 1 ? "company" : "companies"}.`,
1256
- salaryInfoFor: ({ company, board }) => `Salary information for ${company} on ${board}.`,
1257
- categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category} roles at ${company} pay ${range}/yr based on ${count} job postings.`,
1258
- categorySalaryDataFor: ({ category, company, board }) => `${category} salary data for ${company} on ${board}.`
1259
- }
1260
- };
1261
- var DE2 = {
1262
- timeframe: {
1263
- per_year: "pro Jahr",
1264
- per_month: "pro Monat",
1265
- per_week: "pro Woche",
1266
- per_day: "pro Tag",
1267
- per_hour: "pro Stunde"
1268
- },
1269
- rangePrefix: { from: "ab", upTo: "bis zu" },
1270
- seniority: {
1271
- entry_level: "Entry-Level",
1272
- associate: "Associate",
1273
- mid_level: "Mid-Level",
1274
- senior: "Senior",
1275
- lead: "Lead",
1276
- principal: "Principal",
1277
- director: "Director",
1278
- executive: "F\xFChrungskraft"
1279
- },
1280
- frames: {
1281
- entitySalariesTitle: ({ entity, range }) => `${entity} Geh\xE4lter (${range}/Jahr)`,
1282
- salariesInPlaceTitle: ({ place, range }) => `Geh\xE4lter in ${place} (${range}/Jahr)`,
1283
- salariesInPlaceTitleNoRange: ({ place }) => `Geh\xE4lter in ${place}`,
1284
- entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Geh\xE4lter in ${place} (${range}/Jahr)`,
1285
- companySalariesTitle: ({ company, range }) => `${company} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
1286
- companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
1287
- professionalsEarn: ({ entity, range, count }) => `${entity}-Fachkr\xE4fte verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
1288
- rolesPay: ({ entity, range, count }) => `${entity}-Positionen werden mit durchschnittlich ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
1289
- professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity}-Fachkr\xE4fte in ${place} verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
1290
- seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Positionen beginnen bei ${lowAmount}, ${highLabel}-Positionen erreichen ${highAmount}.`,
1291
- seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Geh\xE4lter beginnen bei ${lowAmount}, w\xE4hrend ${highLabel}-Positionen ${highAmount} erreichen.`,
1292
- compareAcrossTopPaying: ({ count }) => `Vergleichen Sie ${count} ${count === 1 ? "bestzahlendes" : "bestzahlende"} Unternehmen.`,
1293
- compareAcrossTop: ({ count }) => `Vergleichen Sie ${count} Top-Unternehmen.`,
1294
- boardWideAverage: ({ entity, range }) => `Gesamtdurchschnitt ${entity}: ${range}/Jahr.`,
1295
- averageSalaryInCity: ({ place, range, count }) => `Das Durchschnittsgehalt in ${place} betr\xE4gt ${range}/Jahr, basierend auf ${count} Stellen. Gehaltsdaten nach Titel und F\xE4higkeiten durchsuchen.`,
1296
- browseLocationsInRegion: ({ count, place }) => `Gehaltsdaten f\xFCr ${count} Standorte in ${place} durchsuchen. Geh\xE4lter nach Titel, F\xE4higkeiten und Unternehmen vergleichen.`,
1297
- companyPays: ({ company, range, count }) => `${company} zahlt durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
1298
- breakdownAcrossCategories: ({ count }) => `Aufschl\xFCsselung \xFCber ${count} Job-${count === 1 ? "Kategorie" : "Kategorien"}.`,
1299
- compareWithSimilar: ({ count }) => `Vergleichen Sie mit ${count} \xE4hnlichen Unternehmen.`,
1300
- salaryInfoFor: ({ company, board }) => `Gehaltsinformationen f\xFCr ${company} auf ${board}.`,
1301
- categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category}-Positionen bei ${company} werden mit ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
1302
- categorySalaryDataFor: ({ category, company, board }) => `${category}-Gehaltsdaten f\xFCr ${company} auf ${board}.`
786
+ // src/format/locale.ts
787
+ function normalizeLocale(language) {
788
+ const trimmed = language.trim();
789
+ if (!trimmed) return null;
790
+ try {
791
+ const candidate = trimmed.replace(/_/g, "-");
792
+ const [canonical] = Intl.getCanonicalLocales(candidate);
793
+ if (!canonical) return null;
794
+ const [supported] = Intl.NumberFormat.supportedLocalesOf([canonical]);
795
+ return supported ?? null;
796
+ } catch {
797
+ return null;
1303
798
  }
1304
- };
1305
- var LEXICONS = { en: EN2, de: DE2 };
1306
- function getSalaryLexicon(locale) {
1307
- return locale && LEXICONS[locale] || EN2;
1308
799
  }
1309
800
 
1310
801
  // src/seo/salary.ts
1311
- var compactCurrencyCache = /* @__PURE__ */ new Map();
1312
- function getCompactCurrencyFormatter(language) {
1313
- const cached = compactCurrencyCache.get(language);
1314
- if (cached) return cached;
1315
- const formatter = new Intl.NumberFormat(language, {
1316
- style: "currency",
1317
- currency: "USD",
1318
- notation: "compact",
1319
- compactDisplay: "short",
1320
- minimumFractionDigits: 0,
1321
- maximumFractionDigits: 1
1322
- });
1323
- compactCurrencyCache.set(language, formatter);
1324
- return formatter;
802
+ var currencyFormatterCache = /* @__PURE__ */ new Map();
803
+ function notationForMagnitude(...values) {
804
+ let anyFinite = false;
805
+ for (const v of values) {
806
+ if (v == null || !Number.isFinite(v)) continue;
807
+ anyFinite = true;
808
+ if (Math.abs(v) >= 1e3) return "compact";
809
+ }
810
+ return anyFinite ? "standard" : "compact";
1325
811
  }
1326
- function formatUsd(locale, value) {
812
+ function getCurrencyFormatter(language, currencyCode, notation) {
813
+ const key = `${language}:${currencyCode}:${notation}`;
814
+ const cached = currencyFormatterCache.get(key);
815
+ if (cached !== void 0) return cached;
816
+ let formatter;
1327
817
  try {
1328
- return getCompactCurrencyFormatter(locale).format(value);
1329
- } catch {
1330
- if (value >= 1e6) {
1331
- const millions = value / 1e6;
1332
- return `$${millions % 1 === 0 ? millions : millions.toFixed(1)}M`;
1333
- }
1334
- if (value >= 1e3) {
1335
- const thousands = value / 1e3;
1336
- return `$${thousands % 1 === 0 ? thousands : thousands.toFixed(1)}k`;
818
+ const options = {
819
+ style: "currency",
820
+ currency: currencyCode,
821
+ notation
822
+ };
823
+ if (notation === "compact") {
824
+ options.compactDisplay = "short";
825
+ options.minimumFractionDigits = 0;
826
+ options.maximumFractionDigits = 1;
1337
827
  }
1338
- return `$${value}`;
828
+ formatter = new Intl.NumberFormat(language, options);
829
+ } catch {
830
+ formatter = null;
1339
831
  }
832
+ currencyFormatterCache.set(key, formatter);
833
+ return formatter;
1340
834
  }
1341
- function formatRange(locale, min, max) {
1342
- return `${formatUsd(locale, min)} \u2013 ${formatUsd(locale, max)}`;
835
+ function formatSalaryStat(locale, value, currency, notation) {
836
+ if (!Number.isFinite(value)) return null;
837
+ const currencyCode = currency?.trim().toUpperCase() || null;
838
+ if (!currencyCode) return null;
839
+ const normalized = normalizeLocale(locale);
840
+ if (!normalized) return null;
841
+ const resolved = notation ?? notationForMagnitude(value);
842
+ const formatter = getCurrencyFormatter(normalized, currencyCode, resolved);
843
+ if (!formatter) return null;
844
+ return formatter.format(value);
1343
845
  }
1344
- function formatSeniority(locale, value, overrides) {
1345
- if (overrides?.[value]) {
1346
- return overrides[value];
1347
- }
1348
- const lexiconLabel = getSalaryLexicon(locale).seniority[value];
1349
- if (lexiconLabel) {
1350
- return lexiconLabel;
1351
- }
1352
- return value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
846
+ function formatSalaryStatRange(locale, min, max, currency, notation) {
847
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return null;
848
+ const currencyCode = currency?.trim().toUpperCase() || null;
849
+ if (!currencyCode) return null;
850
+ const normalized = normalizeLocale(locale);
851
+ if (!normalized) return null;
852
+ const resolved = notation ?? notationForMagnitude(min, max);
853
+ const formatter = getCurrencyFormatter(normalized, currencyCode, resolved);
854
+ if (!formatter) return null;
855
+ const low = Math.min(min, max);
856
+ const high = Math.max(min, max);
857
+ if (low === high) return formatter.format(low);
858
+ return formatter.formatRange(low, high);
1353
859
  }
1354
860
  var SENIORITY_ORDER = [
1355
861
  "entry_level",
@@ -1379,7 +885,7 @@ var yearly = {
1379
885
  function distribution(name, currency, minValue, maxValue, stats = {}) {
1380
886
  return {
1381
887
  "@type": "MonetaryAmountDistribution",
1382
- name,
888
+ ...name !== void 0 ? { name } : {},
1383
889
  currency,
1384
890
  minValue,
1385
891
  maxValue,
@@ -1387,15 +893,19 @@ function distribution(name, currency, minValue, maxValue, stats = {}) {
1387
893
  ...yearly
1388
894
  };
1389
895
  }
1390
- function seniorityDistributions(locale, rows, currency, nameFor) {
1391
- return rows.map(
1392
- (row) => distribution(
1393
- nameFor(formatSeniority(locale, row.seniority)),
896
+ function seniorityDistributions(rows, currency, entity, options) {
897
+ return rows.map((row) => {
898
+ const name = options?.seniorityName?.({
899
+ seniority: row.seniority,
900
+ entity
901
+ });
902
+ return distribution(
903
+ name,
1394
904
  currency,
1395
905
  row.avgSalaryMin,
1396
906
  row.avgSalaryMax
1397
- )
1398
- );
907
+ );
908
+ });
1399
909
  }
1400
910
  function salaryEnvelope(type, name, extra, estimatedSalary) {
1401
911
  return {
@@ -1406,17 +916,27 @@ function salaryEnvelope(type, name, extra, estimatedSalary) {
1406
916
  estimatedSalary
1407
917
  };
1408
918
  }
1409
- function buildSalaryFaq(locale, label, overall) {
919
+ function buildSalaryFaq(locale, label, overall, currency) {
1410
920
  if (!overall) return [];
1411
- const range = formatRange(locale, overall.avgMin, overall.avgMax);
921
+ const range = formatSalaryStatRange(
922
+ locale,
923
+ overall.avgMin,
924
+ overall.avgMax,
925
+ currency
926
+ );
1412
927
  return [
1413
928
  {
1414
- q: `What is the average salary for ${label}?`,
1415
- 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.`
929
+ kind: "average",
930
+ label,
931
+ range,
932
+ avgMin: overall.avgMin,
933
+ avgMax: overall.avgMax,
934
+ currency,
935
+ jobCount: overall.jobCount
1416
936
  },
1417
937
  {
1418
- q: `How is the salary figure for ${label} calculated?`,
1419
- 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.`
938
+ kind: "methodology",
939
+ label
1420
940
  }
1421
941
  ];
1422
942
  }
@@ -1446,11 +966,11 @@ function faqJsonLd(faqs) {
1446
966
  }))
1447
967
  };
1448
968
  }
1449
- function titleSalaryJsonLd(locale, d) {
969
+ function titleSalaryJsonLd(d, options) {
1450
970
  if (!d.overallSalary) return null;
1451
971
  return salaryEnvelope("Occupation", d.categoryName, {}, [
1452
972
  distribution(
1453
- `${d.categoryName} salary (all levels)`,
973
+ d.categoryName,
1454
974
  d.currency,
1455
975
  d.overallSalary.avgMin,
1456
976
  d.overallSalary.avgMax,
@@ -1460,10 +980,10 @@ function titleSalaryJsonLd(locale, d) {
1460
980
  }
1461
981
  ),
1462
982
  ...seniorityDistributions(
1463
- locale,
1464
983
  d.bySeniority,
1465
984
  d.currency,
1466
- (label) => `${label} ${d.categoryName}`
985
+ d.categoryName,
986
+ options
1467
987
  )
1468
988
  ]);
1469
989
  }
@@ -1471,7 +991,7 @@ function skillSalaryJsonLd(d) {
1471
991
  if (!d.overallSalary) return null;
1472
992
  return salaryEnvelope("Occupation", d.skillName, {}, [
1473
993
  distribution(
1474
- `${d.skillName} salary (all levels)`,
994
+ d.skillName,
1475
995
  d.currency,
1476
996
  d.overallSalary.avgMin,
1477
997
  d.overallSalary.avgMax,
@@ -1483,42 +1003,22 @@ function skillSalaryJsonLd(d) {
1483
1003
  )
1484
1004
  ]);
1485
1005
  }
1486
- function locationSalaryJsonLd(d) {
1006
+ function locationSalaryJsonLd(d, options) {
1007
+ if (!options?.occupationUrl) return null;
1487
1008
  const isCity = d.adminLevel === "city" || d.adminLevel === "locality";
1488
- if (!isCity || !d.overallSalary) return null;
1489
- return salaryEnvelope(
1490
- "Occupation",
1491
- `Average Salary in ${d.placeName}`,
1492
- {
1493
- occupationLocation: {
1494
- "@type": "City",
1495
- name: d.placeName,
1496
- address: { "@type": "PostalAddress", addressCountry: d.countryCode }
1497
- }
1498
- },
1499
- [
1500
- distribution(
1501
- `Average salary in ${d.placeName}`,
1502
- d.currency,
1503
- d.overallSalary.avgMin,
1504
- d.overallSalary.avgMax,
1505
- {
1506
- median: Math.round(
1507
- (d.overallSalary.medianMin + d.overallSalary.medianMax) / 2
1508
- ),
1509
- // Hosted includes these unconditionally, nulls and all.
1510
- percentile25: d.overallSalary.p25Min,
1511
- percentile75: d.overallSalary.p75Max
1512
- }
1513
- )
1514
- ]
1009
+ if (!isCity) return null;
1010
+ return itemListJsonLd(
1011
+ d.topCategories.map((c) => ({
1012
+ name: c.categoryName,
1013
+ url: options.occupationUrl(c)
1014
+ }))
1515
1015
  );
1516
1016
  }
1517
- function crossAxisSalaryJsonLd(locale, args) {
1017
+ function crossAxisSalaryJsonLd(args, options) {
1518
1018
  if (!args.overall) return null;
1519
1019
  return salaryEnvelope(
1520
1020
  "Occupation",
1521
- `${args.name} salary in ${args.placeName}`,
1021
+ args.name,
1522
1022
  {
1523
1023
  occupationLocation: {
1524
1024
  "@type": "AdministrativeArea",
@@ -1531,7 +1031,7 @@ function crossAxisSalaryJsonLd(locale, args) {
1531
1031
  },
1532
1032
  [
1533
1033
  distribution(
1534
- `${args.name} salary in ${args.placeName} (all levels)`,
1034
+ args.name,
1535
1035
  args.currency,
1536
1036
  args.overall.avgMin,
1537
1037
  args.overall.avgMax,
@@ -1541,56 +1041,35 @@ function crossAxisSalaryJsonLd(locale, args) {
1541
1041
  }
1542
1042
  ),
1543
1043
  ...seniorityDistributions(
1544
- locale,
1545
1044
  args.bySeniority,
1546
1045
  args.currency,
1547
- (label) => `${label} ${args.name} in ${args.placeName}`
1046
+ args.name,
1047
+ options
1548
1048
  )
1549
1049
  ]
1550
1050
  );
1551
1051
  }
1552
- function companySalaryJsonLd(locale, d) {
1553
- if (!d.overallSalary) return null;
1554
- return salaryEnvelope(
1555
- "OccupationAggregationByEmployer",
1556
- `Jobs at ${d.companyName}`,
1557
- {
1558
- sampleSize: d.overallSalary.jobCount,
1559
- hiringOrganization: { "@type": "Organization", name: d.companyName }
1560
- },
1561
- [
1562
- distribution(
1563
- "base",
1564
- d.currency,
1565
- d.overallSalary.avgMin,
1566
- d.overallSalary.avgMax,
1567
- {
1568
- ...d.boardMedianMin && d.boardMedianMax ? { median: Math.round((d.boardMedianMin + d.boardMedianMax) / 2) } : {},
1569
- ...d.boardP25Min ? { percentile25: d.boardP25Min } : {},
1570
- ...d.boardP75Max ? { percentile75: d.boardP75Max } : {}
1571
- }
1572
- ),
1573
- ...seniorityDistributions(
1574
- locale,
1575
- d.bySeniority,
1576
- d.currency,
1577
- (label) => `${label} salary`
1578
- )
1579
- ]
1052
+ function companySalaryJsonLd(d, options) {
1053
+ if (!options?.occupationUrl) return null;
1054
+ return itemListJsonLd(
1055
+ d.byCategory.map((c) => ({
1056
+ name: c.categoryName,
1057
+ url: options.occupationUrl(c)
1058
+ }))
1580
1059
  );
1581
1060
  }
1582
- function companyCategorySalaryJsonLd(locale, d) {
1061
+ function companyCategorySalaryJsonLd(d, options) {
1583
1062
  if (!d.overallSalary) return null;
1584
1063
  return salaryEnvelope(
1585
1064
  "Occupation",
1586
- `${d.categoryName} at ${d.companyName}`,
1065
+ d.categoryName,
1587
1066
  {
1588
1067
  occupationalCategory: d.categoryName,
1589
1068
  hiringOrganization: { "@type": "Organization", name: d.companyName }
1590
1069
  },
1591
1070
  [
1592
1071
  distribution(
1593
- `${d.categoryName} salary (all levels)`,
1072
+ d.categoryName,
1594
1073
  d.currency,
1595
1074
  d.overallSalary.avgMin,
1596
1075
  d.overallSalary.avgMax,
@@ -1600,10 +1079,10 @@ function companyCategorySalaryJsonLd(locale, d) {
1600
1079
  }
1601
1080
  ),
1602
1081
  ...seniorityDistributions(
1603
- locale,
1604
1082
  d.bySeniority,
1605
1083
  d.currency,
1606
- (label) => `${label} ${d.categoryName}`
1084
+ d.categoryName,
1085
+ options
1607
1086
  )
1608
1087
  ]
1609
1088
  );