@cavuno/board 1.33.1 → 1.35.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.
- package/README.md +7 -8
- package/dist/bin.mjs +91 -21
- package/dist/{board-BTVapQiL.d.mts → board-Be8dY9Ba.d.mts} +1 -1
- package/dist/{board-0yI5ZRJw.d.ts → board-DsRbgz_D.d.ts} +1 -1
- package/dist/filters.d.mts +1 -1
- package/dist/filters.d.ts +1 -1
- package/dist/format.d.mts +2 -2
- package/dist/format.d.ts +2 -2
- package/dist/index.d.mts +32 -16
- package/dist/index.d.ts +32 -16
- package/dist/index.js +6 -3
- package/dist/index.mjs +6 -3
- package/dist/{jobs-Di4AV-02.d.mts → jobs-BFLMDNEN.d.mts} +194 -29
- package/dist/{jobs-Di4AV-02.d.ts → jobs-BFLMDNEN.d.ts} +194 -29
- package/dist/paths.d.mts +72 -0
- package/dist/paths.d.ts +72 -0
- package/dist/paths.js +99 -0
- package/dist/paths.mjs +78 -0
- package/dist/{salaries-B-zJKjkk.d.ts → salaries-negE75t8.d.ts} +4 -4
- package/dist/{salaries-D-BGZpDC.d.mts → salaries-pO_vGORE.d.mts} +4 -4
- package/dist/seo.d.mts +3 -3
- package/dist/seo.d.ts +3 -3
- 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/sitemap.js +89 -29
- package/dist/sitemap.mjs +89 -29
- package/package.json +11 -1
- package/skills/cavuno-board-auth/SKILL.md +0 -1
- package/skills/cavuno-board-client/SKILL.md +1 -4
- package/skills/cavuno-board-job-alerts/SKILL.md +4 -3
- 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 +7 -6
- package/skills/cavuno-board-theme/SKILL.md +33 -45
- package/skills/flavors/tanstack-start/SKILL.md +0 -1
- package/skills/manifest.json +4 -4
package/dist/sitemap.mjs
CHANGED
|
@@ -128,6 +128,65 @@ function paginate(listFn, query, options) {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// src/paths/index.ts
|
|
132
|
+
function jobDetailPath(companySlug, jobSlug) {
|
|
133
|
+
return `/companies/${companySlug}/jobs/${jobSlug}`;
|
|
134
|
+
}
|
|
135
|
+
function jobsCategoryPath(categorySlug) {
|
|
136
|
+
return `/jobs/${categorySlug}`;
|
|
137
|
+
}
|
|
138
|
+
function jobsSkillPath(skillSlug) {
|
|
139
|
+
return `/jobs/skills/${skillSlug}`;
|
|
140
|
+
}
|
|
141
|
+
function jobsLocationPath(placeSlug) {
|
|
142
|
+
return `/jobs/locations/${placeSlug}`;
|
|
143
|
+
}
|
|
144
|
+
function companyPath(companySlug) {
|
|
145
|
+
return `/companies/${companySlug}`;
|
|
146
|
+
}
|
|
147
|
+
function companyMarketPath(marketSlug) {
|
|
148
|
+
return `/companies/markets/${marketSlug}`;
|
|
149
|
+
}
|
|
150
|
+
function companySalaryPath(companySlug) {
|
|
151
|
+
return `/companies/${companySlug}/salaries`;
|
|
152
|
+
}
|
|
153
|
+
function salaryTitlePath(titleSlug) {
|
|
154
|
+
return `/salaries/titles/${titleSlug}`;
|
|
155
|
+
}
|
|
156
|
+
function salarySkillPath(skillSlug) {
|
|
157
|
+
return `/salaries/skills/${skillSlug}`;
|
|
158
|
+
}
|
|
159
|
+
function salaryLocationPath(placeSlug) {
|
|
160
|
+
return `/salaries/locations/${placeSlug}`;
|
|
161
|
+
}
|
|
162
|
+
function blogPostPath(postSlug) {
|
|
163
|
+
return `/blog/${postSlug}`;
|
|
164
|
+
}
|
|
165
|
+
function blogTagPath(tagSlug) {
|
|
166
|
+
return `/blog/tag/${tagSlug}`;
|
|
167
|
+
}
|
|
168
|
+
function blogAuthorPath(authorSlug) {
|
|
169
|
+
return `/blog/author/${authorSlug}`;
|
|
170
|
+
}
|
|
171
|
+
var BOARD_PATHS = {
|
|
172
|
+
home: "/",
|
|
173
|
+
jobs: "/jobs",
|
|
174
|
+
companies: "/companies",
|
|
175
|
+
salaries: "/salaries",
|
|
176
|
+
salaryCompanies: "/salaries/companies",
|
|
177
|
+
salaryTitles: "/salaries/titles",
|
|
178
|
+
salarySkills: "/salaries/skills",
|
|
179
|
+
salaryLocations: "/salaries/locations",
|
|
180
|
+
blog: "/blog",
|
|
181
|
+
about: "/about",
|
|
182
|
+
privacyPolicy: "/privacy-policy",
|
|
183
|
+
termsOfService: "/terms-of-service",
|
|
184
|
+
cookiePolicy: "/cookie-policy",
|
|
185
|
+
impressum: "/impressum",
|
|
186
|
+
talent: "/talent",
|
|
187
|
+
employers: "/employers"
|
|
188
|
+
};
|
|
189
|
+
|
|
131
190
|
// src/sitemap/walker.ts
|
|
132
191
|
var MIN_JOBS_PER_INDEXED_PAGE = 5;
|
|
133
192
|
var MAX_PAGES = 200;
|
|
@@ -210,16 +269,16 @@ async function buildBucketUrls(board, origin, bucket) {
|
|
|
210
269
|
async function marketing(board, origin) {
|
|
211
270
|
const { features } = await board.context();
|
|
212
271
|
const urls = [
|
|
213
|
-
`${origin}
|
|
214
|
-
`${origin}
|
|
215
|
-
`${origin}
|
|
216
|
-
`${origin}
|
|
217
|
-
`${origin}
|
|
218
|
-
`${origin}
|
|
272
|
+
`${origin}${BOARD_PATHS.home}`,
|
|
273
|
+
`${origin}${BOARD_PATHS.jobs}`,
|
|
274
|
+
`${origin}${BOARD_PATHS.about}`,
|
|
275
|
+
`${origin}${BOARD_PATHS.privacyPolicy}`,
|
|
276
|
+
`${origin}${BOARD_PATHS.termsOfService}`,
|
|
277
|
+
`${origin}${BOARD_PATHS.cookiePolicy}`
|
|
219
278
|
];
|
|
220
|
-
if (features.impressum) urls.push(`${origin}
|
|
221
|
-
if (features.talentDirectory) urls.push(`${origin}
|
|
222
|
-
if (features.employers) urls.push(`${origin}
|
|
279
|
+
if (features.impressum) urls.push(`${origin}${BOARD_PATHS.impressum}`);
|
|
280
|
+
if (features.talentDirectory) urls.push(`${origin}${BOARD_PATHS.talent}`);
|
|
281
|
+
if (features.employers) urls.push(`${origin}${BOARD_PATHS.employers}`);
|
|
223
282
|
return urls;
|
|
224
283
|
}
|
|
225
284
|
async function jobsTaxonomy(board, origin, kind) {
|
|
@@ -231,20 +290,20 @@ async function jobsTaxonomy(board, origin, kind) {
|
|
|
231
290
|
);
|
|
232
291
|
for (const slug of slugs) counts.set(slug, (counts.get(slug) ?? 0) + 1);
|
|
233
292
|
}
|
|
234
|
-
const
|
|
235
|
-
return [...counts.entries()].filter(([, n]) => n >= MIN_JOBS_PER_INDEXED_PAGE).map(([slug]) => slug).sort().map((slug) => `${
|
|
293
|
+
const toPath = kind === "categories" ? jobsCategoryPath : jobsSkillPath;
|
|
294
|
+
return [...counts.entries()].filter(([, n]) => n >= MIN_JOBS_PER_INDEXED_PAGE).map(([slug]) => slug).sort().map((slug) => `${origin}${toPath(slug)}`);
|
|
236
295
|
}
|
|
237
296
|
async function jobsLocations(board, origin) {
|
|
238
297
|
const { language } = await board.context();
|
|
239
298
|
const { data } = await board.salaries.locations.list({ locale: language });
|
|
240
|
-
return data.filter((l) => l.jobCount >= MIN_JOBS_PER_INDEXED_PAGE).map((l) => l.placeSlug).sort().map((slug) => `${origin}
|
|
299
|
+
return data.filter((l) => l.jobCount >= MIN_JOBS_PER_INDEXED_PAGE).map((l) => l.placeSlug).sort().map((slug) => `${origin}${jobsLocationPath(slug)}`);
|
|
241
300
|
}
|
|
242
301
|
async function jobDetails(board, origin) {
|
|
243
302
|
const jobs = await enumerateJobs(board);
|
|
244
303
|
const seen = /* @__PURE__ */ new Set();
|
|
245
304
|
for (const job of jobs) {
|
|
246
305
|
if (!job.company?.slug || !job.slug) continue;
|
|
247
|
-
seen.add(`${origin}
|
|
306
|
+
seen.add(`${origin}${jobDetailPath(job.company.slug, job.slug)}`);
|
|
248
307
|
}
|
|
249
308
|
return [...seen].sort();
|
|
250
309
|
}
|
|
@@ -253,10 +312,11 @@ async function companies(board, origin) {
|
|
|
253
312
|
enumerateCompanies(board),
|
|
254
313
|
board.companies.markets()
|
|
255
314
|
]);
|
|
256
|
-
const urls = [`${origin}
|
|
257
|
-
for (const company of list)
|
|
315
|
+
const urls = [`${origin}${BOARD_PATHS.companies}`];
|
|
316
|
+
for (const company of list)
|
|
317
|
+
urls.push(`${origin}${companyPath(company.slug)}`);
|
|
258
318
|
for (const market of markets.data) {
|
|
259
|
-
urls.push(`${origin}
|
|
319
|
+
urls.push(`${origin}${companyMarketPath(market.slug)}`);
|
|
260
320
|
}
|
|
261
321
|
return urls;
|
|
262
322
|
}
|
|
@@ -271,27 +331,27 @@ async function salaries(board, origin) {
|
|
|
271
331
|
if (!salaryCompanies.data.length && !titles.data.length && !skills.data.length && !locations.data.length) {
|
|
272
332
|
return [];
|
|
273
333
|
}
|
|
274
|
-
const urls = [`${origin}
|
|
334
|
+
const urls = [`${origin}${BOARD_PATHS.salaries}`];
|
|
275
335
|
if (salaryCompanies.data.length) {
|
|
276
|
-
urls.push(`${origin}
|
|
336
|
+
urls.push(`${origin}${BOARD_PATHS.salaryCompanies}`);
|
|
277
337
|
for (const c of salaryCompanies.data) {
|
|
278
|
-
urls.push(`${origin}
|
|
338
|
+
urls.push(`${origin}${companySalaryPath(c.companySlug)}`);
|
|
279
339
|
}
|
|
280
340
|
}
|
|
281
341
|
if (titles.data.length) {
|
|
282
|
-
urls.push(`${origin}
|
|
342
|
+
urls.push(`${origin}${BOARD_PATHS.salaryTitles}`);
|
|
283
343
|
for (const t of titles.data)
|
|
284
|
-
urls.push(`${origin}
|
|
344
|
+
urls.push(`${origin}${salaryTitlePath(t.slug)}`);
|
|
285
345
|
}
|
|
286
346
|
if (skills.data.length) {
|
|
287
|
-
urls.push(`${origin}
|
|
347
|
+
urls.push(`${origin}${BOARD_PATHS.salarySkills}`);
|
|
288
348
|
for (const s of skills.data)
|
|
289
|
-
urls.push(`${origin}
|
|
349
|
+
urls.push(`${origin}${salarySkillPath(s.slug)}`);
|
|
290
350
|
}
|
|
291
351
|
if (locations.data.length) {
|
|
292
|
-
urls.push(`${origin}
|
|
352
|
+
urls.push(`${origin}${BOARD_PATHS.salaryLocations}`);
|
|
293
353
|
for (const l of locations.data) {
|
|
294
|
-
urls.push(`${origin}
|
|
354
|
+
urls.push(`${origin}${salaryLocationPath(l.placeSlug)}`);
|
|
295
355
|
}
|
|
296
356
|
}
|
|
297
357
|
return urls;
|
|
@@ -300,18 +360,18 @@ async function blog(board, origin) {
|
|
|
300
360
|
const posts = await drainPages(
|
|
301
361
|
paginate(board.blog.posts.list, { limit: 100 }).pages()
|
|
302
362
|
);
|
|
303
|
-
const urls = [`${origin}
|
|
363
|
+
const urls = [`${origin}${BOARD_PATHS.blog}`];
|
|
304
364
|
const tagSlugs = /* @__PURE__ */ new Set();
|
|
305
365
|
const authorSlugs = /* @__PURE__ */ new Set();
|
|
306
366
|
for (const post of posts) {
|
|
307
|
-
urls.push(`${origin}
|
|
367
|
+
urls.push(`${origin}${blogPostPath(post.slug)}`);
|
|
308
368
|
for (const tag of post.tags) tagSlugs.add(tag.slug);
|
|
309
369
|
for (const author of post.authors) authorSlugs.add(author.slug);
|
|
310
370
|
}
|
|
311
371
|
for (const slug of [...tagSlugs].sort())
|
|
312
|
-
urls.push(`${origin}
|
|
372
|
+
urls.push(`${origin}${blogTagPath(slug)}`);
|
|
313
373
|
for (const slug of [...authorSlugs].sort()) {
|
|
314
|
-
urls.push(`${origin}
|
|
374
|
+
urls.push(`${origin}${blogAuthorPath(slug)}`);
|
|
315
375
|
}
|
|
316
376
|
return urls;
|
|
317
377
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavuno/board",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Typed isomorphic client for the Cavuno Board API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -80,6 +80,16 @@
|
|
|
80
80
|
"default": "./dist/sitemap.js"
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
|
+
"./paths": {
|
|
84
|
+
"import": {
|
|
85
|
+
"types": "./dist/paths.d.mts",
|
|
86
|
+
"default": "./dist/paths.mjs"
|
|
87
|
+
},
|
|
88
|
+
"require": {
|
|
89
|
+
"types": "./dist/paths.d.ts",
|
|
90
|
+
"default": "./dist/paths.js"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
83
93
|
"./server": {
|
|
84
94
|
"import": {
|
|
85
95
|
"types": "./dist/server.d.mts",
|
|
@@ -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
|
```
|
|
@@ -28,7 +28,7 @@ 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
34
|
sub.status; // 'created' | 'duplicate'
|
|
@@ -67,7 +67,7 @@ await board.jobAlerts.updatePreference({
|
|
|
67
67
|
subscriptionId,
|
|
68
68
|
preferenceId: pref.id,
|
|
69
69
|
token,
|
|
70
|
-
frequency: '
|
|
70
|
+
frequency: 'weekly', // required — restate even if unchanged
|
|
71
71
|
filters: pref.filters, // round-trip stored filters you aren't editing
|
|
72
72
|
});
|
|
73
73
|
```
|
|
@@ -94,7 +94,7 @@ await board.me.alerts.remove(alert.id); // 204 → void; the only way to stop an
|
|
|
94
94
|
```ts snippet
|
|
95
95
|
const current = await board.me.alerts.retrieve(alertId);
|
|
96
96
|
await board.me.alerts.update(alertId, {
|
|
97
|
-
frequency: '
|
|
97
|
+
frequency: 'weekly',
|
|
98
98
|
jobFunctions: current.filters.jobFunctions,
|
|
99
99
|
seniorityLevels: current.filters.seniorityLevels,
|
|
100
100
|
remoteOptions: current.filters.remoteOptions,
|
|
@@ -112,4 +112,5 @@ await board.me.alerts.update(alertId, {
|
|
|
112
112
|
- [ ] Manage page reads `subscription` + `token` from the email link's query string; writes send `subscriptionId` in the body.
|
|
113
113
|
- [ ] Editing one preference field leaves the others intact (full-replace round-trip, both surfaces).
|
|
114
114
|
- [ ] Alert UI only promises filtering on job function / place / remote option.
|
|
115
|
+
- [ ] Alert UI offers weekly cadence only and never sends a legacy daily value.
|
|
115
116
|
- [ ] Alert surfaces are hidden when `features.jobAlerts` is false.
|
|
@@ -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
|
|
@@ -1,69 +1,57 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-theme
|
|
3
|
-
description:
|
|
3
|
+
description: Optional hosted-board theme compatibility with @cavuno/board/theme. Use only when a custom frontend deliberately mirrors or migrates an existing Cavuno hosted theme; the consuming application normally owns tokens, fonts, and color mode.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Optional hosted-board theme compatibility
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
standard shadcn theme file — both end up in the same tokens.
|
|
8
|
+
The custom frontend owns its visual system by default: semantic design tokens, component styles, font loading, and light/dark-mode state belong in the application. Do not make remote `board.context().theme` the default source of truth for a new SDK frontend.
|
|
9
|
+
|
|
10
|
+
`@cavuno/board/theme` exists for a narrower job: mirroring an existing hosted board or providing a starting point during a hosted-to-custom migration.
|
|
12
11
|
|
|
13
12
|
## When to use
|
|
14
13
|
|
|
15
|
-
- The
|
|
16
|
-
-
|
|
14
|
+
- The human explicitly wants the custom frontend to mirror the current hosted board.
|
|
15
|
+
- A migration needs a temporary compatibility layer while tokens move into the application.
|
|
16
|
+
- One application intentionally renders many Cavuno boards with different operator-owned themes.
|
|
17
17
|
|
|
18
18
|
## When not to use
|
|
19
19
|
|
|
20
|
-
-
|
|
21
|
-
|
|
20
|
+
- A normal custom frontend whose design system already defines tokens and fonts.
|
|
21
|
+
- Component-level styling.
|
|
22
|
+
- As a replacement for the framework's theme provider or font system.
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## Read compatibility values
|
|
24
25
|
|
|
25
26
|
```ts snippet
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
boardThemeToCss,
|
|
29
|
+
googleFontsUrl,
|
|
30
|
+
themeMode,
|
|
31
|
+
} from '@cavuno/board/theme';
|
|
27
32
|
|
|
28
33
|
const context = await board.context();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const hostedTheme = {
|
|
35
|
+
css: boardThemeToCss(context.theme),
|
|
36
|
+
fontUrl: googleFontsUrl(context.theme),
|
|
37
|
+
mode: themeMode(context.theme),
|
|
38
|
+
};
|
|
32
39
|
```
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
overrides win; render nothing when it's empty (a null theme means the app's
|
|
36
|
-
default theme applies untouched). Apply `mode` by toggling the `.dark`
|
|
37
|
-
class (`system` = follow `prefers-color-scheme`).
|
|
38
|
-
|
|
39
|
-
## The mapping is the contract
|
|
40
|
-
|
|
41
|
-
All 16 board color keys are consumed — a coverage golden in-monorepo
|
|
42
|
-
asserts neither the hosted board nor this module drops one. Standard
|
|
43
|
-
shadcn tokens carry the core (background/foreground, card, popover,
|
|
44
|
-
primary, secondary, muted, accent, destructive, border, input, ring);
|
|
45
|
-
the four keys shadcn has no standard token for ship as
|
|
46
|
-
`--contrast-background`, `--contrast-foreground`, `--foreground-subtle`,
|
|
47
|
-
`--foreground-disabled`, plus `--foreground-error`.
|
|
41
|
+
`boardThemeToCss` validates the stored colors before emitting CSS. A null theme returns no CSS, so the application default remains intact.
|
|
48
42
|
|
|
49
|
-
##
|
|
50
|
-
|
|
51
|
-
```ts no-check
|
|
52
|
-
// NEVER hardcode brand colors in components — they bypass the board theme:
|
|
53
|
-
<button style={{ background: '#7c3aed' }} />
|
|
54
|
-
// NEVER re-map theme keys ad hoc per page; the shell mapping is the single source.
|
|
55
|
-
// NEVER fetch fonts per-family — googleFontsUrl builds ONE deduped request.
|
|
56
|
-
```
|
|
43
|
+
## Migration guidance
|
|
57
44
|
|
|
58
|
-
|
|
45
|
+
1. Compare the returned colors, fonts, and mode with the consuming design system.
|
|
46
|
+
2. Move the chosen values into the application's own semantic tokens and font loader.
|
|
47
|
+
3. Remove the runtime theme injection when the application becomes authoritative.
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
dashboard owns palette design), no per-component theme props, no CSS-in-JS
|
|
62
|
-
runtime. The module emits strings; the app owns injection.
|
|
49
|
+
If permanent multi-board mirroring is an explicit requirement, inject the validated CSS after the static theme and make the framework's theme provider apply the returned mode. Do not interpolate raw stored colors or build font URLs yourself.
|
|
63
50
|
|
|
64
51
|
## Verify
|
|
65
52
|
|
|
66
|
-
- [ ]
|
|
67
|
-
|
|
68
|
-
- [ ]
|
|
69
|
-
- [ ]
|
|
53
|
+
- [ ] The application renders correctly when `context.theme` is null.
|
|
54
|
+
- [ ] New custom frontends do not fetch theme data merely to establish basic styling.
|
|
55
|
+
- [ ] Compatibility CSS never contains unsafe stored color syntax.
|
|
56
|
+
- [ ] Font loading and color-mode ownership are explicit and tested.
|
|
57
|
+
- [ ] A migration has a documented point where the application becomes authoritative.
|
|
@@ -19,7 +19,6 @@ import { createBoardClient } from '@cavuno/board';
|
|
|
19
19
|
|
|
20
20
|
// Module-scoped, no auth.storage → safe across concurrent Workers requests.
|
|
21
21
|
export const board = createBoardClient({
|
|
22
|
-
baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
|
|
23
22
|
board: process.env.PUBLIC_CAVUNO_BOARD!,
|
|
24
23
|
});
|
|
25
24
|
```
|
package/skills/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.35.0",
|
|
3
3
|
"skills": [
|
|
4
4
|
{
|
|
5
5
|
"name": "cavuno-board-account",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"name": "cavuno-board-client",
|
|
34
|
-
"description": "Create and configure the @cavuno/board client —
|
|
34
|
+
"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.",
|
|
35
35
|
"path": "skills/cavuno-board-client/SKILL.md",
|
|
36
36
|
"framework": null,
|
|
37
37
|
"category": "core"
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "cavuno-board-jobs",
|
|
90
|
-
"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,
|
|
90
|
+
"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.",
|
|
91
91
|
"path": "skills/cavuno-board-jobs/SKILL.md",
|
|
92
92
|
"framework": null,
|
|
93
93
|
"category": "core"
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"name": "cavuno-board-theme",
|
|
160
|
-
"description": "
|
|
160
|
+
"description": "Optional hosted-board theme compatibility with @cavuno/board/theme. Use only when a custom frontend deliberately mirrors or migrates an existing Cavuno hosted theme; the consuming application normally owns tokens, fonts, and color mode.",
|
|
161
161
|
"path": "skills/cavuno-board-theme/SKILL.md",
|
|
162
162
|
"framework": null,
|
|
163
163
|
"category": "core"
|