@cavuno/board 4.69.0 → 4.70.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 (42) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/{_spec-CFOj7Qdf.d.mts → _spec-Dk7QyuoJ.d.mts} +58 -2
  3. package/dist/{_spec-CFOj7Qdf.d.ts → _spec-Dk7QyuoJ.d.ts} +58 -2
  4. package/dist/{blog-BaVJi3q1.d.ts → blog-DfUNkngR.d.ts} +1 -1
  5. package/dist/{blog-DCRBPnpZ.d.mts → blog-Oo6YHXSE.d.mts} +1 -1
  6. package/dist/{board-zLt7j6hj.d.ts → board-CfmGT0yV.d.ts} +2 -2
  7. package/dist/{board-DMS3I3sJ.d.mts → board-DzBYIwKz.d.mts} +2 -2
  8. package/dist/{breadcrumbs-yGijUUtR.d.ts → breadcrumbs-CS0nlknq.d.ts} +2 -2
  9. package/dist/{breadcrumbs-Dwf0yq7B.d.mts → breadcrumbs-DeIA0RQp.d.mts} +2 -2
  10. package/dist/browser/cavuno-board.global.min.js +1 -1
  11. package/dist/{chunk-7SAHE7UW.mjs → chunk-UIKN2ERI.mjs} +2 -0
  12. package/dist/filters.d.mts +2 -2
  13. package/dist/filters.d.ts +2 -2
  14. package/dist/format.d.mts +4 -4
  15. package/dist/format.d.ts +4 -4
  16. package/dist/index.d.mts +74 -22
  17. package/dist/index.d.ts +74 -22
  18. package/dist/index.js +50 -2
  19. package/dist/index.mjs +49 -3
  20. package/dist/{jobs-Cew_q69W.d.mts → jobs-BMRvZlNx.d.mts} +1 -1
  21. package/dist/{jobs-Dz_w1jZq.d.ts → jobs-BlAsTCQl.d.ts} +1 -1
  22. package/dist/{search-DwvgvtEg.d.mts → search-BB1TaaLa.d.mts} +1 -1
  23. package/dist/{search-BqAYR47P.d.ts → search-CoBeDgYT.d.ts} +1 -1
  24. package/dist/{seo-CGy4fY5a.d.ts → seo-BmqJ_ChV.d.ts} +1 -1
  25. package/dist/{seo-FKj1pSHZ.d.mts → seo-M_8j5vdP.d.mts} +1 -1
  26. package/dist/seo-files.d.mts +2 -2
  27. package/dist/seo-files.d.ts +2 -2
  28. package/dist/seo.d.mts +6 -6
  29. package/dist/seo.d.ts +6 -6
  30. package/dist/server.d.mts +6 -6
  31. package/dist/server.d.ts +6 -6
  32. package/dist/server.mjs +1 -1
  33. package/dist/sitemap.d.mts +6 -6
  34. package/dist/sitemap.d.ts +6 -6
  35. package/dist/sitemap.mjs +1 -1
  36. package/dist/suggest.d.mts +2 -2
  37. package/dist/suggest.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/skills/cavuno-board-account/SKILL.md +16 -0
  40. package/skills/cavuno-board-job-alerts/SKILL.md +7 -0
  41. package/skills/cavuno-board-newsletter/SKILL.md +79 -0
  42. package/skills/manifest.json +8 -1
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CFOj7Qdf.js';
1
+ import { S as Schemas } from './_spec-Dk7QyuoJ.js';
2
2
 
3
3
  /**
4
4
  * Optional pagination metadata returned by Board API list operations.
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CFOj7Qdf.mjs';
1
+ import { S as Schemas } from './_spec-Dk7QyuoJ.mjs';
2
2
 
3
3
  type Awaitable<T> = T | Promise<T>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CFOj7Qdf.js';
1
+ import { S as Schemas } from './_spec-Dk7QyuoJ.js';
2
2
 
3
3
  type Awaitable<T> = T | Promise<T>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CFOj7Qdf.js';
1
+ import { S as Schemas } from './_spec-Dk7QyuoJ.js';
2
2
 
3
3
  /**
4
4
  * The public SEO-infra payload (`board.seo()`) — platform infrastructure a
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-CFOj7Qdf.mjs';
1
+ import { S as Schemas } from './_spec-Dk7QyuoJ.mjs';
2
2
 
3
3
  /**
4
4
  * The public SEO-infra payload (`board.seo()`) — platform infrastructure a
@@ -1,5 +1,5 @@
1
- import { B as BoardSeo } from './seo-FKj1pSHZ.mjs';
2
- import './_spec-CFOj7Qdf.mjs';
1
+ import { B as BoardSeo } from './seo-M_8j5vdP.mjs';
2
+ import './_spec-Dk7QyuoJ.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/seo-files` — platform-owned crawler/identity file bodies
@@ -1,5 +1,5 @@
1
- import { B as BoardSeo } from './seo-CGy4fY5a.js';
2
- import './_spec-CFOj7Qdf.js';
1
+ import { B as BoardSeo } from './seo-BmqJ_ChV.js';
2
+ import './_spec-Dk7QyuoJ.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/seo-files` — platform-owned crawler/identity file bodies
package/dist/seo.d.mts CHANGED
@@ -1,9 +1,9 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-DCRBPnpZ.mjs';
2
- import { e as JsonLdObject } from './breadcrumbs-Dwf0yq7B.mjs';
3
- export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-Dwf0yq7B.mjs';
4
- import './_spec-CFOj7Qdf.mjs';
5
- import './board-DMS3I3sJ.mjs';
6
- import './jobs-Cew_q69W.mjs';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-Oo6YHXSE.mjs';
2
+ import { e as JsonLdObject } from './breadcrumbs-DeIA0RQp.mjs';
3
+ export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-DeIA0RQp.mjs';
4
+ import './_spec-Dk7QyuoJ.mjs';
5
+ import './board-DzBYIwKz.mjs';
6
+ import './jobs-BMRvZlNx.mjs';
7
7
 
8
8
  /**
9
9
  * Blog structured data on the `@cavuno/board` wire types, transcribed from
package/dist/seo.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-BaVJi3q1.js';
2
- import { e as JsonLdObject } from './breadcrumbs-yGijUUtR.js';
3
- export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-yGijUUtR.js';
4
- import './_spec-CFOj7Qdf.js';
5
- import './board-zLt7j6hj.js';
6
- import './jobs-Dz_w1jZq.js';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-DfUNkngR.js';
2
+ import { e as JsonLdObject } from './breadcrumbs-CS0nlknq.js';
3
+ export { A as ALL_COUNTRY_CODES, B as BreadcrumbItemInput, g as FaqItem, J as JobBreadcrumb, h as JsonLdBoard, N as NumberNotation, i as SENIORITY_ORDER, j as SalaryFaqEntry, k as SalaryJsonLdOptions, l as SalaryOccupationListOptions, b as buildJobBreadcrumbs, m as buildSalaryFaq, o as companyCategorySalaryJsonLd, p as companySalaryJsonLd, q as createBreadcrumbJsonLd, r as createJobPostingJsonLd, s as crossAxisSalaryJsonLd, t as faqJsonLd, c as formatSalaryStat, d as formatSalaryStatRange, u as itemListJsonLd, v as locationSalaryJsonLd, n as normalizeWebsiteUrl, w as skillSalaryJsonLd, x as sortBySeniority, y as titleSalaryJsonLd } from './breadcrumbs-CS0nlknq.js';
4
+ import './_spec-Dk7QyuoJ.js';
5
+ import './board-CfmGT0yV.js';
6
+ import './jobs-BlAsTCQl.js';
7
7
 
8
8
  /**
9
9
  * Blog structured data on the `@cavuno/board` wire types, transcribed from
package/dist/server.d.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-CFOj7Qdf.mjs';
3
- import './search-DwvgvtEg.mjs';
4
- import './jobs-Cew_q69W.mjs';
5
- import './board-DMS3I3sJ.mjs';
6
- import './seo-FKj1pSHZ.mjs';
7
- import './blog-DCRBPnpZ.mjs';
2
+ import './_spec-Dk7QyuoJ.mjs';
3
+ import './search-BB1TaaLa.mjs';
4
+ import './jobs-BMRvZlNx.mjs';
5
+ import './board-DzBYIwKz.mjs';
6
+ import './seo-M_8j5vdP.mjs';
7
+ import './blog-Oo6YHXSE.mjs';
8
8
 
9
9
  /**
10
10
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-CFOj7Qdf.js';
3
- import './search-BqAYR47P.js';
4
- import './jobs-Dz_w1jZq.js';
5
- import './board-zLt7j6hj.js';
6
- import './seo-CGy4fY5a.js';
7
- import './blog-BaVJi3q1.js';
2
+ import './_spec-Dk7QyuoJ.js';
3
+ import './search-CoBeDgYT.js';
4
+ import './jobs-BlAsTCQl.js';
5
+ import './board-CfmGT0yV.js';
6
+ import './seo-BmqJ_ChV.js';
7
+ import './blog-DfUNkngR.js';
8
8
 
9
9
  /**
10
10
  * Session cookie codec — pure (no framework imports, no node imports) so it
package/dist/server.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-Z4ALWIGS.mjs";
5
5
  import {
6
6
  isUnauthorized
7
- } from "./chunk-7SAHE7UW.mjs";
7
+ } from "./chunk-UIKN2ERI.mjs";
8
8
 
9
9
  // src/server/cookie.ts
10
10
  var COOKIE_ATTRIBUTES = "Path=/; HttpOnly; Secure; SameSite=Lax";
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-CFOj7Qdf.mjs';
3
- import './search-DwvgvtEg.mjs';
4
- import './jobs-Cew_q69W.mjs';
5
- import './board-DMS3I3sJ.mjs';
6
- import './seo-FKj1pSHZ.mjs';
7
- import './blog-DCRBPnpZ.mjs';
2
+ import './_spec-Dk7QyuoJ.mjs';
3
+ import './search-BB1TaaLa.mjs';
4
+ import './jobs-BMRvZlNx.mjs';
5
+ import './board-DzBYIwKz.mjs';
6
+ import './seo-M_8j5vdP.mjs';
7
+ import './blog-Oo6YHXSE.mjs';
8
8
 
9
9
  /**
10
10
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.js';
2
- import './_spec-CFOj7Qdf.js';
3
- import './search-BqAYR47P.js';
4
- import './jobs-Dz_w1jZq.js';
5
- import './board-zLt7j6hj.js';
6
- import './seo-CGy4fY5a.js';
7
- import './blog-BaVJi3q1.js';
2
+ import './_spec-Dk7QyuoJ.js';
3
+ import './search-CoBeDgYT.js';
4
+ import './jobs-BlAsTCQl.js';
5
+ import './board-CfmGT0yV.js';
6
+ import './seo-BmqJ_ChV.js';
7
+ import './blog-DfUNkngR.js';
8
8
 
9
9
  /**
10
10
  * Sitemap primitives — the pure XML + bucket-filename logic behind a board
package/dist/sitemap.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-DLSO6XXY.mjs";
4
4
  import {
5
5
  isNotFound
6
- } from "./chunk-7SAHE7UW.mjs";
6
+ } from "./chunk-UIKN2ERI.mjs";
7
7
  import {
8
8
  BOARD_PATHS,
9
9
  blogAuthorPath,
@@ -1,5 +1,5 @@
1
- import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-DwvgvtEg.mjs';
2
- import './_spec-CFOj7Qdf.mjs';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BB1TaaLa.mjs';
2
+ import './_spec-Dk7QyuoJ.mjs';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/dist/suggest.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BqAYR47P.js';
2
- import './_spec-CFOj7Qdf.js';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-CoBeDgYT.js';
2
+ import './_spec-Dk7QyuoJ.js';
3
3
 
4
4
  /**
5
5
  * `@cavuno/board/suggest` — framework-agnostic headless search-suggest
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavuno/board",
3
- "version": "4.69.0",
3
+ "version": "4.70.0",
4
4
  "description": "Build custom job boards and careers pages with Cavuno’s TypeScript SDK.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -217,6 +217,22 @@ await board.me.profile.updateCustomFields({
217
217
  });
218
218
  ```
219
219
 
220
+ Company members upload to the company's upload fields the same way, with the company slug first. `customFieldMedia` on `board.me.companies.retrieveCustomFields` lists the saved files.
221
+
222
+ ```ts snippet
223
+ const company = await board.me.companies.retrieveCustomFields('acme');
224
+ const photos = Array.isArray(company.values.office_photos)
225
+ ? company.values.office_photos
226
+ : [];
227
+ const photo = await board.me.companies.uploadCustomFieldFile(
228
+ 'acme',
229
+ 'office_photos',
230
+ file);
231
+ await board.me.companies.updateCustomFields('acme', {
232
+ values: { office_photos: [...photos, photo.id] },
233
+ });
234
+ ```
235
+
220
236
  Resume upload starts an asynchronous parse that may populate the profile. Poll within a fixed budget, surface failure, and keep manual editing available if parsing runs long.
221
237
 
222
238
  ```ts snippet
@@ -32,14 +32,21 @@ const result = await board.jobAlerts.confirm({ token });
32
32
  switch (result.status) {
33
33
  case 'confirmed':
34
34
  case 'already_confirmed':
35
+ case 'waitlisted':
35
36
  case 'not_found':
36
37
  break;
38
+ case 'unsubscribed':
39
+ // Show the manage page's resume option; the subscribe form sends this
40
+ // address no confirmation email.
41
+ break;
37
42
  case 'expired':
38
43
  await board.jobAlerts.resendConfirmation({ email });
39
44
  break;
40
45
  }
41
46
  ```
42
47
 
48
+ `waitlisted` means the alert is confirmed but the board can't send to more people yet; it starts once a spot opens. `unsubscribed` means the link was sent before the alerts were turned off, so it doesn't turn them back on. Subscribing again with the form sends no confirmation email to that address; the person resumes alerts from the manage link in an alert email (`board.jobAlerts.resubscribe`) or by signing in and creating an alert.
49
+
43
50
  Only `jobFunctions`, `placeSlugs`, and `remoteOptions` filter digest delivery. Seniority and salary fields are stored but do not scope delivery. The supported cadence is `weekly`.
44
51
 
45
52
  `jobFunctions` are category and skill slugs (the `slug` of a category or skill, such as `engineering` or `media-and-communication`). A digest matches a job when any entry equals one of the job's category or skill slugs. Prefer sending slugs. If you send display names or free-text keywords instead, the server slugifies each one when the alert is saved and matches it against the board's category and skill slugs, their aliases, and their board-language slugs. A display name that does not slugify to one of those (for example, a renamed category) is stored slugified and will not match, so send slugs whenever you have them. Alerts read back from the API therefore always carry slugs, so compare and display them as slugs.
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: cavuno-board-newsletter
3
+ description: Newsletter sign-up forms with @cavuno/board. Use for an email sign-up for the board's news (footer, blog, landing page), its consent checkbox, the confirmation email, and sendingPaused.
4
+ ---
5
+
6
+ # Newsletter sign-up
7
+
8
+ `board.subscribe` signs an address up for the board's own email (news, offers, updates). It is separate from job alerts (`board.jobAlerts.subscribe`): use it for a form whose promise is "get our news", not "get matching jobs".
9
+
10
+ Every sign-up is double opt-in. The person gets a confirmation email, and nothing is sent to them until they click it. The response is the same whether the address is new, waiting to confirm, or already subscribed, so it never tells a visitor who is on the list.
11
+
12
+ ## The form
13
+
14
+ - An **unticked** checkbox the person must tick. Its wording must name the board, for example "Send me news and offers from Acme Jobs." Send that exact wording as `consentText`; it is kept as the record of what they agreed to, and a `consentText` without the board's name is refused with a 400.
15
+ - A `source` label for the form, such as `blog-footer` or `homepage-hero` (lowercase letters, digits and hyphens). It shows as the person's Source in the board's dashboard, so give each form its own.
16
+ - A hidden `website` field people leave empty. Bots fill it in; the API then answers as usual and does nothing.
17
+
18
+ Read the board's name from `board.context()` rather than typing it.
19
+
20
+ ```ts snippet
21
+ const { name } = await board.context();
22
+ const consentText = `Send me news and offers from ${name}.`;
23
+
24
+ const result = await board.subscribe({
25
+ email,
26
+ consent: true,
27
+ consentText,
28
+ source: 'blog-footer',
29
+ website: honeypotValue,
30
+ });
31
+
32
+ if (result.sendingPaused) {
33
+ // Recorded, but the board's email is paused for now.
34
+ }
35
+ ```
36
+
37
+ `pageUrl` is optional; it defaults to the request's `Referer`. Pass it when the call is made from your server, where the `Referer` is not the visitor's page.
38
+
39
+ ## After submitting
40
+
41
+ Always show the same message: "Check your inbox to confirm your subscription." Never show "you're already subscribed"; the API deliberately doesn't say.
42
+
43
+ When `sendingPaused` is `true`, add a short note that emails from the board may be delayed. The sign-up is still recorded.
44
+
45
+ ## Confirmation
46
+
47
+ The confirmation email links to the board's job alert confirmation page, the same page job alerts use. That page completes the sign-up with `board.jobAlerts.confirm({ token })`; no separate route is needed. `confirmed` and `already_confirmed` mean the person is subscribed, and `not_found` or `expired` mean the link no longer works.
48
+
49
+ The page serves both kinds of link, so word it by `subscription`. A newsletter link answers `subscription: 'marketing'` and never turns job alerts on, so don't tell that person their job alerts are confirmed:
50
+
51
+ ```ts
52
+ const result = await board.jobAlerts.confirm({ token });
53
+ const subscribed =
54
+ result.status === 'confirmed' || result.status === 'already_confirmed';
55
+ const message = !subscribed
56
+ ? 'This link no longer works.'
57
+ : result.subscription === 'marketing'
58
+ ? "You're subscribed to our emails."
59
+ : 'Your job alert is confirmed.';
60
+ ```
61
+
62
+ ## Errors
63
+
64
+ - `validation_bad_request` (400): a field is missing or malformed, `consent` isn't `true`, or `consentText` doesn't name the board.
65
+ - `rate_limited` (429): 3 sign-ups a minute per address across all boards, or 10 requests a minute per IP. A self-hosted site calls from its own server, so all its visitors share that IP limit with every other sign-in and sign-up call from that server; on a builder site Cavuno hosts it counts each visitor separately, plus 6 sign-ups a minute per visitor. `details.retryAfterSeconds` says when to try again.
66
+ - `rate_limit_unavailable` (503): retry shortly.
67
+
68
+ ```ts snippet
69
+ import { BoardApiError } from '@cavuno/board';
70
+
71
+ try {
72
+ await board.subscribe({ email, consent: true, consentText, source: 'footer' });
73
+ } catch (error) {
74
+ if (error instanceof BoardApiError && error.status === 429) {
75
+ // Ask them to try again in a minute.
76
+ }
77
+ throw error;
78
+ }
79
+ ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.69.0",
2
+ "version": "4.70.0",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",
@@ -106,6 +106,13 @@
106
106
  "framework": null,
107
107
  "category": "core"
108
108
  },
109
+ {
110
+ "name": "cavuno-board-newsletter",
111
+ "description": "Newsletter sign-up forms with @cavuno/board. Use for an email sign-up for the board's news (footer, blog, landing page), its consent checkbox, the confirmation email, and sendingPaused.",
112
+ "path": "skills/cavuno-board-newsletter/SKILL.md",
113
+ "framework": null,
114
+ "category": "core"
115
+ },
109
116
  {
110
117
  "name": "cavuno-board-paywall",
111
118
  "description": "Grant-is-truth candidate paywall with @cavuno/board. Use for offers, gated job counts, embedded checkout handoff, access confirmation, or subscription portal links.",