@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.
- package/CHANGELOG.md +57 -0
- package/dist/{_spec-D1Qg_Twd.d.mts → _spec-DBI6azWs.d.mts} +96 -2
- package/dist/{_spec-D1Qg_Twd.d.ts → _spec-DBI6azWs.d.ts} +96 -2
- package/dist/{blog-Dp2jAVVM.d.ts → blog--VKuBIdw.d.ts} +1 -1
- package/dist/{blog-e0YbZBjg.d.mts → blog-Bx-rjH4V.d.mts} +1 -1
- package/dist/{board-Cc3wbTKI.d.mts → board-BsUhxdjL.d.mts} +2 -2
- package/dist/{board-BeNZLOYC.d.ts → board-USJ9ZZab.d.ts} +2 -2
- package/dist/{breadcrumbs-C5WjB8jm.d.mts → breadcrumbs-BVN_gIJq.d.mts} +2 -2
- package/dist/{breadcrumbs-BOFeIYEX.d.ts → breadcrumbs-maV86vZP.d.ts} +2 -2
- package/dist/browser/cavuno-board.global.min.js +1 -1
- package/dist/{chunk-H3YIWHXR.mjs → chunk-R62XFNS7.mjs} +1 -0
- package/dist/chunk-RDHHRDKN.mjs +6 -0
- package/dist/doctor.js +1 -1
- package/dist/filters.d.mts +2 -2
- package/dist/filters.d.ts +2 -2
- package/dist/format.d.mts +4 -4
- package/dist/format.d.ts +4 -4
- package/dist/index.d.mts +46 -18
- package/dist/index.d.ts +46 -18
- package/dist/index.js +19 -1
- package/dist/index.mjs +22 -5
- package/dist/{jobs-D04ngjCP.d.ts → jobs-Dyj1HZ8W.d.ts} +1 -1
- package/dist/{jobs-CClqlUld.d.mts → jobs-qoVMh_Zd.d.mts} +1 -1
- package/dist/mcp.d.mts +52 -0
- package/dist/mcp.d.ts +52 -0
- package/dist/mcp.js +150 -0
- package/dist/mcp.mjs +128 -0
- package/dist/{search-BtrwcLkc.d.mts → search-CrXCM8Hq.d.mts} +1 -1
- package/dist/{search-DZeNdYN6.d.ts → search-DPHLf8pI.d.ts} +1 -1
- package/dist/{seo-DLoyZRTo.d.ts → seo-BDwe8PwT.d.ts} +1 -1
- package/dist/{seo-LfpgbdnO.d.mts → seo-DRVgmTCQ.d.mts} +1 -1
- package/dist/seo-files.d.mts +2 -2
- package/dist/seo-files.d.ts +2 -2
- package/dist/seo.d.mts +22 -7
- package/dist/seo.d.ts +22 -7
- package/dist/seo.js +12 -3
- package/dist/seo.mjs +12 -3
- package/dist/server.d.mts +6 -6
- package/dist/server.d.ts +6 -6
- package/dist/server.mjs +1 -1
- package/dist/sitemap.d.mts +6 -6
- package/dist/sitemap.d.ts +6 -6
- package/dist/sitemap.mjs +1 -1
- package/dist/skills.js +1 -1
- package/dist/suggest.d.mts +2 -2
- package/dist/suggest.d.ts +2 -2
- package/package.json +13 -2
- package/skills/cavuno-board-filters/SKILL.md +12 -4
- package/skills/cavuno-board-mcp/SKILL.md +80 -0
- package/skills/cavuno-board-post-a-job/SKILL.md +54 -5
- package/skills/cavuno-board-seo/BLOG.md +13 -8
- 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
|
+
};
|
package/dist/seo-files.d.mts
CHANGED
package/dist/seo-files.d.ts
CHANGED
package/dist/seo.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { P as PublicBlogPostSummary, B as BlogAuthorEmbed } from './blog-
|
|
2
|
-
import { e as JsonLdObject } from './breadcrumbs-
|
|
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-
|
|
4
|
-
import './_spec-
|
|
5
|
-
import './board-
|
|
6
|
-
import './jobs-
|
|
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
|
|
2
|
-
import { e as JsonLdObject } from './breadcrumbs-
|
|
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-
|
|
4
|
-
import './_spec-
|
|
5
|
-
import './board-
|
|
6
|
-
import './jobs-
|
|
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 =
|
|
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 =
|
|
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-
|
|
3
|
-
import './search-
|
|
4
|
-
import './jobs-
|
|
5
|
-
import './board-
|
|
6
|
-
import './seo-
|
|
7
|
-
import './blog-
|
|
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-
|
|
3
|
-
import './search-
|
|
4
|
-
import './jobs-
|
|
5
|
-
import './board-
|
|
6
|
-
import './seo-
|
|
7
|
-
import './blog
|
|
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
package/dist/sitemap.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BoardSdk } from './index.mjs';
|
|
2
|
-
import './_spec-
|
|
3
|
-
import './search-
|
|
4
|
-
import './jobs-
|
|
5
|
-
import './board-
|
|
6
|
-
import './seo-
|
|
7
|
-
import './blog-
|
|
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-
|
|
3
|
-
import './search-
|
|
4
|
-
import './jobs-
|
|
5
|
-
import './board-
|
|
6
|
-
import './seo-
|
|
7
|
-
import './blog
|
|
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
|