@lupinum/ginko-docs 0.2.5 → 0.3.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
@@ -8,14 +8,14 @@ Ginko Docs is a Nuxt layer for focused documentation sites. It combines Ginko Co
8
8
  - Nuxt `>=4.4.7 <5`
9
9
  - Vue `^3.5.35`
10
10
  - Vue Router `^5.1.0`
11
- - Ginko Content `>=0.3.5 <0.4.0`
11
+ - Ginko Content `>=0.4.0-rc.1 <0.5.0`
12
12
 
13
13
  ## Install
14
14
 
15
15
  Install the layer and its Ginko Content peer:
16
16
 
17
17
  ```bash
18
- pnpm add -D @lupinum/ginko-docs @lupinum/ginko-content@0.3.5
18
+ pnpm add -D @lupinum/ginko-docs@0.3.0-rc.2 @lupinum/ginko-content@0.4.0-rc.1
19
19
  ```
20
20
 
21
21
  Keep the public identity in one shared value:
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { Button } from "#ginko-docs/components/ui/button";
3
- import { computed, ref } from "vue";
4
- import { useI18n, useRoute } from "#imports";
3
+ import { computed, ref, watch } from "vue";
4
+ import { useI18n, useRouter } from "#imports";
5
5
  import { useGinkoAnalytics } from "#ginko-docs/composables/useGinkoAnalytics";
6
6
  import { useGinkoDocsConfig } from "#ginko-docs/composables/useGinkoDocsConfig";
7
7
  import { buildRepoIssueUrl } from "#ginko-docs/utils/repository";
@@ -15,7 +15,7 @@ withDefaults(
15
15
  },
16
16
  );
17
17
  const { t, locale } = useI18n();
18
- const route = useRoute();
18
+ const router = useRouter();
19
19
 
20
20
  const config = useGinkoDocsConfig();
21
21
  const analytics = useGinkoAnalytics();
@@ -24,20 +24,24 @@ const enabled = config.feedback.enabled && analytics.enabled;
24
24
  type Sentiment = "positive" | "negative";
25
25
 
26
26
  const sentiment = ref<Sentiment | null>(null);
27
+ const routePath = computed(() => router.currentRoute.value.path);
28
+ watch(routePath, () => {
29
+ sentiment.value = null;
30
+ });
27
31
 
28
32
  const issueUrl = computed(() => {
29
33
  const repository = config.repository;
30
34
  if (!repository) return null;
31
35
  return buildRepoIssueUrl(repository, {
32
- title: t("feedback.issueTitle", { path: route.path }),
33
- body: t("docs.issueBody", { path: route.path }),
36
+ title: t("feedback.issueTitle", { path: routePath.value }),
37
+ body: t("docs.issueBody", { path: routePath.value }),
34
38
  });
35
39
  });
36
40
 
37
41
  function selectSentiment(s: Sentiment) {
38
42
  if (sentiment.value !== null) return;
39
43
  analytics.track("docs-feedback", {
40
- path: route.path,
44
+ path: routePath.value,
41
45
  helpful: s === "positive" ? "yes" : "no",
42
46
  locale: locale.value,
43
47
  });
@@ -10,28 +10,32 @@ export interface GinkoAnalytics {
10
10
 
11
11
  /**
12
12
  * Plausible analytics via the Nuxt Scripts registry script. Fully disabled
13
- * unless `ginkoDocs.analytics.plausible.domain` is configured without a
14
- * domain nothing loads and `track()` is a no-op, so callers never guard.
13
+ * unless `ginkoDocs.analytics.plausible.scriptId` is configured. Without an
14
+ * ID, no script loads and `track()` is a no-op, so callers never guard.
15
15
  */
16
16
  export function useGinkoAnalytics(): GinkoAnalytics {
17
17
  const config = useGinkoDocsConfig();
18
- const plausible = config.analytics?.plausible;
18
+ const scriptId = config.analytics?.plausible?.scriptId?.trim();
19
19
 
20
- if (!plausible?.domain) {
20
+ if (!scriptId) {
21
21
  return { enabled: false, track: () => {} };
22
22
  }
23
23
 
24
24
  const script = useScriptPlausibleAnalytics({
25
- domain: plausible.domain,
26
- extension: plausible.extensions ?? ["outbound-links"],
27
- ...(plausible.scriptSrc ? { scriptInput: { src: plausible.scriptSrc } } : {}),
25
+ scriptId,
26
+ // The site ID comes from app.config at runtime. Keep the vendor script
27
+ // external so Nuxt Scripts does not bundle its legacy fallback URL during
28
+ // the build-time transform.
29
+ scriptOptions: { bundle: false },
28
30
  });
29
31
 
30
32
  return {
31
33
  enabled: true,
32
34
  track: (event, props) => {
33
35
  if (!import.meta.client) return;
34
- script.proxy.plausible(event, props ? { props } : undefined);
36
+ const plausible = script.proxy.plausible;
37
+ if (typeof plausible !== "function") return;
38
+ plausible(event, props ? { props } : undefined);
35
39
  },
36
40
  };
37
41
  }
@@ -0,0 +1,85 @@
1
+ import { defineCollection, defineContentConfig, reference } from "@lupinum/ginko-content/config";
2
+ import { z } from "zod";
3
+ import { routeSlugs } from "./shared/route-slugs";
4
+
5
+ const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
6
+ const nonEmptyString = z.string().trim().min(1);
7
+ const redirectFrom = z
8
+ .array(nonEmptyString.regex(/^\//, "redirectFrom entries must be absolute site paths"))
9
+ .optional();
10
+
11
+ const withSitemapLastmod = <T extends object>(data: T, lastmod: string | undefined) =>
12
+ lastmod ? { ...data, sitemap: { lastmod: `${lastmod}T00:00:00.000Z` } } : data;
13
+
14
+ const docsSchema = z.object({
15
+ title: z.string(),
16
+ description: z.string(),
17
+ icon: z.string().optional(),
18
+ badge: z.string().optional(),
19
+ updated: isoDate.optional(),
20
+ redirectFrom,
21
+ sidebar: z.enum(["section", "group"]).optional(),
22
+ navigation: z
23
+ .object({
24
+ title: z.string().optional(),
25
+ icon: z.string().optional(),
26
+ badge: z.string().optional(),
27
+ sidebar: z.enum(["section", "group"]).optional(),
28
+ })
29
+ .optional(),
30
+ });
31
+ const docsSchemaWithLastmod = docsSchema.transform((data) =>
32
+ withSitemapLastmod(data, data.updated),
33
+ );
34
+ const blogSchema = z.object({
35
+ title: z.string(),
36
+ description: z.string(),
37
+ badge: z.string().optional(),
38
+ date: isoDate,
39
+ readingTime: nonEmptyString,
40
+ author: reference("authors"),
41
+ image: z.string().optional(),
42
+ redirectFrom,
43
+ });
44
+ const blogSchemaWithLastmod = blogSchema.transform((data) => withSitemapLastmod(data, data.date));
45
+ const authorsSchema = z.object({
46
+ slug: z.string(),
47
+ name: z.string(),
48
+ role: z.string(),
49
+ bio: z.string(),
50
+ avatar: z.string(),
51
+ links: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
52
+ });
53
+
54
+ export function createGinkoDocsCollections(i18n: boolean) {
55
+ return defineContentConfig({
56
+ collections: {
57
+ docs: defineCollection({
58
+ type: "page",
59
+ source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
60
+ i18n: i18n ? true : undefined,
61
+ route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
62
+ agent: { section: "optional", markdown: true },
63
+ strict: true,
64
+ schema: docsSchemaWithLastmod,
65
+ }),
66
+ blog: defineCollection({
67
+ type: "page",
68
+ source: "2.blog/*.md",
69
+ i18n: i18n ? true : undefined,
70
+ route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
71
+ agent: { section: "blog", markdown: true },
72
+ strict: true,
73
+ schema: blogSchemaWithLastmod,
74
+ }),
75
+ authors: defineCollection({
76
+ type: "data",
77
+ source: "authors/**/*.json",
78
+ i18n: i18n ? true : undefined,
79
+ strict: true,
80
+ sitemap: false,
81
+ schema: authorsSchema,
82
+ }),
83
+ },
84
+ }).collections;
85
+ }
package/content.js CHANGED
@@ -22,17 +22,12 @@ const routeSlugs = {
22
22
  },
23
23
  };
24
24
  //#endregion
25
- //#region layer/content.ts
25
+ //#region layer/content-collections.ts
26
26
  const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
27
27
  const nonEmptyString = z.string().trim().min(1);
28
- /** Former public URLs of a page, as served: locale prefix and translated slugs included. */
29
28
  const redirectFrom = z
30
29
  .array(nonEmptyString.regex(/^\//, "redirectFrom entries must be absolute site paths"))
31
30
  .optional();
32
- /**
33
- * Derives the sitemap lastmod from the authored date so it has one source of
34
- * truth. Route records require normalized UTC ISO values.
35
- */
36
31
  const withSitemapLastmod = (data, lastmod) =>
37
32
  lastmod
38
33
  ? {
@@ -86,6 +81,46 @@ const authorsSchema = z.object({
86
81
  )
87
82
  .optional(),
88
83
  });
84
+ function createGinkoDocsCollections(i18n) {
85
+ return defineContentConfig({
86
+ collections: {
87
+ docs: defineCollection({
88
+ type: "page",
89
+ source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
90
+ i18n: i18n ? true : void 0,
91
+ route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
92
+ agent: {
93
+ section: "optional",
94
+ markdown: true,
95
+ },
96
+ strict: true,
97
+ schema: docsSchemaWithLastmod,
98
+ }),
99
+ blog: defineCollection({
100
+ type: "page",
101
+ source: "2.blog/*.md",
102
+ i18n: i18n ? true : void 0,
103
+ route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
104
+ agent: {
105
+ section: "blog",
106
+ markdown: true,
107
+ },
108
+ strict: true,
109
+ schema: blogSchemaWithLastmod,
110
+ }),
111
+ authors: defineCollection({
112
+ type: "data",
113
+ source: "authors/**/*.json",
114
+ i18n: i18n ? true : void 0,
115
+ strict: true,
116
+ sitemap: false,
117
+ schema: authorsSchema,
118
+ }),
119
+ },
120
+ }).collections;
121
+ }
122
+ //#endregion
123
+ //#region layer/content.ts
89
124
  function defineGinkoDocsConfig(options) {
90
125
  const locales = options.locales ?? ["en"];
91
126
  if (
@@ -106,38 +141,7 @@ function defineGinkoDocsConfig(options) {
106
141
  "source",
107
142
  "updated",
108
143
  ]);
109
- const docs = defineCollection({
110
- type: "page",
111
- source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
112
- i18n: i18n ? true : void 0,
113
- route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
114
- agent: {
115
- section: "optional",
116
- markdown: true,
117
- },
118
- strict: true,
119
- schema: docsSchemaWithLastmod,
120
- });
121
- const blog = defineCollection({
122
- type: "page",
123
- source: "2.blog/*.md",
124
- i18n: i18n ? true : void 0,
125
- route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
126
- agent: {
127
- section: "blog",
128
- markdown: true,
129
- },
130
- strict: true,
131
- schema: blogSchemaWithLastmod,
132
- });
133
- const authors = defineCollection({
134
- type: "data",
135
- source: "authors/**/*.json",
136
- i18n: i18n ? true : void 0,
137
- strict: true,
138
- sitemap: false,
139
- schema: authorsSchema,
140
- });
144
+ const { docs, blog, authors } = createGinkoDocsCollections(i18n);
141
145
  const config = {
142
146
  agent: {
143
147
  site: {
package/content.ts CHANGED
@@ -1,27 +1,10 @@
1
1
  import {
2
2
  defineAgentMetadataFields,
3
3
  defineAgentSection,
4
- defineCollection,
5
4
  defineContentConfig,
6
- reference,
7
5
  } from "@lupinum/ginko-content/config";
8
- import type { ContentCollectionConfig, ContentConfig } from "@lupinum/ginko-content/config";
9
- import { z } from "zod";
10
- import { routeSlugs } from "./shared/route-slugs";
11
-
12
- const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
13
- const nonEmptyString = z.string().trim().min(1);
14
- /** Former public URLs of a page, as served: locale prefix and translated slugs included. */
15
- const redirectFrom = z
16
- .array(nonEmptyString.regex(/^\//, "redirectFrom entries must be absolute site paths"))
17
- .optional();
18
-
19
- /**
20
- * Derives the sitemap lastmod from the authored date so it has one source of
21
- * truth. Route records require normalized UTC ISO values.
22
- */
23
- const withSitemapLastmod = <T extends object>(data: T, lastmod: string | undefined) =>
24
- lastmod ? { ...data, sitemap: { lastmod: `${lastmod}T00:00:00.000Z` } } : data;
6
+ import type { ContentConfig } from "@lupinum/ginko-content/config";
7
+ import { createGinkoDocsCollections } from "./content-collections";
25
8
 
26
9
  export interface GinkoDocsContentOptions {
27
10
  site: {
@@ -33,49 +16,10 @@ export interface GinkoDocsContentOptions {
33
16
  blog?: boolean;
34
17
  }
35
18
 
36
- const docsSchema = z.object({
37
- title: z.string(),
38
- description: z.string(),
39
- icon: z.string().optional(),
40
- badge: z.string().optional(),
41
- updated: isoDate.optional(),
42
- redirectFrom,
43
- sidebar: z.enum(["section", "group"]).optional(),
44
- navigation: z
45
- .object({
46
- title: z.string().optional(),
47
- icon: z.string().optional(),
48
- badge: z.string().optional(),
49
- sidebar: z.enum(["section", "group"]).optional(),
50
- })
51
- .optional(),
52
- });
53
- const docsSchemaWithLastmod = docsSchema.transform((data) =>
54
- withSitemapLastmod(data, data.updated),
55
- );
56
- const blogSchema = z.object({
57
- title: z.string(),
58
- description: z.string(),
59
- badge: z.string().optional(),
60
- date: isoDate,
61
- readingTime: nonEmptyString,
62
- author: reference("authors"),
63
- image: z.string().optional(),
64
- redirectFrom,
65
- });
66
- const blogSchemaWithLastmod = blogSchema.transform((data) => withSitemapLastmod(data, data.date));
67
- const authorsSchema = z.object({
68
- slug: z.string(),
69
- name: z.string(),
70
- role: z.string(),
71
- bio: z.string(),
72
- avatar: z.string(),
73
- links: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
74
- });
75
-
76
- type DocsCollection = ContentCollectionConfig<typeof docsSchemaWithLastmod>;
77
- type BlogCollection = ContentCollectionConfig<typeof blogSchemaWithLastmod>;
78
- type AuthorsCollection = ContentCollectionConfig<typeof authorsSchema>;
19
+ type Collections = ReturnType<typeof createGinkoDocsCollections>;
20
+ type DocsCollection = Collections["docs"];
21
+ type BlogCollection = Collections["blog"];
22
+ type AuthorsCollection = Collections["authors"];
79
23
  type DocsContentConfig = ContentConfig<{ docs: DocsCollection }>;
80
24
  type DocsBlogContentConfig = ContentConfig<{
81
25
  docs: DocsCollection;
@@ -110,32 +54,7 @@ export function defineGinkoDocsConfig(
110
54
  "source",
111
55
  "updated",
112
56
  ]);
113
- const docs = defineCollection({
114
- type: "page",
115
- source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
116
- i18n: i18n ? true : undefined,
117
- route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
118
- agent: { section: "optional", markdown: true },
119
- strict: true,
120
- schema: docsSchemaWithLastmod,
121
- });
122
- const blog = defineCollection({
123
- type: "page",
124
- source: "2.blog/*.md",
125
- i18n: i18n ? true : undefined,
126
- route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
127
- agent: { section: "blog", markdown: true },
128
- strict: true,
129
- schema: blogSchemaWithLastmod,
130
- });
131
- const authors = defineCollection({
132
- type: "data",
133
- source: "authors/**/*.json",
134
- i18n: i18n ? true : undefined,
135
- strict: true,
136
- sitemap: false,
137
- schema: authorsSchema,
138
- });
57
+ const { docs, blog, authors } = createGinkoDocsCollections(i18n);
139
58
  const config = {
140
59
  agent: {
141
60
  site: {
package/nuxt.config.ts CHANGED
@@ -43,7 +43,7 @@ export default defineNuxtConfig({
43
43
  },
44
44
  mcp: {
45
45
  name: "Ginko Docs",
46
- version: "0.2.5",
46
+ version: "0.3.0-rc.2",
47
47
  },
48
48
  components: {
49
49
  dirs: [
@@ -127,7 +127,7 @@ export default defineNuxtConfig({
127
127
  markdown: {
128
128
  plugins: [
129
129
  [
130
- "highlight",
130
+ "shiki",
131
131
  {
132
132
  preStyles: false,
133
133
  transformers: [transformerNotationDiff(), transformerNotationHighlight()],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-docs",
3
- "version": "0.2.5",
3
+ "version": "0.3.0-rc.2",
4
4
  "description": "A Nuxt documentation layer powered by Ginko Content.",
5
5
  "keywords": [
6
6
  "content",
@@ -9,14 +9,14 @@
9
9
  "markdown",
10
10
  "nuxt"
11
11
  ],
12
- "homepage": "https://github.com/Mat4m0/lupinum-docs-shadcn#readme",
12
+ "homepage": "https://ginko-docs.lupinum.com",
13
13
  "bugs": {
14
- "url": "https://github.com/Mat4m0/lupinum-docs-shadcn/issues"
14
+ "url": "https://github.com/lupinum-dev/ginko-docs/issues"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/Mat4m0/lupinum-docs-shadcn.git",
19
+ "url": "git+https://github.com/lupinum-dev/ginko-docs.git",
20
20
  "directory": "layer"
21
21
  },
22
22
  "files": [
@@ -26,6 +26,7 @@
26
26
  "runtime",
27
27
  "server",
28
28
  "shared",
29
+ "content-collections.ts",
29
30
  "content.js",
30
31
  "content.ts",
31
32
  "components.ts",
@@ -72,9 +73,9 @@
72
73
  "clsx": "^2.1.1",
73
74
  "motion-v": "^2.3.0",
74
75
  "nitropack": "^2.13.4",
75
- "nuxt-og-image": "^6.7.7",
76
+ "nuxt-og-image": "^6.7.8",
76
77
  "reka-ui": "^2.10.3",
77
- "satori": "^0.29.0",
78
+ "satori": "^0.19.3",
78
79
  "shiki": "^4.4.3",
79
80
  "tailwind-merge": "^3.6.0",
80
81
  "tailwindcss": "^4.3.3",
@@ -82,7 +83,7 @@
82
83
  "zod": "^4.4.3"
83
84
  },
84
85
  "peerDependencies": {
85
- "@lupinum/ginko-content": ">=0.3.5 <0.4.0",
86
+ "@lupinum/ginko-content": ">=0.4.0-rc.1 <0.5.0",
86
87
  "nuxt": ">=4.4.7 <5",
87
88
  "vue": "^3.5.35",
88
89
  "vue-router": "^5.1.0"
@@ -6,9 +6,11 @@ import { blog } from "../../i18n/messages/global/blog";
6
6
  import { locales, localizedPath, type LocaleCode } from "../../i18n/locales";
7
7
  import { routeSlugs } from "../../shared/route-slugs";
8
8
  import { getLocalizedSiteText } from "../../app/config/site.utils";
9
+ import { createGinkoDocsCollections } from "../../content-collections";
9
10
  import { buildRssFeed } from "./feed";
10
11
 
11
12
  export const MAX_FEED_POSTS = 50;
13
+ const { blog: blogCollection, authors: authorsCollection } = createGinkoDocsCollections(true);
12
14
 
13
15
  export function blogFeedPath(locale: LocaleCode): string {
14
16
  return `${localizedPath(locale, routeSlugs.blog[locale])}/rss.xml`;
@@ -23,10 +25,10 @@ export async function serveBlogFeed(event: H3Event, locale: LocaleCode) {
23
25
  }
24
26
 
25
27
  const site = useAppConfig().ginkoDocs.site;
26
- const posts = await many(event, "blog", {
28
+ const posts = await many(event, blogCollection, {
27
29
  locale,
28
30
  fallback: true,
29
- populate: { author: "authors" },
31
+ populate: { author: authorsCollection },
30
32
  sort: { date: "desc" },
31
33
  limit: MAX_FEED_POSTS,
32
34
  });
@@ -58,17 +58,6 @@ export type GinkoDocsHeroMedia =
58
58
  | { type: "code"; code: string; language?: string; filename?: string }
59
59
  | { type: "code-tabs"; tabs: GinkoDocsHeroCodeTab[] };
60
60
 
61
- export type GinkoDocsPlausibleExtension =
62
- | "hash"
63
- | "outbound-links"
64
- | "file-downloads"
65
- | "tagged-events"
66
- | "revenue"
67
- | "pageview-props"
68
- | "compat"
69
- | "local"
70
- | "manual";
71
-
72
61
  export interface GinkoDocsAppConfig {
73
62
  theme: {
74
63
  neutral: GinkoDocsNeutralPalette;
@@ -99,12 +88,8 @@ export interface GinkoDocsAppConfig {
99
88
  };
100
89
  analytics?: {
101
90
  plausible?: {
102
- /** Leave unset to keep analytics fully disabled. */
103
- domain?: string;
104
- /** Self-hosted Plausible script URL override. */
105
- scriptSrc?: string;
106
- /** Plausible script extensions, e.g. ["outbound-links", "file-downloads"]. */
107
- extensions?: GinkoDocsPlausibleExtension[];
91
+ /** Public site-specific ID from the Plausible pa-<id>.js script URL. */
92
+ scriptId?: string;
108
93
  };
109
94
  };
110
95
  social: Partial<Record<GinkoDocsSocialPlatform, GinkoDocsSocialEntry>>;