@cavuno/board 4.64.0 → 4.66.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 (52) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/{_spec-D1Qg_Twd.d.mts → _spec-DBI6azWs.d.mts} +96 -2
  3. package/dist/{_spec-D1Qg_Twd.d.ts → _spec-DBI6azWs.d.ts} +96 -2
  4. package/dist/{blog-Dp2jAVVM.d.ts → blog--VKuBIdw.d.ts} +1 -1
  5. package/dist/{blog-e0YbZBjg.d.mts → blog-Bx-rjH4V.d.mts} +1 -1
  6. package/dist/{board-Cc3wbTKI.d.mts → board-BsUhxdjL.d.mts} +2 -2
  7. package/dist/{board-BeNZLOYC.d.ts → board-USJ9ZZab.d.ts} +2 -2
  8. package/dist/{breadcrumbs-C5WjB8jm.d.mts → breadcrumbs-BVN_gIJq.d.mts} +2 -2
  9. package/dist/{breadcrumbs-BOFeIYEX.d.ts → breadcrumbs-maV86vZP.d.ts} +2 -2
  10. package/dist/browser/cavuno-board.global.min.js +1 -1
  11. package/dist/{chunk-H3YIWHXR.mjs → chunk-R62XFNS7.mjs} +1 -0
  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 +46 -18
  19. package/dist/index.d.ts +46 -18
  20. package/dist/index.js +19 -1
  21. package/dist/index.mjs +22 -5
  22. package/dist/{jobs-D04ngjCP.d.ts → jobs-Dyj1HZ8W.d.ts} +1 -1
  23. package/dist/{jobs-CClqlUld.d.mts → jobs-qoVMh_Zd.d.mts} +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-BtrwcLkc.d.mts → search-CrXCM8Hq.d.mts} +1 -1
  29. package/dist/{search-DZeNdYN6.d.ts → search-DPHLf8pI.d.ts} +1 -1
  30. package/dist/{seo-DLoyZRTo.d.ts → seo-BDwe8PwT.d.ts} +1 -1
  31. package/dist/{seo-LfpgbdnO.d.mts → seo-DRVgmTCQ.d.mts} +1 -1
  32. package/dist/seo-files.d.mts +2 -2
  33. package/dist/seo-files.d.ts +2 -2
  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-filters/SKILL.md +12 -4
  49. package/skills/cavuno-board-mcp/SKILL.md +80 -0
  50. package/skills/cavuno-board-post-a-job/SKILL.md +54 -5
  51. package/skills/cavuno-board-seo/BLOG.md +13 -8
  52. package/skills/manifest.json +8 -1
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The board's public MCP server on a self-hosted board's own host.
3
+ *
4
+ * Mount the handler at `/mcp`, `/mcp/*` and `/.well-known/ai-catalog.json`.
5
+ * It forwards to the Board API's `POST /v1/boards/{board}/mcp` and its
6
+ * `GET /v1/boards/{board}/mcp/docs`, `/mcp/server-card` and `/mcp/ai-catalog`,
7
+ * the same endpoints Cavuno-hosted boards answer from, so the tools, limits,
8
+ * setup page and discovery documents stay identical everywhere:
9
+ *
10
+ * - `POST /mcp` is the stateless Streamable HTTP transport.
11
+ * - A browser opening `/mcp` is sent to `/mcp/docs`; any other method is 405.
12
+ * - `GET|HEAD /mcp/docs` is the HTML setup page listed in AI directories.
13
+ * - `GET|HEAD /mcp/server-card` is the MCP Server Card: the server's name,
14
+ * version and URL, for an assistant to read before it connects.
15
+ * - `GET|HEAD /.well-known/ai-catalog.json` is the AI Catalog listing that
16
+ * card, so an assistant can find the server from the board's domain alone.
17
+ *
18
+ * Only the MCP transport headers are forwarded: no cookies and no
19
+ * authorization, because every tool is an anonymous read. The Board API's
20
+ * per-visitor limit sees this server's address, so it applies to all of the
21
+ * board's MCP traffic together.
22
+ *
23
+ * Framework-portable: standard Request in, standard Response out.
24
+ */
25
+ interface CreateMcpHandlerConfig {
26
+ /**
27
+ * The board's publishable key (`pk_…`). Job and company links in tool
28
+ * results then point at the key's own origin, this board.
29
+ */
30
+ board: string;
31
+ /** Board API origin. Defaults to `https://api.cavuno.com`. */
32
+ baseUrl?: string;
33
+ }
34
+ /**
35
+ * Create the `(request) => Response` handler for `/mcp`, `/mcp/docs`,
36
+ * `/mcp/server-card` and `/.well-known/ai-catalog.json`.
37
+ *
38
+ * @example
39
+ * // Next.js App Router: app/mcp/[[...path]]/route.ts and
40
+ * // app/.well-known/ai-catalog.json/route.ts, both exporting:
41
+ * import { createMcpHandler } from '@cavuno/board/mcp';
42
+ *
43
+ * const handler = createMcpHandler({
44
+ * board: process.env.NEXT_PUBLIC_CAVUNO_BOARD_KEY!,
45
+ * });
46
+ * export const GET = handler;
47
+ * export const HEAD = handler;
48
+ * export const POST = handler;
49
+ */
50
+ declare function createMcpHandler(config: CreateMcpHandlerConfig): (request: Request) => Promise<Response>;
51
+
52
+ export { type CreateMcpHandlerConfig, createMcpHandler };
package/dist/mcp.js ADDED
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/mcp/index.ts
21
+ var mcp_exports = {};
22
+ __export(mcp_exports, {
23
+ createMcpHandler: () => createMcpHandler
24
+ });
25
+ module.exports = __toCommonJS(mcp_exports);
26
+
27
+ // src/constants.ts
28
+ var DEFAULT_CAVUNO_API_URL = "https://api.cavuno.com";
29
+
30
+ // src/mcp/create-mcp-handler.ts
31
+ var DOCUMENT_UPSTREAMS = [
32
+ ["/mcp/docs", "mcp/docs"],
33
+ ["/mcp/server-card", "mcp/server-card"],
34
+ ["/.well-known/ai-catalog.json", "mcp/ai-catalog"]
35
+ ];
36
+ var MAX_BODY_BYTES = 64 * 1024;
37
+ var UPSTREAM_TIMEOUT_MS = 3e4;
38
+ var FORWARDED_HEADERS = [
39
+ "content-type",
40
+ "accept",
41
+ "mcp-protocol-version",
42
+ "user-agent"
43
+ ];
44
+ function jsonRpcError(status, message, headers = {}) {
45
+ return new Response(
46
+ JSON.stringify({
47
+ jsonrpc: "2.0",
48
+ id: null,
49
+ error: { code: -32e3, message }
50
+ }),
51
+ {
52
+ status,
53
+ headers: { "content-type": "application/json", ...headers }
54
+ }
55
+ );
56
+ }
57
+ async function readCappedBody(request) {
58
+ if (Number(request.headers.get("content-length")) > MAX_BODY_BYTES) {
59
+ return null;
60
+ }
61
+ if (!request.body) return new Uint8Array();
62
+ const reader = request.body.getReader();
63
+ const chunks = [];
64
+ let size = 0;
65
+ for (; ; ) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+ size += value.byteLength;
69
+ if (size > MAX_BODY_BYTES) {
70
+ await reader.cancel();
71
+ return null;
72
+ }
73
+ chunks.push(value);
74
+ }
75
+ const body = new Uint8Array(size);
76
+ let offset = 0;
77
+ for (const chunk of chunks) {
78
+ body.set(chunk, offset);
79
+ offset += chunk.byteLength;
80
+ }
81
+ return body;
82
+ }
83
+ function createMcpHandler(config) {
84
+ const base = `${(config.baseUrl ?? DEFAULT_CAVUNO_API_URL).replace(/\/+$/, "")}/v1/boards/${encodeURIComponent(config.board)}`;
85
+ return async function mcpHandler(request) {
86
+ const { pathname } = new URL(request.url);
87
+ const path = pathname.replace(/\/+$/, "");
88
+ const method = request.method;
89
+ let upstream;
90
+ const document = DOCUMENT_UPSTREAMS.find(
91
+ ([suffix]) => path.endsWith(suffix)
92
+ );
93
+ if (document) {
94
+ if (method !== "GET" && method !== "HEAD") {
95
+ return new Response(null, {
96
+ status: 405,
97
+ headers: { allow: "GET, HEAD" }
98
+ });
99
+ }
100
+ upstream = `${base}/${document[1]}`;
101
+ } else if (!path.endsWith("/mcp")) {
102
+ return new Response(null, { status: 404 });
103
+ } else {
104
+ const accept = request.headers.get("accept") ?? "";
105
+ if (method === "GET" && accept.includes("text/html") && !accept.includes("text/event-stream")) {
106
+ return new Response(null, {
107
+ status: 302,
108
+ headers: { location: `${path}/docs`, "cache-control": "no-store" }
109
+ });
110
+ }
111
+ if (method !== "POST") {
112
+ return jsonRpcError(405, "Method not allowed.", { allow: "POST" });
113
+ }
114
+ upstream = `${base}/mcp`;
115
+ }
116
+ let body;
117
+ if (method === "POST") {
118
+ const read = await readCappedBody(request);
119
+ if (!read) return jsonRpcError(413, "Request body too large.");
120
+ body = read;
121
+ }
122
+ const headers = new Headers();
123
+ for (const name of FORWARDED_HEADERS) {
124
+ const value = request.headers.get(name);
125
+ if (value) headers.set(name, value);
126
+ }
127
+ let response;
128
+ try {
129
+ response = await fetch(upstream, {
130
+ method,
131
+ headers,
132
+ body,
133
+ redirect: "manual",
134
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
135
+ });
136
+ } catch {
137
+ return jsonRpcError(503, "The board MCP server is unavailable.", {
138
+ "retry-after": "30"
139
+ });
140
+ }
141
+ const outHeaders = new Headers(response.headers);
142
+ outHeaders.delete("set-cookie");
143
+ outHeaders.delete("content-encoding");
144
+ outHeaders.delete("content-length");
145
+ return new Response(response.body, {
146
+ status: response.status,
147
+ headers: outHeaders
148
+ });
149
+ };
150
+ }
package/dist/mcp.mjs ADDED
@@ -0,0 +1,128 @@
1
+ import {
2
+ DEFAULT_CAVUNO_API_URL
3
+ } from "./chunk-RDHHRDKN.mjs";
4
+
5
+ // src/mcp/create-mcp-handler.ts
6
+ var DOCUMENT_UPSTREAMS = [
7
+ ["/mcp/docs", "mcp/docs"],
8
+ ["/mcp/server-card", "mcp/server-card"],
9
+ ["/.well-known/ai-catalog.json", "mcp/ai-catalog"]
10
+ ];
11
+ var MAX_BODY_BYTES = 64 * 1024;
12
+ var UPSTREAM_TIMEOUT_MS = 3e4;
13
+ var FORWARDED_HEADERS = [
14
+ "content-type",
15
+ "accept",
16
+ "mcp-protocol-version",
17
+ "user-agent"
18
+ ];
19
+ function jsonRpcError(status, message, headers = {}) {
20
+ return new Response(
21
+ JSON.stringify({
22
+ jsonrpc: "2.0",
23
+ id: null,
24
+ error: { code: -32e3, message }
25
+ }),
26
+ {
27
+ status,
28
+ headers: { "content-type": "application/json", ...headers }
29
+ }
30
+ );
31
+ }
32
+ async function readCappedBody(request) {
33
+ if (Number(request.headers.get("content-length")) > MAX_BODY_BYTES) {
34
+ return null;
35
+ }
36
+ if (!request.body) return new Uint8Array();
37
+ const reader = request.body.getReader();
38
+ const chunks = [];
39
+ let size = 0;
40
+ for (; ; ) {
41
+ const { done, value } = await reader.read();
42
+ if (done) break;
43
+ size += value.byteLength;
44
+ if (size > MAX_BODY_BYTES) {
45
+ await reader.cancel();
46
+ return null;
47
+ }
48
+ chunks.push(value);
49
+ }
50
+ const body = new Uint8Array(size);
51
+ let offset = 0;
52
+ for (const chunk of chunks) {
53
+ body.set(chunk, offset);
54
+ offset += chunk.byteLength;
55
+ }
56
+ return body;
57
+ }
58
+ function createMcpHandler(config) {
59
+ const base = `${(config.baseUrl ?? DEFAULT_CAVUNO_API_URL).replace(/\/+$/, "")}/v1/boards/${encodeURIComponent(config.board)}`;
60
+ return async function mcpHandler(request) {
61
+ const { pathname } = new URL(request.url);
62
+ const path = pathname.replace(/\/+$/, "");
63
+ const method = request.method;
64
+ let upstream;
65
+ const document = DOCUMENT_UPSTREAMS.find(
66
+ ([suffix]) => path.endsWith(suffix)
67
+ );
68
+ if (document) {
69
+ if (method !== "GET" && method !== "HEAD") {
70
+ return new Response(null, {
71
+ status: 405,
72
+ headers: { allow: "GET, HEAD" }
73
+ });
74
+ }
75
+ upstream = `${base}/${document[1]}`;
76
+ } else if (!path.endsWith("/mcp")) {
77
+ return new Response(null, { status: 404 });
78
+ } else {
79
+ const accept = request.headers.get("accept") ?? "";
80
+ if (method === "GET" && accept.includes("text/html") && !accept.includes("text/event-stream")) {
81
+ return new Response(null, {
82
+ status: 302,
83
+ headers: { location: `${path}/docs`, "cache-control": "no-store" }
84
+ });
85
+ }
86
+ if (method !== "POST") {
87
+ return jsonRpcError(405, "Method not allowed.", { allow: "POST" });
88
+ }
89
+ upstream = `${base}/mcp`;
90
+ }
91
+ let body;
92
+ if (method === "POST") {
93
+ const read = await readCappedBody(request);
94
+ if (!read) return jsonRpcError(413, "Request body too large.");
95
+ body = read;
96
+ }
97
+ const headers = new Headers();
98
+ for (const name of FORWARDED_HEADERS) {
99
+ const value = request.headers.get(name);
100
+ if (value) headers.set(name, value);
101
+ }
102
+ let response;
103
+ try {
104
+ response = await fetch(upstream, {
105
+ method,
106
+ headers,
107
+ body,
108
+ redirect: "manual",
109
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
110
+ });
111
+ } catch {
112
+ return jsonRpcError(503, "The board MCP server is unavailable.", {
113
+ "retry-after": "30"
114
+ });
115
+ }
116
+ const outHeaders = new Headers(response.headers);
117
+ outHeaders.delete("set-cookie");
118
+ outHeaders.delete("content-encoding");
119
+ outHeaders.delete("content-length");
120
+ return new Response(response.body, {
121
+ status: response.status,
122
+ headers: outHeaders
123
+ });
124
+ };
125
+ }
126
+ export {
127
+ createMcpHandler
128
+ };
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.mjs';
1
+ import { S as Schemas } from './_spec-DBI6azWs.mjs';
2
2
 
3
3
  type Awaitable<T> = T | Promise<T>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.js';
1
+ import { S as Schemas } from './_spec-DBI6azWs.js';
2
2
 
3
3
  type Awaitable<T> = T | Promise<T>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { S as Schemas } from './_spec-D1Qg_Twd.js';
1
+ import { S as Schemas } from './_spec-DBI6azWs.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-D1Qg_Twd.mjs';
1
+ import { S as Schemas } from './_spec-DBI6azWs.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-LfpgbdnO.mjs';
2
- import './_spec-D1Qg_Twd.mjs';
1
+ import { B as BoardSeo } from './seo-DRVgmTCQ.mjs';
2
+ import './_spec-DBI6azWs.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-DLoyZRTo.js';
2
- import './_spec-D1Qg_Twd.js';
1
+ import { B as BoardSeo } from './seo-BDwe8PwT.js';
2
+ import './_spec-DBI6azWs.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-e0YbZBjg.mjs';
2
- import { e as JsonLdObject } from './breadcrumbs-C5WjB8jm.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-C5WjB8jm.mjs';
4
- import './_spec-D1Qg_Twd.mjs';
5
- import './board-Cc3wbTKI.mjs';
6
- import './jobs-CClqlUld.mjs';
1
+ import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-Bx-rjH4V.mjs';
2
+ import { e as JsonLdObject } from './breadcrumbs-BVN_gIJq.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-BVN_gIJq.mjs';
4
+ import './_spec-DBI6azWs.mjs';
5
+ import './board-BsUhxdjL.mjs';
6
+ import './jobs-qoVMh_Zd.mjs';
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.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--VKuBIdw.js';
2
+ import { e as JsonLdObject } from './breadcrumbs-maV86vZP.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-maV86vZP.js';
4
+ import './_spec-DBI6azWs.js';
5
+ import './board-USJ9ZZab.js';
6
+ import './jobs-Dyj1HZ8W.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-DBI6azWs.mjs';
3
+ import './search-CrXCM8Hq.mjs';
4
+ import './jobs-qoVMh_Zd.mjs';
5
+ import './board-BsUhxdjL.mjs';
6
+ import './seo-DRVgmTCQ.mjs';
7
+ import './blog-Bx-rjH4V.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-DBI6azWs.js';
3
+ import './search-DPHLf8pI.js';
4
+ import './jobs-Dyj1HZ8W.js';
5
+ import './board-USJ9ZZab.js';
6
+ import './seo-BDwe8PwT.js';
7
+ import './blog--VKuBIdw.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-H3YIWHXR.mjs";
7
+ } from "./chunk-R62XFNS7.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-DBI6azWs.mjs';
3
+ import './search-CrXCM8Hq.mjs';
4
+ import './jobs-qoVMh_Zd.mjs';
5
+ import './board-BsUhxdjL.mjs';
6
+ import './seo-DRVgmTCQ.mjs';
7
+ import './blog-Bx-rjH4V.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-DBI6azWs.js';
3
+ import './search-DPHLf8pI.js';
4
+ import './jobs-Dyj1HZ8W.js';
5
+ import './board-USJ9ZZab.js';
6
+ import './seo-BDwe8PwT.js';
7
+ import './blog--VKuBIdw.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-H3YIWHXR.mjs";
6
+ } from "./chunk-R62XFNS7.mjs";
7
7
  import {
8
8
  BOARD_PATHS,
9
9
  blogAuthorPath,