@cavuno/board 2.0.5 → 3.0.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 (73) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -3
  3. package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
  4. package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
  5. package/dist/bin.mjs +40 -57
  6. package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
  7. package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
  8. package/dist/doctor.js +21 -47
  9. package/dist/doctor.mjs +24 -50
  10. package/dist/filters.d.mts +2 -2
  11. package/dist/filters.d.ts +2 -2
  12. package/dist/format.d.mts +3 -3
  13. package/dist/format.d.ts +3 -3
  14. package/dist/index.d.mts +11 -27
  15. package/dist/index.d.ts +11 -27
  16. package/dist/index.js +3 -3
  17. package/dist/index.mjs +3 -3
  18. package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
  19. package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
  20. package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
  21. package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
  22. package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
  23. package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
  24. package/dist/seo.d.mts +4 -4
  25. package/dist/seo.d.ts +4 -4
  26. package/dist/server.d.mts +5 -5
  27. package/dist/server.d.ts +5 -5
  28. package/dist/sitemap.d.mts +5 -5
  29. package/dist/sitemap.d.ts +5 -5
  30. package/dist/skills.d.mts +15 -4
  31. package/dist/skills.d.ts +15 -4
  32. package/dist/skills.js +30 -7
  33. package/dist/skills.mjs +30 -8
  34. package/dist/suggest.d.mts +2 -2
  35. package/dist/suggest.d.ts +2 -2
  36. package/package.json +1 -11
  37. package/skills/cavuno-board-account/SKILL.md +54 -66
  38. package/skills/cavuno-board-api-client/SKILL.md +74 -0
  39. package/skills/cavuno-board-applications/SKILL.md +38 -52
  40. package/skills/cavuno-board-auth/SKILL.md +36 -61
  41. package/skills/cavuno-board-blog/SKILL.md +60 -49
  42. package/skills/cavuno-board-companies/SKILL.md +71 -53
  43. package/skills/cavuno-board-errors/SKILL.md +29 -46
  44. package/skills/cavuno-board-filters/SKILL.md +64 -75
  45. package/skills/cavuno-board-format/SKILL.md +63 -68
  46. package/skills/cavuno-board-i18n/SKILL.md +33 -62
  47. package/skills/cavuno-board-job-alerts/SKILL.md +68 -57
  48. package/skills/cavuno-board-jobs/SKILL.md +63 -55
  49. package/skills/cavuno-board-messaging/SKILL.md +53 -59
  50. package/skills/cavuno-board-paywall/SKILL.md +48 -54
  51. package/skills/cavuno-board-post-a-job/SKILL.md +118 -0
  52. package/skills/cavuno-board-salaries/SKILL.md +73 -50
  53. package/skills/cavuno-board-search-suggestions/SKILL.md +82 -0
  54. package/skills/cavuno-board-seo/BLOG.md +40 -0
  55. package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
  56. package/skills/cavuno-board-seo/LISTING.md +36 -0
  57. package/skills/cavuno-board-seo/SALARY.md +61 -0
  58. package/skills/cavuno-board-seo/SKILL.md +32 -163
  59. package/skills/cavuno-board-server-sessions/SKILL.md +143 -0
  60. package/skills/cavuno-board-setup/SKILL.md +43 -80
  61. package/skills/cavuno-board-sitemap/SKILL.md +76 -93
  62. package/skills/cavuno-board-smoke-test/SKILL.md +34 -69
  63. package/skills/manifest.json +43 -57
  64. package/dist/theme.d.mts +0 -115
  65. package/dist/theme.d.ts +0 -115
  66. package/dist/theme.js +0 -195
  67. package/dist/theme.mjs +0 -174
  68. package/skills/cavuno-board-client/SKILL.md +0 -89
  69. package/skills/cavuno-board-job-posting/SKILL.md +0 -116
  70. package/skills/cavuno-board-server/SKILL.md +0 -229
  71. package/skills/cavuno-board-suggest/SKILL.md +0 -119
  72. package/skills/cavuno-board-theme/SKILL.md +0 -57
  73. package/skills/flavors/tanstack-start/SKILL.md +0 -102
@@ -0,0 +1,61 @@
1
+ # Salary rich results
2
+
3
+ Read this reference for salary structured data, `ItemList`, FAQ, formatters, or
4
+ seniority ordering.
5
+
6
+ ## Build a title page
7
+
8
+ Display-producing helpers require the board language as their leading
9
+ argument. The salary money formatters intentionally retain the hosted page's
10
+ USD assumption; locale changes number formatting, not currency. The current
11
+ FAQ uses templated English sentences until board-custom FAQ copy is exposed.
12
+
13
+ ```ts snippet
14
+ import {
15
+ buildSalaryFaq,
16
+ faqJsonLd,
17
+ formatRange,
18
+ itemListJsonLd,
19
+ sortBySeniority,
20
+ titleSalaryJsonLd,
21
+ } from '@cavuno/board/seo';
22
+
23
+ const { language } = await board.context();
24
+ const detail = await board.salaries.titles.retrieve('software-engineer');
25
+
26
+ const occupation = titleSalaryJsonLd(language, detail);
27
+ const faq = faqJsonLd(
28
+ buildSalaryFaq(language, detail.categoryName, detail.overallSalary));
29
+ const range = detail.overallSalary
30
+ ? formatRange(
31
+ language,
32
+ detail.overallSalary.avgMin,
33
+ detail.overallSalary.avgMax)
34
+ : null;
35
+ const ladder = sortBySeniority(detail.bySeniority);
36
+ const index = itemListJsonLd([
37
+ {
38
+ name: detail.categoryName,
39
+ url: 'https://jobs.example.com/salaries/titles/software-engineer',
40
+ },
41
+ ]);
42
+ ```
43
+
44
+ ## Select other salary builders
45
+
46
+ - `skillSalaryJsonLd(detail)` for a skill detail.
47
+ - `locationSalaryJsonLd(detail)` for a city-level location detail.
48
+ - `crossAxisSalaryJsonLd(locale, args)` for a title/skill and location pair.
49
+ - `companySalaryJsonLd(locale, detail)` for a company overview.
50
+ - `companyCategorySalaryJsonLd(locale, detail)` for a company-category page.
51
+ - `formatUsd(locale, value)` for the hosted USD amount format.
52
+ - `formatSeniority(locale, key)` and `SENIORITY_ORDER` for seniority display.
53
+
54
+ The structured-data families are `Occupation`,
55
+ `MonetaryAmountDistribution`, `ItemList`, and `FAQPage`. Feed them the SDK
56
+ salary read model; preserve nullable aggregates and server-computed values.
57
+
58
+ This branch is complete when every applicable schema object has the expected
59
+ schema.org type, null aggregates produce no fabricated figure, the board
60
+ language comes from context, and every displayed value traces to a response
61
+ field.
@@ -1,178 +1,47 @@
1
1
  ---
2
2
  name: cavuno-board-seo
3
- description: Structured data + head builders for board frontends with @cavuno/board/seo — Google for Jobs JobPosting JSON-LD, breadcrumbs, blog Article/ProfilePage, salary-page Occupation/FAQ structured data, and listing <head> descriptors. Use when building a job-detail, listing, blog, or salary page and it needs JSON-LD or SEO meta tags, or whenever "structured data", "rich results", or "Google for Jobs" comes up.
3
+ description: SEO builders with @cavuno/board/seo. Use for JobPosting or breadcrumb JSON-LD, blog schema, salary rich results, or job-listing head metadata.
4
4
  ---
5
5
 
6
- # SEO: structured data + head builders
6
+ # SEO builders
7
7
 
8
- `@cavuno/board/seo` emits the exact structured data the hosted board emits —
9
- tested against the hosted builders, so a tenant frontend gets Google
10
- for Jobs, breadcrumb, blog, and salary rich results without reinventing (or
11
- drifting from) the platform's rules. Render every builder's output into a
12
- `<script type="application/ld+json">` tag; builders return `null` when there
13
- is nothing worth emitting — skip the tag then.
8
+ `@cavuno/board/seo` returns the hosted board's structured data and
9
+ framework-neutral listing-head descriptors.
14
10
 
15
- ## When to use
11
+ ## Select the page branch
16
12
 
17
- - A job detail page needs Google for Jobs `JobPosting` JSON-LD.
18
- - Any page needs a `BreadcrumbList`.
19
- - Blog post / author pages need `Article` / `ProfilePage` JSON-LD.
20
- - Salary pages need `Occupation`/`MonetaryAmountDistribution`/`ItemList`/
21
- `FAQPage` structured data or the templated salary FAQ.
22
- - A jobs-listing page needs its `<head>` (title with count, canonical, OG).
13
+ Read the matching reference completely before implementing that page:
23
14
 
24
- ## When not to use
15
+ - Job detail or breadcrumbs: [`JOB_AND_BREADCRUMBS.md`](JOB_AND_BREADCRUMBS.md)
16
+ - Blog post or author profile: [`BLOG.md`](BLOG.md)
17
+ - Salary detail, index, comparison, or FAQ: [`SALARY.md`](SALARY.md)
18
+ - Job-listing metadata or structural JSON-LD: [`LISTING.md`](LISTING.md)
25
19
 
26
- - Display text on the page itself — `cavuno-board-format`.
27
- - Sitemaps and robots.txt — the sitemap module/skill.
28
- - OG image generation and route maps — app-owned (see out of scope).
20
+ For a page that combines branches, read every matching reference.
29
21
 
30
- ## Job-posting JSON-LD (the detail page)
22
+ Sitemaps and robots.txt use `cavuno-board-sitemap`. The host app owns OG-image
23
+ generation and its route map. `listingHead` receives app-owned heading copy;
24
+ the SDK does not reproduce the board's configured meta copy.
31
25
 
32
- `PublicJob` carries every field this needs — including the derived remote
33
- permit codes. Worldwide-remote jobs automatically enumerate all 249
34
- countries (Google requires a location signal next to TELECOMMUTE).
26
+ ## Apply the shared rendering contract
35
27
 
36
- ```ts snippet
37
- import { createJobPostingJsonLd } from '@cavuno/board/seo';
28
+ Builders with nullable return types use `null` when no useful object remains.
29
+ Emit a `<script type="application/ld+json">` only for a non-null result.
30
+ Helpers that produce display strings take `board.context().language` as their
31
+ required leading argument; pass the context value as the locale.
38
32
 
39
- const { name, logoUrl } = await board.context();
40
- const job = await board.jobs.retrieve('senior-chef');
33
+ ## Completion gate
41
34
 
42
- const jsonLd = createJobPostingJsonLd({
43
- job,
44
- board: { name, logoUrl },
45
- shareUrl: `https://jobs.example.com/companies/${job.company?.slug}/jobs/${job.slug}`,
46
- });
47
- // null when nothing renderable remains — omit the <script> tag then.
48
- ```
35
+ Finish only after every applicable branch check passes:
49
36
 
50
- ## Breadcrumbs
51
-
52
- Hosted semantics: labels are trimmed, blank crumbs dropped, fewer than two
53
- crumbs → `null`, and the current page (no `href`) omits `item`. Pass
54
- absolute hrefs, or give `origin` and use paths:
55
-
56
- ```ts snippet
57
- import { createBreadcrumbJsonLd } from '@cavuno/board/seo';
58
-
59
- const trail = createBreadcrumbJsonLd(
60
- [
61
- { label: 'Home', href: '/' },
62
- { label: 'Jobs', href: '/jobs' },
63
- { label: job.title }, // current page — no href
64
- ],
65
- { origin: 'https://jobs.example.com' });
66
- ```
67
-
68
- ## Blog Article + author ProfilePage
69
-
70
- ```ts snippet
71
- import {
72
- createBlogArticleJsonLd,
73
- createAuthorProfileJsonLd,
74
- } from '@cavuno/board/seo';
75
-
76
- const post = await board.blog.posts.retrieve('hello-world');
77
- const article = createBlogArticleJsonLd({
78
- post,
79
- boardName: name,
80
- permalink: `https://jobs.example.com/blog/${post.slug}`,
81
- ogImageUrl: `https://jobs.example.com/blog/${post.slug}/og`, // fallback when no cover
82
- });
83
-
84
- const author = await board.blog.authors.retrieve('jane');
85
- const { data: authorPosts } = await board.blog.posts.list({ authorSlug: 'jane' });
86
- const profile = createAuthorProfileJsonLd({
87
- author,
88
- canonical: `https://jobs.example.com/blog/author/${author.slug}`,
89
- description: author.bio ?? `Posts by ${author.name}`,
90
- origin: 'https://jobs.example.com',
91
- posts: authorPosts, // newest first; the builder keeps 5 as hasPart
92
- totalPosts: authorPosts.length,
93
- });
94
- ```
95
-
96
- ## Salary-page structured data + FAQ
97
-
98
- The board language is a REQUIRED leading parameter wherever display strings
99
- are produced (no `en` default). The money formatters are USD-hardcoded —
100
- the hosted salary-page quirk, kept deliberately; the locale drives number
101
- formatting only. The FAQ ships TEMPLATED English sentences (board-custom
102
- FAQ copy is a not currently available and will supersede it).
103
-
104
- ```ts snippet
105
- import {
106
- titleSalaryJsonLd,
107
- buildSalaryFaq,
108
- faqJsonLd,
109
- itemListJsonLd,
110
- formatRange,
111
- sortBySeniority,
112
- } from '@cavuno/board/seo';
113
-
114
- const { language } = await board.context();
115
- const detail = await board.salaries.titles.retrieve('software-engineer');
116
-
117
- const occupation = titleSalaryJsonLd(language, detail);
118
- const faq = faqJsonLd(
119
- buildSalaryFaq(language, detail.categoryName, detail.overallSalary));
120
- // Index pages: itemListJsonLd(items); on-page figures: formatRange(language, min, max)
121
- // and sortBySeniority(detail.bySeniority) for the ladder table.
122
- ```
123
-
124
- Also available: `skillSalaryJsonLd(detail)`, `locationSalaryJsonLd(detail)`
125
- (city-level pages only), `crossAxisSalaryJsonLd(locale, args)`,
126
- `companySalaryJsonLd(locale, detail)`,
127
- `companyCategorySalaryJsonLd(locale, detail)`, `formatUsd(locale, value)`,
128
- `formatSeniority(locale, key)`, `SENIORITY_ORDER`.
129
-
130
- ## Listing pages: head + structural JSON-LD
131
-
132
- Locale-neutral pass-through — you supply the heading copy:
133
-
134
- ```ts snippet
135
- import { listingHead, listingJsonLd } from '@cavuno/board/seo';
136
-
137
- const head = listingHead({
138
- boardName: name,
139
- origin: 'https://jobs.example.com',
140
- path: '/jobs/robotics',
141
- heading: 'Robotics jobs',
142
- count: 42, // → "42 Robotics jobs | Acme Jobs"
143
- });
144
- // head.meta → title/description/OG descriptors; head.links → canonical.
145
-
146
- const objects = listingJsonLd({
147
- origin: 'https://jobs.example.com',
148
- breadcrumbs: [{ name: 'Home', path: '/' }, { name: 'Jobs' }],
149
- jobs: cards.map((c) => ({ slug: c.slug, company: c.company })),
150
- });
151
- ```
152
-
153
- ## Anti-patterns
154
-
155
- ```ts no-check
156
- // NEVER hand-roll JobPosting JSON-LD — the pruning, salary mirroring, and
157
- // worldwide country enumeration are Google-spec rules the SDK encodes:
158
- const ld = { '@type': 'JobPosting', salary: job.salaryMin }; // wrong shape
159
- // NEVER default the locale on salary builders:
160
- titleSalaryJsonLd('en', detail); // hardcoded 'en' on a de board
161
- // NEVER emit a JSON-LD script for a null builder result.
162
- ```
163
-
164
- ## Out of scope — do not invent exports
165
-
166
- OG **image generation** (the `/og` card renderer), the legal/route **map**,
167
- and the meta title/description **copy** (board SEO config + localized copy
168
- is not replicated — `listingHead` takes your strings) are app-owned. No
169
- sitemap builders here — that is `@cavuno/board/sitemap`'s job.
170
-
171
- ## Verify
172
-
173
- - [ ] The job detail page's JSON-LD validates in Google's Rich Results test
174
- as a JobPosting, and a worldwide-remote job lists 249 countries.
175
- - [ ] Pages with a null builder result render NO empty ld+json script.
176
- - [ ] A de board passes `language` from `board.context()` — the salary
177
- FAQ figures render de-formatted, and no call site hardcodes 'en'.
178
- - [ ] Breadcrumb JSON-LD only appears on pages with 2+ crumbs.
37
+ - Each emitted JSON-LD value is non-null and is serialized in an
38
+ `application/ld+json` script.
39
+ - Job detail validates as `JobPosting` in Google's Rich Results test; a
40
+ worldwide-remote job contains all 249 country codes.
41
+ - Breadcrumb schema appears only with at least two non-blank crumbs.
42
+ - Blog post and author pages emit their matching `Article` and `ProfilePage`
43
+ objects with canonical absolute URLs.
44
+ - Salary builders receive the board language wherever their signature
45
+ requires it, and rendered figures match the returned salary read model.
46
+ - Listing head output supplies title, description, Open Graph descriptors,
47
+ and canonical link for the requested absolute origin and path.
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: cavuno-board-server-sessions
3
+ description: Wire Cavuno server sessions and board-password access. Use when a server-rendered app needs httpOnly cookies, per-request bearer headers, single-flight refresh, grant cookies, or safe redirect-back handling.
4
+ ---
5
+
6
+ # Wire server sessions
7
+
8
+ `@cavuno/board/server` provides platform-neutral cookie strings and refresh coordination. The framework skills installed in the app provide request, middleware, and response APIs.
9
+
10
+ The server contract has three invariants:
11
+
12
+ 1. One module-scoped board client uses server-default `nostore`.
13
+ 2. Each request reads an app-owned httpOnly cookie and passes credentials in that call's headers.
14
+ 3. One module-scoped `createSessionRefresher(board)` coordinates refreshes within the process or isolate.
15
+
16
+ ## 1. Persist the board-user session
17
+
18
+ `BoardSession` contains `{ accessToken, refreshToken, expiresAt }`. The codec uses one `__Host-` cookie with `Path=/`, `HttpOnly`, `Secure`, `SameSite=Lax`, and a 30-day `Max-Age`. The framework owns header I/O and CSRF controls; wrap the codec when the app requires cookie encryption.
19
+
20
+ ```ts snippet
21
+ import {
22
+ clearSessionCookie,
23
+ parseSessionCookie,
24
+ serializeSessionCookie,
25
+ } from '@cavuno/board/server';
26
+
27
+ const signedIn = await board.auth.login({ email, password });
28
+ const setCookie = serializeSessionCookie({
29
+ accessToken: signedIn.accessToken,
30
+ refreshToken: signedIn.refreshToken,
31
+ expiresAt: signedIn.expiresAt,
32
+ });
33
+
34
+ const session = parseSessionCookie(request.headers.get('cookie'));
35
+ const clearedCookie = clearSessionCookie();
36
+ ```
37
+
38
+ `parseSessionCookie` returns `null` for absent, malformed, or wrong-shaped values. Treat that state as signed out. On logout, call `board.auth.logout({ refreshToken })`, then emit `clearSessionCookie()` after successful revocation.
39
+
40
+ **Complete when:** login emits the serialized cookie, the next server request restores the same session, malformed input becomes signed out, and logout expires the cookie.
41
+
42
+ ## 2. Resolve one fresh session per request
43
+
44
+ Refresh tokens rotate once. Create the refresher beside the shared client so concurrent requests using the same refresh token await one rotation. `isExpiringSoon` selects sessions within the proactive five-minute window.
45
+
46
+ ```ts snippet
47
+ import {
48
+ clearSessionCookie,
49
+ createSessionRefresher,
50
+ isExpiringSoon,
51
+ parseSessionCookie,
52
+ serializeSessionCookie,
53
+ } from '@cavuno/board/server';
54
+
55
+ const refreshSession = createSessionRefresher(board);
56
+
57
+ async function resolveSession(cookieHeader: string | null) {
58
+ const session = parseSessionCookie(cookieHeader);
59
+ if (!session) return { session: null, setCookie: null };
60
+ if (!isExpiringSoon(session, Date.now())) {
61
+ return { session, setCookie: null };
62
+ }
63
+
64
+ const rotated = await refreshSession(session);
65
+ return rotated
66
+ ? { session: rotated, setCookie: serializeSessionCookie(rotated) }
67
+ : { session: null, setCookie: clearSessionCookie() };
68
+ }
69
+ ```
70
+
71
+ A rotated session is written back before the response completes. A `null` result represents a 401 from a burned or revoked token: clear the cookie and continue signed out. Network, 5xx, and 429 failures rethrow to the app's error boundary. The helper deduplicates within one process or isolate; separate deployment instances can still race.
72
+
73
+ **Complete when:** two simultaneous resolutions for one expiring session invoke one rotation in the process, both observe the same result, success writes the rotated pair, and `null` produces one clear-cookie response with no refresh retry.
74
+
75
+ ## 3. Authenticate each SDK call
76
+
77
+ Pass the resolved access token in the call's trailing `FetchOptions`:
78
+
79
+ ```ts snippet
80
+ const me = await board.me.retrieve(undefined, {
81
+ headers: { authorization: `Bearer ${session.accessToken}` },
82
+ });
83
+ ```
84
+
85
+ This keeps the shared client's storage empty across users.
86
+
87
+ **Complete when:** concurrent requests can present different tokens without mutating the shared client.
88
+
89
+ ## 4. Persist board-password access
90
+
91
+ The board-password grant has its own 24-hour `__Host-` cookie. It remains separate from the board-user session.
92
+
93
+ ```ts snippet
94
+ import {
95
+ clearGrantCookie,
96
+ parseGrantCookie,
97
+ serializeGrantCookie,
98
+ } from '@cavuno/board/server';
99
+
100
+ const { token } = await board.password.verify(password);
101
+ const setGrantCookie = serializeGrantCookie(token);
102
+
103
+ const grant = parseGrantCookie(request.headers.get('cookie'));
104
+ const jobs = await board.jobs.list(undefined, {
105
+ headers: grant ? { 'X-Board-Access': grant } : {},
106
+ });
107
+
108
+ const clearedGrantCookie = clearGrantCookie();
109
+ ```
110
+
111
+ When a read returns `isBoardPasswordRequired`, clear the stale grant and route the visitor through the password challenge again.
112
+
113
+ **Complete when:** verification writes the grant, a later request supplies `X-Board-Access`, and an expired or rotated grant is cleared before rechallenge.
114
+
115
+ ## 5. Guard redirect-back paths
116
+
117
+ Every password challenge and post-login redirect consumes a same-origin absolute path through `safeRedirectPath`. `currentPathFromReferer` derives an already-guarded return path from a Referer header.
118
+
119
+ ```ts snippet
120
+ import {
121
+ currentPathFromReferer,
122
+ safeRedirectPath,
123
+ } from '@cavuno/board/server';
124
+
125
+ const destination = safeRedirectPath(query.redirect);
126
+ const returnTo = currentPathFromReferer(request.headers.get('referer'));
127
+ ```
128
+
129
+ Values that could resolve off-origin collapse to `/` (or the explicit fallback passed as the second argument).
130
+
131
+ **Complete when:** raw query values never reach the redirect API and tests cover a valid local path, a full URL, a protocol-relative URL, backslashes, and control-character normalization.
132
+
133
+ ## Multi-board origins
134
+
135
+ When one origin serves multiple boards, pass the board identifier to every session and grant codec so cookies cannot collide:
136
+
137
+ ```ts snippet
138
+ const scope = { board: 'pk_boardA' };
139
+ const cookie = serializeSessionCookie(session, scope);
140
+ const restored = parseSessionCookie(cookieHeader, scope);
141
+ ```
142
+
143
+ Single-board origins omit the scope.
@@ -1,57 +1,23 @@
1
1
  ---
2
2
  name: cavuno-board-setup
3
- description: End-to-end orchestrator for building a headless Cavuno job board with the @cavuno/board SDK. Start here after `npx @cavuno/board setup` copies the skills — detect the framework, wire the client, render board context, jobs browsing and detail, board-user auth and saved jobs, handle errors and access gating, then verify.
3
+ description: Orchestrate a headless Cavuno board integration. Use when starting a frontend with @cavuno/board or adding Cavuno board surfaces to an existing app.
4
4
  ---
5
5
 
6
- # Setting up a Cavuno board
6
+ # Set up a Cavuno board
7
7
 
8
- `@cavuno/board` is a thin, isomorphic, typed client for the Cavuno Board API (`/v1/boards/:identifier/*`). It brings the commerce of a job board — jobs, companies, blog, search, auth, saved jobs, alerts — to the framework you already use. You bring the framework and own the layout; the SDK brings the data contract.
8
+ Build the board as a sequence of working slices. The framework owns rendering and routing; Cavuno skills own Board API behavior and data contracts.
9
9
 
10
- This skill is the orchestrator. Work top to bottom, delegating each surface to its focused skill.
10
+ ## 1. Map the host app
11
11
 
12
- ## When to use
12
+ Read `package.json`, the route tree, environment conventions, and existing data-fetching boundaries. Use the framework skills already installed in the project for framework APIs, including TanStack Start, Next.js, Remix, Nuxt, SvelteKit, Astro, and SolidStart.
13
13
 
14
- - Standing up a new headless board frontend against the Board API.
15
- - Adding board data (jobs/companies/blog/auth) to an existing app.
16
- - After `npx @cavuno/board setup` has installed the package and copied these skills.
14
+ Choose where public reads run and identify the server boundary for bearer tokens. `@cavuno/board` runs in browsers, Node 20+, and Cloudflare Workers.
17
15
 
18
- ## When not to use
16
+ **Complete when:** the framework, route convention, rendering boundary, and server-only environment boundary are identified from the repository.
19
17
 
20
- - Authoring the hosted board inside the Cavuno admin (that's the operator Puck builder, not the SDK).
21
- - Building operator automation — use the public `cavuno` CLI or REST API;
22
- `@cavuno/board` is for visitor-facing board experiences.
18
+ ## 2. Configure one client
23
19
 
24
- ## Inspect the app
25
-
26
- Read `package.json` and the project layout before writing anything. Identify: the framework (see below), whether it renders on a server (SSR/RSC) or only in the browser, and where server-only secrets are read. Match the project's existing conventions — do not introduce a new data-fetching style.
27
-
28
- ## Detect the framework
29
-
30
- `@cavuno/board` is framework-agnostic: it needs only `fetch` and runs in the browser, Node ≥ 20, and Cloudflare Workers. Detect the framework from dependencies and adapt:
31
-
32
- - `@tanstack/react-start` → the reference flavor. Read `cavuno-board-tanstack-start` for SSR-loader + cookie wiring.
33
- - `next` → use Server Components + per-call `FetchOptions` (`next: { revalidate, tags }`).
34
- - Anything else (Nuxt, SvelteKit, Astro, SolidStart, plain JS) → use the core skills directly; the SDK surface is identical.
35
-
36
- ## Use the standard board environment name
37
-
38
- Read the board key from the environment; never hard-code it:
39
-
40
- - `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
-
42
- 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
-
44
- ## Keep credentials server-side
45
-
46
- The board identifier is public. A board-user **bearer JWT is not** — it must never reach the browser bundle. On a server framework, keep the session in an httpOnly cookie owned by your app and pass it per call; see `cavuno-board-auth`.
47
-
48
- ## Use board route conventions
49
-
50
- The canonical public job-detail URL is `/companies/:companySlug/jobs/:jobSlug` — a job needs both slugs. `/jobs`, `/jobs/:keyword`, `/jobs/locations/:slug` are listing/search pages, never individual jobs. Mirror these paths so a board migrating hosted → headless keeps its indexed URLs.
51
-
52
- ## Wire the client
53
-
54
- Create one client and reuse it. See `cavuno-board-client`.
20
+ Read the board identifier from the host framework's public environment convention. Prefer the immutable, publishable `pk_…` key; it is safe to expose. The default API origin is `https://api.cavuno.com`; set `baseUrl` only for a Cavuno-supplied alternate origin.
55
21
 
56
22
  ```ts
57
23
  import { createBoardClient } from '@cavuno/board';
@@ -61,59 +27,56 @@ export const board = createBoardClient({
61
27
  });
62
28
  ```
63
29
 
64
- ## Build the board shell from context
65
-
66
- `board.context()` (a root method on the client) returns identity, theme, analytics, and the board's capability `features` flags. Render branding from it and **gate every optional surface on its capability flag** — only build a route when its feature is enabled. (A dedicated context skill ships with a later slice; the return type is self-describing until then.)
30
+ Use `cavuno-board-api-client` for hooks, headers, caching, and SSR state boundaries.
67
31
 
68
- ## Build jobs browsing + detail
32
+ **Complete when:** one reused client resolves `board.context()` with the expected board name.
69
33
 
70
- 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`.
34
+ ## 3. Build a vertical jobs slice
71
35
 
72
- ## Add board users + saved jobs
36
+ Render the shell from `board.context()`, including board identity and branding. Treat its `features` as capabilities: expose each optional surface only when its capability is enabled.
73
37
 
74
- Register/login/refresh/logout, then `me.retrieve` and `me.savedJobs.*`. There is **no auto-refresh on 401** — handle it explicitly. See `cavuno-board-auth`.
38
+ Then build the jobs list and detail routes with `cavuno-board-jobs`. Preserve the hosted board's indexed route contract:
75
39
 
76
- ## Handle errors + gating
40
+ - `/jobs`, `/jobs/:keyword`, and `/jobs/locations/:slug` are listings.
41
+ - `/companies/:companySlug/jobs/:jobSlug` is a job detail.
77
42
 
78
- Every method throws `BoardApiError` on a non-2xx; branch with the typed guards. Password-protected boards need a `password.verify()` grant. See `cavuno-board-errors`.
43
+ Use `cavuno-board-format`, `cavuno-board-filters`, and `cavuno-board-search-suggestions` for their respective display, URL-filter, and search-suggestion contracts.
79
44
 
80
- ## Style, format, and filter with the helper subpaths
45
+ **Complete when:** a real job card links to a working canonical detail page, and a fabricated slug renders the app's handled not-found state.
81
46
 
82
- Three subpath modules carry the display layer — use them instead of
83
- hand-rolling (each has a skill):
47
+ ## 4. Add identity and access
84
48
 
85
- - Salary/date/label formatting in the board language → `cavuno-board-format`
86
- - Listing-filter vocabulary + URL param parsing → `cavuno-board-filters`
87
- - Optional hosted-board theme compatibility → `cavuno-board-theme`. By default, the consuming application owns tokens, fonts, and color mode.
49
+ Use `cavuno-board-auth` for board-user registration, login, verification, recovery, and browser storage. In a server-rendered app, use `cavuno-board-server-sessions` as the authority for the httpOnly session cookie, per-call bearer header, and `createSessionRefresher`. Use `cavuno-board-errors` for typed failure branches and board-password grants.
88
50
 
89
- ## Build the remaining surfaces per feature flag
51
+ **Complete when:** an authenticated `board.me.retrieve()` succeeds, logout returns the app to a signed-out state, and concurrent server requests share the server skill's single-flight refresher.
90
52
 
91
- One focused skill per surface — delegate by name, and build a surface only
92
- when its `features` flag (from `board.context()`) is enabled:
53
+ ## 5. Add only the enabled surfaces
93
54
 
94
- - Companies directory, markets, per-company jobs → `cavuno-board-companies`
95
- - Blog (posts, tags, authors, search) → `cavuno-board-blog`
96
- - Programmatic salary pages → `cavuno-board-salaries`
97
- - Job alerts (anonymous double-opt-in + account CRUD) → `cavuno-board-job-alerts`
98
- - Candidate account self-service (profile, resume, avatar, prefs) → `cavuno-board-account`
99
- - Apply flow + my applications + saved jobs → `cavuno-board-applications`
100
- - Candidate↔employer messaging (polled REST) → `cavuno-board-messaging`
101
- - Candidate job-access paywall (offers, checkout, grant) → `cavuno-board-paywall`
102
- - Public job-posting funnel (plans, submit, billing) → `cavuno-board-job-posting`
55
+ Route each enabled capability to its focused skill:
103
56
 
104
- ## App-owned concerns (out of scope)
57
+ | Surface | Skill |
58
+ | --- | --- |
59
+ | Companies and markets | `cavuno-board-companies` |
60
+ | Blog | `cavuno-board-blog` |
61
+ | Salary pages | `cavuno-board-salaries` |
62
+ | Job alerts | `cavuno-board-job-alerts` |
63
+ | Candidate account | `cavuno-board-account` |
64
+ | Saved jobs and applications | `cavuno-board-applications` |
65
+ | Messaging | `cavuno-board-messaging` |
66
+ | Candidate paywall | `cavuno-board-paywall` |
67
+ | Public job posting | `cavuno-board-post-a-job` |
68
+ | Localized chrome | `cavuno-board-i18n` |
69
+ | Metadata and structured data | `cavuno-board-seo` |
70
+ | Sitemap routes | `cavuno-board-sitemap` |
105
71
 
106
- The SDK serves data only. Your app owns: page layout and chrome copy, marketing/legal prose, and the authoring of SEO artifacts (sitemap, RSS, OG images) — built from API data, not served as documents. The Board API never returns layouts or page-builder JSON.
72
+ The app remains the source of page composition, chrome copy, and marketing or legal prose. Cavuno supplies data and helpers for the artifacts its focused skills cover.
107
73
 
108
- ## Verification
74
+ **Complete when:** every implemented optional route maps to an enabled board capability and the relevant focused skill's verification passes.
109
75
 
110
- - `board.context()` resolves and returns your board's name.
111
- - A listing page renders cards from `board.jobs.list()`.
112
- - A detail page renders from `board.jobs.retrieve(slug)`.
113
- - An invalid slug surfaces a handled `isNotFound(err)` path, not a crash.
76
+ ## 6. Prove the integration
114
77
 
115
- Then run the `cavuno-board-smoke-test` skill against your `pk_…` — type checks are not enough for board wiring.
78
+ Run `cavuno-board-smoke-test` with the real `pk_…` value. Its runtime probes and production build are the completion gate for setup.
116
79
 
117
- ## Stop conditions
80
+ Pause with a concrete blocker when the board identifier or a required test credential is unavailable, or the public API probe fails before it reaches app code.
118
81
 
119
- 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.
82
+ **Complete when:** doctor, direct API probes, implemented-surface checks, and the production-build probe all pass or each skipped credentialed check is reported.