@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.
- package/README.md +7 -8
- package/dist/bin.mjs +25 -10
- package/dist/{board-Id19Yg8-.d.ts → board-Z7hjKDp8.d.ts} +1 -1
- package/dist/{board-DgDC0T4g.d.mts → board-t_WltBBa.d.mts} +1 -1
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/filters.js +3 -6
- package/dist/filters.mjs +3 -6
- package/dist/format.d.mts +4 -4
- package/dist/format.d.ts +4 -4
- package/dist/format.js +3 -6
- package/dist/format.mjs +3 -6
- package/dist/index.d.mts +52 -43
- package/dist/index.d.ts +52 -43
- package/dist/index.js +27 -19
- package/dist/index.mjs +27 -19
- package/dist/{jobs-IIJtDgzX.d.mts → jobs-Dq2a9oPj.d.mts} +742 -61
- package/dist/{jobs-IIJtDgzX.d.ts → jobs-Dq2a9oPj.d.ts} +742 -61
- package/dist/{salaries-CL_00fNX.d.ts → salaries-CTin-18R.d.ts} +4 -4
- package/dist/{salaries-BfEEEOHj.d.mts → salaries-CWg82dOz.d.mts} +4 -4
- package/dist/seo.d.mts +4 -4
- package/dist/seo.d.ts +4 -4
- package/dist/seo.js +3 -6
- package/dist/seo.mjs +3 -6
- package/dist/server.d.mts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sitemap.d.mts +3 -3
- package/dist/sitemap.d.ts +3 -3
- package/dist/{ui-copy-rlfoH9P3.d.mts → ui-copy-CKfFTtLk.d.mts} +0 -1
- package/dist/{ui-copy-rlfoH9P3.d.ts → ui-copy-CKfFTtLk.d.ts} +0 -1
- package/package.json +1 -1
- package/skills/cavuno-board-auth/SKILL.md +0 -1
- package/skills/cavuno-board-client/SKILL.md +1 -4
- package/skills/cavuno-board-filters/SKILL.md +43 -3
- package/skills/cavuno-board-job-alerts/SKILL.md +7 -7
- package/skills/cavuno-board-job-posting/SKILL.md +8 -22
- package/skills/cavuno-board-jobs/SKILL.md +2 -2
- package/skills/cavuno-board-setup/SKILL.md +6 -8
- package/skills/cavuno-board-smoke-test/SKILL.md +8 -7
- package/skills/cavuno-board-theme/SKILL.md +33 -45
- package/skills/flavors/tanstack-start/SKILL.md +0 -1
- package/skills/manifest.json +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as Schemas, L as ListEnvelope,
|
|
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
|
-
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
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-
|
|
2
|
-
import { P as PublicBoard } from './board-
|
|
3
|
-
import { P as PublicJob } from './jobs-
|
|
4
|
-
import { B as BoardLabelOverrides } from './ui-copy-
|
|
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-
|
|
2
|
-
import { P as PublicBoard } from './board-
|
|
3
|
-
import { P as PublicJob } from './jobs-
|
|
4
|
-
import { B as BoardLabelOverrides } from './ui-copy-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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-
|
|
3
|
-
import './board-
|
|
4
|
-
import './salaries-
|
|
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-
|
|
3
|
-
import './board-
|
|
4
|
-
import './salaries-
|
|
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
|
package/dist/sitemap.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BoardSdk } from './index.mjs';
|
|
2
|
-
import './jobs-
|
|
3
|
-
import './board-
|
|
4
|
-
import './salaries-
|
|
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-
|
|
3
|
-
import './board-
|
|
4
|
-
import './salaries-
|
|
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
|
package/package.json
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
81
|
-
|
|
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', //
|
|
31
|
+
frequency: 'weekly', // the only supported cadence
|
|
32
32
|
filters: { jobFunctions: ['engineering'], remoteOptions: ['remote'], placeSlugs: ['berlin'] },
|
|
33
33
|
});
|
|
34
|
-
sub.status; // '
|
|
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:
|
|
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: '
|
|
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: '
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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,
|
|
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`:
|
|
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
|
|
35
|
+
## Use the standard board environment name
|
|
36
36
|
|
|
37
|
-
Read
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 "$
|
|
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 "$
|
|
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 "$
|
|
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: "
|
|
93
|
+
- **Alerts**: subscribe → `status: "submitted"`; repeat → same uniform `"submitted"` (never reveals already-subscribed).
|
|
93
94
|
|
|
94
95
|
## 4 — Production build (mandatory)
|
|
95
96
|
|