@cavuno/board 2.0.5 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +12 -3
- package/dist/{_spec-BKtVFcV9.d.mts → _spec-nkWAraS0.d.mts} +4 -18
- package/dist/{_spec-BKtVFcV9.d.ts → _spec-nkWAraS0.d.ts} +4 -18
- package/dist/bin.mjs +40 -57
- package/dist/{board-3Xj_Y-Tx.d.ts → board-NE98PlwN.d.ts} +2 -3
- package/dist/{board-pNN710Zc.d.mts → board-muu8BI8P.d.mts} +2 -3
- package/dist/doctor.js +21 -47
- package/dist/doctor.mjs +24 -50
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +3 -3
- package/dist/format.d.ts +3 -3
- package/dist/index.d.mts +11 -27
- package/dist/index.d.ts +11 -27
- package/dist/index.js +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{jobs-C4qRfCdP.d.ts → jobs-BiEvL9d7.d.ts} +1 -1
- package/dist/{jobs-k84C0v_8.d.mts → jobs-lVvKCBCU.d.mts} +1 -1
- package/dist/{salaries--J5mTylN.d.ts → salaries-BahBpxWT.d.ts} +2 -2
- package/dist/{salaries-C9YLO5NG.d.mts → salaries-ChSqDBAM.d.mts} +2 -2
- package/dist/{search-CE_qQiM7.d.mts → search-CCyFWPi3.d.mts} +1 -1
- package/dist/{search-CLRQ_El9.d.ts → search-CyRvVMUp.d.ts} +1 -1
- package/dist/seo.d.mts +4 -4
- package/dist/seo.d.ts +4 -4
- package/dist/server.d.mts +5 -5
- package/dist/server.d.ts +5 -5
- package/dist/sitemap.d.mts +5 -5
- package/dist/sitemap.d.ts +5 -5
- package/dist/skills.d.mts +15 -4
- package/dist/skills.d.ts +15 -4
- package/dist/skills.js +30 -7
- package/dist/skills.mjs +30 -8
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +1 -11
- package/skills/cavuno-board-account/SKILL.md +58 -66
- package/skills/cavuno-board-api-client/SKILL.md +78 -0
- package/skills/cavuno-board-applications/SKILL.md +42 -52
- package/skills/cavuno-board-auth/SKILL.md +39 -60
- package/skills/cavuno-board-blog/SKILL.md +64 -49
- package/skills/cavuno-board-companies/SKILL.md +75 -53
- package/skills/cavuno-board-errors/SKILL.md +32 -45
- package/skills/cavuno-board-filters/SKILL.md +68 -75
- package/skills/cavuno-board-format/SKILL.md +65 -66
- package/skills/cavuno-board-i18n/SKILL.md +43 -68
- package/skills/cavuno-board-job-alerts/SKILL.md +72 -57
- package/skills/cavuno-board-jobs/SKILL.md +67 -55
- package/skills/cavuno-board-messaging/SKILL.md +57 -59
- package/skills/cavuno-board-paywall/SKILL.md +52 -54
- package/skills/cavuno-board-post-a-job/SKILL.md +122 -0
- package/skills/cavuno-board-salaries/SKILL.md +77 -50
- package/skills/cavuno-board-search-suggestions/SKILL.md +86 -0
- package/skills/cavuno-board-seo/BLOG.md +40 -0
- package/skills/cavuno-board-seo/JOB_AND_BREADCRUMBS.md +49 -0
- package/skills/cavuno-board-seo/LISTING.md +36 -0
- package/skills/cavuno-board-seo/SALARY.md +61 -0
- package/skills/cavuno-board-seo/SKILL.md +34 -161
- package/skills/cavuno-board-server-sessions/SKILL.md +147 -0
- package/skills/cavuno-board-setup/SKILL.md +46 -79
- package/skills/cavuno-board-sitemap/SKILL.md +78 -91
- package/skills/cavuno-board-smoke-test/SKILL.md +38 -69
- package/skills/manifest.json +43 -57
- package/dist/theme.d.mts +0 -115
- package/dist/theme.d.ts +0 -115
- package/dist/theme.js +0 -195
- package/dist/theme.mjs +0 -174
- package/skills/cavuno-board-client/SKILL.md +0 -89
- package/skills/cavuno-board-job-posting/SKILL.md +0 -116
- package/skills/cavuno-board-server/SKILL.md +0 -229
- package/skills/cavuno-board-suggest/SKILL.md +0 -119
- package/skills/cavuno-board-theme/SKILL.md +0 -57
- package/skills/flavors/tanstack-start/SKILL.md +0 -102
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Listing metadata and JSON-LD
|
|
2
|
+
|
|
3
|
+
Read this reference for a job-listing page's head descriptors and structural
|
|
4
|
+
JSON-LD. These builders are locale-neutral pass-throughs; supply the page's
|
|
5
|
+
resolved heading copy.
|
|
6
|
+
|
|
7
|
+
```ts snippet
|
|
8
|
+
import { listingHead, listingJsonLd } from '@cavuno/board/seo';
|
|
9
|
+
|
|
10
|
+
const { name } = await board.context();
|
|
11
|
+
const head = listingHead({
|
|
12
|
+
boardName: name,
|
|
13
|
+
origin: 'https://jobs.example.com',
|
|
14
|
+
path: '/jobs/robotics',
|
|
15
|
+
heading: 'Robotics jobs',
|
|
16
|
+
count: 42,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const objects = listingJsonLd({
|
|
20
|
+
origin: 'https://jobs.example.com',
|
|
21
|
+
breadcrumbs: [{ name: 'Home', path: '/' }, { name: 'Jobs' }],
|
|
22
|
+
jobs: cards.map((card) => ({
|
|
23
|
+
slug: card.slug,
|
|
24
|
+
company: card.company,
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`head.meta` contains title, description, and Open Graph descriptors;
|
|
30
|
+
`head.links` contains the canonical link. For the example, the count-aware
|
|
31
|
+
title is `42 Robotics jobs | Acme Jobs`.
|
|
32
|
+
|
|
33
|
+
This branch is complete when head descriptors reflect the resolved page
|
|
34
|
+
heading and count, the canonical URL combines the intended origin and path,
|
|
35
|
+
and `listingJsonLd` receives card slugs plus company objects from the listing
|
|
36
|
+
response.
|
|
@@ -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,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-seo
|
|
3
|
-
description:
|
|
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
|
|
6
|
+
# SEO builders
|
|
7
7
|
|
|
8
|
-
`@cavuno/board/seo`
|
|
9
|
-
|
|
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
|
-
##
|
|
11
|
+
## Select the page branch
|
|
16
12
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
const job = await board.jobs.retrieve('senior-chef');
|
|
33
|
+
## Completion gate
|
|
41
34
|
|
|
42
|
-
|
|
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
|
-
|
|
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.
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
crumbs → `null`, and the current page (no `href`) omits `item`. Pass
|
|
54
|
-
absolute hrefs, or give `origin` and use paths:
|
|
49
|
+
## Cavuno SDK reference
|
|
55
50
|
|
|
56
|
-
|
|
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.
|
|
51
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|
|
@@ -0,0 +1,147 @@
|
|
|
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.
|
|
144
|
+
|
|
145
|
+
## Cavuno SDK reference
|
|
146
|
+
|
|
147
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|
|
@@ -1,57 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cavuno-board-setup
|
|
3
|
-
description:
|
|
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
|
-
#
|
|
6
|
+
# Set up a Cavuno board
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
## 1. Map the host app
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
**Complete when:** the framework, route convention, rendering boundary, and server-only environment boundary are identified from the repository.
|
|
19
17
|
|
|
20
|
-
|
|
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
|
-
|
|
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,60 @@ export const board = createBoardClient({
|
|
|
61
27
|
});
|
|
62
28
|
```
|
|
63
29
|
|
|
64
|
-
|
|
30
|
+
Use `cavuno-board-api-client` for hooks, headers, caching, and SSR state boundaries.
|
|
31
|
+
|
|
32
|
+
**Complete when:** one reused client resolves `board.context()` with the expected board name.
|
|
65
33
|
|
|
66
|
-
|
|
34
|
+
## 3. Build a vertical jobs slice
|
|
67
35
|
|
|
68
|
-
|
|
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.
|
|
69
37
|
|
|
70
|
-
|
|
38
|
+
Then build the jobs list and detail routes with `cavuno-board-jobs`. Preserve the hosted board's indexed route contract:
|
|
71
39
|
|
|
72
|
-
|
|
40
|
+
- `/jobs`, `/jobs/:keyword`, and `/jobs/locations/:slug` are listings.
|
|
41
|
+
- `/companies/:companySlug/jobs/:jobSlug` is a job detail.
|
|
73
42
|
|
|
74
|
-
|
|
43
|
+
Use `cavuno-board-format`, `cavuno-board-filters`, and `cavuno-board-search-suggestions` for their respective display, URL-filter, and search-suggestion contracts.
|
|
75
44
|
|
|
76
|
-
|
|
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.
|
|
77
46
|
|
|
78
|
-
|
|
47
|
+
## 4. Add identity and access
|
|
79
48
|
|
|
80
|
-
|
|
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.
|
|
81
50
|
|
|
82
|
-
|
|
83
|
-
hand-rolling (each has a skill):
|
|
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.
|
|
84
52
|
|
|
85
|
-
|
|
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.
|
|
53
|
+
## 5. Add only the enabled surfaces
|
|
88
54
|
|
|
89
|
-
|
|
55
|
+
Route each enabled capability to its focused skill:
|
|
90
56
|
|
|
91
|
-
|
|
92
|
-
|
|
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` |
|
|
93
71
|
|
|
94
|
-
|
|
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`
|
|
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.
|
|
103
73
|
|
|
104
|
-
|
|
74
|
+
**Complete when:** every implemented optional route maps to an enabled board capability and the relevant focused skill's verification passes.
|
|
105
75
|
|
|
106
|
-
|
|
76
|
+
## 6. Prove the integration
|
|
107
77
|
|
|
108
|
-
|
|
78
|
+
Run `cavuno-board-smoke-test` with the real `pk_…` value. Its runtime probes and production build are the completion gate for setup.
|
|
109
79
|
|
|
110
|
-
|
|
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.
|
|
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.
|
|
114
81
|
|
|
115
|
-
|
|
82
|
+
**Complete when:** doctor, direct API probes, implemented-surface checks, and the production-build probe all pass or each skipped credentialed check is reported.
|
|
116
83
|
|
|
117
|
-
##
|
|
84
|
+
## Cavuno SDK reference
|
|
118
85
|
|
|
119
|
-
|
|
86
|
+
For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
|