@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/index.mjs CHANGED
@@ -1,162 +1,24 @@
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
- }
1
+ import {
2
+ ACCESS_TOKEN_KEY,
3
+ BOARD_ACCESS_GRANT_KEY,
4
+ BOARD_API_ERROR_CODES,
5
+ BoardApiError,
6
+ REFRESH_TOKEN_KEY,
7
+ clearSession,
8
+ isBoardApiError,
9
+ isBoardPasswordRequired,
10
+ isConflict,
11
+ isForbidden,
12
+ isNotFound,
13
+ isRateLimited,
14
+ isUnauthorized,
15
+ isValidationError,
16
+ resolveStorage,
17
+ writeSession
18
+ } from "./chunk-2ETP5UH4.mjs";
19
+ import {
20
+ paginate
21
+ } from "./chunk-DLSO6XXY.mjs";
160
22
 
161
23
  // src/query.ts
162
24
  function toSearchParams(query) {
@@ -176,96 +38,8 @@ function toSearchParams(query) {
176
38
  return params;
177
39
  }
178
40
 
179
- // src/scope.ts
180
- function scopeToken(board) {
181
- let hash = 5381;
182
- for (let i = 0; i < board.length; i++) {
183
- hash = (hash << 5) + hash + board.charCodeAt(i) >>> 0;
184
- }
185
- const sanitized = board.replace(/[^a-zA-Z0-9_-]/g, "_");
186
- return `${sanitized}-${hash.toString(36)}`;
187
- }
188
-
189
- // src/storage.ts
190
- var ACCESS_TOKEN_KEY = "cavuno_board_access_token";
191
- var REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
192
- var BOARD_ACCESS_GRANT_KEY = "cavuno_board_access_grant";
193
- function isBrowser() {
194
- return typeof globalThis.document !== "undefined";
195
- }
196
- function memoryStorage() {
197
- const backing = /* @__PURE__ */ new Map();
198
- return {
199
- getItem: (key) => backing.get(key) ?? null,
200
- setItem: (key, value) => {
201
- backing.set(key, value);
202
- },
203
- removeItem: (key) => {
204
- backing.delete(key);
205
- }
206
- };
207
- }
208
- function browserStorage(mode, scope) {
209
- if (!isBrowser()) {
210
- throw new Error(
211
- `storage mode '${mode}' is browser-only \u2014 use 'nostore' + per-call headers on the server`
212
- );
213
- }
214
- let backing;
215
- try {
216
- backing = mode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
217
- if (!backing) throw new Error("unavailable");
218
- } catch {
219
- throw new Error(
220
- `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.`
221
- );
222
- }
223
- const suffix = scope ? `:${scopeToken(scope)}` : "";
224
- return {
225
- getItem: (key) => backing.getItem(`${key}${suffix}`),
226
- setItem: (key, value) => {
227
- backing.setItem(`${key}${suffix}`, value);
228
- },
229
- removeItem: (key) => {
230
- backing.removeItem(`${key}${suffix}`);
231
- }
232
- };
233
- }
234
- var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
235
- var NOSTORE = {
236
- [NOSTORE_BRAND]: true,
237
- getItem: () => null,
238
- setItem: () => {
239
- },
240
- removeItem: () => {
241
- }
242
- };
243
- function resolveStorage(mode, scope) {
244
- const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
245
- if (typeof resolved === "object") return resolved;
246
- switch (resolved) {
247
- case "memory":
248
- return memoryStorage();
249
- case "nostore":
250
- return NOSTORE;
251
- case "local":
252
- case "session":
253
- return browserStorage(resolved, scope);
254
- default:
255
- throw new Error(`Unknown storage mode '${String(resolved)}'`);
256
- }
257
- }
258
- async function writeSession(storage, session) {
259
- await storage.setItem(ACCESS_TOKEN_KEY, session.accessToken);
260
- await storage.setItem(REFRESH_TOKEN_KEY, session.refreshToken);
261
- }
262
- async function clearSession(storage) {
263
- await storage.removeItem(ACCESS_TOKEN_KEY);
264
- await storage.removeItem(REFRESH_TOKEN_KEY);
265
- }
266
-
267
41
  // src/version.ts
268
- var SDK_VERSION = "3.1.0";
42
+ var SDK_VERSION = "4.0.0";
269
43
 
270
44
  // src/client.ts
271
45
  function isRawBody(body) {
@@ -1067,28 +841,6 @@ function jobsNamespace(client) {
1067
841
  };
1068
842
  }
1069
843
 
1070
- // src/namespaces/legal.ts
1071
- function legalNamespace(client) {
1072
- return {
1073
- /**
1074
- * Retrieve a board legal/about page — owner-authored prose as portable HTML
1075
- * (`privacy-policy` / `terms-of-service` / `cookie-policy` / `about`) plus,
1076
- * for `impressum`, structured legal-entity facts. The starter authors the
1077
- * layout, breadcrumb labels, and JSON-LD. `impressum` throws
1078
- * `board_page_not_found` (404) when the board has not enabled it.
1079
- *
1080
- * @example
1081
- * const { title, content } = await board.legal.retrieve('privacy-policy');
1082
- */
1083
- retrieve(type, options) {
1084
- return client.fetch(
1085
- `/legal/${encodeURIComponent(type)}`,
1086
- options
1087
- );
1088
- }
1089
- };
1090
- }
1091
-
1092
844
  // src/namespaces/me.ts
1093
845
  function meNamespace(client) {
1094
846
  return {
@@ -2574,12 +2326,18 @@ function salariesNamespace(client) {
2574
2326
  function searchNamespace(client) {
2575
2327
  return {
2576
2328
  /**
2577
- * GET /search/suggest — federated keyword suggestions (companies +
2578
- * taxonomy terms), one interleaved server-ranked list. Order is the
2579
- * contract: do not re-sort.
2329
+ * GET /search/suggest — federated keyword suggestions (companies,
2330
+ * markets, taxonomy terms), one interleaved server-ranked list. Pass
2331
+ * `types` to restrict kinds; `limit` applies after that filter. Order
2332
+ * is the contract: do not re-sort. Routing is application-side via
2333
+ * `suggestionPath` from `@cavuno/board/paths`.
2580
2334
  *
2581
2335
  * @example
2582
- * const { items } = await board.search.suggest({ q: 'acme', limit: 10 });
2336
+ * const { items } = await board.search.suggest({
2337
+ * q: 'acme',
2338
+ * limit: 10,
2339
+ * types: ['company', 'skill'],
2340
+ * });
2583
2341
  */
2584
2342
  suggest(query, options) {
2585
2343
  return client.fetch("/search/suggest", {
@@ -2672,14 +2430,6 @@ function taxonomyNamespace(client) {
2672
2430
  );
2673
2431
  }
2674
2432
  },
2675
- suggestions: {
2676
- list(query, options) {
2677
- return client.fetch("/suggestions", {
2678
- ...options,
2679
- query
2680
- });
2681
- }
2682
- },
2683
2433
  places: {
2684
2434
  ...taxonomyResolver(client, "places"),
2685
2435
  /**
@@ -2697,36 +2447,6 @@ function taxonomyNamespace(client) {
2697
2447
  };
2698
2448
  }
2699
2449
 
2700
- // src/pagination.ts
2701
- function paginate(listFn, query, options) {
2702
- async function* pages() {
2703
- let current = query;
2704
- for (; ; ) {
2705
- const page = await listFn(current, options);
2706
- yield page;
2707
- if (!page.hasMore || page.nextCursor === null) return;
2708
- const { offset: _offset, ...rest } = current ?? {};
2709
- current = { ...rest, cursor: page.nextCursor };
2710
- }
2711
- }
2712
- return {
2713
- pages,
2714
- async *[Symbol.asyncIterator]() {
2715
- for await (const page of pages()) {
2716
- yield* page.data;
2717
- }
2718
- },
2719
- async toArray({ limit }) {
2720
- const items = [];
2721
- for await (const page of pages()) {
2722
- items.push(...page.data);
2723
- if (items.length >= limit) break;
2724
- }
2725
- return items.slice(0, limit);
2726
- }
2727
- };
2728
- }
2729
-
2730
2450
  // src/messaging-derive.ts
2731
2451
  function isOwnMessage(message, counterpartyId) {
2732
2452
  return message.authorBoardUserId !== counterpartyId;
@@ -2806,7 +2526,6 @@ function createBoardClient(options) {
2806
2526
  embed: embedNamespace(client),
2807
2527
  companies: companiesNamespace(client),
2808
2528
  blog: blogNamespace(client),
2809
- legal: legalNamespace(client),
2810
2529
  auth: authNamespace(client),
2811
2530
  me: meNamespace(client),
2812
2531
  password: passwordNamespace(client),
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-nkWAraS0.mjs';
1
+ import { S as Schemas } from './_spec-GVe6WAfT.mjs';
2
2
 
3
3
  /**
4
4
  * Optional pagination metadata returned by Board API list operations.
@@ -36,8 +36,8 @@ type PublicJob = Schemas['PublicJob'];
36
36
  type PublicJobCard = Schemas['PublicJobCard'];
37
37
  /**
38
38
  * A job's opaque, display-only custom-field values, keyed by each
39
- * field's `key`. Resolve labels via the board's `CustomFieldDefinition`s
40
- * (`board.context().customFields`).
39
+ * field's `key`. Resolve labels via the board's job `CustomFieldDefinition`s
40
+ * (`board.context().customFields.job`).
41
41
  */
42
42
  type CustomFieldValues = PublicJob['customFieldValues'];
43
43
  type CustomFieldValue = CustomFieldValues[string];
@@ -104,4 +104,4 @@ type JobsSimilarQuery = {
104
104
  };
105
105
  type JobsSearchBody = Schemas['PublicSearchJobsBody'];
106
106
 
107
- export type { CustomFieldValues as C, EmploymentType as E, JobSort as J, ListEnvelope as L, OfficeLocation as O, PublicJob as P, RemoteOption as R, Seniority as S, PublicJobCard as a, JobsListQuery as b, JobCardListEnvelope as c, JobsSearchBody as d, JobCardSearchEnvelope as e, JobsSimilarQuery as f, SearchEnvelope as g, CustomFieldValue as h, EducationRequirement as i, JobCatalogPagination as j, JobCompany as k, OffsetPagination as l, RelatedSearch as m, RemotePermit as n, RemoteTimezone as o, StorefrontPagination as p };
107
+ export type { CustomFieldValues as C, EmploymentType as E, JobSort as J, ListEnvelope as L, OfficeLocation as O, PublicJob as P, RemoteOption as R, Seniority as S, JobsListQuery as a, JobCardListEnvelope as b, JobsSearchBody as c, JobCardSearchEnvelope as d, JobsSimilarQuery as e, SearchEnvelope as f, CustomFieldValue as g, EducationRequirement as h, JobCatalogPagination as i, JobCompany as j, OffsetPagination as k, PublicJobCard as l, RelatedSearch as m, RemotePermit as n, RemoteTimezone as o, StorefrontPagination as p };
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-nkWAraS0.js';
1
+ import { S as Schemas } from './_spec-GVe6WAfT.js';
2
2
 
3
3
  /**
4
4
  * Optional pagination metadata returned by Board API list operations.
@@ -36,8 +36,8 @@ type PublicJob = Schemas['PublicJob'];
36
36
  type PublicJobCard = Schemas['PublicJobCard'];
37
37
  /**
38
38
  * A job's opaque, display-only custom-field values, keyed by each
39
- * field's `key`. Resolve labels via the board's `CustomFieldDefinition`s
40
- * (`board.context().customFields`).
39
+ * field's `key`. Resolve labels via the board's job `CustomFieldDefinition`s
40
+ * (`board.context().customFields.job`).
41
41
  */
42
42
  type CustomFieldValues = PublicJob['customFieldValues'];
43
43
  type CustomFieldValue = CustomFieldValues[string];
@@ -104,4 +104,4 @@ type JobsSimilarQuery = {
104
104
  };
105
105
  type JobsSearchBody = Schemas['PublicSearchJobsBody'];
106
106
 
107
- export type { CustomFieldValues as C, EmploymentType as E, JobSort as J, ListEnvelope as L, OfficeLocation as O, PublicJob as P, RemoteOption as R, Seniority as S, PublicJobCard as a, JobsListQuery as b, JobCardListEnvelope as c, JobsSearchBody as d, JobCardSearchEnvelope as e, JobsSimilarQuery as f, SearchEnvelope as g, CustomFieldValue as h, EducationRequirement as i, JobCatalogPagination as j, JobCompany as k, OffsetPagination as l, RelatedSearch as m, RemotePermit as n, RemoteTimezone as o, StorefrontPagination as p };
107
+ export type { CustomFieldValues as C, EmploymentType as E, JobSort as J, ListEnvelope as L, OfficeLocation as O, PublicJob as P, RemoteOption as R, Seniority as S, JobsListQuery as a, JobCardListEnvelope as b, JobsSearchBody as c, JobCardSearchEnvelope as d, JobsSimilarQuery as e, SearchEnvelope as f, CustomFieldValue as g, EducationRequirement as h, JobCatalogPagination as i, JobCompany as j, OffsetPagination as k, PublicJobCard as l, RelatedSearch as m, RemotePermit as n, RemoteTimezone as o, StorefrontPagination as p };
package/dist/paths.d.mts CHANGED
@@ -9,11 +9,27 @@
9
9
  * zero-dependency — safe to import in server runtimes, on the edge,
10
10
  * and in the browser.
11
11
  *
12
+ * Dynamic path segments are percent-encoded (URI, not bare IRI) so
13
+ * non-Latin board-language slugs (`SuggestionPathInput.canonicalSlug`)
14
+ * are legal in sitemap `<loc>`, canonical/`og:url`, and email links.
15
+ * Separators (`/`) are never encoded; already-encoded input is not
16
+ * double-encoded.
17
+ *
12
18
  * These paths mirror the hosted board's indexed URLs exactly (migration
13
19
  * parity). Do NOT change a pattern here without updating the hosted board
14
20
  * and the sitemap golden tests in lockstep — the structure is a locked
15
21
  * cross-surface contract.
16
22
  */
23
+ /**
24
+ * Percent-encode one path segment for use in a URL path.
25
+ * Round-trips already-encoded input so `%E6…` is not double-encoded.
26
+ * Does not touch path separators — callers encode segments only.
27
+ *
28
+ * `:` is left unencoded so route-contract / well-known templates built via
29
+ * `jobDetailPath(':companySlug', ':jobSlug')` stay RFC-6570-style. Real
30
+ * board slugs are alphanumeric-plus-hyphen; non-ASCII is still encoded.
31
+ */
32
+ declare function encodePathSegment(segment: string): string;
17
33
  /** Job detail — the canonical, indexed job URL. Requires BOTH slugs. */
18
34
  declare function jobDetailPath(companySlug: string, jobSlug: string): string;
19
35
  /** Category (keyword) job listing. */
@@ -22,10 +38,81 @@ declare function jobsCategoryPath(categorySlug: string): string;
22
38
  declare function jobsSkillPath(skillSlug: string): string;
23
39
  /** Location job listing. */
24
40
  declare function jobsLocationPath(placeSlug: string): string;
41
+ /**
42
+ * Location + category combination job listing
43
+ * (`/jobs/locations/<place>/<category>`).
44
+ */
45
+ declare function jobsLocationCategoryPath(placeSlug: string, categorySlug: string): string;
46
+ /**
47
+ * Location + skill combination job listing
48
+ * (`/jobs/locations/<place>/skills/<skill>`).
49
+ */
50
+ declare function jobsLocationSkillPath(placeSlug: string, skillSlug: string): string;
25
51
  /** Company profile. */
26
52
  declare function companyPath(companySlug: string): string;
27
53
  /** Company market (sector) listing. */
28
54
  declare function companyMarketPath(marketSlug: string): string;
55
+ /**
56
+ * Suggestion kinds `suggestionPath` understands. Mirrors the wire
57
+ * `SuggestionItem` discriminators (and `termType` for terms) without
58
+ * importing generated OpenAPI types into the paths entry.
59
+ */
60
+ type SuggestionPathInput = {
61
+ type: 'company';
62
+ slug: string;
63
+ } | {
64
+ type: 'market';
65
+ slug: string;
66
+ } | {
67
+ type: 'post';
68
+ slug: string;
69
+ } | {
70
+ type: 'tag';
71
+ slug: string;
72
+ } | {
73
+ type: 'term';
74
+ termType: 'category' | 'skill';
75
+ /** Board-language URL slug — use `canonicalSlug` from the wire item. */
76
+ canonicalSlug: string;
77
+ };
78
+ /** Search scopes that own different destinations for the same suggestion. */
79
+ type SuggestionPathScope = 'jobs' | 'companies' | 'blog';
80
+ type SuggestionPathOptions = {
81
+ scope: SuggestionPathScope;
82
+ /**
83
+ * Active place slug. When set on the `jobs` scope with a category or
84
+ * skill term, routes into the location combination pages.
85
+ */
86
+ location?: string;
87
+ };
88
+ /**
89
+ * Resolve a suggestion to a board-relative path for the given search scope
90
+ * The API never returns an `href` because the same
91
+ * `CompanySuggestion` means "navigate to the company page" under companies
92
+ * scope and "apply a `companySlug` filter" under jobs scope — only the app
93
+ * knows which.
94
+ *
95
+ * Returns `null` when the selection is not a navigation (jobs-scope company
96
+ * → filter, or a kind that has no path under the active scope). Compose from
97
+ * the existing `*Path` helpers; never string-build at the call site.
98
+ *
99
+ * | scope | suggestion | result |
100
+ * | --- | --- | --- |
101
+ * | jobs | skill | `/jobs/skills/<slug>` |
102
+ * | jobs | category | `/jobs/<slug>` |
103
+ * | jobs | skill + location | `/jobs/locations/<loc>/skills/<slug>` |
104
+ * | jobs | category + location | `/jobs/locations/<loc>/<slug>` |
105
+ * | jobs | company | `null` (apply `companySlug` filter) |
106
+ * | jobs | market | `null` |
107
+ * | jobs | post / tag | `null` |
108
+ * | companies | company | `/companies/<slug>` |
109
+ * | companies | market | `/companies/markets/<slug>` |
110
+ * | companies | term / post / tag | `null` |
111
+ * | blog | post | `/blog/<slug>` |
112
+ * | blog | tag | `/blog/tag/<slug>` |
113
+ * | blog | other | `null` |
114
+ */
115
+ declare function suggestionPath(suggestion: SuggestionPathInput, options: SuggestionPathOptions): string | null;
29
116
  /** A company's salary overview. */
30
117
  declare function companySalaryPath(companySlug: string): string;
31
118
  /** Salary page for a job title. */
@@ -90,7 +177,8 @@ declare function goAlertsConfirmPath(): string;
90
177
  /**
91
178
  * Prefix a board-relative path with the board origin. A trailing slash on
92
179
  * the origin is tolerated (stripped) so callers need not normalise first.
180
+ * Paths from `*Path` helpers already percent-encode dynamic segments.
93
181
  */
94
182
  declare function boardUrl(origin: string, path: string): string;
95
183
 
96
- export { BOARD_PATHS, blogAuthorPath, blogPostPath, blogTagPath, boardUrl, companyMarketPath, companyPath, companySalaryPath, goAlertsConfirmPath, goAlertsManagePath, goJobPath, jobDetailPath, jobsCategoryPath, jobsLocationPath, jobsSkillPath, salaryLocationPath, salarySkillPath, salaryTitlePath };
184
+ export { BOARD_PATHS, type SuggestionPathInput, type SuggestionPathOptions, type SuggestionPathScope, blogAuthorPath, blogPostPath, blogTagPath, boardUrl, companyMarketPath, companyPath, companySalaryPath, encodePathSegment, goAlertsConfirmPath, goAlertsManagePath, goJobPath, jobDetailPath, jobsCategoryPath, jobsLocationCategoryPath, jobsLocationPath, jobsLocationSkillPath, jobsSkillPath, salaryLocationPath, salarySkillPath, salaryTitlePath, suggestionPath };
package/dist/paths.d.ts CHANGED
@@ -9,11 +9,27 @@
9
9
  * zero-dependency — safe to import in server runtimes, on the edge,
10
10
  * and in the browser.
11
11
  *
12
+ * Dynamic path segments are percent-encoded (URI, not bare IRI) so
13
+ * non-Latin board-language slugs (`SuggestionPathInput.canonicalSlug`)
14
+ * are legal in sitemap `<loc>`, canonical/`og:url`, and email links.
15
+ * Separators (`/`) are never encoded; already-encoded input is not
16
+ * double-encoded.
17
+ *
12
18
  * These paths mirror the hosted board's indexed URLs exactly (migration
13
19
  * parity). Do NOT change a pattern here without updating the hosted board
14
20
  * and the sitemap golden tests in lockstep — the structure is a locked
15
21
  * cross-surface contract.
16
22
  */
23
+ /**
24
+ * Percent-encode one path segment for use in a URL path.
25
+ * Round-trips already-encoded input so `%E6…` is not double-encoded.
26
+ * Does not touch path separators — callers encode segments only.
27
+ *
28
+ * `:` is left unencoded so route-contract / well-known templates built via
29
+ * `jobDetailPath(':companySlug', ':jobSlug')` stay RFC-6570-style. Real
30
+ * board slugs are alphanumeric-plus-hyphen; non-ASCII is still encoded.
31
+ */
32
+ declare function encodePathSegment(segment: string): string;
17
33
  /** Job detail — the canonical, indexed job URL. Requires BOTH slugs. */
18
34
  declare function jobDetailPath(companySlug: string, jobSlug: string): string;
19
35
  /** Category (keyword) job listing. */
@@ -22,10 +38,81 @@ declare function jobsCategoryPath(categorySlug: string): string;
22
38
  declare function jobsSkillPath(skillSlug: string): string;
23
39
  /** Location job listing. */
24
40
  declare function jobsLocationPath(placeSlug: string): string;
41
+ /**
42
+ * Location + category combination job listing
43
+ * (`/jobs/locations/<place>/<category>`).
44
+ */
45
+ declare function jobsLocationCategoryPath(placeSlug: string, categorySlug: string): string;
46
+ /**
47
+ * Location + skill combination job listing
48
+ * (`/jobs/locations/<place>/skills/<skill>`).
49
+ */
50
+ declare function jobsLocationSkillPath(placeSlug: string, skillSlug: string): string;
25
51
  /** Company profile. */
26
52
  declare function companyPath(companySlug: string): string;
27
53
  /** Company market (sector) listing. */
28
54
  declare function companyMarketPath(marketSlug: string): string;
55
+ /**
56
+ * Suggestion kinds `suggestionPath` understands. Mirrors the wire
57
+ * `SuggestionItem` discriminators (and `termType` for terms) without
58
+ * importing generated OpenAPI types into the paths entry.
59
+ */
60
+ type SuggestionPathInput = {
61
+ type: 'company';
62
+ slug: string;
63
+ } | {
64
+ type: 'market';
65
+ slug: string;
66
+ } | {
67
+ type: 'post';
68
+ slug: string;
69
+ } | {
70
+ type: 'tag';
71
+ slug: string;
72
+ } | {
73
+ type: 'term';
74
+ termType: 'category' | 'skill';
75
+ /** Board-language URL slug — use `canonicalSlug` from the wire item. */
76
+ canonicalSlug: string;
77
+ };
78
+ /** Search scopes that own different destinations for the same suggestion. */
79
+ type SuggestionPathScope = 'jobs' | 'companies' | 'blog';
80
+ type SuggestionPathOptions = {
81
+ scope: SuggestionPathScope;
82
+ /**
83
+ * Active place slug. When set on the `jobs` scope with a category or
84
+ * skill term, routes into the location combination pages.
85
+ */
86
+ location?: string;
87
+ };
88
+ /**
89
+ * Resolve a suggestion to a board-relative path for the given search scope
90
+ * The API never returns an `href` because the same
91
+ * `CompanySuggestion` means "navigate to the company page" under companies
92
+ * scope and "apply a `companySlug` filter" under jobs scope — only the app
93
+ * knows which.
94
+ *
95
+ * Returns `null` when the selection is not a navigation (jobs-scope company
96
+ * → filter, or a kind that has no path under the active scope). Compose from
97
+ * the existing `*Path` helpers; never string-build at the call site.
98
+ *
99
+ * | scope | suggestion | result |
100
+ * | --- | --- | --- |
101
+ * | jobs | skill | `/jobs/skills/<slug>` |
102
+ * | jobs | category | `/jobs/<slug>` |
103
+ * | jobs | skill + location | `/jobs/locations/<loc>/skills/<slug>` |
104
+ * | jobs | category + location | `/jobs/locations/<loc>/<slug>` |
105
+ * | jobs | company | `null` (apply `companySlug` filter) |
106
+ * | jobs | market | `null` |
107
+ * | jobs | post / tag | `null` |
108
+ * | companies | company | `/companies/<slug>` |
109
+ * | companies | market | `/companies/markets/<slug>` |
110
+ * | companies | term / post / tag | `null` |
111
+ * | blog | post | `/blog/<slug>` |
112
+ * | blog | tag | `/blog/tag/<slug>` |
113
+ * | blog | other | `null` |
114
+ */
115
+ declare function suggestionPath(suggestion: SuggestionPathInput, options: SuggestionPathOptions): string | null;
29
116
  /** A company's salary overview. */
30
117
  declare function companySalaryPath(companySlug: string): string;
31
118
  /** Salary page for a job title. */
@@ -90,7 +177,8 @@ declare function goAlertsConfirmPath(): string;
90
177
  /**
91
178
  * Prefix a board-relative path with the board origin. A trailing slash on
92
179
  * the origin is tolerated (stripped) so callers need not normalise first.
180
+ * Paths from `*Path` helpers already percent-encode dynamic segments.
93
181
  */
94
182
  declare function boardUrl(origin: string, path: string): string;
95
183
 
96
- export { BOARD_PATHS, blogAuthorPath, blogPostPath, blogTagPath, boardUrl, companyMarketPath, companyPath, companySalaryPath, goAlertsConfirmPath, goAlertsManagePath, goJobPath, jobDetailPath, jobsCategoryPath, jobsLocationPath, jobsSkillPath, salaryLocationPath, salarySkillPath, salaryTitlePath };
184
+ export { BOARD_PATHS, type SuggestionPathInput, type SuggestionPathOptions, type SuggestionPathScope, blogAuthorPath, blogPostPath, blogTagPath, boardUrl, companyMarketPath, companyPath, companySalaryPath, encodePathSegment, goAlertsConfirmPath, goAlertsManagePath, goJobPath, jobDetailPath, jobsCategoryPath, jobsLocationCategoryPath, jobsLocationPath, jobsLocationSkillPath, jobsSkillPath, salaryLocationPath, salarySkillPath, salaryTitlePath, suggestionPath };