@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 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.3.0 @lupinum/ginko-content@0.4.0-rc.2
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
- url: "https://docs.example.com",
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(() => ({
@@ -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(i18n: boolean) {
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: true },
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: true,
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
- url: options.site.url,
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
- url: string;
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
- url: options.site.url,
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: "0.3.0",
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, markdownNegotiation: true, prerender: 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.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.4.0-rc.2 <0.5.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
+ }