@lupinum/ginko-docs 0.3.0 → 0.4.0-rc.2
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 +2 -2
- package/app/app.vue +10 -0
- package/content-collections.ts +6 -2
- package/content.js +7 -4
- package/content.ts +22 -4
- package/nuxt.config.ts +3 -2
- package/package.json +2 -2
- package/server/plugins/sitemap-policy.ts +12 -0
- package/server/routes/.well-known/ai-catalog.json.get.ts +13 -0
- package/server/routes/mcp/server-card.get.ts +17 -0
- package/server/utils/mcp-server-card.ts +45 -0
- package/server/utils/sitemap.ts +45 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Use this package when a Nuxt application needs documentation routes, navigation,
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
pnpm add -D @lupinum/ginko-docs@0.
|
|
32
|
+
pnpm add -D @lupinum/ginko-docs@0.4.0-rc.2 @lupinum/ginko-content@1.0.0-beta.5
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
```ts
|
|
@@ -49,7 +49,7 @@ export default defineGinkoDocsConfig({
|
|
|
49
49
|
site: {
|
|
50
50
|
name: "Example Docs",
|
|
51
51
|
description: "Documentation for Example.",
|
|
52
|
-
|
|
52
|
+
whenToUse: "Use this site to learn and operate Example.",
|
|
53
53
|
},
|
|
54
54
|
locales: ["en"],
|
|
55
55
|
blog: false,
|
package/app/app.vue
CHANGED
|
@@ -18,6 +18,7 @@ const siteUrl = docsConfig.site.url;
|
|
|
18
18
|
|
|
19
19
|
useSeoMeta({
|
|
20
20
|
ogSiteName: computed(() => getLocalizedSiteText(docsConfig.site.name, locale.value)),
|
|
21
|
+
ogType: "website",
|
|
21
22
|
ogUrl: canonicalUrl,
|
|
22
23
|
twitterCard: "summary_large_image",
|
|
23
24
|
});
|
|
@@ -35,6 +36,15 @@ useSchemaJsonLd(() => [
|
|
|
35
36
|
url: siteUrl,
|
|
36
37
|
inLanguage: locale.value,
|
|
37
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"@type": "SoftwareApplication",
|
|
41
|
+
name: getLocalizedSiteText(docsConfig.site.name, locale.value),
|
|
42
|
+
description: getLocalizedSiteText(docsConfig.site.description, locale.value),
|
|
43
|
+
url: siteUrl,
|
|
44
|
+
applicationCategory: "DeveloperApplication",
|
|
45
|
+
operatingSystem: "Web",
|
|
46
|
+
...(docsConfig.social?.github ? { sameAs: [docsConfig.social.github] } : {}),
|
|
47
|
+
},
|
|
38
48
|
]);
|
|
39
49
|
|
|
40
50
|
useHead(() => ({
|
package/content-collections.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineCollection, defineContentConfig, reference } from "@lupinum/ginko-content/config";
|
|
2
|
+
import type { ContentAgentCollectionConfig } from "@lupinum/ginko-content/config";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { routeSlugs } from "./shared/route-slugs";
|
|
4
5
|
|
|
@@ -51,7 +52,10 @@ const authorsSchema = z.object({
|
|
|
51
52
|
links: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
export function createGinkoDocsCollections(
|
|
55
|
+
export function createGinkoDocsCollections(
|
|
56
|
+
i18n: boolean,
|
|
57
|
+
docsMarkdown: ContentAgentCollectionConfig["markdown"] = true,
|
|
58
|
+
) {
|
|
55
59
|
return defineContentConfig({
|
|
56
60
|
collections: {
|
|
57
61
|
docs: defineCollection({
|
|
@@ -59,7 +63,7 @@ export function createGinkoDocsCollections(i18n: boolean) {
|
|
|
59
63
|
source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
|
|
60
64
|
i18n: i18n ? true : undefined,
|
|
61
65
|
route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
|
|
62
|
-
agent: { section: "optional", markdown:
|
|
66
|
+
agent: { section: "optional", markdown: docsMarkdown },
|
|
63
67
|
strict: true,
|
|
64
68
|
schema: docsSchemaWithLastmod,
|
|
65
69
|
}),
|
package/content.js
CHANGED
|
@@ -81,7 +81,7 @@ const authorsSchema = z.object({
|
|
|
81
81
|
)
|
|
82
82
|
.optional(),
|
|
83
83
|
});
|
|
84
|
-
function createGinkoDocsCollections(i18n) {
|
|
84
|
+
function createGinkoDocsCollections(i18n, docsMarkdown = true) {
|
|
85
85
|
return defineContentConfig({
|
|
86
86
|
collections: {
|
|
87
87
|
docs: defineCollection({
|
|
@@ -91,7 +91,7 @@ function createGinkoDocsCollections(i18n) {
|
|
|
91
91
|
route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
|
|
92
92
|
agent: {
|
|
93
93
|
section: "optional",
|
|
94
|
-
markdown:
|
|
94
|
+
markdown: docsMarkdown,
|
|
95
95
|
},
|
|
96
96
|
strict: true,
|
|
97
97
|
schema: docsSchemaWithLastmod,
|
|
@@ -141,13 +141,14 @@ function defineGinkoDocsConfig(options) {
|
|
|
141
141
|
"source",
|
|
142
142
|
"updated",
|
|
143
143
|
]);
|
|
144
|
-
const { docs, blog, authors } = createGinkoDocsCollections(i18n);
|
|
144
|
+
const { docs, blog, authors } = createGinkoDocsCollections(i18n, options.agent?.documentation);
|
|
145
145
|
const config = {
|
|
146
146
|
agent: {
|
|
147
147
|
site: {
|
|
148
148
|
title: options.site.name,
|
|
149
149
|
description: options.site.description,
|
|
150
|
-
|
|
150
|
+
whenToUse: options.site.whenToUse,
|
|
151
|
+
...(options.site.whenNotToUse ? { whenNotToUse: options.site.whenNotToUse } : {}),
|
|
151
152
|
},
|
|
152
153
|
markdown: {
|
|
153
154
|
metadata: {
|
|
@@ -156,6 +157,7 @@ function defineGinkoDocsConfig(options) {
|
|
|
156
157
|
},
|
|
157
158
|
},
|
|
158
159
|
sections: [
|
|
160
|
+
...(options.agent?.sections ?? []),
|
|
159
161
|
...(options.blog
|
|
160
162
|
? [
|
|
161
163
|
defineAgentSection({
|
|
@@ -177,6 +179,7 @@ function defineGinkoDocsConfig(options) {
|
|
|
177
179
|
order: 100,
|
|
178
180
|
}),
|
|
179
181
|
],
|
|
182
|
+
...(options.agent?.pages ? { pages: [...options.agent.pages] } : {}),
|
|
180
183
|
},
|
|
181
184
|
};
|
|
182
185
|
if (options.blog)
|
package/content.ts
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
|
+
defineAgentAppPage,
|
|
2
3
|
defineAgentMetadataFields,
|
|
3
4
|
defineAgentSection,
|
|
4
5
|
defineContentConfig,
|
|
5
6
|
} from "@lupinum/ginko-content/config";
|
|
6
|
-
import type { ContentConfig } from "@lupinum/ginko-content/config";
|
|
7
|
+
import type { ContentAgentMarkdownOptions, ContentConfig } from "@lupinum/ginko-content/config";
|
|
7
8
|
import { createGinkoDocsCollections } from "./content-collections";
|
|
8
9
|
|
|
10
|
+
type GinkoDocsAgentPage = Parameters<typeof defineAgentAppPage>[0];
|
|
11
|
+
type GinkoDocsAgentSection = Parameters<typeof defineAgentSection>[0];
|
|
12
|
+
|
|
13
|
+
export interface GinkoDocsAgentOptions {
|
|
14
|
+
/** Control whether authored documentation appears in compact and full indexes. */
|
|
15
|
+
documentation?: ContentAgentMarkdownOptions;
|
|
16
|
+
/** Add compact index pages that point agents to canonical raw Markdown. */
|
|
17
|
+
pages?: readonly GinkoDocsAgentPage[];
|
|
18
|
+
/** Add sections before the built-in blog and optional documentation sections. */
|
|
19
|
+
sections?: readonly GinkoDocsAgentSection[];
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export interface GinkoDocsContentOptions {
|
|
10
23
|
site: {
|
|
11
24
|
name: string | { en: string; de: string };
|
|
12
25
|
description: string | { en: string; de: string };
|
|
13
|
-
|
|
26
|
+
whenToUse: string | { en: string; de: string };
|
|
27
|
+
whenNotToUse?: string | { en: string; de: string };
|
|
14
28
|
};
|
|
29
|
+
agent?: GinkoDocsAgentOptions;
|
|
15
30
|
locales?: readonly ["en"] | readonly ["en", "de"];
|
|
16
31
|
blog?: boolean;
|
|
17
32
|
}
|
|
@@ -54,16 +69,18 @@ export function defineGinkoDocsConfig(
|
|
|
54
69
|
"source",
|
|
55
70
|
"updated",
|
|
56
71
|
]);
|
|
57
|
-
const { docs, blog, authors } = createGinkoDocsCollections(i18n);
|
|
72
|
+
const { docs, blog, authors } = createGinkoDocsCollections(i18n, options.agent?.documentation);
|
|
58
73
|
const config = {
|
|
59
74
|
agent: {
|
|
60
75
|
site: {
|
|
61
76
|
title: options.site.name,
|
|
62
77
|
description: options.site.description,
|
|
63
|
-
|
|
78
|
+
whenToUse: options.site.whenToUse,
|
|
79
|
+
...(options.site.whenNotToUse ? { whenNotToUse: options.site.whenNotToUse } : {}),
|
|
64
80
|
},
|
|
65
81
|
markdown: { metadata: { enabled: true, defaultFields: metadata } },
|
|
66
82
|
sections: [
|
|
83
|
+
...(options.agent?.sections ?? []),
|
|
67
84
|
...(options.blog
|
|
68
85
|
? [defineAgentSection({ id: "blog", title: { en: "Blog", de: "Blog" }, order: 40 })]
|
|
69
86
|
: []),
|
|
@@ -73,6 +90,7 @@ export function defineGinkoDocsConfig(
|
|
|
73
90
|
order: 100,
|
|
74
91
|
}),
|
|
75
92
|
],
|
|
93
|
+
...(options.agent?.pages ? { pages: [...options.agent.pages] } : {}),
|
|
76
94
|
},
|
|
77
95
|
};
|
|
78
96
|
|
package/nuxt.config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { i18nPages } from "./i18n/routes";
|
|
|
10
10
|
import { defaultLocale, localeCodes, locales, localizedPath } from "./i18n/locales";
|
|
11
11
|
import { routeSlugs } from "./shared/route-slugs";
|
|
12
12
|
import { layerIconCollections, layerIconNames } from "./icon-bundle";
|
|
13
|
+
import packageMetadata from "./package.json" with { type: "json" };
|
|
13
14
|
|
|
14
15
|
const root = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const app = join(root, "app");
|
|
@@ -56,7 +57,7 @@ export default defineNuxtConfig({
|
|
|
56
57
|
},
|
|
57
58
|
mcp: {
|
|
58
59
|
name: "Ginko Docs",
|
|
59
|
-
version:
|
|
60
|
+
version: packageMetadata.version,
|
|
60
61
|
},
|
|
61
62
|
components: {
|
|
62
63
|
dirs: [
|
|
@@ -163,7 +164,7 @@ export default defineNuxtConfig({
|
|
|
163
164
|
},
|
|
164
165
|
search: { engine: "minisearch" },
|
|
165
166
|
sitemap: true,
|
|
166
|
-
agent: { routes: true, linkHeaders: true,
|
|
167
|
+
agent: { routes: true, linkHeaders: true, delivery: "static" },
|
|
167
168
|
},
|
|
168
169
|
sitemap: {
|
|
169
170
|
excludeAppSources: ["nuxt:prerender"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lupinum/ginko-docs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-rc.2",
|
|
4
4
|
"description": "A Nuxt documentation layer powered by Ginko Content.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"content",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"zod": "^4.4.3"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@lupinum/ginko-content": ">=0.
|
|
89
|
+
"@lupinum/ginko-content": ">=1.0.0-beta.5 <2.0.0",
|
|
90
90
|
"nuxt": ">=4.5.1 <5",
|
|
91
91
|
"vue": "^3.5.35",
|
|
92
92
|
"vue-router": "^5.1.0"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineNitroPlugin } from "nitropack/runtime/plugin";
|
|
2
|
+
import { filterSitemapEntries } from "../utils/sitemap";
|
|
3
|
+
|
|
4
|
+
export default defineNitroPlugin((nitro) => {
|
|
5
|
+
nitro.hooks.hook("sitemap:resolved", (context) => {
|
|
6
|
+
const content = useRuntimeConfig(context.event).content;
|
|
7
|
+
context.urls = filterSitemapEntries(context.urls, {
|
|
8
|
+
locales: content.locales,
|
|
9
|
+
blogEnabled: Boolean(content.collections.blog),
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { setResponseHeaders } from "h3";
|
|
2
|
+
import { useAppConfig } from "#imports";
|
|
3
|
+
import { createGinkoDocsAiCatalog } from "../../utils/mcp-server-card";
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler((event) => {
|
|
6
|
+
setResponseHeaders(event, {
|
|
7
|
+
"access-control-allow-origin": "*",
|
|
8
|
+
"cache-control": "public, max-age=3600",
|
|
9
|
+
"content-type": "application/ai-catalog+json; charset=utf-8",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return createGinkoDocsAiCatalog(useAppConfig().ginkoDocs.site.url);
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { setResponseHeaders } from "h3";
|
|
2
|
+
import { useAppConfig } from "#imports";
|
|
3
|
+
import packageMetadata from "../../../package.json" with { type: "json" };
|
|
4
|
+
import { createGinkoDocsServerCard } from "../../utils/mcp-server-card";
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler((event) => {
|
|
7
|
+
setResponseHeaders(event, {
|
|
8
|
+
"access-control-allow-origin": "*",
|
|
9
|
+
"cache-control": "public, max-age=3600",
|
|
10
|
+
"content-type": "application/mcp-server-card+json; charset=utf-8",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return createGinkoDocsServerCard({
|
|
14
|
+
origin: useAppConfig().ginkoDocs.site.url,
|
|
15
|
+
version: packageMetadata.version,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface GinkoDocsServerCardOptions {
|
|
2
|
+
origin: string;
|
|
3
|
+
version: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createGinkoDocsServerCard({ origin, version }: GinkoDocsServerCardOptions) {
|
|
7
|
+
const baseUrl = new URL(origin);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
11
|
+
name: "dev.lupinum/ginko-docs",
|
|
12
|
+
title: "Ginko Docs",
|
|
13
|
+
version,
|
|
14
|
+
description: "Read published Ginko Docs pages through its public MCP tools.",
|
|
15
|
+
websiteUrl: new URL("/docs/features/agent-readable-output", baseUrl).toString(),
|
|
16
|
+
repository: {
|
|
17
|
+
source: "github",
|
|
18
|
+
url: "https://github.com/lupinum-dev/ginko-docs",
|
|
19
|
+
},
|
|
20
|
+
remotes: [
|
|
21
|
+
{
|
|
22
|
+
type: "streamable-http",
|
|
23
|
+
url: new URL("/mcp", baseUrl).toString(),
|
|
24
|
+
supportedProtocolVersions: ["2025-11-25"],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
} as const;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createGinkoDocsAiCatalog(origin: string) {
|
|
31
|
+
const baseUrl = new URL(origin);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
specVersion: "1.0",
|
|
35
|
+
entries: [
|
|
36
|
+
{
|
|
37
|
+
identifier: "urn:air:ginko-docs.lupinum.com:mcp:ginko-docs",
|
|
38
|
+
displayName: "Ginko Docs MCP Server",
|
|
39
|
+
type: "application/mcp-server-card+json",
|
|
40
|
+
description: "Read the public Ginko Docs documentation through MCP.",
|
|
41
|
+
url: new URL("/mcp/server-card", baseUrl).toString(),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
} as const;
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { localeCodes, localizedPath } from "../../i18n/locales";
|
|
2
|
+
import { routeSlugs } from "../../shared/route-slugs";
|
|
3
|
+
|
|
4
|
+
interface SitemapAlternative {
|
|
5
|
+
href: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SitemapEntry {
|
|
9
|
+
loc: string;
|
|
10
|
+
alternatives?: SitemapAlternative[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SitemapContentPolicy {
|
|
14
|
+
locales: readonly string[];
|
|
15
|
+
blogEnabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pathname = (url: string) => new URL(url, "https://ginko.invalid").pathname;
|
|
19
|
+
|
|
20
|
+
export function filterSitemapEntries<Entry extends SitemapEntry>(
|
|
21
|
+
entries: Entry[],
|
|
22
|
+
policy: SitemapContentPolicy,
|
|
23
|
+
): Entry[] {
|
|
24
|
+
const disabledLocaleRoots = localeCodes
|
|
25
|
+
.filter((locale) => !policy.locales.includes(locale))
|
|
26
|
+
.map((locale) => `/${locale}`);
|
|
27
|
+
const disabledBlogRoots = policy.blogEnabled
|
|
28
|
+
? []
|
|
29
|
+
: localeCodes.map((locale) => localizedPath(locale, routeSlugs.blog[locale]));
|
|
30
|
+
const excludedRoots = [...disabledLocaleRoots, ...disabledBlogRoots];
|
|
31
|
+
const isExcluded = (url: string) => {
|
|
32
|
+
const path = pathname(url);
|
|
33
|
+
return excludedRoots.some((root) => path === root || path.startsWith(`${root}/`));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return entries
|
|
37
|
+
.filter((entry) => !isExcluded(entry.loc))
|
|
38
|
+
.map((entry) => {
|
|
39
|
+
if (!entry.alternatives) return entry;
|
|
40
|
+
return {
|
|
41
|
+
...entry,
|
|
42
|
+
alternatives: entry.alternatives.filter((alternative) => !isExcluded(alternative.href)),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|