@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.mjs CHANGED
@@ -1,3 +1,28 @@
1
+ import {
2
+ ALL_COUNTRY_CODES,
3
+ SENIORITY_ORDER,
4
+ buildJobBreadcrumbs,
5
+ buildSalaryFaq,
6
+ companyCategorySalaryJsonLd,
7
+ companySalaryJsonLd,
8
+ createBreadcrumbJsonLd,
9
+ createJobPostingJsonLd,
10
+ crossAxisSalaryJsonLd,
11
+ faqJsonLd,
12
+ formatSalaryStat,
13
+ formatSalaryStatRange,
14
+ itemListJsonLd,
15
+ locationSalaryJsonLd,
16
+ normalizeWebsiteUrl,
17
+ skillSalaryJsonLd,
18
+ sortBySeniority,
19
+ titleSalaryJsonLd
20
+ } from "./chunk-YEAQ3JEI.mjs";
21
+ import {
22
+ boardUrl,
23
+ jobDetailPath
24
+ } from "./chunk-BGKNXG4H.mjs";
25
+
1
26
  // src/seo/blog.ts
2
27
  function createBlogArticleJsonLd({
3
28
  post,
@@ -113,1013 +138,10 @@ function buildAbsoluteUrl(origin, path) {
113
138
  }
114
139
  }
115
140
 
116
- // src/format/ui-copy.ts
117
- var EN = {
118
- jobCard: {
119
- featuredLabel: "Featured",
120
- aiRankedLabel: "AI-ranked",
121
- sortNewestLabel: "Most recent",
122
- sortSalaryHighLabel: "Salary: high to low"
123
- },
124
- jobSearch: {
125
- headingJobs: "Jobs",
126
- keywordLabel: "Keyword",
127
- keywordPlaceholder: "Search jobs\u2026",
128
- locationLabel: "Location",
129
- locationPlaceholder: "City or region",
130
- workplacePlaceholder: "Workplace",
131
- anyWorkplaceLabel: "Any workplace",
132
- typePlaceholder: "Type",
133
- anyTypeLabel: "Any type",
134
- sortPlaceholder: "Sort",
135
- loadMoreLabel: "Load more",
136
- noJobsMatchText: "No jobs match \u2014 try clearing a filter."
137
- },
138
- jobDetail: {
139
- categoriesHeading: "Categories",
140
- skillsHeading: "Skills",
141
- additionalDetailsHeading: "Additional details",
142
- customFieldYesLabel: "Yes",
143
- customFieldNoLabel: "No",
144
- locationsLabel: "Locations",
145
- workPermitsLabel: "Work permits",
146
- timezonesLabel: "Timezones",
147
- educationLabel: "Education",
148
- experienceLabel: "Experience",
149
- worldwideLabel: "Worldwide",
150
- noExperienceRequiredLabel: "No experience required",
151
- experienceYears: (years) => `${years}+ years`,
152
- posted: (published) => `Posted ${published}`,
153
- noDescriptionText: "No description provided.",
154
- viewCompanyProfileLabel: "View company profile",
155
- similarJobsHeading: "Similar jobs",
156
- breadcrumbAriaLabel: "Breadcrumb"
157
- },
158
- apply: {
159
- applyButtonText: "Apply for this job",
160
- applyingLabel: "Applying\u2026",
161
- applyOnEmployerSiteLabel: "Apply on employer site",
162
- signInToApplyLabel: "Sign in to apply",
163
- verifyEmailToApplyLabel: "Verify email to apply",
164
- appliedViewApplicationsLabel: "Applied \u2014 view applications",
165
- applicationSubmitError: "Something went wrong. Please try again."
166
- },
167
- alerts: {
168
- jobAlertTitle: "Get job alerts",
169
- jobAlertEmailPlaceholder: "you@example.com",
170
- jobAlertButtonText: "Get alerts",
171
- subscribingLabel: "Subscribing\u2026",
172
- jobAlertSuccessToast: "If this email isn't already subscribed, we've sent a confirmation link \u2014 check your inbox.",
173
- jobAlertErrorToast: "Something went wrong. Please try again.",
174
- sectionAriaLabel: "Job alerts",
175
- emailAriaLabel: "email",
176
- submitAriaLabel: "get job alerts"
177
- },
178
- copyLink: {
179
- copyLinkLabel: "Copy link",
180
- copiedLabel: "Copied",
181
- ariaLabel: "copy link"
182
- },
183
- salary: {
184
- comparisonHeadlineAverage: "Average salary",
185
- perYearSuffix: "/ year",
186
- comparisonPercentile25Label: "25th percentile",
187
- medianLabel: "Median",
188
- comparisonPercentile75Label: "75th percentile",
189
- basedOnLabel: "Based on",
190
- seniorityTableHeaderLevel: "Level",
191
- seniorityTableHeaderAvg: "Average",
192
- boardBaselineLabel: "Board baseline",
193
- seniorityTableHeaderDiff: "vs. board",
194
- faqHeading: "Frequently asked questions"
195
- },
196
- nav: {
197
- home: "Jobs",
198
- companies: "Companies",
199
- pricing: "Pricing",
200
- talent: "Talent",
201
- post: "Post a job",
202
- blog: "Blog"
203
- },
204
- footer: {
205
- forCandidatesHeading: "For Candidates",
206
- forCompaniesHeading: "For Companies",
207
- resourcesHeading: "Resources",
208
- aboutHeading: "About",
209
- websiteLabel: "Website",
210
- aboutLabel: "About",
211
- contactLabel: "Contact us",
212
- locationsLabel: "Locations",
213
- salariesLabel: "Salaries",
214
- sitemapLabel: "Sitemap",
215
- termsOfServiceLabel: "Terms of Service",
216
- privacyPolicyLabel: "Privacy Policy",
217
- cookiePolicyLabel: "Cookie Policy",
218
- impressumLabel: "Impressum",
219
- allRightsReservedText: "All rights reserved.",
220
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
221
- poweredByText: "Powered by",
222
- defaultDescription: "Discover the latest roles from {{board_name}}."
223
- },
224
- breadcrumbs: {
225
- home: "Home",
226
- jobs: "Jobs",
227
- locations: "Locations",
228
- salaries: "Salaries",
229
- companies: "Companies",
230
- skills: "Skills",
231
- titles: "Titles",
232
- blog: "Blog",
233
- post: "Post a Job",
234
- pricing: "Pricing",
235
- about: "About",
236
- impressum: "Impressum",
237
- termsOfService: "Terms of Service",
238
- privacyPolicy: "Privacy Policy",
239
- cookiePolicy: "Cookie Policy",
240
- talent: "Talent"
241
- },
242
- pagination: {
243
- ariaLabel: "Pagination",
244
- previousLabel: "Previous",
245
- nextLabel: "Next"
246
- },
247
- blog: {
248
- bylineLabel: "By",
249
- readingTimeLabel: "min read",
250
- searchLabel: "Search",
251
- searchPlaceholder: "Search posts...",
252
- clearButtonLabel: "Clear search",
253
- tagFilterLabel: "Topics",
254
- tagFilterAllLabel: "All",
255
- emptyTitle: "No matching posts",
256
- emptyDescription: "Try adjusting your search or clearing your filters.",
257
- emptyResetLabel: "Clear filters"
258
- },
259
- entity: {
260
- jobSingular: "job",
261
- jobPlural: "jobs",
262
- companySingular: "company",
263
- companyPlural: "companies"
264
- }
265
- };
266
- var DE = {
267
- jobCard: {
268
- featuredLabel: "Hervorgehoben",
269
- aiRankedLabel: "KI-sortiert",
270
- sortNewestLabel: "Neueste zuerst",
271
- sortSalaryHighLabel: "Gehalt: absteigend"
272
- },
273
- jobSearch: {
274
- headingJobs: "Jobs",
275
- keywordLabel: "Stichwort",
276
- keywordPlaceholder: "Jobtitel oder -beschreibungen suchen",
277
- locationLabel: "Standort",
278
- locationPlaceholder: "Stadt, Land oder Region",
279
- workplacePlaceholder: "Arbeitsmodell",
280
- anyWorkplaceLabel: "Alle Arbeitsmodelle",
281
- typePlaceholder: "Anstellungsart",
282
- anyTypeLabel: "Alle Anstellungsarten",
283
- sortPlaceholder: "Sortieren",
284
- loadMoreLabel: "Mehr laden",
285
- noJobsMatchText: "Keine passenden Jobs \u2014 setze einen Filter zur\xFCck."
286
- },
287
- jobDetail: {
288
- categoriesHeading: "Kategorien",
289
- skillsHeading: "F\xE4higkeiten",
290
- additionalDetailsHeading: "Weitere Details",
291
- customFieldYesLabel: "Ja",
292
- customFieldNoLabel: "Nein",
293
- locationsLabel: "Standorte",
294
- workPermitsLabel: "Arbeitserlaubnis",
295
- timezonesLabel: "Zeitzonen",
296
- educationLabel: "Ausbildung",
297
- experienceLabel: "Berufserfahrung",
298
- worldwideLabel: "Weltweit",
299
- noExperienceRequiredLabel: "Keine Berufserfahrung erforderlich",
300
- experienceYears: (years) => `${years}+ Jahre`,
301
- posted: (published) => `Ver\xF6ffentlicht ${published}`,
302
- noDescriptionText: "Keine Beschreibung vorhanden.",
303
- viewCompanyProfileLabel: "Unternehmensprofil ansehen",
304
- similarJobsHeading: "\xC4hnliche Jobs",
305
- breadcrumbAriaLabel: "Seitenpfad"
306
- },
307
- apply: {
308
- applyButtonText: "Jetzt bewerben",
309
- applyingLabel: "Wird gesendet \u2026",
310
- applyOnEmployerSiteLabel: "Beim Unternehmen bewerben",
311
- signInToApplyLabel: "Zum Bewerben anmelden",
312
- verifyEmailToApplyLabel: "E-Mail best\xE4tigen, um dich zu bewerben",
313
- appliedViewApplicationsLabel: "Beworben \u2014 Bewerbungen ansehen",
314
- applicationSubmitError: "Etwas ist schiefgelaufen. Bitte versuche es erneut."
315
- },
316
- alerts: {
317
- jobAlertTitle: "Job-Benachrichtigungen erhalten",
318
- jobAlertEmailPlaceholder: "name@beispiel.de",
319
- jobAlertButtonText: "Abonnieren",
320
- subscribingLabel: "Wird abonniert \u2026",
321
- jobAlertSuccessToast: "Falls diese E-Mail noch nicht abonniert ist, haben wir einen Best\xE4tigungslink gesendet \u2014 pr\xFCfe deinen Posteingang.",
322
- jobAlertErrorToast: "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
323
- sectionAriaLabel: "Job-Benachrichtigungen",
324
- emailAriaLabel: "E-Mail",
325
- submitAriaLabel: "Job-Benachrichtigungen erhalten"
326
- },
327
- copyLink: {
328
- copyLinkLabel: "Link kopieren",
329
- copiedLabel: "Kopiert",
330
- ariaLabel: "Link kopieren"
331
- },
332
- salary: {
333
- comparisonHeadlineAverage: "Durchschnittsgehalt",
334
- perYearSuffix: "/ Jahr",
335
- comparisonPercentile25Label: "25. Perzentil",
336
- medianLabel: "Median",
337
- comparisonPercentile75Label: "75. Perzentil",
338
- basedOnLabel: "Basierend auf",
339
- seniorityTableHeaderLevel: "Erfahrungslevel",
340
- seniorityTableHeaderAvg: "Durchschnitt",
341
- boardBaselineLabel: "Board-Durchschnitt",
342
- seniorityTableHeaderDiff: "vs. Board",
343
- faqHeading: "H\xE4ufig gestellte Fragen"
344
- },
345
- nav: {
346
- home: "Jobs",
347
- companies: "Unternehmen",
348
- pricing: "Preise",
349
- talent: "Talente",
350
- post: "Job ver\xF6ffentlichen",
351
- blog: "Blog"
352
- },
353
- footer: {
354
- forCandidatesHeading: "F\xFCr Kandidaten",
355
- forCompaniesHeading: "F\xFCr Unternehmen",
356
- resourcesHeading: "Ressourcen",
357
- aboutHeading: "\xDCber uns",
358
- websiteLabel: "Website",
359
- aboutLabel: "\xDCber uns",
360
- contactLabel: "Kontakt",
361
- locationsLabel: "Standorte",
362
- salariesLabel: "Geh\xE4lter",
363
- sitemapLabel: "Sitemap",
364
- termsOfServiceLabel: "Nutzungsbedingungen",
365
- privacyPolicyLabel: "Datenschutzerkl\xE4rung",
366
- cookiePolicyLabel: "Cookie-Richtlinie",
367
- impressumLabel: "Impressum",
368
- allRightsReservedText: "Alle Rechte vorbehalten.",
369
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
370
- poweredByText: "Bereitgestellt von",
371
- defaultDescription: "Entdecke die neuesten Stellen von {{board_name}}."
372
- },
373
- breadcrumbs: {
374
- home: "Startseite",
375
- jobs: "Jobs",
376
- locations: "Standorte",
377
- salaries: "Geh\xE4lter",
378
- companies: "Unternehmen",
379
- skills: "F\xE4higkeiten",
380
- titles: "Berufsbezeichnungen",
381
- blog: "Blog",
382
- post: "Job ver\xF6ffentlichen",
383
- pricing: "Preise",
384
- about: "\xDCber uns",
385
- impressum: "Impressum",
386
- termsOfService: "Nutzungsbedingungen",
387
- privacyPolicy: "Datenschutzerkl\xE4rung",
388
- cookiePolicy: "Cookie-Richtlinie",
389
- talent: "Talente"
390
- },
391
- pagination: {
392
- ariaLabel: "Seitennummerierung",
393
- previousLabel: "Zur\xFCck",
394
- nextLabel: "Weiter"
395
- },
396
- blog: {
397
- bylineLabel: "Von",
398
- readingTimeLabel: "Minuten Lesezeit",
399
- searchLabel: "Suche",
400
- searchPlaceholder: "Artikel suchen...",
401
- clearButtonLabel: "Suche leeren",
402
- tagFilterLabel: "Themen",
403
- tagFilterAllLabel: "Alle",
404
- emptyTitle: "Keine passenden Artikel gefunden",
405
- emptyDescription: "Versuche, deine Suche anzupassen oder die Filter zur\xFCckzusetzen.",
406
- emptyResetLabel: "Filter zur\xFCcksetzen"
407
- },
408
- entity: {
409
- jobSingular: "Job",
410
- jobPlural: "Jobs",
411
- companySingular: "Unternehmen",
412
- companyPlural: "Unternehmen"
413
- }
414
- };
415
- var FR = {
416
- jobCard: {
417
- featuredLabel: "\xC0 la une",
418
- aiRankedLabel: "Classement IA",
419
- sortNewestLabel: "Plus r\xE9centes",
420
- sortSalaryHighLabel: "Salaire : d\xE9croissant"
421
- },
422
- jobSearch: {
423
- headingJobs: "Offres d'emploi",
424
- keywordLabel: "Mot-cl\xE9",
425
- keywordPlaceholder: "Rechercher une offre\u2026",
426
- locationLabel: "Lieu",
427
- locationPlaceholder: "Ville ou r\xE9gion",
428
- workplacePlaceholder: "Mode de travail",
429
- anyWorkplaceLabel: "Tous les modes de travail",
430
- typePlaceholder: "Type de contrat",
431
- anyTypeLabel: "Tous les types",
432
- sortPlaceholder: "Trier",
433
- loadMoreLabel: "Voir plus",
434
- noJobsMatchText: "Aucune offre ne correspond \u2014 essayez de retirer un filtre."
435
- },
436
- jobDetail: {
437
- categoriesHeading: "Cat\xE9gories",
438
- skillsHeading: "Comp\xE9tences",
439
- additionalDetailsHeading: "Informations compl\xE9mentaires",
440
- customFieldYesLabel: "Oui",
441
- customFieldNoLabel: "Non",
442
- locationsLabel: "Lieux",
443
- workPermitsLabel: "Permis de travail",
444
- timezonesLabel: "Fuseaux horaires",
445
- educationLabel: "Formation",
446
- experienceLabel: "Exp\xE9rience",
447
- worldwideLabel: "Monde entier",
448
- noExperienceRequiredLabel: "Aucune exp\xE9rience requise",
449
- experienceYears: (years) => `${years}+ ans`,
450
- posted: (published) => `Publi\xE9e ${published}`,
451
- noDescriptionText: "Aucune description fournie.",
452
- viewCompanyProfileLabel: "Voir le profil de l'entreprise",
453
- similarJobsHeading: "Offres similaires",
454
- breadcrumbAriaLabel: "Fil d'Ariane"
455
- },
456
- apply: {
457
- applyButtonText: "Postuler \xE0 cette offre",
458
- applyingLabel: "Envoi en cours\u2026",
459
- applyOnEmployerSiteLabel: "Postuler sur le site de l'employeur",
460
- signInToApplyLabel: "Connectez-vous pour postuler",
461
- verifyEmailToApplyLabel: "V\xE9rifiez votre e-mail pour postuler",
462
- appliedViewApplicationsLabel: "Candidature envoy\xE9e \u2014 voir mes candidatures",
463
- applicationSubmitError: "Une erreur est survenue. Veuillez r\xE9essayer."
464
- },
465
- alerts: {
466
- jobAlertTitle: "Recevoir des alertes emploi",
467
- jobAlertEmailPlaceholder: "vous@exemple.fr",
468
- jobAlertButtonText: "Activer les alertes",
469
- subscribingLabel: "Abonnement en cours\u2026",
470
- 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.",
471
- jobAlertErrorToast: "Une erreur est survenue. Veuillez r\xE9essayer.",
472
- sectionAriaLabel: "Alertes emploi",
473
- emailAriaLabel: "e-mail",
474
- submitAriaLabel: "recevoir des alertes emploi"
475
- },
476
- copyLink: {
477
- copyLinkLabel: "Copier le lien",
478
- copiedLabel: "Copi\xE9",
479
- ariaLabel: "copier le lien"
480
- },
481
- salary: {
482
- comparisonHeadlineAverage: "Salaire moyen",
483
- perYearSuffix: "/ an",
484
- comparisonPercentile25Label: "25e percentile",
485
- medianLabel: "M\xE9diane",
486
- comparisonPercentile75Label: "75e percentile",
487
- basedOnLabel: "Bas\xE9 sur",
488
- seniorityTableHeaderLevel: "Niveau",
489
- seniorityTableHeaderAvg: "Moyenne",
490
- boardBaselineLabel: "R\xE9f\xE9rence du site",
491
- seniorityTableHeaderDiff: "vs site",
492
- faqHeading: "Questions fr\xE9quentes"
493
- },
494
- nav: {
495
- home: "Offres",
496
- companies: "Entreprises",
497
- pricing: "Tarifs",
498
- talent: "Talents",
499
- post: "Publier une offre",
500
- blog: "Blog"
501
- },
502
- footer: {
503
- forCandidatesHeading: "Pour les candidats",
504
- forCompaniesHeading: "Pour les entreprises",
505
- resourcesHeading: "Ressources",
506
- aboutHeading: "\xC0 propos",
507
- websiteLabel: "Site web",
508
- aboutLabel: "\xC0 propos",
509
- contactLabel: "Nous contacter",
510
- locationsLabel: "Lieux",
511
- salariesLabel: "Salaires",
512
- sitemapLabel: "Plan du site",
513
- termsOfServiceLabel: "Conditions d'utilisation",
514
- privacyPolicyLabel: "Politique de confidentialit\xE9",
515
- cookiePolicyLabel: "Politique de cookies",
516
- impressumLabel: "Mentions l\xE9gales",
517
- allRightsReservedText: "Tous droits r\xE9serv\xE9s.",
518
- copyrightPrefix: "\xA9 {{year}} {{board_name}}.",
519
- poweredByText: "Propuls\xE9 par",
520
- defaultDescription: "D\xE9couvrez les derni\xE8res offres de {{board_name}}."
521
- },
522
- breadcrumbs: {
523
- home: "Accueil",
524
- jobs: "Offres",
525
- locations: "Lieux",
526
- salaries: "Salaires",
527
- companies: "Entreprises",
528
- skills: "Comp\xE9tences",
529
- titles: "Intitul\xE9s de poste",
530
- blog: "Blog",
531
- post: "Publier une offre",
532
- pricing: "Tarifs",
533
- about: "\xC0 propos",
534
- impressum: "Mentions l\xE9gales",
535
- termsOfService: "Conditions d'utilisation",
536
- privacyPolicy: "Politique de confidentialit\xE9",
537
- cookiePolicy: "Politique de cookies",
538
- talent: "Talents"
539
- },
540
- pagination: {
541
- ariaLabel: "Pagination",
542
- previousLabel: "Pr\xE9c\xE9dent",
543
- nextLabel: "Suivant"
544
- },
545
- blog: {
546
- bylineLabel: "Par",
547
- readingTimeLabel: "min de lecture",
548
- searchLabel: "Recherche",
549
- searchPlaceholder: "Rechercher un article...",
550
- clearButtonLabel: "Effacer la recherche",
551
- tagFilterLabel: "Th\xE8mes",
552
- tagFilterAllLabel: "Tous",
553
- emptyTitle: "Aucun article correspondant",
554
- emptyDescription: "Essayez de modifier votre recherche ou de r\xE9initialiser les filtres.",
555
- emptyResetLabel: "R\xE9initialiser les filtres"
556
- },
557
- entity: {
558
- jobSingular: "offre",
559
- jobPlural: "offres",
560
- companySingular: "entreprise",
561
- companyPlural: "entreprises"
562
- }
563
- };
564
- var CATALOGS = { en: EN, de: DE, fr: FR };
565
- function mergeGroup(group, overrides) {
566
- if (!overrides) return group;
567
- const out = { ...group };
568
- for (const [key, value] of Object.entries(group)) {
569
- if (typeof value !== "string") continue;
570
- const override = overrides[key];
571
- if (typeof override === "string" && override.trim() !== "") {
572
- out[key] = override;
573
- }
574
- }
575
- return out;
576
- }
577
- var GROUP_OVERRIDE_SOURCE = {
578
- jobCard: "jobCardLabels",
579
- jobSearch: "jobSearchLabels",
580
- jobDetail: "jobCardLabels",
581
- apply: "jobCardLabels",
582
- alerts: "jobCardLabels",
583
- copyLink: "jobCardLabels",
584
- salary: "salaryLabels",
585
- nav: "navLabels",
586
- footer: "footerLabels",
587
- breadcrumbs: "breadcrumbsLabels",
588
- pagination: "globalPaginationLabels",
589
- blog: "blogSharedLabels",
590
- entity: "entityLabels"
591
- };
592
- function uiCopy(language, labels) {
593
- const catalog = language && CATALOGS[language] || EN;
594
- if (!labels) return catalog;
595
- const resolved = {};
596
- for (const [group, source] of Object.entries(GROUP_OVERRIDE_SOURCE)) {
597
- resolved[group] = mergeGroup(catalog[group], labels[source]);
598
- }
599
- return resolved;
600
- }
601
-
602
- // src/seo/breadcrumbs.ts
603
- function createBreadcrumbJsonLd(items, options = {}) {
604
- const normalizedItems = items.map((item) => {
605
- const name = item.label?.trim();
606
- return name ? {
607
- name,
608
- href: item.href?.trim() ?? null
609
- } : null;
610
- }).filter((item) => !!item);
611
- if (normalizedItems.length < 2) {
612
- return null;
613
- }
614
- const listItems = normalizedItems.map((item, index) => {
615
- const resolvedHref = item.href ? resolveBreadcrumbHref(item.href, options.origin ?? null) : null;
616
- const listItem = {
617
- "@type": "ListItem",
618
- position: index + 1,
619
- name: item.name
620
- };
621
- if (resolvedHref) {
622
- listItem.item = resolvedHref;
623
- }
624
- return listItem;
625
- });
626
- return {
627
- "@context": "https://schema.org",
628
- "@type": "BreadcrumbList",
629
- itemListElement: listItems
630
- };
631
- }
632
- function resolveBreadcrumbHref(href, origin) {
633
- if (isAbsoluteUrl(href)) {
634
- return href;
635
- }
636
- if (!origin) {
637
- return null;
638
- }
639
- const normalizedPath = href.startsWith("/") ? href : `/${href}`;
640
- return `${origin}${normalizedPath}`;
641
- }
642
- function isAbsoluteUrl(href) {
643
- return /^https?:\/\//i.test(href);
644
- }
645
- function buildJobBreadcrumbs(job, language, labels) {
646
- const copy = uiCopy(language, labels).breadcrumbs;
647
- const crumbs = [
648
- { name: copy.home, path: "/" },
649
- { name: copy.jobs, path: "/jobs" }
650
- ];
651
- let lastPlaceSlug = null;
652
- for (const place of job.placeHierarchy) {
653
- lastPlaceSlug = place.slug;
654
- crumbs.push({ name: place.name, path: `/jobs/locations/${place.slug}` });
655
- }
656
- const primaryCategory = job.categories[0];
657
- if (primaryCategory) {
658
- crumbs.push({
659
- name: primaryCategory.name,
660
- path: lastPlaceSlug ? `/jobs/locations/${lastPlaceSlug}/${primaryCategory.slug}` : `/jobs/${primaryCategory.slug}`
661
- });
662
- }
663
- crumbs.push({ name: job.title });
664
- return crumbs;
665
- }
666
-
667
- // src/seo/job-posting.ts
668
- var EDUCATION_TO_CREDENTIAL = {
669
- high_school: "high school",
670
- associate_degree: "associate degree",
671
- bachelor_degree: "bachelor degree",
672
- professional_certificate: "professional certificate",
673
- postgraduate_degree: "postgraduate degree"
674
- };
675
- var EMPLOYMENT_TYPE_TO_GOOGLE = {
676
- full_time: "FULL_TIME",
677
- part_time: "PART_TIME",
678
- contract: "CONTRACTOR",
679
- internship: "INTERN",
680
- temporary: "TEMPORARY",
681
- volunteer: "VOLUNTEER",
682
- other: "OTHER"
683
- };
684
- var SALARY_TIMEFRAME_TO_UNIT = {
685
- per_year: "YEAR",
686
- per_month: "MONTH",
687
- per_week: "WEEK",
688
- per_day: "DAY",
689
- per_hour: "HOUR"
690
- };
691
- var ALL_COUNTRY_CODES = [
692
- "AF",
693
- "AX",
694
- "AL",
695
- "DZ",
696
- "AS",
697
- "AD",
698
- "AO",
699
- "AI",
700
- "AQ",
701
- "AG",
702
- "AR",
703
- "AM",
704
- "AW",
705
- "AU",
706
- "AT",
707
- "AZ",
708
- "BS",
709
- "BH",
710
- "BD",
711
- "BB",
712
- "BY",
713
- "BE",
714
- "BZ",
715
- "BJ",
716
- "BM",
717
- "BT",
718
- "BO",
719
- "BA",
720
- "BW",
721
- "BV",
722
- "BR",
723
- "IO",
724
- "VG",
725
- "BN",
726
- "BG",
727
- "BF",
728
- "BI",
729
- "KH",
730
- "CM",
731
- "CA",
732
- "CV",
733
- "BQ",
734
- "KY",
735
- "CF",
736
- "TD",
737
- "CL",
738
- "CN",
739
- "CX",
740
- "CC",
741
- "CO",
742
- "KM",
743
- "CK",
744
- "CR",
745
- "HR",
746
- "CU",
747
- "CW",
748
- "CY",
749
- "CZ",
750
- "DK",
751
- "DJ",
752
- "DM",
753
- "DO",
754
- "CD",
755
- "EC",
756
- "EG",
757
- "SV",
758
- "GQ",
759
- "ER",
760
- "EE",
761
- "SZ",
762
- "ET",
763
- "FK",
764
- "FO",
765
- "FJ",
766
- "FI",
767
- "FR",
768
- "GF",
769
- "PF",
770
- "TF",
771
- "GA",
772
- "GM",
773
- "GE",
774
- "DE",
775
- "GH",
776
- "GI",
777
- "GR",
778
- "GL",
779
- "GD",
780
- "GP",
781
- "GU",
782
- "GT",
783
- "GG",
784
- "GN",
785
- "GW",
786
- "GY",
787
- "HT",
788
- "HM",
789
- "HN",
790
- "HK",
791
- "HU",
792
- "IS",
793
- "IN",
794
- "ID",
795
- "IR",
796
- "IQ",
797
- "IE",
798
- "IM",
799
- "IL",
800
- "IT",
801
- "CI",
802
- "JM",
803
- "JP",
804
- "JE",
805
- "JO",
806
- "KZ",
807
- "KE",
808
- "KI",
809
- "XK",
810
- "KW",
811
- "KG",
812
- "LA",
813
- "LV",
814
- "LB",
815
- "LS",
816
- "LR",
817
- "LY",
818
- "LI",
819
- "LT",
820
- "LU",
821
- "MO",
822
- "MG",
823
- "MW",
824
- "MY",
825
- "MV",
826
- "ML",
827
- "MT",
828
- "MH",
829
- "MQ",
830
- "MR",
831
- "MU",
832
- "YT",
833
- "MX",
834
- "FM",
835
- "MD",
836
- "MC",
837
- "MN",
838
- "ME",
839
- "MS",
840
- "MA",
841
- "MZ",
842
- "MM",
843
- "NA",
844
- "NR",
845
- "NP",
846
- "NL",
847
- "NC",
848
- "NZ",
849
- "NI",
850
- "NE",
851
- "NG",
852
- "NU",
853
- "NF",
854
- "MK",
855
- "MP",
856
- "NO",
857
- "OM",
858
- "PK",
859
- "PW",
860
- "PS",
861
- "PA",
862
- "PG",
863
- "PY",
864
- "PE",
865
- "PH",
866
- "PN",
867
- "PL",
868
- "PT",
869
- "PR",
870
- "QA",
871
- "CG",
872
- "RE",
873
- "RO",
874
- "RU",
875
- "RW",
876
- "BL",
877
- "SH",
878
- "KN",
879
- "LC",
880
- "MF",
881
- "PM",
882
- "VC",
883
- "WS",
884
- "SM",
885
- "ST",
886
- "SA",
887
- "SN",
888
- "RS",
889
- "SC",
890
- "SL",
891
- "SG",
892
- "SX",
893
- "SK",
894
- "SI",
895
- "SB",
896
- "SO",
897
- "ZA",
898
- "GS",
899
- "KR",
900
- "SS",
901
- "ES",
902
- "LK",
903
- "SD",
904
- "SR",
905
- "SJ",
906
- "SE",
907
- "CH",
908
- "SY",
909
- "TW",
910
- "TJ",
911
- "TZ",
912
- "TH",
913
- "TL",
914
- "TG",
915
- "TK",
916
- "TO",
917
- "TT",
918
- "TN",
919
- "TR",
920
- "TM",
921
- "TC",
922
- "TV",
923
- "UG",
924
- "UA",
925
- "AE",
926
- "GB",
927
- "US",
928
- "UM",
929
- "VI",
930
- "UY",
931
- "UZ",
932
- "VU",
933
- "VA",
934
- "VE",
935
- "VN",
936
- "WF",
937
- "EH",
938
- "YE",
939
- "ZM",
940
- "ZW"
941
- ];
942
- function createJobPostingJsonLd({
943
- job,
944
- board,
945
- shareUrl
946
- }) {
947
- const educationRequirements = createEducationRequirements(job);
948
- const experienceRequirements = createExperienceRequirements(job);
949
- const jsonLd = pruneJsonLd({
950
- "@context": "https://schema.org/",
951
- "@type": "JobPosting",
952
- title: job.title,
953
- description: job.description ?? "",
954
- identifier: createIdentifier(job, board),
955
- datePosted: toIsoDate(job.publishedAt),
956
- validThrough: toIsoDate(job.expiresAt),
957
- employmentType: job.employmentType ? EMPLOYMENT_TYPE_TO_GOOGLE[job.employmentType] ?? "OTHER" : "OTHER",
958
- hiringOrganization: createHiringOrganization(job, board),
959
- jobLocation: createJobLocation(job),
960
- jobLocationType: job.remoteOption === "remote" ? "TELECOMMUTE" : void 0,
961
- applicantLocationRequirements: job.remoteOption === "remote" ? createApplicantLocationRequirements(job) : null,
962
- baseSalary: createBaseSalary(job),
963
- educationRequirements,
964
- experienceRequirements,
965
- experienceInPlaceOfEducation: job.experienceInPlaceOfEducation === true && educationRequirements && experienceRequirements ? true : void 0,
966
- url: shareUrl
967
- });
968
- if (!jsonLd || Array.isArray(jsonLd) || Object.keys(jsonLd).length === 0) {
969
- return null;
970
- }
971
- return jsonLd;
972
- }
973
- function createIdentifier(job, board) {
974
- const organizationName = job.company?.name ?? board.name ?? null;
975
- const identifier = { "@type": "PropertyValue", value: job.id };
976
- if (organizationName) identifier.name = organizationName;
977
- return identifier;
978
- }
979
- function createHiringOrganization(job, board) {
980
- const name = job.company?.name ?? board.name ?? null;
981
- if (!name) return null;
982
- const organization = { "@type": "Organization", name };
983
- const website = job.company?.website ?? null;
984
- const normalizedWebsite = website ? normalizeWebsiteUrl(website) : null;
985
- if (normalizedWebsite) organization.sameAs = normalizedWebsite;
986
- const logo = job.company?.logoUrl ?? board.logoUrl ?? null;
987
- if (logo) organization.logo = logo;
988
- return organization;
989
- }
990
- function normalizeWebsiteUrl(website) {
991
- const trimmed = website.trim();
992
- if (!trimmed) return null;
993
- return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
994
- }
995
- function createBaseSalary(job) {
996
- const currency = job.salaryCurrency?.trim();
997
- const hasMin = typeof job.salaryMin === "number";
998
- const hasMax = typeof job.salaryMax === "number";
999
- if (!currency || !hasMin && !hasMax) return null;
1000
- const value = { "@type": "QuantitativeValue" };
1001
- if (hasMin) value.minValue = job.salaryMin;
1002
- if (hasMax) value.maxValue = job.salaryMax;
1003
- const unitText = job.salaryTimeframe ? SALARY_TIMEFRAME_TO_UNIT[job.salaryTimeframe] : void 0;
1004
- if (unitText) value.unitText = unitText;
1005
- if (!hasMin && hasMax && typeof job.salaryMax === "number") {
1006
- value.value = job.salaryMax;
1007
- value.minValue = job.salaryMax;
1008
- } else if (!hasMax && hasMin && typeof job.salaryMin === "number") {
1009
- value.value = job.salaryMin;
1010
- value.maxValue = job.salaryMin;
1011
- }
1012
- return { "@type": "MonetaryAmount", currency: currency.toUpperCase(), value };
1013
- }
1014
- function createJobLocation(job) {
1015
- const places = job.officeLocations.map(
1016
- (office) => createPlaceJsonLd({
1017
- countryCode: office.countryCode,
1018
- region: office.region,
1019
- locality: office.city ?? office.locality,
1020
- postalCode: office.postalCode,
1021
- name: office.displayName
1022
- })
1023
- ).filter((place) => place !== null);
1024
- if (places.length === 0) return null;
1025
- return places.length === 1 ? places[0] : places;
1026
- }
1027
- function createPlaceJsonLd({
1028
- countryCode,
1029
- region,
1030
- locality,
1031
- postalCode,
1032
- name
1033
- }) {
1034
- const address = { "@type": "PostalAddress" };
1035
- const country = countryCode?.trim();
1036
- const reg = region?.trim();
1037
- const loc = locality?.trim();
1038
- const postal = postalCode?.trim();
1039
- const placeName = name?.trim();
1040
- if (country) address.addressCountry = country.toUpperCase();
1041
- if (reg) address.addressRegion = reg;
1042
- if (loc) address.addressLocality = loc;
1043
- if (postal) address.postalCode = postal;
1044
- const hasAddressDetails = Object.keys(address).length > 1;
1045
- if (!hasAddressDetails && !placeName) return null;
1046
- const place = { "@type": "Place" };
1047
- if (placeName) place.name = placeName;
1048
- if (hasAddressDetails) place.address = address;
1049
- return place;
1050
- }
1051
- function createApplicantLocationRequirements(job) {
1052
- if (job.remoteWorldwide) {
1053
- return ALL_COUNTRY_CODES.map((code) => ({
1054
- "@type": "Country",
1055
- name: code
1056
- }));
1057
- }
1058
- const entries = [];
1059
- const seen = /* @__PURE__ */ new Set();
1060
- for (const code of job.remoteWorkPermitCountryCodes ?? []) {
1061
- const normalized = code?.trim().toUpperCase();
1062
- if (!normalized || seen.has(normalized)) continue;
1063
- seen.add(normalized);
1064
- entries.push({ "@type": "Country", name: normalized });
1065
- }
1066
- for (const subdivision of job.remoteWorkPermitSubdivisionCodes ?? []) {
1067
- const countryCode = subdivision?.split("-")[0]?.trim().toUpperCase();
1068
- if (!countryCode || seen.has(countryCode)) continue;
1069
- seen.add(countryCode);
1070
- entries.push({ "@type": "Country", name: countryCode });
1071
- }
1072
- if (entries.length === 0) return null;
1073
- return entries.length === 1 ? entries[0] : entries;
1074
- }
1075
- function createEducationRequirements(job) {
1076
- const requirements = job.educationRequirements;
1077
- if (!requirements || requirements.length === 0) return null;
1078
- if (requirements.includes("no_requirements")) return "no requirements";
1079
- const credentials = requirements.filter((value) => value in EDUCATION_TO_CREDENTIAL).map((value) => ({
1080
- "@type": "EducationalOccupationalCredential",
1081
- credentialCategory: EDUCATION_TO_CREDENTIAL[value]
1082
- }));
1083
- return credentials.length > 0 ? credentials : null;
1084
- }
1085
- function createExperienceRequirements(job) {
1086
- const months = job.experienceMonths;
1087
- if (typeof months !== "number") return null;
1088
- if (months === 0) return "no requirements";
1089
- return {
1090
- "@type": "OccupationalExperienceRequirements",
1091
- monthsOfExperience: months
1092
- };
1093
- }
1094
- function toIsoDate(value) {
1095
- if (!value) return null;
1096
- const date = new Date(value);
1097
- if (Number.isNaN(date.getTime())) return null;
1098
- return date.toISOString();
1099
- }
1100
- function pruneJsonLd(value) {
1101
- const keep = (entry) => {
1102
- if (entry == null) return false;
1103
- if (Array.isArray(entry)) return entry.length > 0;
1104
- if (typeof entry === "object")
1105
- return Object.keys(entry).length > 0;
1106
- return true;
1107
- };
1108
- if (Array.isArray(value)) {
1109
- return value.map((entry) => pruneJsonLd(entry)).filter(keep);
1110
- }
1111
- if (value && typeof value === "object") {
1112
- const entries = Object.entries(value).map(([key, entry]) => [key, pruneJsonLd(entry)]).filter(([, entry]) => keep(entry));
1113
- return Object.fromEntries(entries);
1114
- }
1115
- return value;
1116
- }
1117
-
1118
141
  // src/seo/listing.ts
1119
142
  function listingHead(options) {
1120
- const countPrefix = typeof options.count === "number" ? `${options.count} ` : "";
1121
- const title = `${countPrefix}${options.heading} | ${options.boardName}`;
1122
- const description = `Browse ${countPrefix}${options.heading.toLowerCase()} on ${options.boardName}.`;
143
+ const title = options.title;
144
+ const description = options.description;
1123
145
  const url = `${options.origin}${options.path}`;
1124
146
  return {
1125
147
  meta: [
@@ -1134,432 +156,38 @@ function listingHead(options) {
1134
156
  };
1135
157
  }
1136
158
  function listingJsonLd(options) {
1137
- const jobUrl = (job) => job.company ? `${options.origin}/companies/${job.company.slug}/jobs/${job.slug}` : `${options.origin}/jobs/${job.slug}`;
1138
- const objects = [
1139
- {
159
+ const objects = [];
160
+ if (options.breadcrumbs.length >= 2) {
161
+ objects.push({
1140
162
  "@context": "https://schema.org",
1141
- // Deliberately NOT createBreadcrumbJsonLd (breadcrumbs.ts): listing trails
1142
- // are code-constructed (never user copy), so the hosted listing page skips
1143
- // the trim/drop-blank/min-2 gate that module transcribes. Same schema.org
1144
- // fragment, different (intentional) semantics — do not unify blindly.
1145
163
  "@type": "BreadcrumbList",
1146
164
  itemListElement: options.breadcrumbs.map((crumb, index) => ({
1147
165
  "@type": "ListItem",
1148
166
  position: index + 1,
1149
167
  name: crumb.name,
1150
- ...crumb.path ? { item: `${options.origin}${crumb.path}` } : {}
168
+ ...crumb.path ? { item: boardUrl(options.origin, crumb.path) } : {}
1151
169
  }))
1152
- }
1153
- ];
1154
- if (options.jobs && options.jobs.length > 0) {
170
+ });
171
+ }
172
+ const detailJobs = (options.jobs ?? []).filter(
173
+ (job) => job.company != null && Boolean(job.company.slug)
174
+ );
175
+ if (detailJobs.length > 0) {
1155
176
  objects.push({
1156
177
  "@context": "https://schema.org",
1157
178
  "@type": "ItemList",
1158
- itemListElement: options.jobs.map((job, index) => ({
179
+ itemListElement: detailJobs.map((job, index) => ({
1159
180
  "@type": "ListItem",
1160
181
  position: index + 1,
1161
- url: jobUrl(job)
182
+ url: boardUrl(
183
+ options.origin,
184
+ jobDetailPath(job.company.slug, job.slug)
185
+ )
1162
186
  }))
1163
187
  });
1164
188
  }
1165
189
  return objects;
1166
190
  }
1167
-
1168
- // src/format/salary-lexicon.ts
1169
- var EN2 = {
1170
- timeframe: {
1171
- per_year: "Yearly",
1172
- per_month: "Monthly",
1173
- per_week: "Weekly",
1174
- per_day: "Daily",
1175
- per_hour: "Hourly"
1176
- },
1177
- rangePrefix: { from: "From", upTo: "Up to" },
1178
- seniority: {
1179
- entry_level: "Entry level",
1180
- associate: "Associate",
1181
- mid_level: "Mid-level",
1182
- senior: "Senior",
1183
- lead: "Lead",
1184
- principal: "Principal",
1185
- director: "Director",
1186
- executive: "Executive"
1187
- },
1188
- frames: {
1189
- entitySalariesTitle: ({ entity, range }) => `${entity} Salaries (${range}/yr)`,
1190
- salariesInPlaceTitle: ({ place, range }) => `Salaries in ${place} (${range}/yr)`,
1191
- salariesInPlaceTitleNoRange: ({ place }) => `Salaries in ${place}`,
1192
- entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Salaries in ${place} (${range}/yr)`,
1193
- companySalariesTitle: ({ company, range }) => `${company} Salaries${range ? ` (${range}/yr)` : ""}`,
1194
- companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Salaries${range ? ` (${range}/yr)` : ""}`,
1195
- professionalsEarn: ({ entity, range, count }) => `${entity} professionals earn ${range}/yr on average across ${count} job postings.`,
1196
- rolesPay: ({ entity, range, count }) => `${entity} roles pay ${range}/yr on average across ${count} job postings.`,
1197
- professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity} professionals in ${place} earn ${range}/yr on average across ${count} job postings.`,
1198
- seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} roles start at ${lowAmount}, ${highLabel} roles reach ${highAmount}.`,
1199
- seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel} salaries start at ${lowAmount}, while ${highLabel} roles reach ${highAmount}.`,
1200
- compareAcrossTopPaying: ({ count }) => `Compare across ${count} top-paying ${count === 1 ? "company" : "companies"}.`,
1201
- compareAcrossTop: ({ count }) => `Compare across ${count} top ${count === 1 ? "company" : "companies"}.`,
1202
- boardWideAverage: ({ entity, range }) => `Board-wide ${entity} average: ${range}/yr.`,
1203
- averageSalaryInCity: ({ place, range, count }) => `Average salary in ${place} is ${range}/yr across ${count} jobs. Browse salary data by title and skill.`,
1204
- browseLocationsInRegion: ({ count, place }) => `Browse salary data across ${count} locations in ${place}. Compare salaries by title, skill, and company.`,
1205
- companyPays: ({ company, range, count }) => `${company} pays ${range}/yr on average across ${count} job postings.`,
1206
- breakdownAcrossCategories: ({ count }) => `Breakdown across ${count} job ${count === 1 ? "category" : "categories"}.`,
1207
- compareWithSimilar: ({ count }) => `Compare with ${count} similar ${count === 1 ? "company" : "companies"}.`,
1208
- salaryInfoFor: ({ company, board }) => `Salary information for ${company} on ${board}.`,
1209
- categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category} roles at ${company} pay ${range}/yr based on ${count} job postings.`,
1210
- categorySalaryDataFor: ({ category, company, board }) => `${category} salary data for ${company} on ${board}.`
1211
- }
1212
- };
1213
- var DE2 = {
1214
- timeframe: {
1215
- per_year: "pro Jahr",
1216
- per_month: "pro Monat",
1217
- per_week: "pro Woche",
1218
- per_day: "pro Tag",
1219
- per_hour: "pro Stunde"
1220
- },
1221
- rangePrefix: { from: "ab", upTo: "bis zu" },
1222
- seniority: {
1223
- entry_level: "Entry-Level",
1224
- associate: "Associate",
1225
- mid_level: "Mid-Level",
1226
- senior: "Senior",
1227
- lead: "Lead",
1228
- principal: "Principal",
1229
- director: "Director",
1230
- executive: "F\xFChrungskraft"
1231
- },
1232
- frames: {
1233
- entitySalariesTitle: ({ entity, range }) => `${entity} Geh\xE4lter (${range}/Jahr)`,
1234
- salariesInPlaceTitle: ({ place, range }) => `Geh\xE4lter in ${place} (${range}/Jahr)`,
1235
- salariesInPlaceTitleNoRange: ({ place }) => `Geh\xE4lter in ${place}`,
1236
- entitySalariesInPlaceTitle: ({ entity, place, range }) => `${entity} Geh\xE4lter in ${place} (${range}/Jahr)`,
1237
- companySalariesTitle: ({ company, range }) => `${company} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
1238
- companyCategorySalariesTitle: ({ company, category, range }) => `${company} ${category} Geh\xE4lter${range ? ` (${range}/Jahr)` : ""}`,
1239
- professionalsEarn: ({ entity, range, count }) => `${entity}-Fachkr\xE4fte verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
1240
- rolesPay: ({ entity, range, count }) => `${entity}-Positionen werden mit durchschnittlich ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
1241
- professionalsInLocationEarn: ({ entity, place, range, count }) => `${entity}-Fachkr\xE4fte in ${place} verdienen durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
1242
- seniorityRange: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Positionen beginnen bei ${lowAmount}, ${highLabel}-Positionen erreichen ${highAmount}.`,
1243
- seniorityRangeWhile: ({ lowLabel, lowAmount, highLabel, highAmount }) => `${lowLabel}-Geh\xE4lter beginnen bei ${lowAmount}, w\xE4hrend ${highLabel}-Positionen ${highAmount} erreichen.`,
1244
- compareAcrossTopPaying: ({ count }) => `Vergleichen Sie ${count} ${count === 1 ? "bestzahlendes" : "bestzahlende"} Unternehmen.`,
1245
- compareAcrossTop: ({ count }) => `Vergleichen Sie ${count} Top-Unternehmen.`,
1246
- boardWideAverage: ({ entity, range }) => `Gesamtdurchschnitt ${entity}: ${range}/Jahr.`,
1247
- averageSalaryInCity: ({ place, range, count }) => `Das Durchschnittsgehalt in ${place} betr\xE4gt ${range}/Jahr, basierend auf ${count} Stellen. Gehaltsdaten nach Titel und F\xE4higkeiten durchsuchen.`,
1248
- browseLocationsInRegion: ({ count, place }) => `Gehaltsdaten f\xFCr ${count} Standorte in ${place} durchsuchen. Geh\xE4lter nach Titel, F\xE4higkeiten und Unternehmen vergleichen.`,
1249
- companyPays: ({ company, range, count }) => `${company} zahlt durchschnittlich ${range}/Jahr, basierend auf ${count} Stellenanzeigen.`,
1250
- breakdownAcrossCategories: ({ count }) => `Aufschl\xFCsselung \xFCber ${count} Job-${count === 1 ? "Kategorie" : "Kategorien"}.`,
1251
- compareWithSimilar: ({ count }) => `Vergleichen Sie mit ${count} \xE4hnlichen Unternehmen.`,
1252
- salaryInfoFor: ({ company, board }) => `Gehaltsinformationen f\xFCr ${company} auf ${board}.`,
1253
- categoryRolesAtCompanyPay: ({ category, company, range, count }) => `${category}-Positionen bei ${company} werden mit ${range}/Jahr verg\xFCtet, basierend auf ${count} Stellenanzeigen.`,
1254
- categorySalaryDataFor: ({ category, company, board }) => `${category}-Gehaltsdaten f\xFCr ${company} auf ${board}.`
1255
- }
1256
- };
1257
- var LEXICONS = { en: EN2, de: DE2 };
1258
- function getSalaryLexicon(locale) {
1259
- return locale && LEXICONS[locale] || EN2;
1260
- }
1261
-
1262
- // src/seo/salary.ts
1263
- var compactCurrencyCache = /* @__PURE__ */ new Map();
1264
- function getCompactCurrencyFormatter(language) {
1265
- const cached = compactCurrencyCache.get(language);
1266
- if (cached) return cached;
1267
- const formatter = new Intl.NumberFormat(language, {
1268
- style: "currency",
1269
- currency: "USD",
1270
- notation: "compact",
1271
- compactDisplay: "short",
1272
- minimumFractionDigits: 0,
1273
- maximumFractionDigits: 1
1274
- });
1275
- compactCurrencyCache.set(language, formatter);
1276
- return formatter;
1277
- }
1278
- function formatUsd(locale, value) {
1279
- try {
1280
- return getCompactCurrencyFormatter(locale).format(value);
1281
- } catch {
1282
- if (value >= 1e6) {
1283
- const millions = value / 1e6;
1284
- return `$${millions % 1 === 0 ? millions : millions.toFixed(1)}M`;
1285
- }
1286
- if (value >= 1e3) {
1287
- const thousands = value / 1e3;
1288
- return `$${thousands % 1 === 0 ? thousands : thousands.toFixed(1)}k`;
1289
- }
1290
- return `$${value}`;
1291
- }
1292
- }
1293
- function formatRange(locale, min, max) {
1294
- return `${formatUsd(locale, min)} \u2013 ${formatUsd(locale, max)}`;
1295
- }
1296
- function formatSeniority(locale, value, overrides) {
1297
- if (overrides?.[value]) {
1298
- return overrides[value];
1299
- }
1300
- const lexiconLabel = getSalaryLexicon(locale).seniority[value];
1301
- if (lexiconLabel) {
1302
- return lexiconLabel;
1303
- }
1304
- return value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1305
- }
1306
- var SENIORITY_ORDER = [
1307
- "entry_level",
1308
- "associate",
1309
- "mid_level",
1310
- "senior",
1311
- "lead",
1312
- "principal",
1313
- "director",
1314
- "executive"
1315
- ];
1316
- function sortBySeniority(items) {
1317
- return [...items].sort((a, b) => {
1318
- const aIdx = SENIORITY_ORDER.indexOf(
1319
- a.seniority
1320
- );
1321
- const bIdx = SENIORITY_ORDER.indexOf(
1322
- b.seniority
1323
- );
1324
- return (aIdx === -1 ? 999 : aIdx) - (bIdx === -1 ? 999 : bIdx);
1325
- });
1326
- }
1327
- var yearly = {
1328
- unitText: "YEAR",
1329
- duration: "P1Y"
1330
- };
1331
- function distribution(name, currency, minValue, maxValue, stats = {}) {
1332
- return {
1333
- "@type": "MonetaryAmountDistribution",
1334
- name,
1335
- currency,
1336
- minValue,
1337
- maxValue,
1338
- ...stats,
1339
- ...yearly
1340
- };
1341
- }
1342
- function seniorityDistributions(locale, rows, currency, nameFor) {
1343
- return rows.map(
1344
- (row) => distribution(
1345
- nameFor(formatSeniority(locale, row.seniority)),
1346
- currency,
1347
- row.avgSalaryMin,
1348
- row.avgSalaryMax
1349
- )
1350
- );
1351
- }
1352
- function salaryEnvelope(type, name, extra, estimatedSalary) {
1353
- return {
1354
- "@context": "https://schema.org",
1355
- "@type": type,
1356
- name,
1357
- ...extra,
1358
- estimatedSalary
1359
- };
1360
- }
1361
- function buildSalaryFaq(locale, label, overall) {
1362
- if (!overall) return [];
1363
- const range = formatRange(locale, overall.avgMin, overall.avgMax);
1364
- return [
1365
- {
1366
- q: `What is the average salary for ${label}?`,
1367
- 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.`
1368
- },
1369
- {
1370
- q: `How is the salary figure for ${label} calculated?`,
1371
- 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.`
1372
- }
1373
- ];
1374
- }
1375
- function itemListJsonLd(items) {
1376
- if (items.length === 0) return null;
1377
- return {
1378
- "@context": "https://schema.org",
1379
- "@type": "ItemList",
1380
- numberOfItems: items.length,
1381
- itemListElement: items.map((it, i) => ({
1382
- "@type": "ListItem",
1383
- position: i + 1,
1384
- name: it.name,
1385
- url: it.url
1386
- }))
1387
- };
1388
- }
1389
- function faqJsonLd(faqs) {
1390
- if (faqs.length === 0) return null;
1391
- return {
1392
- "@context": "https://schema.org",
1393
- "@type": "FAQPage",
1394
- mainEntity: faqs.map((f) => ({
1395
- "@type": "Question",
1396
- name: f.q,
1397
- acceptedAnswer: { "@type": "Answer", text: f.a }
1398
- }))
1399
- };
1400
- }
1401
- function titleSalaryJsonLd(locale, d) {
1402
- if (!d.overallSalary) return null;
1403
- return salaryEnvelope("Occupation", d.categoryName, {}, [
1404
- distribution(
1405
- `${d.categoryName} salary (all levels)`,
1406
- d.currency,
1407
- d.overallSalary.avgMin,
1408
- d.overallSalary.avgMax,
1409
- {
1410
- ...d.boardP25Min ? { percentile25: d.boardP25Min } : {},
1411
- ...d.boardP75Max ? { percentile75: d.boardP75Max } : {}
1412
- }
1413
- ),
1414
- ...seniorityDistributions(
1415
- locale,
1416
- d.bySeniority,
1417
- d.currency,
1418
- (label) => `${label} ${d.categoryName}`
1419
- )
1420
- ]);
1421
- }
1422
- function skillSalaryJsonLd(d) {
1423
- if (!d.overallSalary) return null;
1424
- return salaryEnvelope("Occupation", d.skillName, {}, [
1425
- distribution(
1426
- `${d.skillName} salary (all levels)`,
1427
- d.currency,
1428
- d.overallSalary.avgMin,
1429
- d.overallSalary.avgMax,
1430
- {
1431
- median: Math.round(
1432
- (d.overallSalary.medianMin + d.overallSalary.medianMax) / 2
1433
- )
1434
- }
1435
- )
1436
- ]);
1437
- }
1438
- function locationSalaryJsonLd(d) {
1439
- const isCity = d.adminLevel === "city" || d.adminLevel === "locality";
1440
- if (!isCity || !d.overallSalary) return null;
1441
- return salaryEnvelope(
1442
- "Occupation",
1443
- `Average Salary in ${d.placeName}`,
1444
- {
1445
- occupationLocation: {
1446
- "@type": "City",
1447
- name: d.placeName,
1448
- address: { "@type": "PostalAddress", addressCountry: d.countryCode }
1449
- }
1450
- },
1451
- [
1452
- distribution(
1453
- `Average salary in ${d.placeName}`,
1454
- d.currency,
1455
- d.overallSalary.avgMin,
1456
- d.overallSalary.avgMax,
1457
- {
1458
- median: Math.round(
1459
- (d.overallSalary.medianMin + d.overallSalary.medianMax) / 2
1460
- ),
1461
- // Hosted includes these unconditionally, nulls and all.
1462
- percentile25: d.overallSalary.p25Min,
1463
- percentile75: d.overallSalary.p75Max
1464
- }
1465
- )
1466
- ]
1467
- );
1468
- }
1469
- function crossAxisSalaryJsonLd(locale, args) {
1470
- if (!args.overall) return null;
1471
- return salaryEnvelope(
1472
- "Occupation",
1473
- `${args.name} salary in ${args.placeName}`,
1474
- {
1475
- occupationLocation: {
1476
- "@type": "AdministrativeArea",
1477
- name: args.placeName,
1478
- address: {
1479
- "@type": "PostalAddress",
1480
- addressCountry: args.countryCode
1481
- }
1482
- }
1483
- },
1484
- [
1485
- distribution(
1486
- `${args.name} salary in ${args.placeName} (all levels)`,
1487
- args.currency,
1488
- args.overall.avgMin,
1489
- args.overall.avgMax,
1490
- {
1491
- ...args.overall.p25Min ? { percentile25: args.overall.p25Min } : {},
1492
- ...args.overall.p75Max ? { percentile75: args.overall.p75Max } : {}
1493
- }
1494
- ),
1495
- ...seniorityDistributions(
1496
- locale,
1497
- args.bySeniority,
1498
- args.currency,
1499
- (label) => `${label} ${args.name} in ${args.placeName}`
1500
- )
1501
- ]
1502
- );
1503
- }
1504
- function companySalaryJsonLd(locale, d) {
1505
- if (!d.overallSalary) return null;
1506
- return salaryEnvelope(
1507
- "OccupationAggregationByEmployer",
1508
- `Jobs at ${d.companyName}`,
1509
- {
1510
- sampleSize: d.overallSalary.jobCount,
1511
- hiringOrganization: { "@type": "Organization", name: d.companyName }
1512
- },
1513
- [
1514
- distribution(
1515
- "base",
1516
- d.currency,
1517
- d.overallSalary.avgMin,
1518
- d.overallSalary.avgMax,
1519
- {
1520
- ...d.boardMedianMin && d.boardMedianMax ? { median: Math.round((d.boardMedianMin + d.boardMedianMax) / 2) } : {},
1521
- ...d.boardP25Min ? { percentile25: d.boardP25Min } : {},
1522
- ...d.boardP75Max ? { percentile75: d.boardP75Max } : {}
1523
- }
1524
- ),
1525
- ...seniorityDistributions(
1526
- locale,
1527
- d.bySeniority,
1528
- d.currency,
1529
- (label) => `${label} salary`
1530
- )
1531
- ]
1532
- );
1533
- }
1534
- function companyCategorySalaryJsonLd(locale, d) {
1535
- if (!d.overallSalary) return null;
1536
- return salaryEnvelope(
1537
- "Occupation",
1538
- `${d.categoryName} at ${d.companyName}`,
1539
- {
1540
- occupationalCategory: d.categoryName,
1541
- hiringOrganization: { "@type": "Organization", name: d.companyName }
1542
- },
1543
- [
1544
- distribution(
1545
- `${d.categoryName} salary (all levels)`,
1546
- d.currency,
1547
- d.overallSalary.avgMin,
1548
- d.overallSalary.avgMax,
1549
- {
1550
- ...d.boardCategoryP25Min ? { percentile25: d.boardCategoryP25Min } : {},
1551
- ...d.boardCategoryP75Max ? { percentile75: d.boardCategoryP75Max } : {}
1552
- }
1553
- ),
1554
- ...seniorityDistributions(
1555
- locale,
1556
- d.bySeniority,
1557
- d.currency,
1558
- (label) => `${label} ${d.categoryName}`
1559
- )
1560
- ]
1561
- );
1562
- }
1563
191
  export {
1564
192
  ALL_COUNTRY_CODES,
1565
193
  SENIORITY_ORDER,
@@ -1573,9 +201,8 @@ export {
1573
201
  createJobPostingJsonLd,
1574
202
  crossAxisSalaryJsonLd,
1575
203
  faqJsonLd,
1576
- formatRange,
1577
- formatSeniority,
1578
- formatUsd,
204
+ formatSalaryStat,
205
+ formatSalaryStatRange,
1579
206
  itemListJsonLd,
1580
207
  listingHead,
1581
208
  listingJsonLd,