@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/seo.mjs CHANGED
@@ -9,17 +9,19 @@ import {
9
9
  createJobPostingJsonLd,
10
10
  crossAxisSalaryJsonLd,
11
11
  faqJsonLd,
12
- formatRange,
13
- formatSeniority,
14
- formatUsd,
12
+ formatSalaryStat,
13
+ formatSalaryStatRange,
15
14
  itemListJsonLd,
16
15
  locationSalaryJsonLd,
17
16
  normalizeWebsiteUrl,
18
17
  skillSalaryJsonLd,
19
18
  sortBySeniority,
20
19
  titleSalaryJsonLd
21
- } from "./chunk-STE77MAT.mjs";
22
- import "./chunk-IJCJ26SQ.mjs";
20
+ } from "./chunk-YEAQ3JEI.mjs";
21
+ import {
22
+ boardUrl,
23
+ jobDetailPath
24
+ } from "./chunk-BGKNXG4H.mjs";
23
25
 
24
26
  // src/seo/blog.ts
25
27
  function createBlogArticleJsonLd({
@@ -138,9 +140,8 @@ function buildAbsoluteUrl(origin, path) {
138
140
 
139
141
  // src/seo/listing.ts
140
142
  function listingHead(options) {
141
- const countPrefix = typeof options.count === "number" ? `${options.language ? new Intl.NumberFormat(options.language).format(options.count) : options.count} ` : "";
142
- const title = `${countPrefix}${options.heading} | ${options.boardName}`;
143
- const description = `Browse ${countPrefix}${options.heading.toLowerCase()} on ${options.boardName}.`;
143
+ const title = options.title;
144
+ const description = options.description;
144
145
  const url = `${options.origin}${options.path}`;
145
146
  return {
146
147
  meta: [
@@ -155,31 +156,33 @@ function listingHead(options) {
155
156
  };
156
157
  }
157
158
  function listingJsonLd(options) {
158
- const jobUrl = (job) => job.company ? `${options.origin}/companies/${job.company.slug}/jobs/${job.slug}` : `${options.origin}/jobs/${job.slug}`;
159
- const objects = [
160
- {
159
+ const objects = [];
160
+ if (options.breadcrumbs.length >= 2) {
161
+ objects.push({
161
162
  "@context": "https://schema.org",
162
- // Deliberately NOT createBreadcrumbJsonLd (breadcrumbs.ts): listing trails
163
- // are code-constructed (never user copy), so the hosted listing page skips
164
- // the trim/drop-blank/min-2 gate that module transcribes. Same schema.org
165
- // fragment, different (intentional) semantics — do not unify blindly.
166
163
  "@type": "BreadcrumbList",
167
164
  itemListElement: options.breadcrumbs.map((crumb, index) => ({
168
165
  "@type": "ListItem",
169
166
  position: index + 1,
170
167
  name: crumb.name,
171
- ...crumb.path ? { item: `${options.origin}${crumb.path}` } : {}
168
+ ...crumb.path ? { item: boardUrl(options.origin, crumb.path) } : {}
172
169
  }))
173
- }
174
- ];
175
- if (options.jobs && options.jobs.length > 0) {
170
+ });
171
+ }
172
+ const detailJobs = (options.jobs ?? []).filter(
173
+ (job) => job.company != null && Boolean(job.company.slug)
174
+ );
175
+ if (detailJobs.length > 0) {
176
176
  objects.push({
177
177
  "@context": "https://schema.org",
178
178
  "@type": "ItemList",
179
- itemListElement: options.jobs.map((job, index) => ({
179
+ itemListElement: detailJobs.map((job, index) => ({
180
180
  "@type": "ListItem",
181
181
  position: index + 1,
182
- url: jobUrl(job)
182
+ url: boardUrl(
183
+ options.origin,
184
+ jobDetailPath(job.company.slug, job.slug)
185
+ )
183
186
  }))
184
187
  });
185
188
  }
@@ -198,9 +201,8 @@ export {
198
201
  createJobPostingJsonLd,
199
202
  crossAxisSalaryJsonLd,
200
203
  faqJsonLd,
201
- formatRange,
202
- formatSeniority,
203
- formatUsd,
204
+ formatSalaryStat,
205
+ formatSalaryStatRange,
204
206
  itemListJsonLd,
205
207
  listingHead,
206
208
  listingJsonLd,
package/dist/server.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-nkWAraS0.mjs';
3
- import './search-CCyFWPi3.mjs';
4
- import './board-BJbXd_9_.mjs';
5
- import './jobs-lVvKCBCU.mjs';
6
- import './blog-DGBHmExV.mjs';
2
+ import './_spec-BZDv2dc5.mjs';
3
+ import './search-OKrdAHv8.mjs';
4
+ import './board-BF3Qao6J.mjs';
5
+ import './jobs-BX6d_sdd.mjs';
6
+ import './blog-DyDvyQrI.mjs';
7
7
 
8
8
  /**
9
9
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-nkWAraS0.js';
3
- import './search-CyRvVMUp.js';
4
- import './board-JhdUWIoq.js';
5
- import './jobs-BiEvL9d7.js';
6
- import './blog-iV9XEIGf.js';
2
+ import './_spec-BZDv2dc5.js';
3
+ import './search-7B-mVRNA.js';
4
+ import './board-BTEZ0uiI.js';
5
+ import './jobs-BbLJfz1z.js';
6
+ import './blog-BHx6iH3Z.js';
7
7
 
8
8
  /**
9
9
  * Session cookie codec — pure (no framework imports, no node imports) so it
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-nkWAraS0.mjs';
3
- import './search-CCyFWPi3.mjs';
4
- import './board-BJbXd_9_.mjs';
5
- import './jobs-lVvKCBCU.mjs';
6
- import './blog-DGBHmExV.mjs';
2
+ import './_spec-BZDv2dc5.mjs';
3
+ import './search-OKrdAHv8.mjs';
4
+ import './board-BF3Qao6J.mjs';
5
+ import './jobs-BX6d_sdd.mjs';
6
+ import './blog-DyDvyQrI.mjs';
7
7
 
8
8
  /**
9
9
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-nkWAraS0.js';
3
- import './search-CyRvVMUp.js';
4
- import './board-JhdUWIoq.js';
5
- import './jobs-BiEvL9d7.js';
6
- import './blog-iV9XEIGf.js';
2
+ import './_spec-BZDv2dc5.js';
3
+ import './search-7B-mVRNA.js';
4
+ import './board-BTEZ0uiI.js';
5
+ import './jobs-BbLJfz1z.js';
6
+ import './blog-BHx6iH3Z.js';
7
7
 
8
8
  /**
9
9
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.js CHANGED
@@ -165,44 +165,54 @@ function paginate(listFn, query, options) {
165
165
  }
166
166
 
167
167
  // src/paths/index.ts
168
+ function encodePathSegment(segment) {
169
+ try {
170
+ return encodeURIComponent(decodeURIComponent(segment)).replace(
171
+ /%3A/gi,
172
+ ":"
173
+ );
174
+ } catch {
175
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
176
+ }
177
+ }
168
178
  function jobDetailPath(companySlug, jobSlug) {
169
- return `/companies/${companySlug}/jobs/${jobSlug}`;
179
+ return `/companies/${encodePathSegment(companySlug)}/jobs/${encodePathSegment(jobSlug)}`;
170
180
  }
171
181
  function jobsCategoryPath(categorySlug) {
172
- return `/jobs/${categorySlug}`;
182
+ return `/jobs/${encodePathSegment(categorySlug)}`;
173
183
  }
174
184
  function jobsSkillPath(skillSlug) {
175
- return `/jobs/skills/${skillSlug}`;
185
+ return `/jobs/skills/${encodePathSegment(skillSlug)}`;
176
186
  }
177
187
  function jobsLocationPath(placeSlug) {
178
- return `/jobs/locations/${placeSlug}`;
188
+ return `/jobs/locations/${encodePathSegment(placeSlug)}`;
179
189
  }
180
190
  function companyPath(companySlug) {
181
- return `/companies/${companySlug}`;
191
+ return `/companies/${encodePathSegment(companySlug)}`;
182
192
  }
183
193
  function companyMarketPath(marketSlug) {
184
- return `/companies/markets/${marketSlug}`;
194
+ return `/companies/markets/${encodePathSegment(marketSlug)}`;
185
195
  }
186
196
  function companySalaryPath(companySlug) {
187
- return `/companies/${companySlug}/salaries`;
197
+ return `/companies/${encodePathSegment(companySlug)}/salaries`;
188
198
  }
189
199
  function salaryTitlePath(titleSlug) {
190
- return `/salaries/titles/${titleSlug}`;
200
+ return `/salaries/titles/${encodePathSegment(titleSlug)}`;
191
201
  }
192
202
  function salarySkillPath(skillSlug) {
193
- return `/salaries/skills/${skillSlug}`;
203
+ return `/salaries/skills/${encodePathSegment(skillSlug)}`;
194
204
  }
195
205
  function salaryLocationPath(placeSlug) {
196
- return `/salaries/locations/${placeSlug}`;
206
+ return `/salaries/locations/${encodePathSegment(placeSlug)}`;
197
207
  }
198
208
  function blogPostPath(postSlug) {
199
- return `/blog/${postSlug}`;
209
+ return `/blog/${encodePathSegment(postSlug)}`;
200
210
  }
201
211
  function blogTagPath(tagSlug) {
202
- return `/blog/tag/${tagSlug}`;
212
+ return `/blog/tag/${encodePathSegment(tagSlug)}`;
203
213
  }
204
214
  function blogAuthorPath(authorSlug) {
205
- return `/blog/author/${authorSlug}`;
215
+ return `/blog/author/${encodePathSegment(authorSlug)}`;
206
216
  }
207
217
  var BOARD_PATHS = {
208
218
  home: "/",
@@ -320,7 +330,8 @@ async function marketing(board, origin) {
320
330
  `${origin}${BOARD_PATHS.cookiePolicy}`
321
331
  ];
322
332
  if (features.impressum) urls.push(`${origin}${BOARD_PATHS.impressum}`);
323
- if (features.talentDirectory) urls.push(`${origin}${BOARD_PATHS.talent}`);
333
+ if (features.talentDirectory === "public")
334
+ urls.push(`${origin}${BOARD_PATHS.talent}`);
324
335
  if (features.employers) urls.push(`${origin}${BOARD_PATHS.employers}`);
325
336
  return urls;
326
337
  }
package/dist/sitemap.mjs CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  salaryLocationPath,
17
17
  salarySkillPath,
18
18
  salaryTitlePath
19
- } from "./chunk-L7ZOYWHW.mjs";
19
+ } from "./chunk-BGKNXG4H.mjs";
20
20
 
21
21
  // src/sitemap/xml.ts
22
22
  var SITEMAP_BUCKETS = [
@@ -208,7 +208,8 @@ async function marketing(board, origin) {
208
208
  `${origin}${BOARD_PATHS.cookiePolicy}`
209
209
  ];
210
210
  if (features.impressum) urls.push(`${origin}${BOARD_PATHS.impressum}`);
211
- if (features.talentDirectory) urls.push(`${origin}${BOARD_PATHS.talent}`);
211
+ if (features.talentDirectory === "public")
212
+ urls.push(`${origin}${BOARD_PATHS.talent}`);
212
213
  if (features.employers) urls.push(`${origin}${BOARD_PATHS.employers}`);
213
214
  return urls;
214
215
  }
package/dist/skills.js CHANGED
@@ -57,7 +57,7 @@ function loadSkillCorpus(baseDir) {
57
57
  ).map((entry) => ({
58
58
  path: entry.name,
59
59
  content: (0, import_node_fs.readFileSync)((0, import_node_path.resolve)(skillDir, entry.name), "utf8")
60
- })).sort((a, b) => a.path.localeCompare(b.path));
60
+ })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
61
61
  return {
62
62
  ...skill,
63
63
  content: (0, import_node_fs.readFileSync)(skillPath, "utf8"),
package/dist/skills.mjs CHANGED
@@ -24,7 +24,7 @@ function loadSkillCorpus(baseDir) {
24
24
  ).map((entry) => ({
25
25
  path: entry.name,
26
26
  content: readFileSync(resolve(skillDir, entry.name), "utf8")
27
- })).sort((a, b) => a.path.localeCompare(b.path));
27
+ })).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
28
28
  return {
29
29
  ...skill,
30
30
  content: readFileSync(skillPath, "utf8"),
@@ -1,5 +1,5 @@
1
- import { g as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, f as SuggestResult } from './search-CCyFWPi3.mjs';
2
- import './_spec-nkWAraS0.mjs';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-OKrdAHv8.mjs';
2
+ import './_spec-BZDv2dc5.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/dist/suggest.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { g as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, f as SuggestResult } from './search-CyRvVMUp.js';
2
- import './_spec-nkWAraS0.js';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-7B-mVRNA.js';
2
+ import './_spec-BZDv2dc5.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/dist/suggest.js CHANGED
@@ -28,10 +28,24 @@ var DEFAULT_DEBOUNCE_MS = 250;
28
28
  function isAbortError(error) {
29
29
  return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
30
30
  }
31
+ function queryCharCount(query) {
32
+ try {
33
+ let n = 0;
34
+ for (const _ of new Intl.Segmenter(void 0, {
35
+ granularity: "grapheme"
36
+ }).segment(query)) {
37
+ n += 1;
38
+ }
39
+ return n;
40
+ } catch {
41
+ return [...query].length;
42
+ }
43
+ }
31
44
  function applyExclusions(items, excluded) {
32
45
  if (excluded.size === 0) return items.slice();
33
46
  return items.filter(
34
- (item) => !(item.type === "company" && excluded.has(item.slug.toLowerCase()))
47
+ (item) => !(item.type === "company" && // NFC + toLowerCase — same keying as setExcludedCompanySlugs.
48
+ excluded.has(item.slug.toLowerCase().normalize("NFC")))
35
49
  );
36
50
  }
37
51
  function createSuggestController(board, options) {
@@ -104,7 +118,7 @@ function createSuggestController(board, options) {
104
118
  if (disposed) return;
105
119
  const trimmed = query.trim();
106
120
  cancelTimer();
107
- if (trimmed.length < minChars) {
121
+ if (queryCharCount(trimmed) < minChars) {
108
122
  sequence += 1;
109
123
  abortInFlight();
110
124
  rawItems = [];
@@ -133,7 +147,7 @@ function createSuggestController(board, options) {
133
147
  setExcludedCompanySlugs(slugs) {
134
148
  if (disposed) return;
135
149
  excluded = new Set(
136
- slugs.map((s) => s.trim().toLowerCase()).filter(Boolean)
150
+ slugs.map((s) => s.trim().toLowerCase().normalize("NFC")).filter(Boolean)
137
151
  );
138
152
  setState({
139
153
  query: state.query,
package/dist/suggest.mjs CHANGED
@@ -4,10 +4,24 @@ var DEFAULT_DEBOUNCE_MS = 250;
4
4
  function isAbortError(error) {
5
5
  return typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
6
6
  }
7
+ function queryCharCount(query) {
8
+ try {
9
+ let n = 0;
10
+ for (const _ of new Intl.Segmenter(void 0, {
11
+ granularity: "grapheme"
12
+ }).segment(query)) {
13
+ n += 1;
14
+ }
15
+ return n;
16
+ } catch {
17
+ return [...query].length;
18
+ }
19
+ }
7
20
  function applyExclusions(items, excluded) {
8
21
  if (excluded.size === 0) return items.slice();
9
22
  return items.filter(
10
- (item) => !(item.type === "company" && excluded.has(item.slug.toLowerCase()))
23
+ (item) => !(item.type === "company" && // NFC + toLowerCase — same keying as setExcludedCompanySlugs.
24
+ excluded.has(item.slug.toLowerCase().normalize("NFC")))
11
25
  );
12
26
  }
13
27
  function createSuggestController(board, options) {
@@ -80,7 +94,7 @@ function createSuggestController(board, options) {
80
94
  if (disposed) return;
81
95
  const trimmed = query.trim();
82
96
  cancelTimer();
83
- if (trimmed.length < minChars) {
97
+ if (queryCharCount(trimmed) < minChars) {
84
98
  sequence += 1;
85
99
  abortInFlight();
86
100
  rawItems = [];
@@ -109,7 +123,7 @@ function createSuggestController(board, options) {
109
123
  setExcludedCompanySlugs(slugs) {
110
124
  if (disposed) return;
111
125
  excluded = new Set(
112
- slugs.map((s) => s.trim().toLowerCase()).filter(Boolean)
126
+ slugs.map((s) => s.trim().toLowerCase().normalize("NFC")).filter(Boolean)
113
127
  );
114
128
  setState({
115
129
  query: state.query,
@@ -32,44 +32,54 @@ __export(well_known_exports, {
32
32
  module.exports = __toCommonJS(well_known_exports);
33
33
 
34
34
  // src/paths/index.ts
35
+ function encodePathSegment(segment) {
36
+ try {
37
+ return encodeURIComponent(decodeURIComponent(segment)).replace(
38
+ /%3A/gi,
39
+ ":"
40
+ );
41
+ } catch {
42
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
43
+ }
44
+ }
35
45
  function jobDetailPath(companySlug, jobSlug) {
36
- return `/companies/${companySlug}/jobs/${jobSlug}`;
46
+ return `/companies/${encodePathSegment(companySlug)}/jobs/${encodePathSegment(jobSlug)}`;
37
47
  }
38
48
  function jobsCategoryPath(categorySlug) {
39
- return `/jobs/${categorySlug}`;
49
+ return `/jobs/${encodePathSegment(categorySlug)}`;
40
50
  }
41
51
  function jobsSkillPath(skillSlug) {
42
- return `/jobs/skills/${skillSlug}`;
52
+ return `/jobs/skills/${encodePathSegment(skillSlug)}`;
43
53
  }
44
54
  function jobsLocationPath(placeSlug) {
45
- return `/jobs/locations/${placeSlug}`;
55
+ return `/jobs/locations/${encodePathSegment(placeSlug)}`;
46
56
  }
47
57
  function companyPath(companySlug) {
48
- return `/companies/${companySlug}`;
58
+ return `/companies/${encodePathSegment(companySlug)}`;
49
59
  }
50
60
  function companyMarketPath(marketSlug) {
51
- return `/companies/markets/${marketSlug}`;
61
+ return `/companies/markets/${encodePathSegment(marketSlug)}`;
52
62
  }
53
63
  function companySalaryPath(companySlug) {
54
- return `/companies/${companySlug}/salaries`;
64
+ return `/companies/${encodePathSegment(companySlug)}/salaries`;
55
65
  }
56
66
  function salaryTitlePath(titleSlug) {
57
- return `/salaries/titles/${titleSlug}`;
67
+ return `/salaries/titles/${encodePathSegment(titleSlug)}`;
58
68
  }
59
69
  function salarySkillPath(skillSlug) {
60
- return `/salaries/skills/${skillSlug}`;
70
+ return `/salaries/skills/${encodePathSegment(skillSlug)}`;
61
71
  }
62
72
  function salaryLocationPath(placeSlug) {
63
- return `/salaries/locations/${placeSlug}`;
73
+ return `/salaries/locations/${encodePathSegment(placeSlug)}`;
64
74
  }
65
75
  function blogPostPath(postSlug) {
66
- return `/blog/${postSlug}`;
76
+ return `/blog/${encodePathSegment(postSlug)}`;
67
77
  }
68
78
  function blogTagPath(tagSlug) {
69
- return `/blog/tag/${tagSlug}`;
79
+ return `/blog/tag/${encodePathSegment(tagSlug)}`;
70
80
  }
71
81
  function blogAuthorPath(authorSlug) {
72
- return `/blog/author/${authorSlug}`;
82
+ return `/blog/author/${encodePathSegment(authorSlug)}`;
73
83
  }
74
84
  var BOARD_PATHS = {
75
85
  home: "/",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  compileManifest
3
- } from "./chunk-NXY3OWGF.mjs";
4
- import "./chunk-L7ZOYWHW.mjs";
3
+ } from "./chunk-T2KC6XGV.mjs";
4
+ import "./chunk-BGKNXG4H.mjs";
5
5
 
6
6
  // src/well-known/handler.ts
7
7
  var WELL_KNOWN_CACHE_CONTROL = "public, max-age=300";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "3.2.0",
3
+ "version": "4.1.0",
4
4
  "description": "Build custom job boards and careers pages with Cavuno’s TypeScript SDK.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -203,9 +203,7 @@
203
203
  "clean": "git clean -xdf .turbo dist node_modules src/generated",
204
204
  "typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.node.json",
205
205
  "test": "vitest run",
206
- "check:publish": "publint --strict && attw --pack . --profile node16",
207
- "check:surface": "node ../../scripts/check-public-surface.mjs",
208
- "gen:surface": "node ../../scripts/check-public-surface.mjs --write",
206
+ "check:publish": "publint --strict && attw --pack . --profile node16 && node scripts/check-no-private-kit-types.mjs",
209
207
  "gen:skills-manifest": "tsx scripts/generate-skills-manifest.ts",
210
208
  "assert-publish-target": "node -e \"const p=require('./package.json'); if(p.name!=='@cavuno/board'){throw new Error('Refusing to publish: package.json name is '+p.name+', expected @cavuno/board')}; if(p.private){throw new Error('Refusing to publish: package.json has private:true')}\"",
211
209
  "prepublishOnly": "pnpm run assert-publish-target && pnpm run gen:skills-manifest && pnpm run build"
@@ -60,6 +60,48 @@ A module-scoped client is safe for concurrent SSR requests when it uses the serv
60
60
 
61
61
  **Complete when:** a shared server client contains no per-user state and each authenticated request supplies its own headers.
62
62
 
63
+ ## Record marketing consent for the signed-in person
64
+
65
+ Marketing consent is a property of the board user, never a guest capture. The
66
+ frontend owns the checkbox wording and the privacy-policy link — render your
67
+ own copy beside the control; the API records the decision, not the prose.
68
+ Leave any checkbox unticked by default and call nothing while it stays
69
+ unticked: absence of a record means no consent, never a default.
70
+
71
+ At sign-up, pass the tick through the register body so consent is recorded in
72
+ the same transaction that creates the user:
73
+
74
+ ```ts snippet
75
+ await board.auth.register({
76
+ role: 'candidate',
77
+ method: 'emailpass',
78
+ email: form.email,
79
+ password: form.password,
80
+ displayName: form.displayName,
81
+ marketingConsent: form.marketingChecked,
82
+ });
83
+ ```
84
+
85
+ Later, read or change only the signed-in person's own consent through the
86
+ authenticated `me` namespace. There is intentionally no email parameter, so a
87
+ frontend cannot target another person. Withdrawal is always an explicit POST —
88
+ never a state-changing GET, which mail scanners would follow.
89
+
90
+ ```ts snippet
91
+ const current = await board.me.marketingConsent.retrieve();
92
+ if (current?.status !== 'granted') {
93
+ await board.me.marketingConsent.grant();
94
+ }
95
+ await board.me.marketingConsent.withdraw();
96
+ ```
97
+
98
+ Call `grant()` only from a surface that displayed your disclosure wording.
99
+ Both calls are idempotent: repeating one changes nothing and emits no event.
100
+
101
+ **Complete when:** the checkbox defaults to unticked, unticked submits
102
+ nothing, grant is only reachable beside rendered disclosure copy, and
103
+ withdrawal is never implemented as a state-changing GET.
104
+
63
105
  ## Use the escape hatch for an untyped endpoint
64
106
 
65
107
  `board.client.fetch<T>(path, options)` keeps the full request pipeline while providing a response type locally.
@@ -76,7 +76,10 @@ await board.me.applications.withdraw(applicationId);
76
76
 
77
77
  ## Save jobs
78
78
 
79
- Saved rows embed the full `PublicJob`. `save` converges on the existing row and `unsave` is idempotent.
79
+ Saved rows embed the same slim `PublicJobCard` as the jobs list — render them
80
+ with the same card view-model, and do not expect full-job fields (description,
81
+ custom field values) on saved rows. `save` converges on the existing row and
82
+ `unsave` is idempotent.
80
83
 
81
84
  ```ts snippet
82
85
  const saved = await board.me.savedJobs.list({ limit: 20 });
@@ -49,21 +49,19 @@ import {
49
49
  JOB_SORTS,
50
50
  REMOTE_OPTIONS,
51
51
  SENIORITIES,
52
- seniorityLabels,
53
- sortLabels,
54
52
  } from '@cavuno/board/filters';
55
53
 
56
- const { language } = await board.context();
57
- const seniorityCopy = seniorityLabels(language);
58
- const sortCopy = sortLabels(language);
54
+ // Wire enums only — display labels are application-owned chrome.
55
+ const seniorityOptions = SENIORITIES;
56
+ const sortOptions = JOB_SORTS;
59
57
  ```
60
58
 
61
59
  Render seniority as a multi-select with all eight `SENIORITIES`.
62
60
  `EMPLOYMENT_TYPES` contains five listing options; `volunteer` and `other`
63
61
  remain valid job wire values but are absent from the filter control.
64
62
  `JOB_SORTS` contains exactly `relevance`, `newest`, and `salary_high`;
65
- `relevance` is the featured-ranked default. Sort labels follow the board copy
66
- catalog and operator overrides supplied to `sortLabels`.
63
+ `relevance` is the featured-ranked default. Label each option with your
64
+ application's copy (message catalog or hard-coded board language).
67
65
 
68
66
  ## Load taxonomy options
69
67
 
@@ -95,15 +93,16 @@ const second = first.nextCursor
95
93
  : null;
96
94
  ```
97
95
 
98
- Keyword suggestions accept `q` and `limit`. A present query shorter than two
99
- characters returns no results. Suggestions contain categories and skills only;
100
- because both types may share a slug, key each option by
101
- `type + canonicalSlug`.
96
+ Keyword suggestions accept `q`, `limit`, and optional `types`. A present query
97
+ shorter than two characters returns no results. Restrict to taxonomy terms with
98
+ `types: ['category', 'skill']`; because both may share a slug, key each option
99
+ by `termType + canonicalSlug`.
102
100
 
103
101
  ```ts snippet
104
- const suggestions = await board.taxonomy.suggestions.list({
102
+ const { items } = await board.search.suggest({
105
103
  q: searchText,
106
104
  limit: 10,
105
+ types: ['category', 'skill'],
107
106
  });
108
107
  ```
109
108