@cavuno/board 3.2.0 → 4.1.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 (78) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/README.md +6 -2
  3. package/dist/{_spec-nkWAraS0.d.mts → _spec-BZDv2dc5.d.mts} +100 -68
  4. package/dist/{_spec-nkWAraS0.d.ts → _spec-BZDv2dc5.d.ts} +100 -68
  5. package/dist/bin.mjs +7 -2
  6. package/dist/{blog-iV9XEIGf.d.ts → blog-BHx6iH3Z.d.ts} +1 -1
  7. package/dist/{blog-DGBHmExV.d.mts → blog-DyDvyQrI.d.mts} +1 -1
  8. package/dist/{board-JhdUWIoq.d.ts → board-BF3Qao6J.d.mts} +7 -6
  9. package/dist/{board-BJbXd_9_.d.mts → board-BTEZ0uiI.d.ts} +7 -6
  10. package/dist/breadcrumbs-SGaWth6C.d.ts +372 -0
  11. package/dist/breadcrumbs-icxRVydh.d.mts +372 -0
  12. package/dist/chunk-BGKNXG4H.mjs +146 -0
  13. package/dist/{chunk-NXY3OWGF.mjs → chunk-T2KC6XGV.mjs} +1 -1
  14. package/dist/{chunk-STE77MAT.mjs → chunk-YEAQ3JEI.mjs} +143 -134
  15. package/dist/doctor.js +1 -1
  16. package/dist/doctor.mjs +1 -1
  17. package/dist/filters.d.mts +25 -20
  18. package/dist/filters.d.ts +25 -20
  19. package/dist/filters.js +3 -692
  20. package/dist/filters.mjs +3 -29
  21. package/dist/format.d.mts +57 -231
  22. package/dist/format.d.ts +57 -231
  23. package/dist/format.js +327 -968
  24. package/dist/format.mjs +207 -264
  25. package/dist/go.js +11 -1
  26. package/dist/go.mjs +1 -1
  27. package/dist/index.d.mts +67 -76
  28. package/dist/index.d.ts +67 -76
  29. package/dist/index.js +45 -36
  30. package/dist/index.mjs +45 -36
  31. package/dist/{jobs-BiEvL9d7.d.ts → jobs-BX6d_sdd.d.mts} +4 -4
  32. package/dist/{jobs-lVvKCBCU.d.mts → jobs-BbLJfz1z.d.ts} +4 -4
  33. package/dist/paths.d.mts +89 -1
  34. package/dist/paths.d.ts +89 -1
  35. package/dist/paths.js +62 -14
  36. package/dist/paths.mjs +11 -3
  37. package/dist/route-contract.js +23 -13
  38. package/dist/route-contract.mjs +2 -2
  39. package/dist/{search-CCyFWPi3.d.mts → search-7B-mVRNA.d.ts} +14 -3
  40. package/dist/{search-CyRvVMUp.d.ts → search-OKrdAHv8.d.mts} +14 -3
  41. package/dist/seo.d.mts +30 -29
  42. package/dist/seo.d.ts +30 -29
  43. package/dist/seo.js +197 -718
  44. package/dist/seo.mjs +26 -24
  45. package/dist/server.d.mts +5 -5
  46. package/dist/server.d.ts +5 -5
  47. package/dist/sitemap.d.mts +5 -5
  48. package/dist/sitemap.d.ts +5 -5
  49. package/dist/sitemap.js +25 -14
  50. package/dist/sitemap.mjs +3 -2
  51. package/dist/skills.js +1 -1
  52. package/dist/skills.mjs +1 -1
  53. package/dist/suggest.d.mts +2 -2
  54. package/dist/suggest.d.ts +2 -2
  55. package/dist/suggest.js +17 -3
  56. package/dist/suggest.mjs +17 -3
  57. package/dist/well-known.js +23 -13
  58. package/dist/well-known.mjs +2 -2
  59. package/package.json +2 -4
  60. package/skills/cavuno-board-api-client/SKILL.md +42 -0
  61. package/skills/cavuno-board-applications/SKILL.md +4 -1
  62. package/skills/cavuno-board-filters/SKILL.md +11 -12
  63. package/skills/cavuno-board-format/SKILL.md +68 -56
  64. package/skills/cavuno-board-i18n/SKILL.md +102 -21
  65. package/skills/cavuno-board-search-suggestions/SKILL.md +8 -3
  66. package/skills/cavuno-board-seo/BLOG.md +4 -1
  67. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +5 -2
  68. package/skills/cavuno-board-seo/LISTING.md +35 -12
  69. package/skills/cavuno-board-seo/SALARY.md +83 -18
  70. package/skills/cavuno-board-seo/SKILL.md +3 -2
  71. package/skills/manifest.json +3 -3
  72. package/dist/breadcrumbs-1hMjcsqA.d.mts +0 -171
  73. package/dist/breadcrumbs-DexU07TT.d.ts +0 -171
  74. package/dist/chunk-IJCJ26SQ.mjs +0 -596
  75. package/dist/chunk-L7ZOYWHW.mjs +0 -98
  76. package/dist/chunk-LUGJWOT7.mjs +0 -96
  77. package/dist/ui-copy-CCkz6CVF.d.mts +0 -230
  78. package/dist/ui-copy-CCkz6CVF.d.ts +0 -230
package/dist/index.js CHANGED
@@ -312,7 +312,7 @@ async function clearSession(storage) {
312
312
  }
313
313
 
314
314
  // src/version.ts
315
- var SDK_VERSION = "3.2.0";
315
+ var SDK_VERSION = "4.1.0";
316
316
 
317
317
  // src/client.ts
318
318
  function isRawBody(body) {
@@ -1114,28 +1114,6 @@ function jobsNamespace(client) {
1114
1114
  };
1115
1115
  }
1116
1116
 
1117
- // src/namespaces/legal.ts
1118
- function legalNamespace(client) {
1119
- return {
1120
- /**
1121
- * Retrieve a board legal/about page — owner-authored prose as portable HTML
1122
- * (`privacy-policy` / `terms-of-service` / `cookie-policy` / `about`) plus,
1123
- * for `impressum`, structured legal-entity facts. The starter authors the
1124
- * layout, breadcrumb labels, and JSON-LD. `impressum` throws
1125
- * `board_page_not_found` (404) when the board has not enabled it.
1126
- *
1127
- * @example
1128
- * const { title, content } = await board.legal.retrieve('privacy-policy');
1129
- */
1130
- retrieve(type, options) {
1131
- return client.fetch(
1132
- `/legal/${encodeURIComponent(type)}`,
1133
- options
1134
- );
1135
- }
1136
- };
1137
- }
1138
-
1139
1117
  // src/namespaces/me.ts
1140
1118
  function meNamespace(client) {
1141
1119
  return {
@@ -2347,6 +2325,40 @@ function meNamespace(client) {
2347
2325
  );
2348
2326
  }
2349
2327
  },
2328
+ /**
2329
+ * My marketing-email consent.
2330
+ *
2331
+ * Consent is a property of the board user, given at sign-up or from the
2332
+ * board's own settings page. Both surfaces render board-authored
2333
+ * disclosure wording — the API records the decision, never the prose, so
2334
+ * whatever you show beside these calls is what the person agreed to.
2335
+ */
2336
+ marketingConsent: {
2337
+ /** My consent, or `null` when no decision has ever been recorded. */
2338
+ retrieve(options) {
2339
+ return client.fetch(
2340
+ "/me/marketing-consent",
2341
+ options
2342
+ );
2343
+ },
2344
+ /**
2345
+ * Grant. Call this only from a surface that displayed your disclosure
2346
+ * copy — nothing server-side can check that you did.
2347
+ */
2348
+ grant(options) {
2349
+ return client.fetch("/me/marketing-consent/grant", {
2350
+ ...options,
2351
+ method: "POST"
2352
+ });
2353
+ },
2354
+ /** Withdraw. Repeating it returns the same state, no new event. */
2355
+ withdraw(options) {
2356
+ return client.fetch(
2357
+ "/me/marketing-consent/withdraw",
2358
+ { ...options, method: "POST" }
2359
+ );
2360
+ }
2361
+ },
2350
2362
  /**
2351
2363
  * The candidate job-access paywall — the authenticated money flow . Enumerate offers publicly with `board.paywall.offers()`, then
2352
2364
  * `checkout` here; poll `retrieveCheckout` until complete and confirm with
@@ -2621,12 +2633,18 @@ function salariesNamespace(client) {
2621
2633
  function searchNamespace(client) {
2622
2634
  return {
2623
2635
  /**
2624
- * GET /search/suggest — federated keyword suggestions (companies +
2625
- * taxonomy terms), one interleaved server-ranked list. Order is the
2626
- * contract: do not re-sort.
2636
+ * GET /search/suggest — federated keyword suggestions (companies,
2637
+ * markets, taxonomy terms), one interleaved server-ranked list. Pass
2638
+ * `types` to restrict kinds; `limit` applies after that filter. Order
2639
+ * is the contract: do not re-sort. Routing is application-side via
2640
+ * `suggestionPath` from `@cavuno/board/paths`.
2627
2641
  *
2628
2642
  * @example
2629
- * const { items } = await board.search.suggest({ q: 'acme', limit: 10 });
2643
+ * const { items } = await board.search.suggest({
2644
+ * q: 'acme',
2645
+ * limit: 10,
2646
+ * types: ['company', 'skill'],
2647
+ * });
2630
2648
  */
2631
2649
  suggest(query, options) {
2632
2650
  return client.fetch("/search/suggest", {
@@ -2719,14 +2737,6 @@ function taxonomyNamespace(client) {
2719
2737
  );
2720
2738
  }
2721
2739
  },
2722
- suggestions: {
2723
- list(query, options) {
2724
- return client.fetch("/suggestions", {
2725
- ...options,
2726
- query
2727
- });
2728
- }
2729
- },
2730
2740
  places: {
2731
2741
  ...taxonomyResolver(client, "places"),
2732
2742
  /**
@@ -2853,7 +2863,6 @@ function createBoardClient(options) {
2853
2863
  embed: embedNamespace(client),
2854
2864
  companies: companiesNamespace(client),
2855
2865
  blog: blogNamespace(client),
2856
- legal: legalNamespace(client),
2857
2866
  auth: authNamespace(client),
2858
2867
  me: meNamespace(client),
2859
2868
  password: passwordNamespace(client),
package/dist/index.mjs CHANGED
@@ -39,7 +39,7 @@ function toSearchParams(query) {
39
39
  }
40
40
 
41
41
  // src/version.ts
42
- var SDK_VERSION = "3.2.0";
42
+ var SDK_VERSION = "4.1.0";
43
43
 
44
44
  // src/client.ts
45
45
  function isRawBody(body) {
@@ -841,28 +841,6 @@ function jobsNamespace(client) {
841
841
  };
842
842
  }
843
843
 
844
- // src/namespaces/legal.ts
845
- function legalNamespace(client) {
846
- return {
847
- /**
848
- * Retrieve a board legal/about page — owner-authored prose as portable HTML
849
- * (`privacy-policy` / `terms-of-service` / `cookie-policy` / `about`) plus,
850
- * for `impressum`, structured legal-entity facts. The starter authors the
851
- * layout, breadcrumb labels, and JSON-LD. `impressum` throws
852
- * `board_page_not_found` (404) when the board has not enabled it.
853
- *
854
- * @example
855
- * const { title, content } = await board.legal.retrieve('privacy-policy');
856
- */
857
- retrieve(type, options) {
858
- return client.fetch(
859
- `/legal/${encodeURIComponent(type)}`,
860
- options
861
- );
862
- }
863
- };
864
- }
865
-
866
844
  // src/namespaces/me.ts
867
845
  function meNamespace(client) {
868
846
  return {
@@ -2074,6 +2052,40 @@ function meNamespace(client) {
2074
2052
  );
2075
2053
  }
2076
2054
  },
2055
+ /**
2056
+ * My marketing-email consent.
2057
+ *
2058
+ * Consent is a property of the board user, given at sign-up or from the
2059
+ * board's own settings page. Both surfaces render board-authored
2060
+ * disclosure wording — the API records the decision, never the prose, so
2061
+ * whatever you show beside these calls is what the person agreed to.
2062
+ */
2063
+ marketingConsent: {
2064
+ /** My consent, or `null` when no decision has ever been recorded. */
2065
+ retrieve(options) {
2066
+ return client.fetch(
2067
+ "/me/marketing-consent",
2068
+ options
2069
+ );
2070
+ },
2071
+ /**
2072
+ * Grant. Call this only from a surface that displayed your disclosure
2073
+ * copy — nothing server-side can check that you did.
2074
+ */
2075
+ grant(options) {
2076
+ return client.fetch("/me/marketing-consent/grant", {
2077
+ ...options,
2078
+ method: "POST"
2079
+ });
2080
+ },
2081
+ /** Withdraw. Repeating it returns the same state, no new event. */
2082
+ withdraw(options) {
2083
+ return client.fetch(
2084
+ "/me/marketing-consent/withdraw",
2085
+ { ...options, method: "POST" }
2086
+ );
2087
+ }
2088
+ },
2077
2089
  /**
2078
2090
  * The candidate job-access paywall — the authenticated money flow . Enumerate offers publicly with `board.paywall.offers()`, then
2079
2091
  * `checkout` here; poll `retrieveCheckout` until complete and confirm with
@@ -2348,12 +2360,18 @@ function salariesNamespace(client) {
2348
2360
  function searchNamespace(client) {
2349
2361
  return {
2350
2362
  /**
2351
- * GET /search/suggest — federated keyword suggestions (companies +
2352
- * taxonomy terms), one interleaved server-ranked list. Order is the
2353
- * contract: do not re-sort.
2363
+ * GET /search/suggest — federated keyword suggestions (companies,
2364
+ * markets, taxonomy terms), one interleaved server-ranked list. Pass
2365
+ * `types` to restrict kinds; `limit` applies after that filter. Order
2366
+ * is the contract: do not re-sort. Routing is application-side via
2367
+ * `suggestionPath` from `@cavuno/board/paths`.
2354
2368
  *
2355
2369
  * @example
2356
- * const { items } = await board.search.suggest({ q: 'acme', limit: 10 });
2370
+ * const { items } = await board.search.suggest({
2371
+ * q: 'acme',
2372
+ * limit: 10,
2373
+ * types: ['company', 'skill'],
2374
+ * });
2357
2375
  */
2358
2376
  suggest(query, options) {
2359
2377
  return client.fetch("/search/suggest", {
@@ -2446,14 +2464,6 @@ function taxonomyNamespace(client) {
2446
2464
  );
2447
2465
  }
2448
2466
  },
2449
- suggestions: {
2450
- list(query, options) {
2451
- return client.fetch("/suggestions", {
2452
- ...options,
2453
- query
2454
- });
2455
- }
2456
- },
2457
2467
  places: {
2458
2468
  ...taxonomyResolver(client, "places"),
2459
2469
  /**
@@ -2550,7 +2560,6 @@ function createBoardClient(options) {
2550
2560
  embed: embedNamespace(client),
2551
2561
  companies: companiesNamespace(client),
2552
2562
  blog: blogNamespace(client),
2553
- legal: legalNamespace(client),
2554
2563
  auth: authNamespace(client),
2555
2564
  me: meNamespace(client),
2556
2565
  password: passwordNamespace(client),
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-nkWAraS0.js';
1
+ import { S as Schemas } from './_spec-BZDv2dc5.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.mjs';
1
+ import { S as Schemas } from './_spec-BZDv2dc5.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 };
package/dist/paths.js CHANGED
@@ -28,56 +28,104 @@ __export(paths_exports, {
28
28
  companyMarketPath: () => companyMarketPath,
29
29
  companyPath: () => companyPath,
30
30
  companySalaryPath: () => companySalaryPath,
31
+ encodePathSegment: () => encodePathSegment,
31
32
  goAlertsConfirmPath: () => goAlertsConfirmPath,
32
33
  goAlertsManagePath: () => goAlertsManagePath,
33
34
  goJobPath: () => goJobPath,
34
35
  jobDetailPath: () => jobDetailPath,
35
36
  jobsCategoryPath: () => jobsCategoryPath,
37
+ jobsLocationCategoryPath: () => jobsLocationCategoryPath,
36
38
  jobsLocationPath: () => jobsLocationPath,
39
+ jobsLocationSkillPath: () => jobsLocationSkillPath,
37
40
  jobsSkillPath: () => jobsSkillPath,
38
41
  salaryLocationPath: () => salaryLocationPath,
39
42
  salarySkillPath: () => salarySkillPath,
40
- salaryTitlePath: () => salaryTitlePath
43
+ salaryTitlePath: () => salaryTitlePath,
44
+ suggestionPath: () => suggestionPath
41
45
  });
42
46
  module.exports = __toCommonJS(paths_exports);
47
+ function encodePathSegment(segment) {
48
+ try {
49
+ return encodeURIComponent(decodeURIComponent(segment)).replace(
50
+ /%3A/gi,
51
+ ":"
52
+ );
53
+ } catch {
54
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
55
+ }
56
+ }
43
57
  function jobDetailPath(companySlug, jobSlug) {
44
- return `/companies/${companySlug}/jobs/${jobSlug}`;
58
+ return `/companies/${encodePathSegment(companySlug)}/jobs/${encodePathSegment(jobSlug)}`;
45
59
  }
46
60
  function jobsCategoryPath(categorySlug) {
47
- return `/jobs/${categorySlug}`;
61
+ return `/jobs/${encodePathSegment(categorySlug)}`;
48
62
  }
49
63
  function jobsSkillPath(skillSlug) {
50
- return `/jobs/skills/${skillSlug}`;
64
+ return `/jobs/skills/${encodePathSegment(skillSlug)}`;
51
65
  }
52
66
  function jobsLocationPath(placeSlug) {
53
- return `/jobs/locations/${placeSlug}`;
67
+ return `/jobs/locations/${encodePathSegment(placeSlug)}`;
68
+ }
69
+ function jobsLocationCategoryPath(placeSlug, categorySlug) {
70
+ return `${jobsLocationPath(placeSlug)}/${encodePathSegment(categorySlug)}`;
71
+ }
72
+ function jobsLocationSkillPath(placeSlug, skillSlug) {
73
+ return `${jobsLocationPath(placeSlug)}/skills/${encodePathSegment(skillSlug)}`;
54
74
  }
55
75
  function companyPath(companySlug) {
56
- return `/companies/${companySlug}`;
76
+ return `/companies/${encodePathSegment(companySlug)}`;
57
77
  }
58
78
  function companyMarketPath(marketSlug) {
59
- return `/companies/markets/${marketSlug}`;
79
+ return `/companies/markets/${encodePathSegment(marketSlug)}`;
80
+ }
81
+ function suggestionPath(suggestion, options) {
82
+ const location = options.location?.trim() || void 0;
83
+ if (options.scope === "blog") {
84
+ if (suggestion.type === "post") {
85
+ return blogPostPath(suggestion.slug);
86
+ }
87
+ if (suggestion.type === "tag") {
88
+ return blogTagPath(suggestion.slug);
89
+ }
90
+ return null;
91
+ }
92
+ if (options.scope === "jobs") {
93
+ if (suggestion.type === "company" || suggestion.type === "market" || suggestion.type === "post" || suggestion.type === "tag") {
94
+ return null;
95
+ }
96
+ if (suggestion.termType === "skill") {
97
+ return location ? jobsLocationSkillPath(location, suggestion.canonicalSlug) : jobsSkillPath(suggestion.canonicalSlug);
98
+ }
99
+ return location ? jobsLocationCategoryPath(location, suggestion.canonicalSlug) : jobsCategoryPath(suggestion.canonicalSlug);
100
+ }
101
+ if (suggestion.type === "company") {
102
+ return companyPath(suggestion.slug);
103
+ }
104
+ if (suggestion.type === "market") {
105
+ return companyMarketPath(suggestion.slug);
106
+ }
107
+ return null;
60
108
  }
61
109
  function companySalaryPath(companySlug) {
62
- return `/companies/${companySlug}/salaries`;
110
+ return `/companies/${encodePathSegment(companySlug)}/salaries`;
63
111
  }
64
112
  function salaryTitlePath(titleSlug) {
65
- return `/salaries/titles/${titleSlug}`;
113
+ return `/salaries/titles/${encodePathSegment(titleSlug)}`;
66
114
  }
67
115
  function salarySkillPath(skillSlug) {
68
- return `/salaries/skills/${skillSlug}`;
116
+ return `/salaries/skills/${encodePathSegment(skillSlug)}`;
69
117
  }
70
118
  function salaryLocationPath(placeSlug) {
71
- return `/salaries/locations/${placeSlug}`;
119
+ return `/salaries/locations/${encodePathSegment(placeSlug)}`;
72
120
  }
73
121
  function blogPostPath(postSlug) {
74
- return `/blog/${postSlug}`;
122
+ return `/blog/${encodePathSegment(postSlug)}`;
75
123
  }
76
124
  function blogTagPath(tagSlug) {
77
- return `/blog/tag/${tagSlug}`;
125
+ return `/blog/tag/${encodePathSegment(tagSlug)}`;
78
126
  }
79
127
  function blogAuthorPath(authorSlug) {
80
- return `/blog/author/${authorSlug}`;
128
+ return `/blog/author/${encodePathSegment(authorSlug)}`;
81
129
  }
82
130
  var BOARD_PATHS = {
83
131
  home: "/",