@gusnips/vite 0.8.43 → 0.8.44
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/README.md +81 -0
- package/dist/discovery.d.ts +76 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +37 -0
- package/dist/discovery.js.map +1 -0
- package/dist/head.d.ts +6 -6
- package/dist/head.d.ts.map +1 -1
- package/dist/head.js +11 -10
- package/dist/head.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/discovery.test.ts +120 -0
- package/src/discovery.ts +103 -0
- package/src/head.test.ts +41 -11
- package/src/head.ts +12 -10
- package/src/index.ts +12 -0
package/README.md
CHANGED
|
@@ -63,6 +63,87 @@ Four rules in there are load-bearing:
|
|
|
63
63
|
points back at the others, which is why you pass the full list to every page rather than "the
|
|
64
64
|
other two".
|
|
65
65
|
|
|
66
|
+
## Discovery files
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { jsonLd } from "@gusnips/vite";
|
|
70
|
+
|
|
71
|
+
jsonLd({ "@type": "SoftwareApplication", name: "Example" });
|
|
72
|
+
// A <script type="application/ld+json"> tag containing the object.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pass the result to `bakeHead` as `headExtra`. `jsonLd` escapes HTML control text while keeping
|
|
76
|
+
valid JSON. `faqJsonLd` uses the same serializer. You supply the schema.org fields, including
|
|
77
|
+
`SoftwareApplication` for an app or `WebAPI` for a public API. A local command is not a public API.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { llmsTxt, mcpDiscoveryJson, writeDist } from "@gusnips/vite";
|
|
81
|
+
|
|
82
|
+
await writeDist(
|
|
83
|
+
"dist",
|
|
84
|
+
"llms.txt",
|
|
85
|
+
llmsTxt({
|
|
86
|
+
title: "Example",
|
|
87
|
+
summary: "Read and update tasks.",
|
|
88
|
+
sections: [
|
|
89
|
+
{
|
|
90
|
+
title: "Reference",
|
|
91
|
+
links: [{ title: "Connect", url: "https://example.com/docs/mcp" }],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await writeDist(
|
|
98
|
+
"dist",
|
|
99
|
+
"llms-full.txt",
|
|
100
|
+
llmsTxt({
|
|
101
|
+
title: "Example reference",
|
|
102
|
+
body: referenceMarkdown,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await writeDist(
|
|
107
|
+
"dist",
|
|
108
|
+
".well-known/mcp.json",
|
|
109
|
+
mcpDiscoveryJson({
|
|
110
|
+
name: "Example",
|
|
111
|
+
homepage: "https://example.com",
|
|
112
|
+
documentation: "https://example.com/docs/mcp",
|
|
113
|
+
llmsTxt: "https://example.com/llms.txt",
|
|
114
|
+
llmsFullTxt: "https://example.com/llms-full.txt",
|
|
115
|
+
servers: [
|
|
116
|
+
{
|
|
117
|
+
name: "tasks",
|
|
118
|
+
transport: "streamable-http",
|
|
119
|
+
url: "https://api.example.com/mcp",
|
|
120
|
+
authentication: [
|
|
121
|
+
{
|
|
122
|
+
type: "oauth2",
|
|
123
|
+
protectedResourceMetadata:
|
|
124
|
+
"https://api.example.com/.well-known/oauth-protected-resource/mcp",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
tools: ["list_tasks", "update_task"],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Build `referenceMarkdown` from your existing docs. The formatter adds no content or access rules.
|
|
135
|
+
Each server lists only its own tools, as names or objects with `name`, `title`, `description`,
|
|
136
|
+
`inputSchema` and `annotations`. Supplied schemas and annotations are kept unchanged.
|
|
137
|
+
|
|
138
|
+
For a local server, use `transport: "stdio"`, `command` and optional `args` instead of `url`.
|
|
139
|
+
`authentication` is a list of accepted methods: `oauth2` with `protectedResourceMetadata`, `api-key`
|
|
140
|
+
with `in` (`header` or `environment`), `name` and optional `prefix`, `session` with a `description`,
|
|
141
|
+
or `none` for public access. Methods can also include `description` and `documentation`.
|
|
142
|
+
|
|
143
|
+
`/.well-known/mcp.json` is discovery metadata, not a universal MCP standard or a client config.
|
|
144
|
+
It does not replace the live server's `tools/list`. These files do not promise search rankings or
|
|
145
|
+
support in a particular client.
|
|
146
|
+
|
|
66
147
|
## Rendering
|
|
67
148
|
|
|
68
149
|
```ts
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Static discovery files. Content, addresses and access rules stay in the app. */
|
|
2
|
+
export interface LlmsLink {
|
|
3
|
+
title: string;
|
|
4
|
+
url: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface LlmsSection {
|
|
8
|
+
title: string;
|
|
9
|
+
links: readonly LlmsLink[];
|
|
10
|
+
}
|
|
11
|
+
export interface LlmsTxtOptions {
|
|
12
|
+
title: string;
|
|
13
|
+
/** A short description, written as a blockquote. */
|
|
14
|
+
summary?: string;
|
|
15
|
+
/** Markdown kept as supplied. Use this for the text in llms-full.txt. */
|
|
16
|
+
body?: string;
|
|
17
|
+
sections?: readonly LlmsSection[];
|
|
18
|
+
}
|
|
19
|
+
/** llms.txt or llms-full.txt, as Markdown with a trailing newline. */
|
|
20
|
+
export declare function llmsTxt({ title, summary, body, sections }: LlmsTxtOptions): string;
|
|
21
|
+
/** Declare each accepted method. This is documentation, not an access check. */
|
|
22
|
+
export type McpDiscoveryAuthentication = ({
|
|
23
|
+
type: "oauth2";
|
|
24
|
+
protectedResourceMetadata: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "api-key";
|
|
27
|
+
in: "header" | "environment";
|
|
28
|
+
name: string;
|
|
29
|
+
prefix?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "none";
|
|
32
|
+
} | {
|
|
33
|
+
type: "session";
|
|
34
|
+
description: string;
|
|
35
|
+
}) & {
|
|
36
|
+
description?: string;
|
|
37
|
+
documentation?: string;
|
|
38
|
+
};
|
|
39
|
+
/** A tool as this server exposes it, not the combined catalog of every server. */
|
|
40
|
+
export interface McpDiscoveryTool {
|
|
41
|
+
name: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
inputSchema?: object;
|
|
45
|
+
annotations?: object;
|
|
46
|
+
}
|
|
47
|
+
export type McpDiscoveryServer = {
|
|
48
|
+
name: string;
|
|
49
|
+
/** Several entries mean the server accepts any one of these methods. */
|
|
50
|
+
authentication: readonly McpDiscoveryAuthentication[];
|
|
51
|
+
tools?: readonly (string | McpDiscoveryTool)[];
|
|
52
|
+
} & ({
|
|
53
|
+
transport: "streamable-http";
|
|
54
|
+
url: string;
|
|
55
|
+
} | {
|
|
56
|
+
transport: "stdio";
|
|
57
|
+
command: string;
|
|
58
|
+
args?: readonly string[];
|
|
59
|
+
});
|
|
60
|
+
export interface McpDiscoveryDocument {
|
|
61
|
+
name: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
homepage: string;
|
|
64
|
+
documentation: string;
|
|
65
|
+
llmsTxt?: string;
|
|
66
|
+
llmsFullTxt?: string;
|
|
67
|
+
openApi?: string;
|
|
68
|
+
servers: readonly McpDiscoveryServer[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* /.well-known/mcp.json, as JSON with a trailing newline. This is a discovery convention,
|
|
72
|
+
* not a standard client configuration or a replacement for MCP tools/list.
|
|
73
|
+
* No transport, access rule or tool is inferred, and supplied schemas and annotations are kept.
|
|
74
|
+
*/
|
|
75
|
+
export declare function mcpDiscoveryJson(document: McpDiscoveryDocument): string;
|
|
76
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACnC;AAED,sEAAsE;AACtE,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAa,EAAE,EAAE,cAAc,GAAG,MAAM,CAuBvF;AAED,gFAAgF;AAChF,MAAM,MAAM,0BAA0B,GAAG,CACrC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,yBAAyB,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,QAAQ,GAAG,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAChF;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAC3C,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,cAAc,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,KAAK,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,gBAAgB,CAAC,EAAE,CAAC;CAChD,GAAG,CACA;IAAE,SAAS,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CACpE,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACxC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAWvE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Static discovery files. Content, addresses and access rules stay in the app. */
|
|
2
|
+
/** llms.txt or llms-full.txt, as Markdown with a trailing newline. */
|
|
3
|
+
export function llmsTxt({ title, summary, body, sections = [] }) {
|
|
4
|
+
if (title.trim() === "")
|
|
5
|
+
throw new Error("llms: `title` is empty. Name the site or package.");
|
|
6
|
+
const parts = [`# ${title}`];
|
|
7
|
+
if (summary)
|
|
8
|
+
parts.push(summary
|
|
9
|
+
.split(/\r?\n/)
|
|
10
|
+
.map((line) => `> ${line}`)
|
|
11
|
+
.join("\n"));
|
|
12
|
+
if (body)
|
|
13
|
+
parts.push(body);
|
|
14
|
+
for (const section of sections) {
|
|
15
|
+
parts.push(`## ${section.title}`);
|
|
16
|
+
parts.push(section.links
|
|
17
|
+
.map((link) => `- [${link.title}](${link.url})${link.description ? `: ${link.description}` : ""}`)
|
|
18
|
+
.join("\n"));
|
|
19
|
+
}
|
|
20
|
+
return `${parts.join("\n\n").trimEnd()}\n`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* /.well-known/mcp.json, as JSON with a trailing newline. This is a discovery convention,
|
|
24
|
+
* not a standard client configuration or a replacement for MCP tools/list.
|
|
25
|
+
* No transport, access rule or tool is inferred, and supplied schemas and annotations are kept.
|
|
26
|
+
*/
|
|
27
|
+
export function mcpDiscoveryJson(document) {
|
|
28
|
+
if (document.name.trim() === "")
|
|
29
|
+
throw new Error("mcp discovery: `name` is empty. Name the site or package.");
|
|
30
|
+
if (document.servers.length === 0)
|
|
31
|
+
throw new Error("mcp discovery: `servers` is empty. List the servers this file describes.");
|
|
32
|
+
for (const server of document.servers)
|
|
33
|
+
if (server.authentication.length === 0)
|
|
34
|
+
throw new Error(`mcp discovery: "${server.name}" has no authentication entry. Declare how access works, including type "none" when it is public.`);
|
|
35
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAsBnF,sEAAsE;AACtE,MAAM,UAAU,OAAO,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,EAAE,EAAkB;IAC7E,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IAC9F,MAAM,KAAK,GAAG,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;IAC7B,IAAI,OAAO;QACT,KAAK,CAAC,IAAI,CACR,OAAO;aACJ,KAAK,CAAC,OAAO,CAAC;aACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;aAC1B,IAAI,CAAC,IAAI,CAAC,CACd,CAAC;IACJ,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,KAAK;aACV,GAAG,CACF,CAAC,IAAI,EAAE,EAAE,CACP,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACrF;aACA,IAAI,CAAC,IAAI,CAAC,CACd,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC;AAC7C,CAAC;AAwCD;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAA8B;IAC7D,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAC7B,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAC;IAC9F,KAAK,MAAM,MAAM,IAAI,QAAQ,CAAC,OAAO;QACnC,IAAI,MAAM,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,mBAAmB,MAAM,CAAC,IAAI,mGAAmG,CAClI,CAAC;IACN,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC"}
|
package/dist/head.d.ts
CHANGED
|
@@ -83,14 +83,14 @@ export interface FaqEntry {
|
|
|
83
83
|
answer: string;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
86
|
+
* Structured data for `headExtra`. The caller owns the schema and its claims.
|
|
87
87
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* structural characters are all brackets, quotes and digits. The replacement only ever touches
|
|
92
|
-
* copy.
|
|
88
|
+
* Escape every less-than character as JSON, not HTML: `</script>` can close the element, and
|
|
89
|
+
* `<!--<script>` can make the HTML parser swallow the real closing tag. Escaping only a slash
|
|
90
|
+
* misses that second case; adding a backslash before `!` makes the JSON invalid.
|
|
93
91
|
*/
|
|
92
|
+
export declare function jsonLd(data: object): string;
|
|
93
|
+
/** FAQ structured data from the same strings the page renders. */
|
|
94
94
|
export declare function faqJsonLd(faqs: readonly FaqEntry[]): string;
|
|
95
95
|
/**
|
|
96
96
|
* One page's `<head>` and body, written into the built template.
|
package/dist/head.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAiBA,0FAA0F;AAC1F,eAAO,MAAM,UAAU,4BAA0B,CAAC;AA0FlD,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB;yBACqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wIAAwI;IACxI,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;gDAC4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED;;mFAEmF;AACnF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"head.d.ts","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAiBA,0FAA0F;AAC1F,eAAO,MAAM,UAAU,4BAA0B,CAAC;AA0FlD,yFAAyF;AACzF,MAAM,WAAW,SAAS;IACxB;yBACqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wIAAwI;IACxI,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;gDAC4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IAClC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACxC;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED;;mFAEmF;AACnF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAU3D;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAuHjE;AAED,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;uDACmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,cAAmB,GAC1B,IAAI,CAgEN"}
|
package/dist/head.js
CHANGED
|
@@ -113,16 +113,19 @@ export function ogLocale(tag) {
|
|
|
113
113
|
return TERRITORY[tag] ?? tag.replace("-", "_");
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
|
-
*
|
|
116
|
+
* Structured data for `headExtra`. The caller owns the schema and its claims.
|
|
117
117
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* structural characters are all brackets, quotes and digits. The replacement only ever touches
|
|
122
|
-
* copy.
|
|
118
|
+
* Escape every less-than character as JSON, not HTML: `</script>` can close the element, and
|
|
119
|
+
* `<!--<script>` can make the HTML parser swallow the real closing tag. Escaping only a slash
|
|
120
|
+
* misses that second case; adding a backslash before `!` makes the JSON invalid.
|
|
123
121
|
*/
|
|
122
|
+
export function jsonLd(data) {
|
|
123
|
+
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
124
|
+
return `<script type="application/ld+json">${json}</script>`;
|
|
125
|
+
}
|
|
126
|
+
/** FAQ structured data from the same strings the page renders. */
|
|
124
127
|
export function faqJsonLd(faqs) {
|
|
125
|
-
|
|
128
|
+
return jsonLd({
|
|
126
129
|
"@context": "https://schema.org",
|
|
127
130
|
"@type": "FAQPage",
|
|
128
131
|
mainEntity: faqs.map((faq) => ({
|
|
@@ -130,9 +133,7 @@ export function faqJsonLd(faqs) {
|
|
|
130
133
|
name: faq.question,
|
|
131
134
|
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
|
132
135
|
})),
|
|
133
|
-
};
|
|
134
|
-
const json = JSON.stringify(data).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
|
|
135
|
-
return `<script type="application/ld+json">${json}</script>`;
|
|
136
|
+
});
|
|
136
137
|
}
|
|
137
138
|
/**
|
|
138
139
|
* One page's `<head>` and body, written into the built template.
|
package/dist/head.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,yFAAyF;AACzF,8FAA8F;AAC9F,yDAAyD;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAqB,EAAE,CAAC;IACzD,IAAI,MAAM,CAAC,cAAc,OAAO,yBAAyB,CAAC;IAC1D,IAAI,MAAM,CAAC,oCAAoC,OAAO,GAAG,CAAC;CAC3D,CAAC;AAEF,2FAA2F;AAC3F,MAAM,OAAO,GAAG,CAAC,QAAgB,EAAqB,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE3F;;;;;;;;GAQG;AACH,SAAS,WAAW,CAAC,IAAY,EAAE,QAA2B,EAAE,KAAa;IAC3E,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;YACf,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAc,EAAE,KAAa,EAAE,EAAE;gBAC9D,OAAO,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;YAC5C,CAAC,CAAC,CAAC;IACP,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,OAAO,CAAC,IAAY,EAAE,QAAgB,EAAE,KAAa;IAC5D,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC;IACxD,IAAI,GAAG,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,2BAA2B,CAAC,CAAC;IAC3F,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,SAAS,GAAG,CAAC,GAAW,EAAqB,EAAE,CACnD,UAAU,CAAC,sBAAsB,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,SAAS,gBAAgB,CAAC,IAAY,EAAE,QAA2B,EAAE,KAAa;IAChF,OAAO,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC;AACpD,CAAC;AAiED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,MAAM,SAAS,GAA2B,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;IACvE,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACjD,CAAC;AAUD
|
|
1
|
+
{"version":3,"file":"head.js","sourceRoot":"","sources":["../src/head.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,yFAAyF;AACzF,8FAA8F;AAC9F,yDAAyD;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEtD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,UAAU,GAAG,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe,EAAqB,EAAE,CAAC;IACzD,IAAI,MAAM,CAAC,cAAc,OAAO,yBAAyB,CAAC;IAC1D,IAAI,MAAM,CAAC,oCAAoC,OAAO,GAAG,CAAC;CAC3D,CAAC;AAEF,2FAA2F;AAC3F,MAAM,OAAO,GAAG,CAAC,QAAgB,EAAqB,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE3F;;;;;;;;GAQG;AACH,SAAS,WAAW,CAAC,IAAY,EAAE,QAA2B,EAAE,KAAa;IAC3E,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;YACf,OAAO,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAc,EAAE,KAAa,EAAE,EAAE;gBAC9D,OAAO,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;YAC5C,CAAC,CAAC,CAAC;IACP,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,OAAO,CAAC,IAAY,EAAE,QAAgB,EAAE,KAAa;IAC5D,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC;IACxD,IAAI,GAAG,KAAK,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,QAAQ,2BAA2B,CAAC,CAAC;IAC3F,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,SAAS,GAAG,CAAC,GAAW,EAAqB,EAAE,CACnD,UAAU,CAAC,sBAAsB,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,SAAS,gBAAgB,CAAC,IAAY,EAAE,QAA2B,EAAE,KAAa;IAChF,OAAO,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC;AACpD,CAAC;AAiED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,MAAM,SAAS,GAA2B,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;IACvE,OAAO,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACjD,CAAC;AAUD;;;;;;GAMG;AACH,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC3D,OAAO,sCAAsC,IAAI,WAAW,CAAC;AAC/D,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,SAAS,CAAC,IAAyB;IACjD,OAAO,MAAM,CAAC;QACZ,UAAU,EAAE,oBAAoB;QAChC,OAAO,EAAE,SAAS;QAClB,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC7B,OAAO,EAAE,UAAU;YACnB,IAAI,EAAE,GAAG,CAAC,QAAQ;YAClB,cAAc,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE;SACxD,CAAC,CAAC;KACJ,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,IAAc;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC;IAC9C,MAAM,gBAAgB,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW,CAAC;IAEhE,IAAI,IAAI,GAAG,QAAQ,CAAC,OAAO,CACzB,uBAAuB,EACvB,GAAG,EAAE,CAAC,UAAU,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CACjD,CAAC;IACF,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,oBAAoB,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7D,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,qBAAqB,EAAE,UAAU,CAAC,CAAC;IACxD,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,2BAA2B,EAAE,gBAAgB,CAAC,CAAC;IACpE,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACnE,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,UAAU,CAAC,CAAC;IACtE,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,qBAAqB,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAElF,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC7B,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACxD,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACtE,2FAA2F;QAC3F,4FAA4F;QAC5F,uDAAuD;QACvD,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAC5B,2FAA2F;QAC3F,2FAA2F;QAC3F,uFAAuF;QACvF,IAAI,GAAG,IAAI;aACR,OAAO,CAAC,oCAAoC,EAAE,EAAE,CAAC;aACjD,OAAO,CAAC,4DAA4D,EAAE,EAAE,CAAC,CAAC;IAC/E,CAAC;SAAM,CAAC;QACN,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC1D,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,SAAS,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACxE,0FAA0F;QAC1F,yFAAyF;QACzF,0FAA0F;QAC1F,0FAA0F;QAC1F,mEAAmE;QACnE,MAAM,OAAO,GAAG,WAAW,CACzB,IAAI,EACJ;YACE,gDAAgD;YAChD,gDAAgD;SACjD,EACD,IAAI,CAAC,SAAS,CACf,CAAC;QACF,IAAI,OAAO,KAAK,IAAI;YAClB,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;QAC/E,IAAI,GAAG,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU;aAC1B,GAAG,CACF,CAAC,GAAG,EAAE,EAAE,CACN,uCAAuC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CACvG;aACA,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,GAAG,KAAK,aAAa,CAAC,CAAC;IAC9D,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,wFAAwF;QACxF,uFAAuF;QACvF,uFAAuF;QACvF,oFAAoF;QACpF,2FAA2F;QAC3F,sFAAsF;QACtF,2FAA2F;QAC3F,wCAAwC;QACxC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,yDAAyD,EAAE,EAAE,CAAC,CAAC;QACnF,MAAM,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;aACvC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,IAAI,CAAC;aAC3E,GAAG,CACF,CAAC,GAAG,EAAE,EAAE,CACN,qDAAqD,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAChG,CAAC;QACJ,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/C,IAAI,GAAG,IAAI,CAAC,OAAO,CACjB,SAAS,EACT,GAAG,EAAE,CACH,2CAA2C,MAAM,SAC/C,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EACrD,WAAW,CACd,CAAC;IACJ,CAAC;IAED,kGAAkG;IAClG,+FAA+F;IAC/F,4BAA4B;IAC5B,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,IAAI;YACF,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,EAAE,SAAS,CAAC;gBACtD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,yDAAyD,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC3B,IAAI,SAAS;QAAE,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,SAAS,aAAa,CAAC,CAAC;IAEnF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,EAAE,GAAG,8BAA8B,CAAC;QAC1C,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACtF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAc,EAAE,KAAa,EAAE,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC;IAC1F,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC5B,wFAAwF;QACxF,4DAA4D;QAC5D,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,cAAc,UAAU,0BAA0B,CAAC,CAAC;QACtE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;QACxC,IAAI,GAAG,IAAI,CAAC,OAAO,CACjB,UAAU,EACV,GAAG,EAAE,CAAC,kBAAkB,sBAAsB,KAAK,UAAU,CAAC,KAAK,CAAC,KAAK,IAAI,QAAQ,CACtF,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAUD;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B;;;;;;;GAOG;AACH,MAAM,cAAc,GAClB,wFAAwF,CAAC;AAE3F;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,IAAY,EACZ,QAAgB,EAChB,SAAyB,EAAE;IAE3B,SAAS,IAAI,CAAC,GAAW;QACvB,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,6FAA6F;IAC7F,oFAAoF;IACpF,yEAAyE;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,IAAI,GAAG,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC3C,+FAA+F;IAC/F,gFAAgF;IAChF,IAAI,IAAI,GAAG,KAAK;QACd,IAAI,CACF,WAAW,MAAM,CAAC,IAAI,CAAC,2CAA2C,MAAM,CAAC,KAAK,CAAC,eAAe;YAC5F,6FAA6F,CAChG,CAAC;IACJ,IAAI,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACnE,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,MAAM,CAAC,IAAI,GAAG,CAAC;QAC5E,IAAI,CAAC,oBAAoB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAE1C,6FAA6F;IAC7F,8FAA8F;IAC9F,kBAAkB;IAClB,MAAM,WAAW,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,WAAW;QAAE,IAAI,CAAC,+CAA+C,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAEvF,6FAA6F;IAC7F,8FAA8F;IAC9F,+FAA+F;IAC/F,8FAA8F;IAC9F,+FAA+F;IAC/F,6FAA6F;IAC7F,sEAAsE;IACtE,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QAC3B,IAAI,CACF,uFAAuF;YACrF,wFAAwF;YACxF,wFAAwF;YACxF,sBAAsB,CACzB,CAAC;IAEJ,0FAA0F;IAC1F,+FAA+F;IAC/F,gFAAgF;IAChF,6FAA6F;IAC7F,0DAA0D;IAC1D,MAAM,MAAM,GAAG,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,CAAC,MAAM;QAAE,IAAI,CAAC,4BAA4B,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,IAAI,GAAG,IAAI;SACd,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,GAAG,CAAC;SACzB,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC;SAC1B,SAAS,EAAE,CAAC;IAEf,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAClE,2FAA2F;IAC3F,+FAA+F;IAC/F,gGAAgG;IAChG,4FAA4F;IAC5F,8FAA8F;IAC9F,yFAAyF;IACzF,IAAI,IAAI,GAAG,iBAAiB,IAAI,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC;QAC5D,IAAI,CAAC,mFAAmF,CAAC,CAAC;AAC9F,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* and a repo that only wants a sitemap all install exactly what they use.
|
|
19
19
|
*/
|
|
20
20
|
export { resolveKey } from "./catalog.ts";
|
|
21
|
+
export { llmsTxt, mcpDiscoveryJson, type LlmsLink, type LlmsSection, type LlmsTxtOptions, type McpDiscoveryAuthentication, type McpDiscoveryDocument, type McpDiscoveryServer, type McpDiscoveryTool, } from "./discovery.ts";
|
|
21
22
|
export { escapeAttr } from "./escape.ts";
|
|
22
|
-
export { assertRendered, bakeHead, EMPTY_ROOT, faqJsonLd, ogLocale, type Alternate, type FaqEntry, type HeadTags, type RenderedChecks, } from "./head.ts";
|
|
23
|
+
export { assertRendered, bakeHead, EMPTY_ROOT, faqJsonLd, jsonLd, ogLocale, type Alternate, type FaqEntry, type HeadTags, type RenderedChecks, } from "./head.ts";
|
|
23
24
|
export { assertOgImages, loadRenderer, loadTemplate, writeDist, writeOgCards, type OgCard, type WriteOgCardsOptions, } from "./node.ts";
|
|
24
25
|
export { webManifest, writeIconSet, type ManifestIcon, type PackIco, type RenderIcon, type WebManifestOptions, type WriteIconSetOptions, } from "./icons.ts";
|
|
25
26
|
export { describeOverflow, fitText, OG_CANVAS, type FitOptions, type FitResult, type OgOverflow, } from "./og.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,0BAA0B,EAC/B,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,EACR,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,KAAK,MAAM,EACX,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,YAAY,EACZ,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,EACT,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAK5E,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
* and a repo that only wants a sitemap all install exactly what they use.
|
|
19
19
|
*/
|
|
20
20
|
export { resolveKey } from "./catalog.js";
|
|
21
|
+
export { llmsTxt, mcpDiscoveryJson, } from "./discovery.js";
|
|
21
22
|
// `headExtra` takes raw markup, so the moment an adopter builds a tag out of its own copy it
|
|
22
23
|
// needs the same escaping every tag this package writes already gets. Exported rather than
|
|
23
24
|
// re-invented per app: an adopter's hand-rolled copy is one that can drift from ours, and the
|
|
24
25
|
// first one to need it was building a `<link title="…">` out of a translated page title.
|
|
25
26
|
export { escapeAttr } from "./escape.js";
|
|
26
|
-
export { assertRendered, bakeHead, EMPTY_ROOT, faqJsonLd, ogLocale, } from "./head.js";
|
|
27
|
+
export { assertRendered, bakeHead, EMPTY_ROOT, faqJsonLd, jsonLd, ogLocale, } from "./head.js";
|
|
27
28
|
export { assertOgImages, loadRenderer, loadTemplate, writeDist, writeOgCards, } from "./node.js";
|
|
28
29
|
export { webManifest, writeIconSet, } from "./icons.js";
|
|
29
30
|
export { describeOverflow, fitText, OG_CANVAS, } from "./og.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,6FAA6F;AAC7F,2FAA2F;AAC3F,8FAA8F;AAC9F,yFAAyF;AACzF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,SAAS,EACT,QAAQ,GAKT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,YAAY,GAMb,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,SAAS,CAAC;AACjB,0FAA0F;AAC1F,OAAO,EAAE,cAAc,EAA8B,MAAM,gBAAgB,CAAC;AAM5E,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,GAIf,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,OAAO,EACP,gBAAgB,GAQjB,MAAM,gBAAgB,CAAC;AACxB,6FAA6F;AAC7F,2FAA2F;AAC3F,8FAA8F;AAC9F,yFAAyF;AACzF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,cAAc,EACd,QAAQ,EACR,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,GAKT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,YAAY,GAGb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,WAAW,EACX,YAAY,GAMb,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,gBAAgB,EAChB,OAAO,EACP,SAAS,GAIV,MAAM,SAAS,CAAC;AACjB,0FAA0F;AAC1F,OAAO,EAAE,cAAc,EAA8B,MAAM,gBAAgB,CAAC;AAM5E,OAAO,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,GAIf,MAAM,cAAc,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/vite",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.44",
|
|
4
4
|
"description": "The build-time half of a prerendered Vite SPA: head baking, sitemap, robots, the prerender driver, a shared vite preset, and the translation catalog check.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { llmsTxt, mcpDiscoveryJson, type McpDiscoveryDocument } from "./discovery.ts";
|
|
3
|
+
|
|
4
|
+
describe("llmsTxt", () => {
|
|
5
|
+
it("writes the title, summary and links in the supplied order", () => {
|
|
6
|
+
expect(
|
|
7
|
+
llmsTxt({
|
|
8
|
+
title: "Example",
|
|
9
|
+
summary: "Read and update tasks.\nSign in before making changes.",
|
|
10
|
+
sections: [
|
|
11
|
+
{
|
|
12
|
+
title: "Reference",
|
|
13
|
+
links: [
|
|
14
|
+
{
|
|
15
|
+
title: "Connect",
|
|
16
|
+
url: "https://example.com/docs/mcp",
|
|
17
|
+
description: "Set up access.",
|
|
18
|
+
},
|
|
19
|
+
{ title: "Full reference", url: "https://example.com/llms-full.txt" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
).toBe(
|
|
25
|
+
"# Example\n\n" +
|
|
26
|
+
"> Read and update tasks.\n> Sign in before making changes.\n\n" +
|
|
27
|
+
"## Reference\n\n" +
|
|
28
|
+
"- [Connect](https://example.com/docs/mcp): Set up access.\n" +
|
|
29
|
+
"- [Full reference](https://example.com/llms-full.txt)\n",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps app-owned Markdown and writes exactly one final newline", () => {
|
|
34
|
+
const body = '## Tools\n\n```json\n{"name":"list_tasks"}\n```\n\n';
|
|
35
|
+
expect(llmsTxt({ title: "Example", body })).toBe(`# Example\n\n${body.trimEnd()}\n`);
|
|
36
|
+
expect(llmsTxt({ title: "Example" })).toBe("# Example\n");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("refuses an empty title", () => {
|
|
40
|
+
expect(() => llmsTxt({ title: " " })).toThrow(/title.*empty.*Name/);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const DOCUMENT: McpDiscoveryDocument = {
|
|
45
|
+
name: "Example",
|
|
46
|
+
homepage: "https://example.com",
|
|
47
|
+
documentation: "https://example.com/docs/mcp",
|
|
48
|
+
llmsTxt: "https://example.com/llms.txt",
|
|
49
|
+
llmsFullTxt: "https://example.com/llms-full.txt",
|
|
50
|
+
openApi: "https://api.example.com/openapi.json",
|
|
51
|
+
servers: [
|
|
52
|
+
{
|
|
53
|
+
name: "full",
|
|
54
|
+
transport: "streamable-http",
|
|
55
|
+
url: "https://api.example.com/mcp",
|
|
56
|
+
authentication: [
|
|
57
|
+
{
|
|
58
|
+
type: "oauth2",
|
|
59
|
+
protectedResourceMetadata:
|
|
60
|
+
"https://api.example.com/.well-known/oauth-protected-resource/mcp",
|
|
61
|
+
},
|
|
62
|
+
{ type: "api-key", in: "header", name: "Authorization", prefix: "Bearer " },
|
|
63
|
+
],
|
|
64
|
+
tools: [
|
|
65
|
+
{
|
|
66
|
+
name: "update_task",
|
|
67
|
+
title: "Update a task",
|
|
68
|
+
description: "Change a task's title.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: { id: { type: "string" }, title: { type: "string", minLength: 1 } },
|
|
72
|
+
required: ["id", "title"],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "read-only",
|
|
81
|
+
transport: "streamable-http",
|
|
82
|
+
url: "https://api.example.com/mcp/read",
|
|
83
|
+
authentication: [{ type: "none" }],
|
|
84
|
+
tools: ["list_tasks"],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "local",
|
|
88
|
+
transport: "stdio",
|
|
89
|
+
command: "bunx",
|
|
90
|
+
args: ["example-mcp"],
|
|
91
|
+
authentication: [
|
|
92
|
+
{ type: "session", description: "Connect the local app before you run a tool." },
|
|
93
|
+
{ type: "api-key", in: "environment", name: "EXAMPLE_API_KEY" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
describe("mcpDiscoveryJson", () => {
|
|
100
|
+
it("keeps per-server transports, access methods and full tool cards unchanged", () => {
|
|
101
|
+
const output = mcpDiscoveryJson(DOCUMENT);
|
|
102
|
+
expect(JSON.parse(output)).toEqual(DOCUMENT);
|
|
103
|
+
expect(output.startsWith('{\n "name": "Example",')).toBe(true);
|
|
104
|
+
expect(output.endsWith("\n")).toBe(true);
|
|
105
|
+
expect(output.endsWith("\n\n")).toBe(false);
|
|
106
|
+
expect(output).not.toContain('"url": "stdio');
|
|
107
|
+
expect(output).not.toContain('"tools": []');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("refuses empty names, server lists or access declarations", () => {
|
|
111
|
+
expect(() => mcpDiscoveryJson({ ...DOCUMENT, name: " " })).toThrow(/name.*empty/);
|
|
112
|
+
expect(() => mcpDiscoveryJson({ ...DOCUMENT, servers: [] })).toThrow(/servers.*empty/);
|
|
113
|
+
expect(() =>
|
|
114
|
+
mcpDiscoveryJson({
|
|
115
|
+
...DOCUMENT,
|
|
116
|
+
servers: [{ name: "local", transport: "stdio", command: "bunx", authentication: [] }],
|
|
117
|
+
}),
|
|
118
|
+
).toThrow(/local.*no authentication.*Declare/);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Static discovery files. Content, addresses and access rules stay in the app. */
|
|
2
|
+
|
|
3
|
+
export interface LlmsLink {
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LlmsSection {
|
|
10
|
+
title: string;
|
|
11
|
+
links: readonly LlmsLink[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LlmsTxtOptions {
|
|
15
|
+
title: string;
|
|
16
|
+
/** A short description, written as a blockquote. */
|
|
17
|
+
summary?: string;
|
|
18
|
+
/** Markdown kept as supplied. Use this for the text in llms-full.txt. */
|
|
19
|
+
body?: string;
|
|
20
|
+
sections?: readonly LlmsSection[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** llms.txt or llms-full.txt, as Markdown with a trailing newline. */
|
|
24
|
+
export function llmsTxt({ title, summary, body, sections = [] }: LlmsTxtOptions): string {
|
|
25
|
+
if (title.trim() === "") throw new Error("llms: `title` is empty. Name the site or package.");
|
|
26
|
+
const parts = [`# ${title}`];
|
|
27
|
+
if (summary)
|
|
28
|
+
parts.push(
|
|
29
|
+
summary
|
|
30
|
+
.split(/\r?\n/)
|
|
31
|
+
.map((line) => `> ${line}`)
|
|
32
|
+
.join("\n"),
|
|
33
|
+
);
|
|
34
|
+
if (body) parts.push(body);
|
|
35
|
+
for (const section of sections) {
|
|
36
|
+
parts.push(`## ${section.title}`);
|
|
37
|
+
parts.push(
|
|
38
|
+
section.links
|
|
39
|
+
.map(
|
|
40
|
+
(link) =>
|
|
41
|
+
`- [${link.title}](${link.url})${link.description ? `: ${link.description}` : ""}`,
|
|
42
|
+
)
|
|
43
|
+
.join("\n"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return `${parts.join("\n\n").trimEnd()}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Declare each accepted method. This is documentation, not an access check. */
|
|
50
|
+
export type McpDiscoveryAuthentication = (
|
|
51
|
+
| { type: "oauth2"; protectedResourceMetadata: string }
|
|
52
|
+
| { type: "api-key"; in: "header" | "environment"; name: string; prefix?: string }
|
|
53
|
+
| { type: "none" }
|
|
54
|
+
| { type: "session"; description: string }
|
|
55
|
+
) & { description?: string; documentation?: string };
|
|
56
|
+
|
|
57
|
+
/** A tool as this server exposes it, not the combined catalog of every server. */
|
|
58
|
+
export interface McpDiscoveryTool {
|
|
59
|
+
name: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
inputSchema?: object;
|
|
63
|
+
annotations?: object;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type McpDiscoveryServer = {
|
|
67
|
+
name: string;
|
|
68
|
+
/** Several entries mean the server accepts any one of these methods. */
|
|
69
|
+
authentication: readonly McpDiscoveryAuthentication[];
|
|
70
|
+
tools?: readonly (string | McpDiscoveryTool)[];
|
|
71
|
+
} & (
|
|
72
|
+
| { transport: "streamable-http"; url: string }
|
|
73
|
+
| { transport: "stdio"; command: string; args?: readonly string[] }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
export interface McpDiscoveryDocument {
|
|
77
|
+
name: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
homepage: string;
|
|
80
|
+
documentation: string;
|
|
81
|
+
llmsTxt?: string;
|
|
82
|
+
llmsFullTxt?: string;
|
|
83
|
+
openApi?: string;
|
|
84
|
+
servers: readonly McpDiscoveryServer[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* /.well-known/mcp.json, as JSON with a trailing newline. This is a discovery convention,
|
|
89
|
+
* not a standard client configuration or a replacement for MCP tools/list.
|
|
90
|
+
* No transport, access rule or tool is inferred, and supplied schemas and annotations are kept.
|
|
91
|
+
*/
|
|
92
|
+
export function mcpDiscoveryJson(document: McpDiscoveryDocument): string {
|
|
93
|
+
if (document.name.trim() === "")
|
|
94
|
+
throw new Error("mcp discovery: `name` is empty. Name the site or package.");
|
|
95
|
+
if (document.servers.length === 0)
|
|
96
|
+
throw new Error("mcp discovery: `servers` is empty. List the servers this file describes.");
|
|
97
|
+
for (const server of document.servers)
|
|
98
|
+
if (server.authentication.length === 0)
|
|
99
|
+
throw new Error(
|
|
100
|
+
`mcp discovery: "${server.name}" has no authentication entry. Declare how access works, including type "none" when it is public.`,
|
|
101
|
+
);
|
|
102
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
103
|
+
}
|
package/src/head.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { PRERENDERED_ROUTE_ATTR } from "@gusnips/react";
|
|
3
|
-
import { assertRendered, bakeHead, faqJsonLd, ogLocale } from "./head.ts";
|
|
3
|
+
import { assertRendered, bakeHead, faqJsonLd, jsonLd, ogLocale } from "./head.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Every failure `bakeHead` has is invisible until somebody reads a search result months later
|
|
@@ -433,6 +433,37 @@ describe("assertRendered", () => {
|
|
|
433
433
|
});
|
|
434
434
|
});
|
|
435
435
|
|
|
436
|
+
describe("jsonLd", () => {
|
|
437
|
+
it("keeps the caller's schema and fields", () => {
|
|
438
|
+
const data = {
|
|
439
|
+
"@context": "https://schema.org",
|
|
440
|
+
"@graph": [
|
|
441
|
+
{
|
|
442
|
+
"@type": "SoftwareApplication",
|
|
443
|
+
name: "Example",
|
|
444
|
+
applicationCategory: "DeveloperApplication",
|
|
445
|
+
},
|
|
446
|
+
{ "@type": "WebAPI", name: "Example API", documentation: "https://example.com/docs" },
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
const script = jsonLd(data);
|
|
450
|
+
expect(script.startsWith('<script type="application/ld+json">')).toBe(true);
|
|
451
|
+
expect(JSON.parse(script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, ""))).toEqual(
|
|
452
|
+
data,
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("round-trips HTML parser control text without exposing markup", () => {
|
|
457
|
+
const data = { name: '</ScRiPt><img src=x onerror="x"> <!-- <script> $$ \\u003c' };
|
|
458
|
+
const script = jsonLd(data);
|
|
459
|
+
const inner = script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
|
|
460
|
+
expect(inner).not.toContain("<");
|
|
461
|
+
expect(JSON.parse(inner)).toEqual(data);
|
|
462
|
+
expect(script.match(/<script/g)).toHaveLength(1);
|
|
463
|
+
expect(script.match(/<\/script>/g)).toHaveLength(1);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
436
467
|
describe("faqJsonLd", () => {
|
|
437
468
|
// One donor's template carried four questions where the page renders seven: the fix is to
|
|
438
469
|
// generate the block from the same strings, not to keep a second copy beside them.
|
|
@@ -448,15 +479,12 @@ describe("faqJsonLd", () => {
|
|
|
448
479
|
expect(script.match(/"@type":"Question"/g)).toHaveLength(2);
|
|
449
480
|
});
|
|
450
481
|
|
|
451
|
-
// A `</script>` inside an answer ends the element early and ships the rest of the JSON as
|
|
452
|
-
// markup. `<\/` is the same string to a JSON parser and inert to an HTML one.
|
|
453
482
|
it("escapes a script-closing sequence inside an answer", () => {
|
|
454
483
|
const script = faqJsonLd([
|
|
455
484
|
{ question: "What if I write code?", answer: "Wrap it: </script> ends the block." },
|
|
456
485
|
]);
|
|
457
486
|
expect(script).not.toContain("</script> ends");
|
|
458
|
-
expect(script).toContain("
|
|
459
|
-
// …and what it emits still parses, back to the original copy.
|
|
487
|
+
expect(script).toContain("\\u003c/script> ends");
|
|
460
488
|
const json = script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
|
|
461
489
|
const parsed = JSON.parse(json) as {
|
|
462
490
|
mainEntity: Array<{ acceptedAnswer: { text: string } }>;
|
|
@@ -464,11 +492,13 @@ describe("faqJsonLd", () => {
|
|
|
464
492
|
expect(parsed.mainEntity[0]?.acceptedAnswer.text).toBe("Wrap it: </script> ends the block.");
|
|
465
493
|
});
|
|
466
494
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
expect(inner).
|
|
495
|
+
it("keeps comments valid JSON and inert to the HTML parser", () => {
|
|
496
|
+
const answer = "a </b> b <!-- <script> c";
|
|
497
|
+
const script = faqJsonLd([{ question: "q", answer }]);
|
|
498
|
+
const inner = script.replace(/^<script[^>]*>/, "").replace(/<\/script>$/, "");
|
|
499
|
+
expect(inner).not.toContain("<");
|
|
500
|
+
expect(JSON.parse(inner)).toMatchObject({
|
|
501
|
+
mainEntity: [{ acceptedAnswer: { text: answer } }],
|
|
502
|
+
});
|
|
473
503
|
});
|
|
474
504
|
});
|
package/src/head.ts
CHANGED
|
@@ -193,16 +193,20 @@ export interface FaqEntry {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
*
|
|
196
|
+
* Structured data for `headExtra`. The caller owns the schema and its claims.
|
|
197
197
|
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* structural characters are all brackets, quotes and digits. The replacement only ever touches
|
|
202
|
-
* copy.
|
|
198
|
+
* Escape every less-than character as JSON, not HTML: `</script>` can close the element, and
|
|
199
|
+
* `<!--<script>` can make the HTML parser swallow the real closing tag. Escaping only a slash
|
|
200
|
+
* misses that second case; adding a backslash before `!` makes the JSON invalid.
|
|
203
201
|
*/
|
|
202
|
+
export function jsonLd(data: object): string {
|
|
203
|
+
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
204
|
+
return `<script type="application/ld+json">${json}</script>`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** FAQ structured data from the same strings the page renders. */
|
|
204
208
|
export function faqJsonLd(faqs: readonly FaqEntry[]): string {
|
|
205
|
-
|
|
209
|
+
return jsonLd({
|
|
206
210
|
"@context": "https://schema.org",
|
|
207
211
|
"@type": "FAQPage",
|
|
208
212
|
mainEntity: faqs.map((faq) => ({
|
|
@@ -210,9 +214,7 @@ export function faqJsonLd(faqs: readonly FaqEntry[]): string {
|
|
|
210
214
|
name: faq.question,
|
|
211
215
|
acceptedAnswer: { "@type": "Answer", text: faq.answer },
|
|
212
216
|
})),
|
|
213
|
-
};
|
|
214
|
-
const json = JSON.stringify(data).replace(/<\//g, "<\\/").replace(/<!--/g, "<\\!--");
|
|
215
|
-
return `<script type="application/ld+json">${json}</script>`;
|
|
217
|
+
});
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
/**
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,17 @@
|
|
|
18
18
|
* and a repo that only wants a sitemap all install exactly what they use.
|
|
19
19
|
*/
|
|
20
20
|
export { resolveKey } from "./catalog.ts";
|
|
21
|
+
export {
|
|
22
|
+
llmsTxt,
|
|
23
|
+
mcpDiscoveryJson,
|
|
24
|
+
type LlmsLink,
|
|
25
|
+
type LlmsSection,
|
|
26
|
+
type LlmsTxtOptions,
|
|
27
|
+
type McpDiscoveryAuthentication,
|
|
28
|
+
type McpDiscoveryDocument,
|
|
29
|
+
type McpDiscoveryServer,
|
|
30
|
+
type McpDiscoveryTool,
|
|
31
|
+
} from "./discovery.ts";
|
|
21
32
|
// `headExtra` takes raw markup, so the moment an adopter builds a tag out of its own copy it
|
|
22
33
|
// needs the same escaping every tag this package writes already gets. Exported rather than
|
|
23
34
|
// re-invented per app: an adopter's hand-rolled copy is one that can drift from ours, and the
|
|
@@ -28,6 +39,7 @@ export {
|
|
|
28
39
|
bakeHead,
|
|
29
40
|
EMPTY_ROOT,
|
|
30
41
|
faqJsonLd,
|
|
42
|
+
jsonLd,
|
|
31
43
|
ogLocale,
|
|
32
44
|
type Alternate,
|
|
33
45
|
type FaqEntry,
|