@cavuno/board 1.34.0 → 1.35.1

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 (42) hide show
  1. package/README.md +7 -8
  2. package/dist/bin.mjs +25 -10
  3. package/dist/{board-Id19Yg8-.d.ts → board-Z7hjKDp8.d.ts} +1 -1
  4. package/dist/{board-DgDC0T4g.d.mts → board-t_WltBBa.d.mts} +1 -1
  5. package/dist/filters.d.mts +2 -2
  6. package/dist/filters.d.ts +2 -2
  7. package/dist/filters.js +3 -6
  8. package/dist/filters.mjs +3 -6
  9. package/dist/format.d.mts +4 -4
  10. package/dist/format.d.ts +4 -4
  11. package/dist/format.js +3 -6
  12. package/dist/format.mjs +3 -6
  13. package/dist/index.d.mts +52 -43
  14. package/dist/index.d.ts +52 -43
  15. package/dist/index.js +27 -19
  16. package/dist/index.mjs +27 -19
  17. package/dist/{jobs-IIJtDgzX.d.mts → jobs-Dq2a9oPj.d.mts} +742 -61
  18. package/dist/{jobs-IIJtDgzX.d.ts → jobs-Dq2a9oPj.d.ts} +742 -61
  19. package/dist/{salaries-CL_00fNX.d.ts → salaries-CTin-18R.d.ts} +4 -4
  20. package/dist/{salaries-BfEEEOHj.d.mts → salaries-CWg82dOz.d.mts} +4 -4
  21. package/dist/seo.d.mts +4 -4
  22. package/dist/seo.d.ts +4 -4
  23. package/dist/seo.js +3 -6
  24. package/dist/seo.mjs +3 -6
  25. package/dist/server.d.mts +3 -3
  26. package/dist/server.d.ts +3 -3
  27. package/dist/sitemap.d.mts +3 -3
  28. package/dist/sitemap.d.ts +3 -3
  29. package/dist/{ui-copy-rlfoH9P3.d.mts → ui-copy-CKfFTtLk.d.mts} +0 -1
  30. package/dist/{ui-copy-rlfoH9P3.d.ts → ui-copy-CKfFTtLk.d.ts} +0 -1
  31. package/package.json +1 -1
  32. package/skills/cavuno-board-auth/SKILL.md +0 -1
  33. package/skills/cavuno-board-client/SKILL.md +1 -4
  34. package/skills/cavuno-board-filters/SKILL.md +43 -3
  35. package/skills/cavuno-board-job-alerts/SKILL.md +7 -7
  36. package/skills/cavuno-board-job-posting/SKILL.md +8 -22
  37. package/skills/cavuno-board-jobs/SKILL.md +2 -2
  38. package/skills/cavuno-board-setup/SKILL.md +6 -8
  39. package/skills/cavuno-board-smoke-test/SKILL.md +8 -7
  40. package/skills/cavuno-board-theme/SKILL.md +33 -45
  41. package/skills/flavors/tanstack-start/SKILL.md +0 -1
  42. package/skills/manifest.json +5 -5
@@ -1,4 +1,4 @@
1
- import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-IIJtDgzX.js';
1
+ import { b as Schemas, L as ListEnvelope, o as RelatedSearch } from './jobs-Dq2a9oPj.js';
2
2
 
3
3
  /** Author shape embedded on posts (no `object` discriminator). */
4
4
  type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
@@ -44,9 +44,9 @@ type CompanySalary = Schemas['CompanySalary'];
44
44
  */
45
45
  type CompanyCategorySalary = Schemas['CompanyCategorySalary'];
46
46
  /** The companies browse list — `PublicCompany`s + `market` `relatedSearches`. */
47
- interface CompanyListEnvelope extends ListEnvelope<PublicCompany> {
47
+ type CompanyListEnvelope = ListEnvelope<PublicCompany> & {
48
48
  relatedSearches?: RelatedSearch[];
49
- }
49
+ };
50
50
  type CompaniesListQuery = {
51
51
  cursor?: string;
52
52
  /** Scope to a single market (sector) by slug. Unknown slugs 404. */
@@ -54,7 +54,7 @@ type CompaniesListQuery = {
54
54
  /** 1–100. */
55
55
  limit?: number;
56
56
  /**
57
- * Storefront page offset (companies to skip); takes precedence over
57
+ * Company catalog page offset (companies to skip); takes precedence over
58
58
  * `cursor`. Pair with the response `count` to page in parallel.
59
59
  */
60
60
  offset?: number;
@@ -1,4 +1,4 @@
1
- import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-IIJtDgzX.mjs';
1
+ import { b as Schemas, L as ListEnvelope, o as RelatedSearch } from './jobs-Dq2a9oPj.mjs';
2
2
 
3
3
  /** Author shape embedded on posts (no `object` discriminator). */
4
4
  type BlogAuthorEmbed = Schemas['PublicBlogAuthorEmbed'];
@@ -44,9 +44,9 @@ type CompanySalary = Schemas['CompanySalary'];
44
44
  */
45
45
  type CompanyCategorySalary = Schemas['CompanyCategorySalary'];
46
46
  /** The companies browse list — `PublicCompany`s + `market` `relatedSearches`. */
47
- interface CompanyListEnvelope extends ListEnvelope<PublicCompany> {
47
+ type CompanyListEnvelope = ListEnvelope<PublicCompany> & {
48
48
  relatedSearches?: RelatedSearch[];
49
- }
49
+ };
50
50
  type CompaniesListQuery = {
51
51
  cursor?: string;
52
52
  /** Scope to a single market (sector) by slug. Unknown slugs 404. */
@@ -54,7 +54,7 @@ type CompaniesListQuery = {
54
54
  /** 1–100. */
55
55
  limit?: number;
56
56
  /**
57
- * Storefront page offset (companies to skip); takes precedence over
57
+ * Company catalog page offset (companies to skip); takes precedence over
58
58
  * `cursor`. Pair with the response `count` to page in parallel.
59
59
  */
60
60
  offset?: number;
package/dist/seo.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-BfEEEOHj.mjs';
2
- import { P as PublicBoard } from './board-DgDC0T4g.mjs';
3
- import { P as PublicJob } from './jobs-IIJtDgzX.mjs';
4
- import { B as BoardLabelOverrides } from './ui-copy-rlfoH9P3.mjs';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-CWg82dOz.mjs';
2
+ import { P as PublicBoard } from './board-t_WltBBa.mjs';
3
+ import { P as PublicJob } from './jobs-Dq2a9oPj.mjs';
4
+ import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.mjs';
5
5
 
6
6
  /**
7
7
  * Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
package/dist/seo.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-CL_00fNX.js';
2
- import { P as PublicBoard } from './board-Id19Yg8-.js';
3
- import { P as PublicJob } from './jobs-IIJtDgzX.js';
4
- import { B as BoardLabelOverrides } from './ui-copy-rlfoH9P3.js';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, a as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-CTin-18R.js';
2
+ import { P as PublicBoard } from './board-Z7hjKDp8.js';
3
+ import { P as PublicJob } from './jobs-Dq2a9oPj.js';
4
+ import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.js';
5
5
 
6
6
  /**
7
7
  * Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
package/dist/seo.js CHANGED
@@ -217,8 +217,7 @@ var EN = {
217
217
  jobAlertEmailPlaceholder: "you@example.com",
218
218
  jobAlertButtonText: "Get alerts",
219
219
  subscribingLabel: "Subscribing\u2026",
220
- jobAlertSuccessToast: "Check your email to confirm your subscription.",
221
- jobAlertDuplicateToast: "You're already subscribed to this alert.",
220
+ jobAlertSuccessToast: "If this email isn't already subscribed, we've sent a confirmation link \u2014 check your inbox.",
222
221
  jobAlertErrorToast: "Something went wrong. Please try again.",
223
222
  sectionAriaLabel: "Job alerts",
224
223
  emailAriaLabel: "email",
@@ -367,8 +366,7 @@ var DE = {
367
366
  jobAlertEmailPlaceholder: "name@beispiel.de",
368
367
  jobAlertButtonText: "Abonnieren",
369
368
  subscribingLabel: "Wird abonniert \u2026",
370
- jobAlertSuccessToast: "\xDCberpr\xFCfe deine E-Mails, um dein Abonnement zu best\xE4tigen.",
371
- jobAlertDuplicateToast: "Du hast diese Benachrichtigung bereits abonniert.",
369
+ jobAlertSuccessToast: "Falls diese E-Mail noch nicht abonniert ist, haben wir einen Best\xE4tigungslink gesendet \u2014 pr\xFCfe deinen Posteingang.",
372
370
  jobAlertErrorToast: "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
373
371
  sectionAriaLabel: "Job-Benachrichtigungen",
374
372
  emailAriaLabel: "E-Mail",
@@ -517,8 +515,7 @@ var FR = {
517
515
  jobAlertEmailPlaceholder: "vous@exemple.fr",
518
516
  jobAlertButtonText: "Activer les alertes",
519
517
  subscribingLabel: "Abonnement en cours\u2026",
520
- jobAlertSuccessToast: "V\xE9rifiez votre e-mail pour confirmer votre abonnement.",
521
- jobAlertDuplicateToast: "Vous \xEAtes d\xE9j\xE0 abonn\xE9 \xE0 cette alerte.",
518
+ jobAlertSuccessToast: "Si cet e-mail n'est pas d\xE9j\xE0 abonn\xE9, nous avons envoy\xE9 un lien de confirmation \u2014 v\xE9rifiez votre bo\xEEte de r\xE9ception.",
522
519
  jobAlertErrorToast: "Une erreur est survenue. Veuillez r\xE9essayer.",
523
520
  sectionAriaLabel: "Alertes emploi",
524
521
  emailAriaLabel: "e-mail",
package/dist/seo.mjs CHANGED
@@ -169,8 +169,7 @@ var EN = {
169
169
  jobAlertEmailPlaceholder: "you@example.com",
170
170
  jobAlertButtonText: "Get alerts",
171
171
  subscribingLabel: "Subscribing\u2026",
172
- jobAlertSuccessToast: "Check your email to confirm your subscription.",
173
- jobAlertDuplicateToast: "You're already subscribed to this alert.",
172
+ jobAlertSuccessToast: "If this email isn't already subscribed, we've sent a confirmation link \u2014 check your inbox.",
174
173
  jobAlertErrorToast: "Something went wrong. Please try again.",
175
174
  sectionAriaLabel: "Job alerts",
176
175
  emailAriaLabel: "email",
@@ -319,8 +318,7 @@ var DE = {
319
318
  jobAlertEmailPlaceholder: "name@beispiel.de",
320
319
  jobAlertButtonText: "Abonnieren",
321
320
  subscribingLabel: "Wird abonniert \u2026",
322
- jobAlertSuccessToast: "\xDCberpr\xFCfe deine E-Mails, um dein Abonnement zu best\xE4tigen.",
323
- jobAlertDuplicateToast: "Du hast diese Benachrichtigung bereits abonniert.",
321
+ jobAlertSuccessToast: "Falls diese E-Mail noch nicht abonniert ist, haben wir einen Best\xE4tigungslink gesendet \u2014 pr\xFCfe deinen Posteingang.",
324
322
  jobAlertErrorToast: "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
325
323
  sectionAriaLabel: "Job-Benachrichtigungen",
326
324
  emailAriaLabel: "E-Mail",
@@ -469,8 +467,7 @@ var FR = {
469
467
  jobAlertEmailPlaceholder: "vous@exemple.fr",
470
468
  jobAlertButtonText: "Activer les alertes",
471
469
  subscribingLabel: "Abonnement en cours\u2026",
472
- jobAlertSuccessToast: "V\xE9rifiez votre e-mail pour confirmer votre abonnement.",
473
- jobAlertDuplicateToast: "Vous \xEAtes d\xE9j\xE0 abonn\xE9 \xE0 cette alerte.",
470
+ jobAlertSuccessToast: "Si cet e-mail n'est pas d\xE9j\xE0 abonn\xE9, nous avons envoy\xE9 un lien de confirmation \u2014 v\xE9rifiez votre bo\xEEte de r\xE9ception.",
474
471
  jobAlertErrorToast: "Une erreur est survenue. Veuillez r\xE9essayer.",
475
472
  sectionAriaLabel: "Alertes emploi",
476
473
  emailAriaLabel: "e-mail",
package/dist/server.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './jobs-IIJtDgzX.mjs';
3
- import './board-DgDC0T4g.mjs';
4
- import './salaries-BfEEEOHj.mjs';
2
+ import './jobs-Dq2a9oPj.mjs';
3
+ import './board-t_WltBBa.mjs';
4
+ import './salaries-CWg82dOz.mjs';
5
5
 
6
6
  /**
7
7
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './jobs-IIJtDgzX.js';
3
- import './board-Id19Yg8-.js';
4
- import './salaries-CL_00fNX.js';
2
+ import './jobs-Dq2a9oPj.js';
3
+ import './board-Z7hjKDp8.js';
4
+ import './salaries-CTin-18R.js';
5
5
 
6
6
  /**
7
7
  * Session cookie codec — pure (no framework imports, no node imports) so it
@@ -1,7 +1,7 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './jobs-IIJtDgzX.mjs';
3
- import './board-DgDC0T4g.mjs';
4
- import './salaries-BfEEEOHj.mjs';
2
+ import './jobs-Dq2a9oPj.mjs';
3
+ import './board-t_WltBBa.mjs';
4
+ import './salaries-CWg82dOz.mjs';
5
5
 
6
6
  /**
7
7
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './jobs-IIJtDgzX.js';
3
- import './board-Id19Yg8-.js';
4
- import './salaries-CL_00fNX.js';
2
+ import './jobs-Dq2a9oPj.js';
3
+ import './board-Z7hjKDp8.js';
4
+ import './salaries-CTin-18R.js';
5
5
 
6
6
  /**
7
7
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
@@ -99,7 +99,6 @@ interface AlertsCopy {
99
99
  jobAlertButtonText: string;
100
100
  subscribingLabel: string;
101
101
  jobAlertSuccessToast: string;
102
- jobAlertDuplicateToast: string;
103
102
  jobAlertErrorToast: string;
104
103
  sectionAriaLabel: string;
105
104
  emailAriaLabel: string;
@@ -99,7 +99,6 @@ interface AlertsCopy {
99
99
  jobAlertButtonText: string;
100
100
  subscribingLabel: string;
101
101
  jobAlertSuccessToast: string;
102
- jobAlertDuplicateToast: string;
103
102
  jobAlertErrorToast: string;
104
103
  sectionAriaLabel: string;
105
104
  emailAriaLabel: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "1.34.0",
3
+ "version": "1.35.1",
4
4
  "description": "Typed isomorphic client for the Cavuno Board API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -49,7 +49,6 @@ import { createBoardClient } from '@cavuno/board';
49
49
  // Browser/SPA: 'local' persists the pair across tabs + reloads via
50
50
  // localStorage; 'session' scopes it to the tab; 'memory' drops it on reload.
51
51
  const board = createBoardClient({
52
- baseUrl: 'https://api.cavuno.com',
53
52
  board: 'pk_a8f3...',
54
53
  auth: { storage: 'local' },
55
54
  });
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cavuno-board-client
3
- description: Create and configure the @cavuno/board client — baseUrl and the pk_ board identifier, global headers, request/response hooks, per-call FetchOptions caching passthrough, the client.fetch escape hatch, and the rule that keeps one shared instance safe under SSR.
3
+ description: Create and configure the @cavuno/board client — the pk_ board identifier, optional API-origin override, global headers, hooks, per-call FetchOptions, and the rule that keeps one shared instance safe under SSR.
4
4
  ---
5
5
 
6
6
  # The Board API client
@@ -23,7 +23,6 @@ description: Create and configure the @cavuno/board client — baseUrl and the p
23
23
  import { createBoardClient } from '@cavuno/board';
24
24
 
25
25
  const board = createBoardClient({
26
- baseUrl: 'https://api.cavuno.com',
27
26
  board: 'pk_a8f3...', // pk_ key (preferred) | boards_ id | slug
28
27
  });
29
28
  ```
@@ -34,7 +33,6 @@ Use the `pk_…` publishable key for `board`, not the slug: the slug is operator
34
33
 
35
34
  ```ts no-check
36
35
  const board = createBoardClient({
37
- baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
38
36
  board: process.env.PUBLIC_CAVUNO_BOARD!,
39
37
  globalHeaders: { 'Accept-Language': 'en' },
40
38
  onRequest: async (req) => req, // mutate/replace the request (locale, URL rewrite)
@@ -69,7 +67,6 @@ import { createBoardClient } from '@cavuno/board';
69
67
 
70
68
  // Safe: shared, stateless. Per-user token rides per call.
71
69
  export const board = createBoardClient({
72
- baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
73
70
  board: process.env.PUBLIC_CAVUNO_BOARD!,
74
71
  });
75
72
  ```
@@ -64,6 +64,46 @@ deliberately offers 5 of the 7 wire values (`volunteer`/`other` exist on
64
64
  jobs but are not filter options). `JOB_SORTS` deliberately excludes
65
65
  `oldest` (ADR-0048); `relevance` is the featured-ranked default.
66
66
 
67
+ ## Load category, skill, and keyword options
68
+
69
+ Use the Board API taxonomy collections for filter/autocomplete options. Items
70
+ are backed by published jobs on this board and already carry the board-language
71
+ display name and canonical URL slug. Keep `sourceSlug` for job filtering and
72
+ `canonicalSlug` for links.
73
+
74
+ ```ts snippet
75
+ const categories = await board.taxonomy.categories.list({ limit: 50 });
76
+ const skills = await board.taxonomy.skills.list({ limit: 50 });
77
+ const suggestions = await board.taxonomy.suggestions.list({
78
+ q: searchText,
79
+ limit: 10,
80
+ });
81
+
82
+ const categoryOptions = categories.data.map((term) => ({
83
+ label: term.displayName,
84
+ filterValue: term.sourceSlug,
85
+ href: `/jobs/${term.canonicalSlug}`,
86
+ }));
87
+ ```
88
+
89
+ Category and skill lists accept `q`, `limit` (1–100), and the opaque
90
+ `cursor` request field. Pass the previous response's `nextCursor` into it:
91
+
92
+ ```ts snippet
93
+ const first = await board.taxonomy.categories.list({ limit: 50 });
94
+ const second = first.nextCursor
95
+ ? await board.taxonomy.categories.list({
96
+ limit: 50,
97
+ cursor: first.nextCursor,
98
+ })
99
+ : null;
100
+ ```
101
+
102
+ Suggestions accept `q` and `limit`;
103
+ a present query under two characters returns no suggestions. Suggestions contain
104
+ categories and skills only. A category and skill may legitimately share a slug,
105
+ so use `type + canonicalSlug` as the option identity.
106
+
67
107
  ## Anti-patterns
68
108
 
69
109
  ```ts no-check
@@ -76,9 +116,9 @@ board.jobs.list({ seniority: rawSearch.seniority });
76
116
 
77
117
  ## Out of scope — do not invent exports
78
118
 
79
- No URL BUILDER (routes are app-owned — serialize with your router), no
80
- category/skill/location taxonomy lists (those come from the API:
81
- `board.taxonomy.*`, `jobs.list` filters), no saved-filter persistence.
119
+ No URL BUILDER (routes are app-owned — serialize with your router), no location
120
+ taxonomy vocabulary (locations come from `board.taxonomy.places`), no
121
+ saved-filter persistence.
82
122
 
83
123
  ## Verify
84
124
 
@@ -28,17 +28,16 @@ Only the methods and fields shown here exist. There is no pause/suspend: deactiv
28
28
  const sub = await board.jobAlerts.subscribe({
29
29
  email: 'ada@example.com',
30
30
  consent: true,
31
- frequency: 'weekly', // 'daily' | 'weekly'
31
+ frequency: 'weekly', // the only supported cadence
32
32
  filters: { jobFunctions: ['engineering'], remoteOptions: ['remote'], placeSlugs: ['berlin'] },
33
33
  });
34
- sub.status; // 'created' | 'duplicate'
35
- sub.requiresConfirmation; // true when the opt-in email was sent
34
+ sub.status; // always 'submitted' uniform, never reveals new vs. already-subscribed
36
35
 
37
36
  // Confirm page — token from the email link:
38
37
  const res = await board.jobAlerts.confirm({ token });
39
38
  res.status; // 'confirmed' | 'already_confirmed' | 'expired' | 'not_found'
40
39
  if (res.status === 'expired') {
41
- await board.jobAlerts.resendConfirmation({ email }); // status: 'sent' | 'throttled' | ...
40
+ await board.jobAlerts.resendConfirmation({ email }); // status: always 'submitted'
42
41
  }
43
42
  ```
44
43
 
@@ -67,7 +66,7 @@ await board.jobAlerts.updatePreference({
67
66
  subscriptionId,
68
67
  preferenceId: pref.id,
69
68
  token,
70
- frequency: 'daily', // required — restate even if unchanged
69
+ frequency: 'weekly', // required — restate even if unchanged
71
70
  filters: pref.filters, // round-trip stored filters you aren't editing
72
71
  });
73
72
  ```
@@ -94,7 +93,7 @@ await board.me.alerts.remove(alert.id); // 204 → void; the only way to stop an
94
93
  ```ts snippet
95
94
  const current = await board.me.alerts.retrieve(alertId);
96
95
  await board.me.alerts.update(alertId, {
97
- frequency: 'daily', // the one edit
96
+ frequency: 'weekly',
98
97
  jobFunctions: current.filters.jobFunctions,
99
98
  seniorityLevels: current.filters.seniorityLevels,
100
99
  remoteOptions: current.filters.remoteOptions,
@@ -107,9 +106,10 @@ await board.me.alerts.update(alertId, {
107
106
 
108
107
  ## Verify
109
108
 
110
- - [ ] Subscribe form sends `consent: true` and handles `status: 'duplicate'` without erroring.
109
+ - [ ] Subscribe form sends `consent: true` and shows a uniform "check your email" message on `status: 'submitted'` (never reveals already-subscribed).
111
110
  - [ ] Confirm page branches on all four statuses (including `expired` → resend).
112
111
  - [ ] Manage page reads `subscription` + `token` from the email link's query string; writes send `subscriptionId` in the body.
113
112
  - [ ] Editing one preference field leaves the others intact (full-replace round-trip, both surfaces).
114
113
  - [ ] Alert UI only promises filtering on job function / place / remote option.
114
+ - [ ] Alert UI offers weekly cadence only and never sends a legacy daily value.
115
115
  - [ ] Alert surfaces are hidden when `features.jobAlerts` is false.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cavuno-board-job-posting
3
- description: Build the anonymous "Post a job" funnel with the @cavuno/board SDK — jobPosting.plans, create and its status-discriminated JobPostingResult (checkout / published / pending_approval / invoice_sent), logo upload + fetchLogoByDomain, and the email-verified billing helpers (checkBilling, sendBillingVerification, getBillingOptions, checkSubscriptionEntitlements).
3
+ description: Build the anonymous "Post a job" funnel with the @cavuno/board SDK — jobPosting.plans, create and its status-discriminated JobPostingResult (checkout / published / pending_approval / invoice_sent), logo upload + fetchLogoByDomain, and the email-verified billing helpers (sendBillingVerification, getBillingOptions).
4
4
  ---
5
5
 
6
6
  # Job posting: the anonymous submission funnel
@@ -87,16 +87,15 @@ The five billing paths map onto those four statuses: paid-Stripe → `checkout`;
87
87
 
88
88
  ## Existing credit: the verified-email branch
89
89
 
90
- A submitter whose email already holds a plan/bundle can post against remaining capacity — no new charge. Verify the email first; an option's fields become `selectedBilling`:
90
+ A submitter whose email already holds a plan/bundle can post against remaining capacity — no new charge. Verification is the gate: there is deliberately no pre-check by bare email (that would let anyone probe whether an address holds billing). Send the verification email, then read the options with the token; an option's fields become `selectedBilling`:
91
91
 
92
92
  ```ts snippet
93
- const { hasActiveBilling } = await board.jobPosting.checkBilling({ email });
94
- if (hasActiveBilling) {
95
- await board.jobPosting.sendBillingVerification({ email }); // emails a token
96
- // …the poster pastes/clicks the token…
97
- const { options } = await board.jobPosting.getBillingOptions({
98
- verificationToken,
99
- });
93
+ await board.jobPosting.sendBillingVerification({ email }); // emails a token
94
+ // …the poster pastes/clicks the token…
95
+ const { options } = await board.jobPosting.getBillingOptions({
96
+ verificationToken,
97
+ });
98
+ if (options.length > 0) {
100
99
  const option = options[0]!;
101
100
  option.jobsRemaining; // capacity left
102
101
  option.featuredRemaining; // featured slots left
@@ -109,19 +108,6 @@ if (hasActiveBilling) {
109
108
  }
110
109
  ```
111
110
 
112
- For subscription holders, gate the "featured" toggle on entitlements:
113
-
114
- ```ts snippet
115
- const ent = await board.jobPosting.checkSubscriptionEntitlements({
116
- email,
117
- planId: plan.id,
118
- });
119
- ent.hasSubscription;
120
- ent.canFeature; // may the wizard offer isFeatured?
121
- ent.featuredSlotsRemaining; // and how many
122
- ent.maxActiveRemaining;
123
- ```
124
-
125
111
  ## Verify
126
112
 
127
113
  - [ ] Every `create` call site handles all four `result.status` variants — no fallthrough that assumes `published`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cavuno-board-jobs
3
- description: Browse, search, and render jobs with the @cavuno/board SDK — jobs.list, jobs.search, jobs.retrieve, jobs.similar. Covers the slim card vs full job shapes, storefront pagination (count/limit/offset + opaque cursor), filters, and the candidate-paywall gatedCount.
3
+ description: Browse, search, and render jobs with the @cavuno/board SDK — jobs.list, jobs.search, jobs.retrieve, jobs.similar. Covers the slim card vs full job shapes, catalog pagination (count/limit/offset + opaque cursor), filters, and the candidate-paywall gatedCount.
4
4
  ---
5
5
 
6
6
  # Jobs: browse, search, detail
@@ -19,7 +19,7 @@ The highest-traffic surface. Listing and search return slim `PublicJobCard`s; th
19
19
 
20
20
  ## List and render cards
21
21
 
22
- `jobs.list` returns a `JobCardListEnvelope`: storefront pagination fields plus `data: PublicJobCard[]` and optional `relatedSearches`.
22
+ `jobs.list` returns a `JobCardListEnvelope`: catalog pagination fields plus `data: PublicJobCard[]` and optional `relatedSearches`.
23
23
 
24
24
  ```ts snippet
25
25
  const page = await board.jobs.list({ limit: 20, seniority: ['senior', 'lead'] });
@@ -32,14 +32,13 @@ Read `package.json` and the project layout before writing anything. Identify: th
32
32
  - `next` → use Server Components + per-call `FetchOptions` (`next: { revalidate, tags }`).
33
33
  - Anything else (Nuxt, SvelteKit, Astro, SolidStart, plain JS) → use the core skills directly; the SDK surface is identical.
34
34
 
35
- ## Use standard environment names
35
+ ## Use the standard board environment name
36
36
 
37
- Read these two values from the environment; never hard-code them:
37
+ Read the board key from the environment; never hard-code it:
38
38
 
39
- - `PUBLIC_CAVUNO_API_URL` — the API base, e.g. `https://api.cavuno.com`.
40
39
  - `PUBLIC_CAVUNO_BOARD` — the board identifier. Use the `pk_…` publishable key, not the slug (the slug is operator-mutable and breaks deployed frontends on rename).
41
40
 
42
- Both are public-safe (the `pk_…` key is client-safe by design). Use your framework's public-env convention for the variable name (`VITE_`, `PUBLIC_`, `NEXT_PUBLIC_`); the values are the same.
41
+ The `pk_…` key is public-safe by design. Use your framework's public-env convention for the variable name (`VITE_`, `PUBLIC_`, `NEXT_PUBLIC_`). The SDK uses `https://api.cavuno.com` by default. Pass `baseUrl` only when Cavuno supplies a staging or development origin.
43
42
 
44
43
  ## Keep credentials server-side
45
44
 
@@ -57,7 +56,6 @@ Create one client and reuse it. See `cavuno-board-client`.
57
56
  import { createBoardClient } from '@cavuno/board';
58
57
 
59
58
  export const board = createBoardClient({
60
- baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
61
59
  board: process.env.PUBLIC_CAVUNO_BOARD!,
62
60
  });
63
61
  ```
@@ -68,7 +66,7 @@ export const board = createBoardClient({
68
66
 
69
67
  ## Build jobs browsing + detail
70
68
 
71
- The core surface. `jobs.list` / `jobs.search` for listing pages, `jobs.retrieve` for the detail page, `jobs.similar` for the related rail. Honor storefront pagination and the candidate-paywall `gatedCount`; use `paginate()` for full-catalog walks (sitemaps, feeds). See `cavuno-board-jobs`.
69
+ The core surface. `jobs.list` / `jobs.search` for listing pages, `jobs.retrieve` for the detail page, `jobs.similar` for the related rail. Honor catalog pagination and the candidate-paywall `gatedCount`; use `paginate()` for full-catalog walks (sitemaps, feeds). See `cavuno-board-jobs`.
72
70
 
73
71
  ## Add board users + saved jobs
74
72
 
@@ -85,7 +83,7 @@ hand-rolling (each has a skill):
85
83
 
86
84
  - Salary/date/label formatting in the board language → `cavuno-board-format`
87
85
  - Listing-filter vocabulary + URL param parsing → `cavuno-board-filters`
88
- - Board theme → shadcn CSS variables + fonts `cavuno-board-theme`
86
+ - Optional hosted-board theme compatibility `cavuno-board-theme`. By default, the consuming application owns tokens, fonts, and color mode.
89
87
 
90
88
  ## Build the remaining surfaces per feature flag
91
89
 
@@ -117,4 +115,4 @@ Then run the `cavuno-board-smoke-test` skill against your `pk_…` — type chec
117
115
 
118
116
  ## Stop conditions
119
117
 
120
- Stop and ask the human when: no `pk_…` board identifier or API URL is available; the framework is unrecognized and has no server boundary for secrets; or a surface you need (e.g. job alerts, applications) has no corresponding skill yet — the SDK only exposes endpoints that are live, so a missing skill means the endpoint isn't shipped.
118
+ Stop and ask the human when: no `pk_…` board identifier is available; the framework is unrecognized and has no server boundary for credentials; or a surface you need (e.g. job alerts, applications) has no corresponding skill yet — the SDK only exposes endpoints that are live, so a missing skill means the endpoint isn't shipped.
@@ -18,7 +18,7 @@ auth, and SEO surfaces behave. Verify at runtime, in this order.
18
18
  ## 0 — Run `doctor` first (deterministic pass)
19
19
 
20
20
  ```bash
21
- PUBLIC_CAVUNO_API_URL=... PUBLIC_CAVUNO_BOARD=pk_... \
21
+ PUBLIC_CAVUNO_BOARD=pk_... \
22
22
  npx @cavuno/board doctor --frontend http://localhost:3000
23
23
  ```
24
24
 
@@ -45,21 +45,22 @@ board; wait a few minutes.
45
45
 
46
46
  ## 1 — Probe the API directly (before blaming app code)
47
47
 
48
- Use the real env values the app reads (`PUBLIC_CAVUNO_API_URL`,
49
- `PUBLIC_CAVUNO_BOARD`). Expected outputs are exact.
48
+ Use the real `PUBLIC_CAVUNO_BOARD` value the app reads. The SDK and doctor use `https://api.cavuno.com` by default; set `PUBLIC_CAVUNO_API_URL` only for a Cavuno-supplied non-production override. Expected outputs are exact.
50
49
 
51
50
  ```bash
51
+ CAVUNO_API_URL="${PUBLIC_CAVUNO_API_URL:-https://api.cavuno.com}"
52
+
52
53
  # Board context: MUST return JSON with "object": "public_board" and your
53
54
  # board's name — not an HTML error page, not {"error":{"code":"boards_not_found"}}.
54
- curl -s "$PUBLIC_CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD" | head -c 300
55
+ curl -s "$CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD" | head -c 300
55
56
 
56
57
  # Jobs list: MUST return {"object":"list", ... "data":[...]}. An empty data
57
58
  # array on a board you know has jobs means the wrong board identifier.
58
- curl -s "$PUBLIC_CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD/jobs?limit=2" | head -c 300
59
+ curl -s "$CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD/jobs?limit=2" | head -c 300
59
60
 
60
61
  # Error envelope: a bogus job slug MUST return the v1 error shape with
61
62
  # "code":"jobs_not_found" — anything else means a proxy is rewriting responses.
62
- curl -s "$PUBLIC_CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD/jobs/definitely-not-a-job"
63
+ curl -s "$CAVUNO_API_URL/v1/boards/$PUBLIC_CAVUNO_BOARD/jobs/definitely-not-a-job"
63
64
  ```
64
65
 
65
66
  On a password-protected board, every content read above returns 401 with
@@ -89,7 +90,7 @@ Start the app (dev is fine for this step) and probe its own routes:
89
90
  (idempotent), not a duplicate.
90
91
  - **Paywall** (gated boards): anonymous list shows the `gatedCount` upsell;
91
92
  an entitled login makes the same URL return the ungated view.
92
- - **Alerts**: subscribe → `status: "created"`; repeat → `"duplicate"`.
93
+ - **Alerts**: subscribe → `status: "submitted"`; repeat → same uniform `"submitted"` (never reveals already-subscribed).
93
94
 
94
95
  ## 4 — Production build (mandatory)
95
96