@cavuno/board 3.1.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +421 -0
- package/README.md +6 -2
- package/dist/{_spec-nkWAraS0.d.mts → _spec-GVe6WAfT.d.mts} +68 -59
- package/dist/{_spec-nkWAraS0.d.ts → _spec-GVe6WAfT.d.ts} +68 -59
- package/dist/bin.mjs +7 -2
- package/dist/blog-BsmKizpd.d.ts +29 -0
- package/dist/blog-Cf64mI8A.d.mts +29 -0
- package/dist/{salaries-BahBpxWT.d.ts → board-DUhgiacm.d.ts} +17 -29
- package/dist/{salaries-ChSqDBAM.d.mts → board-GGWJ4coe.d.mts} +17 -29
- package/dist/breadcrumbs-DFKr0HzU.d.mts +372 -0
- package/dist/breadcrumbs-DiccJhk4.d.ts +372 -0
- package/dist/chunk-2ETP5UH4.mjs +271 -0
- package/dist/chunk-BGKNXG4H.mjs +146 -0
- package/dist/chunk-DLSO6XXY.mjs +33 -0
- package/dist/chunk-T2KC6XGV.mjs +1060 -0
- package/dist/chunk-YEAQ3JEI.mjs +853 -0
- package/dist/doctor.js +1 -1
- package/dist/doctor.mjs +1 -1
- package/dist/filters.d.mts +25 -20
- package/dist/filters.d.ts +25 -20
- package/dist/filters.js +3 -692
- package/dist/filters.mjs +3 -690
- package/dist/format.d.mts +57 -230
- package/dist/format.d.ts +57 -230
- package/dist/format.js +355 -927
- package/dist/format.mjs +213 -931
- package/dist/go.js +11 -1
- package/dist/go.mjs +4 -29
- package/dist/index.d.mts +23 -75
- package/dist/index.d.ts +23 -75
- package/dist/index.js +11 -36
- package/dist/index.mjs +32 -313
- package/dist/{jobs-lVvKCBCU.d.mts → jobs-BuLpV8iy.d.mts} +4 -4
- package/dist/{jobs-BiEvL9d7.d.ts → jobs-QrbXOYB9.d.ts} +4 -4
- package/dist/paths.d.mts +89 -1
- package/dist/paths.d.ts +89 -1
- package/dist/paths.js +62 -14
- package/dist/paths.mjs +29 -78
- package/dist/route-contract.js +23 -13
- package/dist/route-contract.mjs +30 -1079
- package/dist/{search-CCyFWPi3.d.mts → search-Cr4noJJZ.d.mts} +14 -3
- package/dist/{search-CyRvVMUp.d.ts → search-DdsniXQv.d.ts} +14 -3
- package/dist/seo.d.mts +33 -189
- package/dist/seo.d.ts +33 -189
- package/dist/seo.js +197 -718
- package/dist/seo.mjs +44 -1417
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/server.mjs +5 -31
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/sitemap.js +25 -14
- package/dist/sitemap.mjs +22 -97
- package/dist/skills.js +1 -1
- package/dist/skills.mjs +1 -1
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/dist/suggest.js +17 -3
- package/dist/suggest.mjs +17 -3
- package/dist/well-known.js +23 -13
- package/dist/well-known.mjs +4 -421
- package/package.json +5 -4
- package/skills/cavuno-board-applications/SKILL.md +4 -1
- package/skills/cavuno-board-filters/SKILL.md +11 -12
- package/skills/cavuno-board-format/SKILL.md +68 -56
- package/skills/cavuno-board-i18n/SKILL.md +102 -21
- package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
- package/skills/cavuno-board-seo/BLOG.md +4 -1
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
- package/skills/cavuno-board-seo/LISTING.md +35 -12
- package/skills/cavuno-board-seo/SALARY.md +83 -18
- package/skills/cavuno-board-seo/SKILL.md +3 -2
- package/skills/manifest.json +3 -3
- package/dist/board-NE98PlwN.d.ts +0 -16
- package/dist/board-muu8BI8P.d.mts +0 -16
- package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
- package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { P as PublicBoard, a as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './board-DUhgiacm.js';
|
|
2
|
+
import { P as PublicJob } from './jobs-QrbXOYB9.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wire enum for `salaryTimeframe`. Defined here (not re-exported from any
|
|
6
|
+
* private catalog) so the published `@cavuno/board/format` types
|
|
7
|
+
* stay self-contained.
|
|
8
|
+
*/
|
|
9
|
+
type SalaryTimeframeValue = 'per_year' | 'per_month' | 'per_week' | 'per_day' | 'per_hour';
|
|
10
|
+
type SalaryTimeframeInput = SalaryTimeframeValue | string | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Which ICU currency form to render.
|
|
13
|
+
*
|
|
14
|
+
* `'symbol'` (default) is the UNAMBIGUOUS form — `CA$`, `A$`, `CN¥` — and is
|
|
15
|
+
* the right default for a board listing several currencies side by side.
|
|
16
|
+
* `'narrowSymbol'` is the short form (`$`, `¥`); on a single-currency board
|
|
17
|
+
* there is nothing to confuse it with, and it reads cleaner. A caller-supplied
|
|
18
|
+
* presentation preference, passed straight to `Intl` — not a word.
|
|
19
|
+
*/
|
|
20
|
+
type CurrencyDisplay = 'symbol' | 'narrowSymbol';
|
|
21
|
+
/**
|
|
22
|
+
* Number notation preference, passed straight to `Intl.NumberFormat`.
|
|
23
|
+
*
|
|
24
|
+
* When omitted, magnitude picks: `'compact'` only where ICU actually
|
|
25
|
+
* shortens (absolute value ≥ 1000 → `$90K`); smaller values stay
|
|
26
|
+
* `'standard'` so hourly/day rates keep CLDR minor units (`$22.50`,
|
|
27
|
+
* `KWD 22.567`). Compact on a two-figure rate is meaningless and drops
|
|
28
|
+
* cents. Caller override always wins.
|
|
29
|
+
*/
|
|
30
|
+
type NumberNotation = 'compact' | 'standard';
|
|
31
|
+
/**
|
|
32
|
+
* Formatted salary amount + which open/closed bound it represents.
|
|
33
|
+
* Applications own:
|
|
34
|
+
* - chrome words for `from` / `upTo` (e.g. "From", "ab")
|
|
35
|
+
* - joining `text` with `timeframe` (order, spacing, `/`, particles)
|
|
36
|
+
* - bidi isolation around each operand when directions may differ
|
|
37
|
+
* (U+2066/U+2069 FSI/PDI, or HTML `<bdi>` / `dir="auto"`)
|
|
38
|
+
*/
|
|
39
|
+
type FormattedSalaryRange = {
|
|
40
|
+
/** Intl-formatted amount (or range). No timeframe attached. */
|
|
41
|
+
text: string;
|
|
42
|
+
/**
|
|
43
|
+
* The wire timeframe enum, passed straight back — NOT a word.
|
|
44
|
+
*
|
|
45
|
+
* Every rendering of it is presentation: "year", "Yearly", "/yr",
|
|
46
|
+
* "per annum", "pro Jahr", "年収". `Intl` can produce exactly one of
|
|
47
|
+
* those forms, so producing it here would pick for the application.
|
|
48
|
+
* The app maps the enum through its own catalog, the same way it maps
|
|
49
|
+
* `bound` and breadcrumb `kind`.
|
|
50
|
+
*
|
|
51
|
+
* `null` when the wire value is absent or not a known timeframe.
|
|
52
|
+
*/
|
|
53
|
+
timeframe: SalaryTimeframeValue | null;
|
|
54
|
+
bound: 'range' | 'from' | 'upTo';
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Format a per-job salary range in the BOARD language
|
|
58
|
+
* (`board.context().language` — required, never defaulted; see ).
|
|
59
|
+
*
|
|
60
|
+
* Returns `{ text, timeframe, bound }` so applications can:
|
|
61
|
+
* - attach open-range chrome in their own locale (`bound`)
|
|
62
|
+
* - compose amount + timeframe order themselves (prefix vs postfix, with or
|
|
63
|
+
* without `/`, with or without a space — ja/zh often glue with no space)
|
|
64
|
+
* - isolate bidi when composing (see module JSDoc)
|
|
65
|
+
*
|
|
66
|
+
* Returns `null` when both bounds are null, currency is missing/empty, the
|
|
67
|
+
* locale is invalid/unsupported, or `Intl` rejects the currency format.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* formatSalaryRange('en', 90000, 120000, 'per_year', 'USD');
|
|
71
|
+
* // { text: "$90–120K", timeframe: "per_year", bound: "range" }
|
|
72
|
+
* formatSalaryRange('en', 90000, null, 'per_year', 'USD');
|
|
73
|
+
* // { text: "$90K", timeframe: "per_year", bound: "from" }
|
|
74
|
+
* formatSalaryRange('de', 90000, 120000, 'per_year', 'EUR');
|
|
75
|
+
* // { text: "90.000–120.000 €", timeframe: "per_year", bound: "range" }
|
|
76
|
+
* formatSalaryRange('de', 90000, 120000, 'per_year', null);
|
|
77
|
+
* // null // no currency → no salary
|
|
78
|
+
* formatSalaryRange('en', 22.5, null, 'per_hour', 'USD');
|
|
79
|
+
* // { text: "$22.50", timeframe: "per_hour", bound: "from" } // magnitude → standard
|
|
80
|
+
*/
|
|
81
|
+
declare function formatSalaryRange(language: string, min: number | null, max: number | null, timeframe: SalaryTimeframeInput, currency?: string | null, currencyDisplay?: CurrencyDisplay, notation?: NumberNotation): FormattedSalaryRange | null;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
|
|
85
|
+
* types, transcribed field-by-field from the hosted board's generator
|
|
86
|
+
* (the hosted board implementation + `job-posting-location-json-ld.ts`) and
|
|
87
|
+
* tested against the hosted behavior.
|
|
88
|
+
*
|
|
89
|
+
* JSON-LD structure is locale-neutral per schema.org — no board-language
|
|
90
|
+
* parameter here; the only display strings are wire values passed through.
|
|
91
|
+
*
|
|
92
|
+
* Named exclusions (hosted behaviours with no wire twin, recorded in the
|
|
93
|
+
* public API contract):
|
|
94
|
+
* - `board.companyLegalName` — hosted falls back to it for the
|
|
95
|
+
* organization name between `company.name` and `board.name`; the wire
|
|
96
|
+
* board context does not expose it.
|
|
97
|
+
* - `board.companyWebsiteUrl` — hosted falls back to it for `sameAs` when
|
|
98
|
+
* the company has no website; not on the wire.
|
|
99
|
+
* - the hosted `job.place`/`locationLabel` jobLocation fallback (used when
|
|
100
|
+
* a job has no office locations) — the wire `PublicJob` carries no
|
|
101
|
+
* resolved place geo, only `officeLocations`.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
type JsonLdObject = Record<string, unknown>;
|
|
105
|
+
/** The only board fields JSON-LD needs — keeps callers' shapes flexible. */
|
|
106
|
+
type JsonLdBoard = Pick<PublicBoard, 'name' | 'logoUrl'>;
|
|
107
|
+
/**
|
|
108
|
+
* Worldwide-remote jobs must still emit `applicantLocationRequirements`
|
|
109
|
+
* next to TELECOMMUTE (Google requires a location signal), so the hosted
|
|
110
|
+
* board — and this transcription — lists every country it recognises.
|
|
111
|
+
*
|
|
112
|
+
* This is the hosted board's exact country catalog: sorted by English common
|
|
113
|
+
* name, includes `XK` (Kosovo), and excludes `KP` (North Korea). tested
|
|
114
|
+
* against the hosted list so the two cannot drift.
|
|
115
|
+
*/
|
|
116
|
+
declare const ALL_COUNTRY_CODES: readonly ["AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG", "AR", "AM", "AW", "AU", "AT", "AZ", "BS", "BH", "BD", "BB", "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BA", "BW", "BV", "BR", "IO", "VG", "BN", "BG", "BF", "BI", "KH", "CM", "CA", "CV", "BQ", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM", "CK", "CR", "HR", "CU", "CW", "CY", "CZ", "DK", "DJ", "DM", "DO", "CD", "EC", "EG", "SV", "GQ", "ER", "EE", "SZ", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF", "GA", "GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP", "GU", "GT", "GG", "GN", "GW", "GY", "HT", "HM", "HN", "HK", "HU", "IS", "IN", "ID", "IR", "IQ", "IE", "IM", "IL", "IT", "CI", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "XK", "KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", "LT", "LU", "MO", "MG", "MW", "MY", "MV", "ML", "MT", "MH", "MQ", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME", "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NC", "NZ", "NI", "NE", "NG", "NU", "NF", "MK", "MP", "NO", "OM", "PK", "PW", "PS", "PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR", "QA", "CG", "RE", "RO", "RU", "RW", "BL", "SH", "KN", "LC", "MF", "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SX", "SK", "SI", "SB", "SO", "ZA", "GS", "KR", "SS", "ES", "LK", "SD", "SR", "SJ", "SE", "CH", "SY", "TW", "TJ", "TZ", "TH", "TL", "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "AE", "GB", "US", "UM", "VI", "UY", "UZ", "VU", "VA", "VE", "VN", "WF", "EH", "YE", "ZM", "ZW"];
|
|
117
|
+
/**
|
|
118
|
+
* Build the Google for Jobs `JobPosting` JSON-LD object for a job detail
|
|
119
|
+
* page, or `null` when nothing renderable remains after pruning. Embed the
|
|
120
|
+
* result in a `<script type="application/ld+json">` tag.
|
|
121
|
+
*/
|
|
122
|
+
declare function createJobPostingJsonLd({ job, board, shareUrl, }: {
|
|
123
|
+
job: PublicJob;
|
|
124
|
+
board: JsonLdBoard;
|
|
125
|
+
shareUrl: string;
|
|
126
|
+
}): JsonLdObject | null;
|
|
127
|
+
/**
|
|
128
|
+
* Prefix `https://` onto scheme-less company websites; `null` for
|
|
129
|
+
* blank input (hosted shape — a whitespace-only website never becomes
|
|
130
|
+
* a bare `https://` `sameAs`).
|
|
131
|
+
*/
|
|
132
|
+
declare function normalizeWebsiteUrl(website: string): string | null;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Salary amount in the board language and the job's currency
|
|
136
|
+
* (`$90K`, `$90,000.00`, `90.000 €`, …) — the hosted salary-page money
|
|
137
|
+
* formatter with locale-driven digits/grouping and a real currency code.
|
|
138
|
+
*
|
|
139
|
+
* `notation` matches `formatSalaryRange`: omit to pick by magnitude
|
|
140
|
+
* (`|value| ≥ 1000` → compact; smaller → standard so hourly/day rates keep
|
|
141
|
+
* CLDR minor units). Pass `'standard'` for full figures (`$90,000`) or
|
|
142
|
+
* `'compact'` to force short form. Compact vs standard is register (hero
|
|
143
|
+
* card vs FAQ prose), not locale.
|
|
144
|
+
*
|
|
145
|
+
* Empty/missing/`null` currency is not USD — returns `null` (same stance as
|
|
146
|
+
* `job-posting` omitting `baseSalary`). When `Intl` rejects the locale or
|
|
147
|
+
* currency, or the value is non-finite (`NaN` / `±Infinity`), returns `null`
|
|
148
|
+
* rather than English `M`/`k` abbreviations, a bare `$`, or a literal `$NaN`.
|
|
149
|
+
*/
|
|
150
|
+
declare function formatSalaryStat(locale: string, value: number, currency: string | null | undefined, notation?: NumberNotation): string | null;
|
|
151
|
+
/**
|
|
152
|
+
* Salary range via `Intl.NumberFormat.prototype.formatRange` so the
|
|
153
|
+
* locale owns separator, repeated currency/magnitude, and bidi marks.
|
|
154
|
+
* Returns `null` when currency is empty/null, either bound is non-finite, or
|
|
155
|
+
* `Intl` rejects the inputs.
|
|
156
|
+
*
|
|
157
|
+
* `notation` matches `formatSalaryRange` / `formatSalaryStat` (magnitude
|
|
158
|
+
* default when omitted). Pass `'standard'` for full-figure ranges used in
|
|
159
|
+
* FAQ prose (`$100,000 – $150,000`); default compact suits hero cards.
|
|
160
|
+
*
|
|
161
|
+
* When `min === max` the figure is a fixed salary, not a range: format a
|
|
162
|
+
* single amount. ICU's `formatRange(X, X)` emits `approximatelySign`
|
|
163
|
+
* (`~$140K`) because a degenerate range is not silently flattened — correct
|
|
164
|
+
* for "about X", wrong for a pin where min and max are the same posted
|
|
165
|
+
* salary.
|
|
166
|
+
*/
|
|
167
|
+
declare function formatSalaryStatRange(locale: string, min: number, max: number, currency: string | null | undefined, notation?: NumberNotation): string | null;
|
|
168
|
+
/** The canonical seniority ladder order the hosted salary pages sort by. */
|
|
169
|
+
declare const SENIORITY_ORDER: readonly ["entry_level", "associate", "mid_level", "senior", "lead", "principal", "director", "executive"];
|
|
170
|
+
/** Sort rows by `SENIORITY_ORDER` (unknown keys sink to the end). Non-mutating. */
|
|
171
|
+
declare function sortBySeniority<T extends {
|
|
172
|
+
seniority: string;
|
|
173
|
+
}>(items: T[]): T[];
|
|
174
|
+
/**
|
|
175
|
+
* Options shared by salary JSON-LD builders that emit per-seniority
|
|
176
|
+
* distributions. Applications compose the finished distribution `name` —
|
|
177
|
+
* the SDK never invents display words or word order.
|
|
178
|
+
*/
|
|
179
|
+
interface SalaryJsonLdOptions {
|
|
180
|
+
/**
|
|
181
|
+
* Composes the finished `MonetaryAmountDistribution.name` for one
|
|
182
|
+
* seniority row. Receives the wire seniority enum (`entry_level`,
|
|
183
|
+
* `senior`, …) and the occupation/entity label (category name, title,
|
|
184
|
+
* …). Return the whole string in board-language order — e.g. French
|
|
185
|
+
* `"Ingénieur logiciel sénior"`, Japanese without a space, English
|
|
186
|
+
* `"Senior Software Engineer"`. When omitted, per-seniority
|
|
187
|
+
* distributions ship without a `name` (valid structured data;
|
|
188
|
+
* wrong-language / wrong-order names are not).
|
|
189
|
+
*/
|
|
190
|
+
seniorityName?: (args: {
|
|
191
|
+
seniority: string;
|
|
192
|
+
entity: string;
|
|
193
|
+
}) => string;
|
|
194
|
+
}
|
|
195
|
+
/** Finished FAQ copy ready for `faqJsonLd` (application-composed). */
|
|
196
|
+
interface FaqItem {
|
|
197
|
+
q: string;
|
|
198
|
+
a: string;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Structured salary FAQ entry — kinds + values, never prose.
|
|
202
|
+
*
|
|
203
|
+
* - `average`: overall range (Intl compact convenience + raw figures) and
|
|
204
|
+
* sample size for the "what is the average?" question. `range` is a
|
|
205
|
+
* convenience string; `avgMin` / `avgMax` / `currency` stay so the app can
|
|
206
|
+
* reformat (e.g. standard notation for prose FAQ answers).
|
|
207
|
+
* - `methodology`: label only; the application writes the calculation
|
|
208
|
+
* explanation from its catalog.
|
|
209
|
+
*
|
|
210
|
+
* Map each entry to `{ q, a }` with board-language plural rules before
|
|
211
|
+
* calling `faqJsonLd`.
|
|
212
|
+
*/
|
|
213
|
+
type SalaryFaqEntry = {
|
|
214
|
+
kind: 'average';
|
|
215
|
+
/**
|
|
216
|
+
* Entity label echoed so FAQ mapping is a pure map over entries —
|
|
217
|
+
* the app does not re-close over the page's category/skill name.
|
|
218
|
+
* The SDK never reads it for formatting.
|
|
219
|
+
*/
|
|
220
|
+
label: string;
|
|
221
|
+
/**
|
|
222
|
+
* Convenience compact range from `formatSalaryStatRange`. Prefer
|
|
223
|
+
* `avgMin`/`avgMax`/`currency` when composing full-figure prose.
|
|
224
|
+
* `null` when Intl cannot format (raw figures still present).
|
|
225
|
+
*/
|
|
226
|
+
range: string | null;
|
|
227
|
+
avgMin: number;
|
|
228
|
+
avgMax: number;
|
|
229
|
+
currency: string;
|
|
230
|
+
jobCount: number;
|
|
231
|
+
} | {
|
|
232
|
+
kind: 'methodology';
|
|
233
|
+
/** Same echo as average — pure map over entries without re-closing. */
|
|
234
|
+
label: string;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Salary FAQ *data* for a page that has an overall figure. Returns entry
|
|
238
|
+
* kinds plus values; the application composes questions and answers.
|
|
239
|
+
* Empty when there is no overall figure. Carries raw `avgMin` / `avgMax` /
|
|
240
|
+
* `currency` alongside the compact `range` convenience so FAQ prose can
|
|
241
|
+
* reformat (standard vs compact is register, not locale). Sentences leave
|
|
242
|
+
* the SDK.
|
|
243
|
+
*
|
|
244
|
+
* `label` is echoed on both entries so the application can map kinds →
|
|
245
|
+
* `{ q, a }` without re-threading the entity name from the page scope.
|
|
246
|
+
*/
|
|
247
|
+
declare function buildSalaryFaq(locale: string, label: string, overall: {
|
|
248
|
+
avgMin: number;
|
|
249
|
+
avgMax: number;
|
|
250
|
+
jobCount: number;
|
|
251
|
+
} | null, currency: string): SalaryFaqEntry[];
|
|
252
|
+
/** An `ItemList` for a salary index page (the listing's items + their URLs). */
|
|
253
|
+
declare function itemListJsonLd(items: {
|
|
254
|
+
name: string;
|
|
255
|
+
url: string;
|
|
256
|
+
}[]): JsonLdObject | null;
|
|
257
|
+
/** A `FAQPage` from application-composed `{q,a}` items. */
|
|
258
|
+
declare function faqJsonLd(faqs: FaqItem[]): JsonLdObject | null;
|
|
259
|
+
/**
|
|
260
|
+
* `Occupation` for a job-title salary page.
|
|
261
|
+
*
|
|
262
|
+
* Names are data labels only (category name). Pass `seniorityName` so the
|
|
263
|
+
* application composes per-seniority distribution names (word order included).
|
|
264
|
+
*/
|
|
265
|
+
declare function titleSalaryJsonLd(d: TitleSalaryDetail, options?: SalaryJsonLdOptions): JsonLdObject | null;
|
|
266
|
+
/** `Occupation` for a skill salary page (overall distribution + median). */
|
|
267
|
+
declare function skillSalaryJsonLd(d: SkillSalaryDetail): JsonLdObject | null;
|
|
268
|
+
/**
|
|
269
|
+
* Options for builders that list occupations present on a non-occupation
|
|
270
|
+
* page (location overview, company salary overview). Callers supply absolute
|
|
271
|
+
* URLs for each occupation's own salary page — that page carries the real
|
|
272
|
+
* Occupation rich result.
|
|
273
|
+
*/
|
|
274
|
+
interface SalaryOccupationListOptions {
|
|
275
|
+
/**
|
|
276
|
+
* Absolute URL for an occupation (title / category) row on this page.
|
|
277
|
+
* Required so the ItemList links to pages that carry real Occupation
|
|
278
|
+
* structured data rather than inventing an Occupation for a place or
|
|
279
|
+
* employer.
|
|
280
|
+
*/
|
|
281
|
+
occupationUrl: (row: {
|
|
282
|
+
categorySlug: string;
|
|
283
|
+
categoryName: string;
|
|
284
|
+
}) => string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* `ItemList` of occupations (top categories) on a city-level location
|
|
288
|
+
* salary page. Location and employer overviews are not occupations — they
|
|
289
|
+
* must not claim `schema.org/Occupation`. City-level pages only (hosted
|
|
290
|
+
* gate); `null` when not a city/locality or when there are no category rows.
|
|
291
|
+
*/
|
|
292
|
+
declare function locationSalaryJsonLd(d: LocationSalaryDetail, options?: SalaryOccupationListOptions): JsonLdObject | null;
|
|
293
|
+
/**
|
|
294
|
+
* Cross-axis (title×location / skill×location) `Occupation` JSON-LD. Takes
|
|
295
|
+
* primitives to stay decoupled from the two cross-axis detail wire types.
|
|
296
|
+
*
|
|
297
|
+
* Occupation name is the axis name (data); place lives in
|
|
298
|
+
* `occupationLocation`. Pass `seniorityName` for per-seniority names.
|
|
299
|
+
*/
|
|
300
|
+
declare function crossAxisSalaryJsonLd(args: {
|
|
301
|
+
name: string;
|
|
302
|
+
placeName: string;
|
|
303
|
+
countryCode: string;
|
|
304
|
+
overall: {
|
|
305
|
+
avgMin: number;
|
|
306
|
+
avgMax: number;
|
|
307
|
+
p25Min: number | null;
|
|
308
|
+
p75Max: number | null;
|
|
309
|
+
} | null;
|
|
310
|
+
bySeniority: {
|
|
311
|
+
seniority: string;
|
|
312
|
+
avgSalaryMin: number;
|
|
313
|
+
avgSalaryMax: number;
|
|
314
|
+
}[];
|
|
315
|
+
currency: string;
|
|
316
|
+
}, options?: SalaryJsonLdOptions): JsonLdObject | null;
|
|
317
|
+
/**
|
|
318
|
+
* `ItemList` of occupations (categories) on a company salary overview.
|
|
319
|
+
* An employer is not an occupation — do not emit
|
|
320
|
+
* `OccupationAggregationByEmployer` with the company name as `Occupation.name`.
|
|
321
|
+
* Each list entry links to the category-at-company page that carries the
|
|
322
|
+
* real Occupation structured data. `null` when `byCategory` is empty.
|
|
323
|
+
*/
|
|
324
|
+
declare function companySalaryJsonLd(d: CompanySalary, options?: SalaryOccupationListOptions): JsonLdObject | null;
|
|
325
|
+
/**
|
|
326
|
+
* `Occupation` for one job category at a company (the hosted category page).
|
|
327
|
+
* Occupation name is the category (data); company lives on
|
|
328
|
+
* `hiringOrganization`. Pass `seniorityName` for per-seniority names.
|
|
329
|
+
*/
|
|
330
|
+
declare function companyCategorySalaryJsonLd(d: CompanyCategorySalary, options?: SalaryJsonLdOptions): JsonLdObject | null;
|
|
331
|
+
|
|
332
|
+
interface BreadcrumbItemInput {
|
|
333
|
+
label: string | null | undefined;
|
|
334
|
+
href?: string | null;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Build a `BreadcrumbList`, hosted semantics: labels are trimmed and empty
|
|
338
|
+
* items dropped; fewer than 2 surviving items → `null` (a single crumb is
|
|
339
|
+
* not a trail); the current page (no `href`) omits `item` per schema.org.
|
|
340
|
+
* Relative hrefs resolve against `options.origin`; without an origin a
|
|
341
|
+
* relative item is omitted (never emit a relative `item` URL).
|
|
342
|
+
*/
|
|
343
|
+
declare function createBreadcrumbJsonLd(items: BreadcrumbItemInput[], options?: {
|
|
344
|
+
origin?: string | null;
|
|
345
|
+
}): JsonLdObject | null;
|
|
346
|
+
/**
|
|
347
|
+
* A single crumb in the job-detail trail. Chrome crumbs (`home`, `jobs`) are
|
|
348
|
+
* kinds — the application supplies display names. Record-derived crumbs keep
|
|
349
|
+
* `name` because that name is data off the job (place, category, title).
|
|
350
|
+
*/
|
|
351
|
+
type JobBreadcrumb = {
|
|
352
|
+
kind: 'home' | 'jobs';
|
|
353
|
+
path: string;
|
|
354
|
+
} | {
|
|
355
|
+
kind: 'place' | 'category' | 'job';
|
|
356
|
+
name: string;
|
|
357
|
+
path?: string;
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Home › Jobs › [country › region › city] › primaryCategory › Title.
|
|
361
|
+
* The place crumbs link into the listing routes (`/jobs/locations/:slug`);
|
|
362
|
+
* the category crumb nests under the most-specific place when present
|
|
363
|
+
* (`/jobs/locations/:place/:category`), else `/jobs/:category`. The last
|
|
364
|
+
* crumb (the job title) carries no `path` — the current page. Mirrors
|
|
365
|
+
* page.tsx:336-367.
|
|
366
|
+
*
|
|
367
|
+
* Returns structural kinds for chrome crumbs and data names for record
|
|
368
|
+
* crumbs. The application maps `kind` → display labels.
|
|
369
|
+
*/
|
|
370
|
+
declare function buildJobBreadcrumbs(job: PublicJob): JobBreadcrumb[];
|
|
371
|
+
|
|
372
|
+
export { ALL_COUNTRY_CODES as A, type BreadcrumbItemInput as B, type CurrencyDisplay as C, type FormattedSalaryRange as F, type JobBreadcrumb as J, type NumberNotation as N, type SalaryTimeframeInput as S, type SalaryTimeframeValue as a, buildJobBreadcrumbs as b, formatSalaryStat as c, formatSalaryStatRange as d, type JsonLdObject as e, formatSalaryRange as f, type FaqItem as g, type JsonLdBoard as h, SENIORITY_ORDER as i, type SalaryFaqEntry as j, type SalaryJsonLdOptions as k, type SalaryOccupationListOptions as l, buildSalaryFaq as m, normalizeWebsiteUrl as n, companyCategorySalaryJsonLd as o, companySalaryJsonLd as p, createBreadcrumbJsonLd as q, createJobPostingJsonLd as r, crossAxisSalaryJsonLd as s, faqJsonLd as t, itemListJsonLd as u, locationSalaryJsonLd as v, skillSalaryJsonLd as w, sortBySeniority as x, titleSalaryJsonLd as y };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var BOARD_API_ERROR_CODES = [
|
|
3
|
+
// Cross-cutting envelope
|
|
4
|
+
"internal_error",
|
|
5
|
+
"validation_bad_request",
|
|
6
|
+
"validation_payload_too_large",
|
|
7
|
+
"rate_limited",
|
|
8
|
+
"rate_limit_unavailable",
|
|
9
|
+
"search_unavailable",
|
|
10
|
+
"too_many_filter_values",
|
|
11
|
+
"pagination_invalid_cursor",
|
|
12
|
+
"pagination_offset_too_large",
|
|
13
|
+
"auth_unauthenticated",
|
|
14
|
+
"auth_forbidden",
|
|
15
|
+
"plan_upgrade_required",
|
|
16
|
+
"unknown_error",
|
|
17
|
+
// Board resolution + custom pages
|
|
18
|
+
"boards_not_found",
|
|
19
|
+
"board_page_not_found",
|
|
20
|
+
// Board-user auth
|
|
21
|
+
"board_auth_email_taken",
|
|
22
|
+
"board_auth_invalid_credentials",
|
|
23
|
+
"board_auth_invalid_token",
|
|
24
|
+
"board_auth_registration_disabled",
|
|
25
|
+
"board_auth_token_expired",
|
|
26
|
+
// Board password gate
|
|
27
|
+
"board_password_required",
|
|
28
|
+
"board_password_invalid",
|
|
29
|
+
// Jobs browse
|
|
30
|
+
"jobs_not_found",
|
|
31
|
+
"jobs_invalid_filter",
|
|
32
|
+
// Taxonomy
|
|
33
|
+
"categories_not_found",
|
|
34
|
+
"skills_not_found",
|
|
35
|
+
"places_not_found",
|
|
36
|
+
// Companies (public reads)
|
|
37
|
+
"companies_not_found",
|
|
38
|
+
"company_markets_not_found",
|
|
39
|
+
"company_salary_not_found",
|
|
40
|
+
"company_category_salary_not_found",
|
|
41
|
+
// Salaries
|
|
42
|
+
"salaries_title_not_found",
|
|
43
|
+
"salaries_skill_not_found",
|
|
44
|
+
"salaries_location_not_found",
|
|
45
|
+
"salaries_title_location_not_found",
|
|
46
|
+
"salaries_skill_location_not_found",
|
|
47
|
+
// Blog (public reads)
|
|
48
|
+
"blog_post_not_found",
|
|
49
|
+
"blog_author_not_found",
|
|
50
|
+
"blog_tag_not_found",
|
|
51
|
+
// Talent directory
|
|
52
|
+
"talent_not_found",
|
|
53
|
+
"talent_directory_not_found",
|
|
54
|
+
"talent_directory_restricted",
|
|
55
|
+
// Job alerts (anonymous + authenticated)
|
|
56
|
+
"job_alert_not_found",
|
|
57
|
+
"job_alerts_disabled",
|
|
58
|
+
"candidate_alert_limit_reached",
|
|
59
|
+
"candidate_alert_not_found",
|
|
60
|
+
// Candidate account
|
|
61
|
+
"candidate_entry_not_found",
|
|
62
|
+
"candidate_handle_taken",
|
|
63
|
+
"candidate_job_not_found",
|
|
64
|
+
"resume_invalid_file",
|
|
65
|
+
"resume_upload_forbidden",
|
|
66
|
+
// Applications
|
|
67
|
+
"applications_external_apply_only",
|
|
68
|
+
"applications_guest_not_allowed",
|
|
69
|
+
"applications_job_not_found",
|
|
70
|
+
"applications_not_found",
|
|
71
|
+
"applications_resume_invalid_file",
|
|
72
|
+
"applications_unprocessable",
|
|
73
|
+
// Messaging
|
|
74
|
+
"messaging_application_not_found",
|
|
75
|
+
"messaging_application_not_linked",
|
|
76
|
+
"messaging_blocked",
|
|
77
|
+
"messaging_cannot_block_self",
|
|
78
|
+
"messaging_cold_rule",
|
|
79
|
+
"messaging_conversation_not_found",
|
|
80
|
+
"messaging_disabled",
|
|
81
|
+
"messaging_edit_window_expired",
|
|
82
|
+
"messaging_message_deleted",
|
|
83
|
+
"messaging_message_not_found",
|
|
84
|
+
"messaging_not_author",
|
|
85
|
+
"messaging_not_participant",
|
|
86
|
+
"messaging_not_permitted",
|
|
87
|
+
"messaging_not_recipient",
|
|
88
|
+
"messaging_rate_limited",
|
|
89
|
+
"messaging_recipient_not_found",
|
|
90
|
+
"messaging_recipient_not_open",
|
|
91
|
+
"messaging_unsend_window_expired",
|
|
92
|
+
// Employer self-service
|
|
93
|
+
"employer_applicant_not_found",
|
|
94
|
+
"employer_ats_unprocessable",
|
|
95
|
+
"employer_checkout_failed",
|
|
96
|
+
"employer_company_exists",
|
|
97
|
+
"employer_company_name_taken",
|
|
98
|
+
"employer_company_not_found",
|
|
99
|
+
"employer_job_not_found",
|
|
100
|
+
"employer_job_slug_taken",
|
|
101
|
+
"employer_jobs_quota_exceeded",
|
|
102
|
+
"employer_not_member",
|
|
103
|
+
"employer_payment_required",
|
|
104
|
+
"employer_pipeline_stage_not_found",
|
|
105
|
+
// Candidate job-access paywall
|
|
106
|
+
"paywall_already_active",
|
|
107
|
+
"paywall_disabled",
|
|
108
|
+
"paywall_invalid_checkout_session",
|
|
109
|
+
"paywall_no_candidate_profile",
|
|
110
|
+
"paywall_no_recurring_subscription",
|
|
111
|
+
"paywall_offer_not_found",
|
|
112
|
+
// Public job posting
|
|
113
|
+
"job_posting_logo_lookup_unavailable",
|
|
114
|
+
"job_posting_logo_not_found",
|
|
115
|
+
"job_posting_rejected"
|
|
116
|
+
];
|
|
117
|
+
var BoardApiError = class extends Error {
|
|
118
|
+
status;
|
|
119
|
+
/** `<domain>_<snake_reason>` code from the v1 error envelope. */
|
|
120
|
+
code;
|
|
121
|
+
/** Structured, per-code details — shape varies by `code`. */
|
|
122
|
+
details;
|
|
123
|
+
requestId;
|
|
124
|
+
/** The parsed response body, untouched. */
|
|
125
|
+
raw;
|
|
126
|
+
constructor(input) {
|
|
127
|
+
super(input.message);
|
|
128
|
+
this.name = "BoardApiError";
|
|
129
|
+
this.status = input.status;
|
|
130
|
+
this.code = input.code;
|
|
131
|
+
this.details = input.details;
|
|
132
|
+
this.requestId = input.requestId;
|
|
133
|
+
this.raw = input.raw;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function isBoardApiError(e) {
|
|
137
|
+
return e instanceof Error && e.name === "BoardApiError" && typeof e.status === "number" && typeof e.code === "string";
|
|
138
|
+
}
|
|
139
|
+
function isNotFound(e) {
|
|
140
|
+
return isBoardApiError(e) && e.status === 404;
|
|
141
|
+
}
|
|
142
|
+
function isUnauthorized(e) {
|
|
143
|
+
return isBoardApiError(e) && e.status === 401;
|
|
144
|
+
}
|
|
145
|
+
function isBoardPasswordRequired(e) {
|
|
146
|
+
return isBoardApiError(e) && e.status === 401 && e.code === "board_password_required";
|
|
147
|
+
}
|
|
148
|
+
function isForbidden(e) {
|
|
149
|
+
return isBoardApiError(e) && e.status === 403;
|
|
150
|
+
}
|
|
151
|
+
function isValidationError(e) {
|
|
152
|
+
return isBoardApiError(e) && e.status === 400 && e.code === "validation_bad_request";
|
|
153
|
+
}
|
|
154
|
+
function isRateLimited(e) {
|
|
155
|
+
return isBoardApiError(e) && e.status === 429;
|
|
156
|
+
}
|
|
157
|
+
function isConflict(e) {
|
|
158
|
+
return isBoardApiError(e) && e.status === 409;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/scope.ts
|
|
162
|
+
function scopeToken(board) {
|
|
163
|
+
let hash = 5381;
|
|
164
|
+
for (let i = 0; i < board.length; i++) {
|
|
165
|
+
hash = (hash << 5) + hash + board.charCodeAt(i) >>> 0;
|
|
166
|
+
}
|
|
167
|
+
const sanitized = board.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
168
|
+
return `${sanitized}-${hash.toString(36)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/storage.ts
|
|
172
|
+
var ACCESS_TOKEN_KEY = "cavuno_board_access_token";
|
|
173
|
+
var REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
|
|
174
|
+
var BOARD_ACCESS_GRANT_KEY = "cavuno_board_access_grant";
|
|
175
|
+
function isBrowser() {
|
|
176
|
+
return typeof globalThis.document !== "undefined";
|
|
177
|
+
}
|
|
178
|
+
function memoryStorage() {
|
|
179
|
+
const backing = /* @__PURE__ */ new Map();
|
|
180
|
+
return {
|
|
181
|
+
getItem: (key) => backing.get(key) ?? null,
|
|
182
|
+
setItem: (key, value) => {
|
|
183
|
+
backing.set(key, value);
|
|
184
|
+
},
|
|
185
|
+
removeItem: (key) => {
|
|
186
|
+
backing.delete(key);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function browserStorage(mode, scope) {
|
|
191
|
+
if (!isBrowser()) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`storage mode '${mode}' is browser-only \u2014 use 'nostore' + per-call headers on the server`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
let backing;
|
|
197
|
+
try {
|
|
198
|
+
backing = mode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
|
|
199
|
+
if (!backing) throw new Error("unavailable");
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`storage mode '${mode}' is unavailable in this browsing context (storage access is blocked \u2014 e.g. a sandboxed or third-party iframe). Use 'memory', or 'nostore' + per-call headers.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const suffix = scope ? `:${scopeToken(scope)}` : "";
|
|
206
|
+
return {
|
|
207
|
+
getItem: (key) => backing.getItem(`${key}${suffix}`),
|
|
208
|
+
setItem: (key, value) => {
|
|
209
|
+
backing.setItem(`${key}${suffix}`, value);
|
|
210
|
+
},
|
|
211
|
+
removeItem: (key) => {
|
|
212
|
+
backing.removeItem(`${key}${suffix}`);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
|
|
217
|
+
function isNoStore(storage) {
|
|
218
|
+
return storage[NOSTORE_BRAND] === true;
|
|
219
|
+
}
|
|
220
|
+
var NOSTORE = {
|
|
221
|
+
[NOSTORE_BRAND]: true,
|
|
222
|
+
getItem: () => null,
|
|
223
|
+
setItem: () => {
|
|
224
|
+
},
|
|
225
|
+
removeItem: () => {
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
function resolveStorage(mode, scope) {
|
|
229
|
+
const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
|
|
230
|
+
if (typeof resolved === "object") return resolved;
|
|
231
|
+
switch (resolved) {
|
|
232
|
+
case "memory":
|
|
233
|
+
return memoryStorage();
|
|
234
|
+
case "nostore":
|
|
235
|
+
return NOSTORE;
|
|
236
|
+
case "local":
|
|
237
|
+
case "session":
|
|
238
|
+
return browserStorage(resolved, scope);
|
|
239
|
+
default:
|
|
240
|
+
throw new Error(`Unknown storage mode '${String(resolved)}'`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function writeSession(storage, session) {
|
|
244
|
+
await storage.setItem(ACCESS_TOKEN_KEY, session.accessToken);
|
|
245
|
+
await storage.setItem(REFRESH_TOKEN_KEY, session.refreshToken);
|
|
246
|
+
}
|
|
247
|
+
async function clearSession(storage) {
|
|
248
|
+
await storage.removeItem(ACCESS_TOKEN_KEY);
|
|
249
|
+
await storage.removeItem(REFRESH_TOKEN_KEY);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export {
|
|
253
|
+
BOARD_API_ERROR_CODES,
|
|
254
|
+
BoardApiError,
|
|
255
|
+
isBoardApiError,
|
|
256
|
+
isNotFound,
|
|
257
|
+
isUnauthorized,
|
|
258
|
+
isBoardPasswordRequired,
|
|
259
|
+
isForbidden,
|
|
260
|
+
isValidationError,
|
|
261
|
+
isRateLimited,
|
|
262
|
+
isConflict,
|
|
263
|
+
scopeToken,
|
|
264
|
+
ACCESS_TOKEN_KEY,
|
|
265
|
+
REFRESH_TOKEN_KEY,
|
|
266
|
+
BOARD_ACCESS_GRANT_KEY,
|
|
267
|
+
isNoStore,
|
|
268
|
+
resolveStorage,
|
|
269
|
+
writeSession,
|
|
270
|
+
clearSession
|
|
271
|
+
};
|