@cavuno/board 4.63.0 → 4.65.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 (50) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/{_spec-D1Qg_Twd.d.mts → _spec-CIufstif.d.mts} +57 -1
  3. package/dist/{_spec-D1Qg_Twd.d.ts → _spec-CIufstif.d.ts} +57 -1
  4. package/dist/{blog-Dp2jAVVM.d.ts → blog-DuNJLfIh.d.ts} +1 -1
  5. package/dist/{blog-e0YbZBjg.d.mts → blog-teDJ9KaO.d.mts} +1 -1
  6. package/dist/{board-Cc3wbTKI.d.mts → board-DZNCHGgl.d.mts} +2 -2
  7. package/dist/{board-BeNZLOYC.d.ts → board-iCHYuNhr.d.ts} +2 -2
  8. package/dist/{breadcrumbs-C5WjB8jm.d.mts → breadcrumbs-Bt3oNIAx.d.mts} +2 -2
  9. package/dist/{breadcrumbs-BOFeIYEX.d.ts → breadcrumbs-DYGo0yB9.d.ts} +2 -2
  10. package/dist/browser/cavuno-board.global.min.js +1 -1
  11. package/dist/{chunk-COHTNSU2.mjs → chunk-H3YIWHXR.mjs} +1 -1
  12. package/dist/chunk-RDHHRDKN.mjs +6 -0
  13. package/dist/doctor.js +1 -1
  14. package/dist/filters.d.mts +2 -2
  15. package/dist/filters.d.ts +2 -2
  16. package/dist/format.d.mts +4 -4
  17. package/dist/format.d.ts +4 -4
  18. package/dist/index.d.mts +15 -14
  19. package/dist/index.d.ts +15 -14
  20. package/dist/index.js +2 -2
  21. package/dist/index.mjs +5 -5
  22. package/dist/{jobs-CClqlUld.d.mts → jobs-CbXbDBij.d.mts} +1 -1
  23. package/dist/{jobs-D04ngjCP.d.ts → jobs-DM_QMccX.d.ts} +1 -1
  24. package/dist/mcp.d.mts +52 -0
  25. package/dist/mcp.d.ts +52 -0
  26. package/dist/mcp.js +150 -0
  27. package/dist/mcp.mjs +128 -0
  28. package/dist/{search-DZeNdYN6.d.ts → search-B7x4qt9A.d.ts} +1 -1
  29. package/dist/{search-BtrwcLkc.d.mts → search-PrqcJMft.d.mts} +1 -1
  30. package/dist/{seo-DLoyZRTo.d.ts → seo-C1Br7ygq.d.ts} +1 -1
  31. package/dist/seo-files.d.mts +2 -2
  32. package/dist/seo-files.d.ts +2 -2
  33. package/dist/{seo-LfpgbdnO.d.mts → seo-ohehUCzN.d.mts} +1 -1
  34. package/dist/seo.d.mts +22 -7
  35. package/dist/seo.d.ts +22 -7
  36. package/dist/seo.js +12 -3
  37. package/dist/seo.mjs +12 -3
  38. package/dist/server.d.mts +6 -6
  39. package/dist/server.d.ts +6 -6
  40. package/dist/server.mjs +1 -1
  41. package/dist/sitemap.d.mts +6 -6
  42. package/dist/sitemap.d.ts +6 -6
  43. package/dist/sitemap.mjs +1 -1
  44. package/dist/skills.js +1 -1
  45. package/dist/suggest.d.mts +2 -2
  46. package/dist/suggest.d.ts +2 -2
  47. package/package.json +13 -2
  48. package/skills/cavuno-board-mcp/SKILL.md +80 -0
  49. package/skills/cavuno-board-seo/BLOG.md +13 -8
  50. package/skills/manifest.json +8 -1
package/dist/seo.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-Dp2jAVVM.js';
2
- import { e as JsonLdObject } from './breadcrumbs-BOFeIYEX.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-BOFeIYEX.js';
4
- import './_spec-D1Qg_Twd.js';
5
- import './board-BeNZLOYC.js';
6
- import './jobs-D04ngjCP.js';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-DuNJLfIh.js';
2
+ import { e as JsonLdObject } from './breadcrumbs-DYGo0yB9.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-DYGo0yB9.js';
4
+ import './_spec-CIufstif.js';
5
+ import './board-iCHYuNhr.js';
6
+ import './jobs-DM_QMccX.js';
7
7
 
8
8
  /**
9
9
  * Blog structured data on the `@cavuno/board` wire types, transcribed from
@@ -28,13 +28,24 @@ type ArticleJsonLdPost = Pick<PublicBlogPostSummary, 'title' | 'customExcerpt' |
28
28
  * first, then `ogImageUrl` — pass your page's OG image URL (hosted passes
29
29
  * its generated `/blog/:slug/og` card; OG generation itself is app-owned
30
30
  * per ).
31
+ *
32
+ * Pass `authorProfileUrl` (the first author's profile page URL) so the
33
+ * `author` is the same `Person` (`@id` + `url`) that
34
+ * `createAuthorProfileJsonLd` declares on that page. Without it the author
35
+ * carries only a name and their website, and nothing links the post to the
36
+ * profile.
31
37
  */
32
- declare function createBlogArticleJsonLd({ post, boardName, permalink, ogImageUrl, }: {
38
+ declare function createBlogArticleJsonLd({ post, boardName, permalink, ogImageUrl, authorProfileUrl, }: {
33
39
  post: ArticleJsonLdPost;
34
40
  boardName: string;
35
41
  permalink: string;
36
42
  /** Fallback image when the post has no cover, e.g. your generated OG card URL. */
37
43
  ogImageUrl?: string | null;
44
+ /**
45
+ * Absolute canonical URL of the first author's profile page: the
46
+ * `canonical` you pass to `createAuthorProfileJsonLd` on that page.
47
+ */
48
+ authorProfileUrl?: string | null;
38
49
  }): JsonLdObject;
39
50
  /**
40
51
  * `ProfilePage` for a blog author page — the hosted shape: a `Person` main
@@ -55,6 +66,10 @@ declare function createAuthorProfileJsonLd({ author, canonical, description, ori
55
66
  origin: string | null;
56
67
  /** The author's recent posts (newest first) — the builder keeps the first 5. */
57
68
  posts: Pick<PublicBlogPostSummary, 'title' | 'slug' | 'publishedAt'>[];
69
+ /**
70
+ * The author's total published post count: the posts list `count` for
71
+ * `authorSlug`, not the length of one page.
72
+ */
58
73
  totalPosts: number;
59
74
  }): JsonLdObject | null;
60
75
 
package/dist/seo.js CHANGED
@@ -50,7 +50,8 @@ function createBlogArticleJsonLd({
50
50
  post,
51
51
  boardName,
52
52
  permalink,
53
- ogImageUrl
53
+ ogImageUrl,
54
+ authorProfileUrl
54
55
  }) {
55
56
  const article = {
56
57
  "@context": "https://schema.org",
@@ -66,7 +67,12 @@ function createBlogArticleJsonLd({
66
67
  if (image) article.image = image;
67
68
  const author = post.authors[0];
68
69
  if (author) {
69
- article.author = {
70
+ article.author = authorProfileUrl ? {
71
+ "@type": "Person",
72
+ "@id": authorProfileId(authorProfileUrl),
73
+ name: author.name,
74
+ url: authorProfileUrl
75
+ } : {
70
76
  "@type": "Person",
71
77
  name: author.name,
72
78
  ...author.websiteUrl ? { url: author.websiteUrl } : {}
@@ -92,7 +98,7 @@ function createAuthorProfileJsonLd({
92
98
  normalizeUrl(author.linkedinUrl),
93
99
  normalizeUrl(author.githubUrl)
94
100
  ].filter((value) => Boolean(value));
95
- const authorId = `${canonical}#profile`;
101
+ const authorId = authorProfileId(canonical);
96
102
  const hasPart = posts.slice(0, 5).map((post) => {
97
103
  const postUrl = buildAbsoluteUrl(origin, `/blog/${post.slug}`);
98
104
  if (!postUrl) {
@@ -134,6 +140,9 @@ function createAuthorProfileJsonLd({
134
140
  }
135
141
  return profilePage;
136
142
  }
143
+ function authorProfileId(authorCanonical) {
144
+ return `${authorCanonical}#profile`;
145
+ }
137
146
  function normalizeUrl(value) {
138
147
  if (!value) {
139
148
  return null;
package/dist/seo.mjs CHANGED
@@ -28,7 +28,8 @@ function createBlogArticleJsonLd({
28
28
  post,
29
29
  boardName,
30
30
  permalink,
31
- ogImageUrl
31
+ ogImageUrl,
32
+ authorProfileUrl
32
33
  }) {
33
34
  const article = {
34
35
  "@context": "https://schema.org",
@@ -44,7 +45,12 @@ function createBlogArticleJsonLd({
44
45
  if (image) article.image = image;
45
46
  const author = post.authors[0];
46
47
  if (author) {
47
- article.author = {
48
+ article.author = authorProfileUrl ? {
49
+ "@type": "Person",
50
+ "@id": authorProfileId(authorProfileUrl),
51
+ name: author.name,
52
+ url: authorProfileUrl
53
+ } : {
48
54
  "@type": "Person",
49
55
  name: author.name,
50
56
  ...author.websiteUrl ? { url: author.websiteUrl } : {}
@@ -70,7 +76,7 @@ function createAuthorProfileJsonLd({
70
76
  normalizeUrl(author.linkedinUrl),
71
77
  normalizeUrl(author.githubUrl)
72
78
  ].filter((value) => Boolean(value));
73
- const authorId = `${canonical}#profile`;
79
+ const authorId = authorProfileId(canonical);
74
80
  const hasPart = posts.slice(0, 5).map((post) => {
75
81
  const postUrl = buildAbsoluteUrl(origin, `/blog/${post.slug}`);
76
82
  if (!postUrl) {
@@ -112,6 +118,9 @@ function createAuthorProfileJsonLd({
112
118
  }
113
119
  return profilePage;
114
120
  }
121
+ function authorProfileId(authorCanonical) {
122
+ return `${authorCanonical}#profile`;
123
+ }
115
124
  function normalizeUrl(value) {
116
125
  if (!value) {
117
126
  return null;
package/dist/server.d.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BoardSdk } from './index.mjs';
2
- import './_spec-D1Qg_Twd.mjs';
3
- import './search-BtrwcLkc.mjs';
4
- import './jobs-CClqlUld.mjs';
5
- import './board-Cc3wbTKI.mjs';
6
- import './seo-LfpgbdnO.mjs';
7
- import './blog-e0YbZBjg.mjs';
2
+ import './_spec-CIufstif.mjs';
3
+ import './search-PrqcJMft.mjs';
4
+ import './jobs-CbXbDBij.mjs';
5
+ import './board-DZNCHGgl.mjs';
6
+ import './seo-ohehUCzN.mjs';
7
+ import './blog-teDJ9KaO.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-D1Qg_Twd.js';
3
- import './search-DZeNdYN6.js';
4
- import './jobs-D04ngjCP.js';
5
- import './board-BeNZLOYC.js';
6
- import './seo-DLoyZRTo.js';
7
- import './blog-Dp2jAVVM.js';
2
+ import './_spec-CIufstif.js';
3
+ import './search-B7x4qt9A.js';
4
+ import './jobs-DM_QMccX.js';
5
+ import './board-iCHYuNhr.js';
6
+ import './seo-C1Br7ygq.js';
7
+ import './blog-DuNJLfIh.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-COHTNSU2.mjs";
7
+ } from "./chunk-H3YIWHXR.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-D1Qg_Twd.mjs';
3
- import './search-BtrwcLkc.mjs';
4
- import './jobs-CClqlUld.mjs';
5
- import './board-Cc3wbTKI.mjs';
6
- import './seo-LfpgbdnO.mjs';
7
- import './blog-e0YbZBjg.mjs';
2
+ import './_spec-CIufstif.mjs';
3
+ import './search-PrqcJMft.mjs';
4
+ import './jobs-CbXbDBij.mjs';
5
+ import './board-DZNCHGgl.mjs';
6
+ import './seo-ohehUCzN.mjs';
7
+ import './blog-teDJ9KaO.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-D1Qg_Twd.js';
3
- import './search-DZeNdYN6.js';
4
- import './jobs-D04ngjCP.js';
5
- import './board-BeNZLOYC.js';
6
- import './seo-DLoyZRTo.js';
7
- import './blog-Dp2jAVVM.js';
2
+ import './_spec-CIufstif.js';
3
+ import './search-B7x4qt9A.js';
4
+ import './jobs-DM_QMccX.js';
5
+ import './board-iCHYuNhr.js';
6
+ import './seo-C1Br7ygq.js';
7
+ import './blog-DuNJLfIh.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-COHTNSU2.mjs";
6
+ } from "./chunk-H3YIWHXR.mjs";
7
7
  import {
8
8
  BOARD_PATHS,
9
9
  blogAuthorPath,
package/dist/skills.js CHANGED
@@ -27,7 +27,7 @@ __export(skills_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(skills_exports);
29
29
 
30
- // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.22_tsx@4.23.1_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
30
+ // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.32_tsx@4.23.1_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
31
31
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
32
32
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
33
33
 
@@ -1,5 +1,5 @@
1
- import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-BtrwcLkc.mjs';
2
- import './_spec-D1Qg_Twd.mjs';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-PrqcJMft.mjs';
2
+ import './_spec-CIufstif.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-DZeNdYN6.js';
2
- import './_spec-D1Qg_Twd.js';
1
+ import { h as SuggestionItem, b as SearchSuggestQuery, F as FetchOptions, g as SuggestResult } from './search-B7x4qt9A.js';
2
+ import './_spec-CIufstif.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.63.0",
3
+ "version": "4.65.0",
4
4
  "description": "Build custom job boards and careers pages with Cavuno’s TypeScript SDK.",
5
5
  "keywords": [
6
6
  "api-client",
@@ -134,6 +134,16 @@
134
134
  "default": "./dist/go.js"
135
135
  }
136
136
  },
137
+ "./mcp": {
138
+ "import": {
139
+ "types": "./dist/mcp.d.mts",
140
+ "default": "./dist/mcp.mjs"
141
+ },
142
+ "require": {
143
+ "types": "./dist/mcp.d.ts",
144
+ "default": "./dist/mcp.js"
145
+ }
146
+ },
137
147
  "./route-contract": {
138
148
  "import": {
139
149
  "types": "./dist/route-contract.d.mts",
@@ -240,7 +250,8 @@
240
250
  "publint": "^0.3.12",
241
251
  "tsup": "^8.4.0",
242
252
  "tsx": "^4.19.2",
243
- "vitest": "^2.1.9",
253
+ "vite": "^8.3.6",
254
+ "vitest": "^5.1.0",
244
255
  "typescript": "^5.9.3",
245
256
  "@typescript/native-preview": "7.0.0-dev.20260707.2"
246
257
  },
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: cavuno-board-mcp
3
+ description: Serve the board's public MCP server at /mcp with @cavuno/board/mcp. Use when connecting a self-hosted board to Claude, ChatGPT, Cursor or another MCP client, or when /mcp or /mcp/docs returns 404 on the board's host.
4
+ ---
5
+
6
+ # Board MCP server
7
+
8
+ Every paid Cavuno board has a public, read-only MCP server: AI assistants can
9
+ search its jobs, read job and company pages, and look up salaries. Boards that
10
+ Cavuno hosts serve it at `/mcp` on their own domain already. A self-hosted
11
+ board serves it by mounting `createMcpHandler` from `@cavuno/board/mcp`, which
12
+ forwards to the Board API, so the tools, limits and setup page match every
13
+ other Cavuno board.
14
+
15
+ ## Mount the handler
16
+
17
+ Mount one handler at `/mcp`, `/mcp/*` and `/.well-known/ai-catalog.json`, for
18
+ GET, HEAD and POST. Pass
19
+ the board's publishable key: job and company links in tool results then point
20
+ at the key's own origin.
21
+
22
+ ```ts snippet
23
+ import { createMcpHandler } from '@cavuno/board/mcp';
24
+
25
+ // app/mcp/[[...path]]/route.ts, and the same lines in
26
+ // app/.well-known/ai-catalog.json/route.ts
27
+ const handler = createMcpHandler({
28
+ board: process.env.NEXT_PUBLIC_CAVUNO_BOARD_KEY!,
29
+ });
30
+ export const GET = handler;
31
+ export const HEAD = handler;
32
+ export const POST = handler;
33
+ ```
34
+
35
+ Other frameworks mount the same `(request: Request) => Promise<Response>`
36
+ function on each path. Do not add authentication, cookies or caching in front
37
+ of `/mcp`: every tool is an anonymous read, and MCP clients POST without
38
+ credentials.
39
+
40
+ ## What each path answers
41
+
42
+ - `POST /mcp`: the stateless Streamable HTTP transport.
43
+ - `GET /mcp` from a browser: a redirect to `/mcp/docs`. Any other non-POST
44
+ request answers 405.
45
+ - `GET /mcp/docs`: an HTML page listing the tools and how to add the server
46
+ to each assistant. Use this URL as the documentation link in a Claude or
47
+ ChatGPT directory submission.
48
+ - `GET /mcp/server-card`: the MCP Server Card, the server's name, version,
49
+ title, icons and URL as `application/mcp-server-card+json`, for an
50
+ assistant to read before it connects.
51
+ - `GET /.well-known/ai-catalog.json`: the AI Catalog, which lists the Server
52
+ Card so an assistant that knows only the board's domain can find the server.
53
+
54
+ A free-plan board, a password-protected board, or a board whose operator has
55
+ switched the server off answers 404 on every path.
56
+
57
+ ## Limits
58
+
59
+ The Board API allows 1,000 MCP requests a minute per visitor address. Requests
60
+ forwarded from a self-hosted server arrive from that server's address, so the
61
+ limit applies to the board's MCP traffic as a whole. Over the limit, `/mcp`
62
+ answers 429 with `Retry-After`.
63
+
64
+ ## Completion gate
65
+
66
+ Finish only after every check passes against the deployed board:
67
+
68
+ - `POST /mcp` with `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`,
69
+ `Content-Type: application/json` and
70
+ `Accept: application/json, text/event-stream` returns the tool list.
71
+ - Opening `/mcp` in a browser lands on `/mcp/docs`, and that page names the
72
+ board and shows `https://<board-host>/mcp` as the server URL.
73
+ - `GET /.well-known/ai-catalog.json` returns an entry whose `url` is
74
+ `https://<board-host>/mcp/server-card`, and that card's `remotes` URL is
75
+ `https://<board-host>/mcp`.
76
+ - A job URL in a `search_jobs` result opens on the board's own host.
77
+
78
+ ## Cavuno SDK reference
79
+
80
+ For setup and API details beyond this workflow, use the [Cavuno Board SDK documentation](https://cavuno.com/docs/sdk).
@@ -10,17 +10,19 @@ import {
10
10
 
11
11
  const { name } = await board.context();
12
12
  const post = await board.blog.posts.retrieve('hello-world');
13
+ const [firstAuthor] = post.authors;
13
14
  const article = createBlogArticleJsonLd({
14
15
  post,
15
16
  boardName: name,
16
17
  permalink: `https://jobs.example.com/blog/${post.slug}`,
17
18
  ogImageUrl: `https://jobs.example.com/blog/${post.slug}/og`,
19
+ authorProfileUrl: firstAuthor
20
+ ? `https://jobs.example.com/blog/author/${firstAuthor.slug}`
21
+ : null,
18
22
  });
19
23
 
20
24
  const author = await board.blog.authors.retrieve('jane');
21
- const { data: authorPosts } = await board.blog.posts.list({
22
- authorSlug: 'jane',
23
- });
25
+ const authorPosts = await board.blog.posts.list({ authorSlug: 'jane' });
24
26
  // Profile description is application-owned. Prefer author.bio; otherwise
25
27
  // compose from the board catalog (shape only — not an English sentence).
26
28
  const profile = createAuthorProfileJsonLd({
@@ -29,14 +31,17 @@ const profile = createAuthorProfileJsonLd({
29
31
  description:
30
32
  author.bio ?? messages.authorProfileDescription({ name: author.name }),
31
33
  origin: 'https://jobs.example.com',
32
- posts: authorPosts,
33
- totalPosts: authorPosts.length,
34
+ posts: authorPosts.data,
35
+ // The author's total, not this page's length.
36
+ totalPosts: authorPosts.count ?? authorPosts.data.length,
34
37
  });
35
38
  ```
36
39
 
37
- The article `ogImageUrl` is the fallback when the post has no cover. Supply
38
- author posts newest-first; the profile builder keeps the first five as
39
- `hasPart`.
40
+ The article `ogImageUrl` is the fallback when the post has no cover. Pass
41
+ `authorProfileUrl` as the same URL you give the profile's `canonical`, so the
42
+ post's author and the profile's `Person` share one `@id`. Supply author posts
43
+ newest-first; the profile builder keeps the first five as `hasPart`. The list's
44
+ `count` is the author's total published posts; `data` is one page.
40
45
 
41
46
  This branch is complete when the post emits an `Article`, the author emits a
42
47
  `ProfilePage`, every URL is absolute and canonical, and a null builder result
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.63.0",
2
+ "version": "4.65.0",
3
3
  "skills": [
4
4
  {
5
5
  "name": "cavuno-board-account",
@@ -92,6 +92,13 @@
92
92
  "framework": null,
93
93
  "category": "core"
94
94
  },
95
+ {
96
+ "name": "cavuno-board-mcp",
97
+ "description": "Serve the board's public MCP server at /mcp with @cavuno/board/mcp. Use when connecting a self-hosted board to Claude, ChatGPT, Cursor or another MCP client, or when /mcp or /mcp/docs returns 404 on the board's host.",
98
+ "path": "skills/cavuno-board-mcp/SKILL.md",
99
+ "framework": null,
100
+ "category": "core"
101
+ },
95
102
  {
96
103
  "name": "cavuno-board-messaging",
97
104
  "description": "Polled messaging contract with @cavuno/board. Use for inboxes, unread badges, threads, read receipts, message moderation, or user blocks.",