@lupinum/ginko-content 0.1.0 → 0.1.1

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
@@ -1,57 +1,78 @@
1
1
  # @lupinum/ginko-content
2
2
 
3
- Filesystem-first, provider-neutral content for Nuxt 4.
3
+ Filesystem-first content for Nuxt 4.
4
4
 
5
- `@lupinum/ginko-content` gives you:
5
+ Write Markdown and data files in `content/`, define collections once in
6
+ `content.config.ts`, then use those collection handles for route pages, lists,
7
+ navigation, search, i18n, and sitemap output.
6
8
 
7
- - file-authored pages and navigation from `content/`
8
- - required typed collections via `content.config.ts`
9
- - Markdown, MDC, YAML, JSON, and CSV ingestion
10
- - locale-aware content routing
11
- - built-in search endpoints and first-class search composables
12
- - a server-side provider contract for advanced custom sources
9
+ Use it when you want content files to stay simple, but your Nuxt app still
10
+ needs explicit APIs for route resolution, typed frontmatter, localized content,
11
+ and server-side reads.
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 20 or later
16
+ - Nuxt 4.0 or later
13
17
 
14
18
  ## Install
15
19
 
16
20
  ```bash
17
- pnpm add @lupinum/ginko-content zod
21
+ npx nuxi module add @lupinum/ginko-content
18
22
  ```
19
23
 
20
- Add the module to your `nuxt.config.ts`:
24
+ The Nuxt CLI installs the package and registers the module in `nuxt.config.ts`.
25
+ If you prefer to install by hand:
26
+
27
+ ```bash
28
+ pnpm add @lupinum/ginko-content
29
+ ```
21
30
 
22
31
  ```ts
23
32
  export default defineNuxtConfig({
24
- modules: ['@lupinum/ginko-content']
33
+ modules: ['@lupinum/ginko-content'],
34
+ imports: {
35
+ autoImport: true
36
+ }
25
37
  })
26
38
  ```
27
39
 
28
- Ginko is published as `@lupinum/ginko-content` and should be registered under that package name.
40
+ ## Quick Start
29
41
 
30
- The default provider reads files from your Nuxt project. The package does not include a CMS UI, Studio, admin panel, or content editing workflow.
42
+ Define a collection:
31
43
 
32
- ## Compatibility
44
+ ```ts
45
+ // content.config.ts
46
+ import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
33
47
 
34
- `@lupinum/ginko-content@2.13.4` targets Nuxt `>=4.0.0` and is verified against
35
- Nuxt `4.4.6`. It is compatible with the first Ginko CMS release line:
36
- `@lupinum/ginko-cms@0.1.0`, `@lupinum/ginko-cms-convex@0.1.0`, and
37
- `@lupinum/ginko-cms-contract@0.1.0`.
48
+ export const pages = defineCollection('pages', {
49
+ type: 'page',
50
+ source: '**/*.md'
51
+ })
38
52
 
39
- The CMS boundary uses `./cms-contract` and `./cms-import`. Those subpaths are
40
- runtime-neutral seams for package integration, not a bundled CMS runtime.
53
+ export default defineContentConfig({
54
+ collections: {
55
+ pages
56
+ }
57
+ })
58
+ ```
41
59
 
42
- ## Basic usage
60
+ Create `content/index.md`:
43
61
 
44
- Create content files:
62
+ ```md
63
+ ---
64
+ title: Welcome
65
+ ---
45
66
 
46
- ```text
47
- content/
48
- index.md
49
- guide/getting-started.md
67
+ # Welcome
68
+
69
+ This file renders at `/`.
50
70
  ```
51
71
 
52
- Render content in a catch-all page:
72
+ Render the current route through the collection:
53
73
 
54
74
  ```vue
75
+ <!-- pages/[...slug].vue -->
55
76
  <script setup lang="ts">
56
77
  import { pages } from '~/content.config'
57
78
 
@@ -63,28 +84,37 @@ const { page } = await useContentPage(pages)
63
84
  </template>
64
85
  ```
65
86
 
66
- Define the collection in `content.config.ts`:
87
+ ## Features
67
88
 
68
- ```ts
69
- import { defineCollection, defineContentConfig, fields } from '@lupinum/ginko-content/config'
70
- import { z } from 'zod'
89
+ - file-authored pages and navigation from `content/`
90
+ - collection definitions as the source of truth for content shape and source
91
+ files
92
+ - Markdown, MDC, YAML, JSON, and CSV ingestion
93
+ - locale-aware content routing
94
+ - route-aware page loading with `useContentPage(handle)`
95
+ - server reads through `one`, `many`, `paginate`, `resolveOne`, `tree`, and
96
+ `neighbors`
97
+ - Vue composables for the same read model
98
+ - search helpers for MiniSearch, Pagefind, and provider-owned search
99
+ - sitemap integration for public content routes
100
+ - a server-side provider contract for advanced custom sources
71
101
 
72
- export const pages = defineCollection('pages', {
73
- type: 'page',
74
- source: ['index.md', 'guide/**/*.md'],
75
- schema: z.object({
76
- title: fields.text()
77
- })
78
- })
102
+ ## Scope
79
103
 
80
- export default defineContentConfig({
81
- collections: {
82
- pages
83
- }
84
- })
85
- ```
104
+ The default provider reads files from your Nuxt project. The package does not
105
+ include a CMS UI, Studio, admin panel, or content editing workflow.
86
106
 
87
107
  ## Docs
88
108
 
89
109
  - Documentation: [ginko-content.nuxt.dev](https://ginko-content.nuxt.dev)
90
110
  - Repository: [github.com/lupinum-dev/ginko-content](https://github.com/lupinum-dev/ginko-content)
111
+
112
+ ## Credits
113
+
114
+ Ginko Content is its own implementation, with clear inspiration from
115
+ [Nuxt Content](https://content.nuxt.com/), [Nuxt UI](https://ui.nuxt.com/), and
116
+ [Comark](https://comark.dev/), the successor to the previous MDC work.
117
+
118
+ ## License
119
+
120
+ [MIT](./LICENSE)
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "Ginko Content release compatibility",
3
3
  "releaseStack": {
4
- "@lupinum/ginko-content": "0.1.0",
5
- "@lupinum/ginko-cms": "0.1.0",
6
- "@lupinum/ginko-cms-convex": "0.1.0",
7
- "@lupinum/ginko-cms-contract": "0.1.0"
4
+ "@lupinum/ginko-content": "0.1.1",
5
+ "@lupinum/ginko-cms": "0.1.2",
6
+ "@lupinum/ginko-cms-convex": "0.1.1",
7
+ "@lupinum/ginko-cms-contract": "0.1.1"
8
8
  },
9
9
  "tracked": {
10
10
  "@convex-dev/better-auth": ["^0.12.2", "0.12.2"],
package/dist/cli.mjs CHANGED
@@ -99,6 +99,93 @@ const lockfileNames = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.
99
99
  const localeCodePattern = /^[a-z]{2}(?:-[A-Z]{2})?$/;
100
100
  const toRelativePath = (rootDir, file) => relative(rootDir, file) || ".";
101
101
  const countSitemapUrls = (text) => (text.match(/<url>/g) || []).length;
102
+ function findMatchingBrace(text, start) {
103
+ let depth = 0;
104
+ let quote;
105
+ let escaped = false;
106
+ let lineComment = false;
107
+ let blockComment = false;
108
+ for (let index = start; index < text.length; index++) {
109
+ const char = text[index];
110
+ const next = text[index + 1];
111
+ if (lineComment) {
112
+ if (char === "\n") {
113
+ lineComment = false;
114
+ }
115
+ continue;
116
+ }
117
+ if (blockComment) {
118
+ if (char === "*" && next === "/") {
119
+ blockComment = false;
120
+ index++;
121
+ }
122
+ continue;
123
+ }
124
+ if (quote) {
125
+ if (escaped) {
126
+ escaped = false;
127
+ } else if (char === "\\") {
128
+ escaped = true;
129
+ } else if (char === quote) {
130
+ quote = void 0;
131
+ }
132
+ continue;
133
+ }
134
+ if (char === "/" && next === "/") {
135
+ lineComment = true;
136
+ index++;
137
+ continue;
138
+ }
139
+ if (char === "/" && next === "*") {
140
+ blockComment = true;
141
+ index++;
142
+ continue;
143
+ }
144
+ if (char === "'" || char === '"' || char === "`") {
145
+ quote = char;
146
+ continue;
147
+ }
148
+ if (char === "{") {
149
+ depth++;
150
+ } else if (char === "}") {
151
+ depth--;
152
+ if (depth === 0) {
153
+ return index;
154
+ }
155
+ }
156
+ }
157
+ return text.length - 1;
158
+ }
159
+ function findCollectionDefinitions(text) {
160
+ const definitions = [];
161
+ const callPattern = /\bdefineCollection\s*\(/g;
162
+ for (const match of text.matchAll(callPattern)) {
163
+ const callStart = match.index || 0;
164
+ const argsStart = callStart + match[0].length;
165
+ const args = text.slice(argsStart);
166
+ const namedMatch = args.match(/^\s*(['"])([^'"]+)\1\s*,\s*\{/);
167
+ if (namedMatch) {
168
+ const bodyStart = argsStart + namedMatch[0].lastIndexOf("{");
169
+ const bodyEnd = findMatchingBrace(text, bodyStart);
170
+ definitions.push({
171
+ name: namedMatch[2],
172
+ block: text.slice(bodyStart, bodyEnd + 1)
173
+ });
174
+ continue;
175
+ }
176
+ const objectMatch = args.match(/^\s*\{/);
177
+ const propertyMatch = text.slice(0, callStart).match(/([a-z_$][\w$]*)\s*:\s*$/i);
178
+ if (objectMatch && propertyMatch) {
179
+ const bodyStart = argsStart + objectMatch[0].lastIndexOf("{");
180
+ const bodyEnd = findMatchingBrace(text, bodyStart);
181
+ definitions.push({
182
+ name: propertyMatch[1],
183
+ block: text.slice(bodyStart, bodyEnd + 1)
184
+ });
185
+ }
186
+ }
187
+ return definitions;
188
+ }
102
189
  async function collectFiles(dir, rootDir, files = []) {
103
190
  const entries = await readdir(dir, { withFileTypes: true });
104
191
  for (const entry of entries) {
@@ -372,25 +459,21 @@ async function inspectI18nCollections(rootDir) {
372
459
  suggestion: "Declare i18n collections with defineContentConfig({ collections })."
373
460
  }];
374
461
  }
375
- const matches = [...text.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*defineCollection\s*\(\s*\{/g)];
376
- if (!matches.length) {
462
+ const collections = findCollectionDefinitions(text);
463
+ if (!collections.length) {
377
464
  return [];
378
465
  }
379
466
  const findings = [];
380
- matches.forEach((match, index) => {
381
- const collectionName = match[1];
382
- const start = match.index || 0;
383
- const end = matches[index + 1]?.index || text.length;
384
- const collectionBlock = text.slice(start, end);
385
- if (!/\bi18n\s*:\s*(true|\{)/.test(collectionBlock)) {
467
+ for (const collection of collections) {
468
+ if (!/\bi18n\s*:\s*(true|\{)/.test(collection.block)) {
386
469
  findings.push({
387
470
  severity: "error",
388
471
  file: "content.config.ts",
389
- message: `Collection "${collectionName}" is not marked as i18n-aware.`,
390
- suggestion: `Add i18n: true to the ${collectionName} collection or provide collection-level i18n locales.`
472
+ message: `Collection "${collection.name}" is not marked as i18n-aware.`,
473
+ suggestion: `Add i18n: true to the ${collection.name} collection or provide collection-level i18n locales.`
391
474
  });
392
475
  }
393
- });
476
+ }
394
477
  return findings;
395
478
  }
396
479
  async function inspectI18nContentFolders(rootDir, locales) {
@@ -13,14 +13,13 @@
13
13
  *
14
14
  * The context is attached to `event.context.__contentRuntime` and created
15
15
  * lazily on first access. Memoization uses `memoizeRuntimeValue` so the
16
- * `graph` / `contents` caches are shared across helpers within a single
16
+ * expensive per-request values are shared across helpers within a single
17
17
  * request but torn down with the event.
18
18
  */
19
19
  import type { H3Event } from 'h3';
20
20
  import type { Storage } from 'unstorage';
21
21
  import type { ParsedContent } from '../../types/content';
22
22
  import type { ContentContext as RuntimeContentConfig } from '../../types/module';
23
- import type { ContentGraph } from '../../core/content/graph';
24
23
  import type { ContentCacheStore } from '../../core/cache';
25
24
  import type { ContentCacheHint } from '../../public/provider';
26
25
  /**
@@ -41,16 +40,13 @@ export interface ContentRuntimeContext {
41
40
  cacheStorage: Storage;
42
41
  cacheParsedStorage: Storage;
43
42
  };
44
- memo: {
45
- contents?: ParsedContent[] | Promise<ParsedContent[]>;
46
- graph?: ContentGraph | Promise<ContentGraph>;
47
- };
43
+ memo: Record<string, unknown | Promise<unknown> | undefined>;
48
44
  caches?: ContentCacheStore<ParsedContent>;
49
45
  cacheHint?: ContentCacheHint | false;
50
46
  }
51
47
  export declare const getContentRuntimeContext: (event: H3Event) => ContentRuntimeContext;
52
48
  /**
53
- * Memoize an expensive per-request value (typically `contents` or `graph`).
49
+ * Memoize an expensive per-request value.
54
50
  *
55
51
  * Concurrent callers within a single request that hit the same key all await
56
52
  * the one `create()` promise — we do not start a second compute. The cache
@@ -60,4 +56,4 @@ export declare const getContentRuntimeContext: (event: H3Event) => ContentRuntim
60
56
  * you cannot use this helper — mutate `runtime.memo[key]` directly. In
61
57
  * practice, nothing we build mid-request invalidates the graph.
62
58
  */
63
- export declare const memoizeRuntimeValue: <T>(event: H3Event, key: keyof ContentRuntimeContext["memo"], create: () => Promise<T>) => Promise<T>;
59
+ export declare const memoizeRuntimeValue: <T>(event: H3Event, key: string, create: () => Promise<T>) => Promise<T>;
package/dist/module.d.mts CHANGED
@@ -141,8 +141,7 @@ interface ContentCollectionConfig<TSchema extends ZodType | undefined = ZodType
141
141
  cms?: ContentCmsCollectionConfig;
142
142
  }
143
143
  type ContentCollectionKind = 'page' | 'data';
144
- type BuiltinContentProviderName = 'filesystem' | 'cms' | 'ginko-cms' | 'ginko';
145
- type ContentProviderName = BuiltinContentProviderName | (string & {});
144
+ type ContentProviderName = 'filesystem' | (string & {});
146
145
  type DefineCollectionOptions<TSchema extends ZodType | undefined = ZodType | undefined> = Omit<ContentCollectionConfig<TSchema>, 'source' | 'exclude'>;
147
146
  interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType | undefined> extends DefineCollectionOptions<TSchema> {
148
147
  /**
@@ -162,14 +161,13 @@ interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType |
162
161
  */
163
162
  interface ContentConfig<TCollections extends Record<string, ContentCollectionConfig> = Record<string, ContentCollectionConfig>> {
164
163
  /**
165
- * Content backing implementation. `filesystem` is the default. `cms`,
166
- * `ginko-cms`, and `ginko` resolve to the official Ginko CMS provider when
167
- * `@lupinum/ginko-cms` is installed.
164
+ * Content backing implementation. `filesystem` is the default. Provider
165
+ * modules can register named implementations, for example `cms`.
168
166
  */
169
167
  provider?: ContentProviderName;
170
168
  /**
171
- * External provider modules keyed by provider name. Built-in providers do not
172
- * need to be registered here.
169
+ * External provider modules keyed by provider name. First-party provider
170
+ * modules register themselves, so app configs usually do not need this.
173
171
  */
174
172
  providers?: Record<string, string>;
175
173
  /**
@@ -1012,6 +1010,7 @@ interface ContentSitemapImage {
1012
1010
  interface ContentSitemapEntry {
1013
1011
  loc: string;
1014
1012
  _sitemap?: string;
1013
+ lastmod?: string;
1015
1014
  alternatives?: ContentSitemapAlternative[];
1016
1015
  images?: ContentSitemapImage[];
1017
1016
  }
@@ -1886,12 +1885,13 @@ interface ModuleOptions {
1886
1885
  /**
1887
1886
  * Backing implementation for public content reads.
1888
1887
  *
1889
- * `filesystem` is the default. `cms`, `ginko-cms`, and `ginko` resolve to the
1890
- * official Ginko CMS provider when `@lupinum/ginko-cms` is installed.
1888
+ * `filesystem` is the default. Provider modules can register named
1889
+ * implementations, for example `cms`.
1891
1890
  */
1892
1891
  provider?: ContentProviderName;
1893
1892
  /**
1894
- * External provider modules keyed by provider name.
1893
+ * External provider modules keyed by provider name. First-party provider
1894
+ * modules register themselves, so app configs usually do not need this.
1895
1895
  */
1896
1896
  providers?: Record<string, string>;
1897
1897
  /**
@@ -1973,7 +1973,8 @@ declare function createSearchRuntimeConfig(search: Exclude<ModuleOptions$1['sear
1973
1973
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions$1, ModuleOptions$1, false>;
1974
1974
 
1975
1975
  interface ModuleHooks {
1976
- 'content:context'(ctx: ContentContext$1): void;
1976
+ 'content:providers'(providers: Record<string, string>): void | Promise<void>;
1977
+ 'content:context'(ctx: ContentContext$1): void | Promise<void>;
1977
1978
  }
1978
1979
  interface ModulePublicRuntimeConfig {
1979
1980
  experimental: {
@@ -2041,4 +2042,4 @@ declare module 'nitropack' {
2041
2042
  }
2042
2043
 
2043
2044
  export { _default as default };
2044
- export type { BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, BuiltinContentProviderName, CollectionQueryBuilder, CollectionQueryOperator, CollectionQueryValue, CollectionSchema, ContentCacheArtifact, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionHandle, ContentCollectionI18nConfig, ContentCollectionI18nMap, ContentCollectionItem, ContentCollectionItemSurroundingsOptions, ContentCollectionKind, ContentCollectionMap, ContentCollectionName, ContentCollectionNavigationOptions, ContentCollectionPageOptions, ContentCollectionRouteConfig, ContentCollectionRouteMetaOptions, ContentCollectionSearchSectionsOptions, ContentCollectionSource, ContentCollectionSourceObject, ContentConfig, ContentContext, ContentI18nOptions, ContentLocaleEntry, ContentLocaleRoute, ContentManifest, ContentMiniSearchOptions, ContentNavigationItem, ContentPageResult, ContentPreviewOptions, ContentProviderName, ContentProviderSearchRequest, ContentQueryBuilder, ContentQueryBuilderParams, ContentQueryBuilderWhere, ContentQueryFetcher, ContentQueryRequest, ContentQuerySortFields, ContentQuerySortOptions, ContentQuerySortParams, ContentReferenceSchema, ContentResolvedMeta, ContentRevalidateOptions, ContentRouteMeta, ContentSearchEngine, ContentSearchIndexRecord, ContentSearchOptions, ContentSearchPublicRuntimeConfig, ContentSearchResult, ContentSearchSection, ContentSelector, ContentSeoImage, ContentSeoMeta, ContentSitemapAlternative, ContentSitemapAssertOptions, ContentSitemapAssertSitemapOptions, ContentSitemapEntry, ContentSitemapImage, ContentSitemapOptions, ContentTransformer, ContentTreeItem, ContentVariant, DefineCollectionObject, DefineCollectionOptions, DocumentFromHandle, LocaleFallback, LocalePathEntry, LocalizedDoc, ManifestVariant, ManyOptions, MarkdownNode, MarkdownOptions, MarkdownParsedContent, MarkdownPluginDescriptor, MarkdownPluginOptions, MarkdownRoot, ModuleHooks, ModuleOptions, MountOptions, NavItem, NeighborsOptions, NeighborsResult, OneOptions, PaginationOptions, PaginationResult, ParseContentOptions, ParsedContent, ParsedContentInternalMeta, ParsedContentMeta, PopulateFromOptions, PopulateSpec, PopulatedDocument, QueryGroupBuilder, QueryGroupFunction, QueryMatchOperator, QueryOperators, QueryOrderDirection, QueryOrderOptions, QueryWhere, ResolutionEnvelope, ResolveContentReferenceOptions, ResolveOneOptions, ResolveOneResult, ResolvedContentI18nOptions, ResolvedMarkdownPlugin, ResolvedVariant, SortDirection, SortSpec, StrictParsedContent, StrictParsedContentMeta, Toc, TocLink, TransformContentOptions, TransformContentSource, TreeOptions, VariantsOptions };
2045
+ export type { BacklinkFields, BacklinkSource, BacklinksOptions, BacklinksResult, CollectionQueryBuilder, CollectionQueryOperator, CollectionQueryValue, CollectionSchema, ContentCacheArtifact, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionHandle, ContentCollectionI18nConfig, ContentCollectionI18nMap, ContentCollectionItem, ContentCollectionItemSurroundingsOptions, ContentCollectionKind, ContentCollectionMap, ContentCollectionName, ContentCollectionNavigationOptions, ContentCollectionPageOptions, ContentCollectionRouteConfig, ContentCollectionRouteMetaOptions, ContentCollectionSearchSectionsOptions, ContentCollectionSource, ContentCollectionSourceObject, ContentConfig, ContentContext, ContentI18nOptions, ContentLocaleEntry, ContentLocaleRoute, ContentManifest, ContentMiniSearchOptions, ContentNavigationItem, ContentPageResult, ContentPreviewOptions, ContentProviderName, ContentProviderSearchRequest, ContentQueryBuilder, ContentQueryBuilderParams, ContentQueryBuilderWhere, ContentQueryFetcher, ContentQueryRequest, ContentQuerySortFields, ContentQuerySortOptions, ContentQuerySortParams, ContentReferenceSchema, ContentResolvedMeta, ContentRevalidateOptions, ContentRouteMeta, ContentSearchEngine, ContentSearchIndexRecord, ContentSearchOptions, ContentSearchPublicRuntimeConfig, ContentSearchResult, ContentSearchSection, ContentSelector, ContentSeoImage, ContentSeoMeta, ContentSitemapAlternative, ContentSitemapAssertOptions, ContentSitemapAssertSitemapOptions, ContentSitemapEntry, ContentSitemapImage, ContentSitemapOptions, ContentTransformer, ContentTreeItem, ContentVariant, DefineCollectionObject, DefineCollectionOptions, DocumentFromHandle, LocaleFallback, LocalePathEntry, LocalizedDoc, ManifestVariant, ManyOptions, MarkdownNode, MarkdownOptions, MarkdownParsedContent, MarkdownPluginDescriptor, MarkdownPluginOptions, MarkdownRoot, ModuleHooks, ModuleOptions, MountOptions, NavItem, NeighborsOptions, NeighborsResult, OneOptions, PaginationOptions, PaginationResult, ParseContentOptions, ParsedContent, ParsedContentInternalMeta, ParsedContentMeta, PopulateFromOptions, PopulateSpec, PopulatedDocument, QueryGroupBuilder, QueryGroupFunction, QueryMatchOperator, QueryOperators, QueryOrderDirection, QueryOrderOptions, QueryWhere, ResolutionEnvelope, ResolveContentReferenceOptions, ResolveOneOptions, ResolveOneResult, ResolvedContentI18nOptions, ResolvedMarkdownPlugin, ResolvedVariant, SortDirection, SortSpec, StrictParsedContent, StrictParsedContentMeta, Toc, TocLink, TransformContentOptions, TransformContentSource, TreeOptions, VariantsOptions };
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "configKey": "content",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.0.0"
package/dist/module.mjs CHANGED
@@ -18,6 +18,7 @@ import { genSafeVariableName, genImport } from 'knitwork';
18
18
  import { listen } from 'listhen';
19
19
  import { makeIgnored } from '../dist/core/content/ignore';
20
20
  import { readFile, readdir } from 'node:fs/promises';
21
+ import { resolveContentSitemapSource, GINKO_SITEMAP_SOURCE_NAME } from '../dist/runtime/utils/sitemap-source';
21
22
  import { resolveCollectionI18nConfig } from '../dist/features/localization/config';
22
23
  import { globby } from 'globby';
23
24
  import { transformContent } from '../dist/parsers/index';
@@ -29,7 +30,7 @@ import { resolveCollection } from '../dist/core/content/collection';
29
30
  export { defineCollection, defineContentConfig, reference } from '../dist/types/config';
30
31
 
31
32
  const name = "@lupinum/ginko-content";
32
- const version = "0.1.0";
33
+ const version = "0.1.1";
33
34
 
34
35
  const logger = consola.withTag("@lupinum/ginko-content");
35
36
  const CACHE_VERSION = 3;
@@ -643,16 +644,37 @@ function hasNuxtI18nModule(modules = []) {
643
644
  function hasNuxtSitemapModule(modules = []) {
644
645
  return hasNuxtModule(modules, "@nuxtjs/sitemap");
645
646
  }
647
+ function configureNuxtSitemapSource(nuxt, apiBaseURL, sitemapPath = "/sitemap") {
648
+ if (!hasNuxtSitemapModule(nuxt.options.modules)) {
649
+ return;
650
+ }
651
+ const source = {
652
+ context: {
653
+ name: GINKO_SITEMAP_SOURCE_NAME
654
+ },
655
+ fetch: resolveContentSitemapSource(apiBaseURL, sitemapPath)
656
+ };
657
+ const sitemap = nuxt.options.sitemap ??= {};
658
+ const sources = Array.isArray(sitemap.sources) ? sitemap.sources : [];
659
+ sitemap.sources = [
660
+ ...sources.filter((item) => {
661
+ if (typeof item === "string") return item !== source.fetch;
662
+ return item?.fetch !== source.fetch && item?.context?.name !== GINKO_SITEMAP_SOURCE_NAME;
663
+ }),
664
+ source
665
+ ];
666
+ sitemap.excludeAppSources = true;
667
+ }
646
668
  function resolveNuxtSitemapPrerenderRoutes(nuxt) {
647
669
  if (!hasNuxtSitemapModule(nuxt.options.modules)) {
648
670
  return [];
649
671
  }
650
672
  const nuxtI18n = nuxt.options.i18n || {};
651
673
  if (!hasNuxtI18nModule(nuxt.options.modules) || !Array.isArray(nuxtI18n.locales) || nuxtI18n.locales.length === 0) {
652
- return [];
674
+ return ["/sitemap.xml"];
653
675
  }
654
676
  const childRoutes = nuxtI18n.locales.map((locale) => typeof locale === "string" ? locale : locale.language || locale.code).filter(Boolean).map((locale) => `/__sitemap__/${locale}.xml`);
655
- return Array.from(/* @__PURE__ */ new Set(["/sitemap_index.xml", ...childRoutes]));
677
+ return Array.from(/* @__PURE__ */ new Set(["/sitemap.xml", "/sitemap_index.xml", ...childRoutes]));
656
678
  }
657
679
  function resolveModuleI18nOptions(options, nuxt) {
658
680
  if (options.i18n === false) {
@@ -1018,9 +1040,7 @@ const collectSitemapCollectionRouteCounts = async (rootDir, contentContext) => {
1018
1040
  return counts;
1019
1041
  };
1020
1042
  const registerContentNitroIntegrationHooks = (nitroConfig, options, contentContext) => {
1021
- if (contentContext.provider && contentContext.provider !== "filesystem") {
1022
- return;
1023
- }
1043
+ const usesFilesystemProvider = !contentContext.provider || contentContext.provider === "filesystem";
1024
1044
  nitroConfig.hooks ||= {};
1025
1045
  if (contentContext.sitemap?.assert?.enabled) {
1026
1046
  appendHook(nitroConfig.hooks, "compiled", async (nitro) => {
@@ -1032,7 +1052,7 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
1032
1052
  await assertGeneratedSitemaps({
1033
1053
  outputPublicDir: nitro.options.output.publicDir,
1034
1054
  options: assertOptions,
1035
- collectionRouteCounts: await collectSitemapCollectionRouteCounts(options.rootDir, contentContext),
1055
+ collectionRouteCounts: usesFilesystemProvider ? await collectSitemapCollectionRouteCounts(options.rootDir, contentContext) : {},
1036
1056
  logger: nitro.logger
1037
1057
  });
1038
1058
  } catch (error) {
@@ -1044,8 +1064,10 @@ const registerContentNitroIntegrationHooks = (nitroConfig, options, contentConte
1044
1064
  });
1045
1065
  }
1046
1066
  appendHook(nitroConfig.hooks, "prerender:routes", async (routes) => {
1047
- for (const route of await collectPrerenderRoutes(options.rootDir, contentContext)) {
1048
- routes.add(route);
1067
+ if (usesFilesystemProvider) {
1068
+ for (const route of await collectPrerenderRoutes(options.rootDir, contentContext)) {
1069
+ routes.add(route);
1070
+ }
1049
1071
  }
1050
1072
  for (const route of options.sitemapPrerenderRoutes || []) {
1051
1073
  routes.add(route);
@@ -1170,10 +1192,11 @@ const module$1 = defineNuxtModule({
1170
1192
  }
1171
1193
  ]));
1172
1194
  options.provider = appContentConfig.provider || options.provider || "filesystem";
1173
- options.providers = {
1195
+ const providerRegistry = {
1174
1196
  ...options.providers || {},
1175
1197
  ...appContentConfig.providers || {}
1176
1198
  };
1199
+ options.providers = providerRegistry;
1177
1200
  const buildIntegrity = nuxt.options.dev ? void 0 : Date.now();
1178
1201
  const contentContext = {
1179
1202
  ...options,
@@ -1186,6 +1209,9 @@ const module$1 = defineNuxtModule({
1186
1209
  sitemap: resolvedSitemap,
1187
1210
  search: resolvedSearch
1188
1211
  };
1212
+ if (resolvedSitemap !== false) {
1213
+ configureNuxtSitemapSource(nuxt, options.api.baseURL, resolvedSitemap.path);
1214
+ }
1189
1215
  if (resolvedSitemap && resolvedSitemap.assert.enabled) {
1190
1216
  nuxt.hook("sitemap:prerender:done", async ({ sitemaps }) => {
1191
1217
  const assertOptions = contentContext.sitemap?.assert;
@@ -1310,6 +1336,8 @@ const module$1 = defineNuxtModule({
1310
1336
  });
1311
1337
  }
1312
1338
  nuxt.hook("modules:done", async () => {
1339
+ await nuxt.callHook("content:providers", contentContext.providers ||= {});
1340
+ assertConfiguredProviderAvailable(contentContext);
1313
1341
  await nuxt.callHook("content:context", contentContext);
1314
1342
  if (contentContext.search !== false) {
1315
1343
  registerContentSearchServerHandlers(options.api.baseURL, contentContext.search, resolveRuntimeModule);
@@ -1348,6 +1376,15 @@ const module$1 = defineNuxtModule({
1348
1376
  }
1349
1377
  }
1350
1378
  });
1379
+ function assertConfiguredProviderAvailable(contentContext) {
1380
+ const provider = contentContext.provider || "filesystem";
1381
+ if (provider === "filesystem") return;
1382
+ if (contentContext.providers?.[provider]) return;
1383
+ if (provider === "cms") {
1384
+ throw new Error('content.config.ts sets provider "cms", but no CMS provider module registered it. Add @lupinum/ginko-cms to nuxt.config.ts modules or remove provider: "cms".');
1385
+ }
1386
+ throw new Error(`content.config.ts sets provider "${provider}", but no provider module registered it. Register a module for "${provider}" or add it to content providers.`);
1387
+ }
1351
1388
  function validateRemovedMarkdownOptions(options) {
1352
1389
  if (options.highlight !== void 0) {
1353
1390
  throw new Error("`content.highlight` was removed. Enable syntax highlighting with `content.markdown.plugins`, for example `[['highlight', { ...options }]]`.");
@@ -57,8 +57,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
57
57
  default: () => {};
58
58
  };
59
59
  }>> & Readonly<{}>, {
60
- data: Record<string, any>;
61
60
  excerpt: boolean;
61
+ data: Record<string, any>;
62
62
  components: Record<string, any>;
63
63
  prose: boolean;
64
64
  tag: string;
@@ -57,8 +57,8 @@ declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractP
57
57
  default: () => {};
58
58
  };
59
59
  }>> & Readonly<{}>, {
60
- data: Record<string, any>;
61
60
  excerpt: boolean;
61
+ data: Record<string, any>;
62
62
  components: Record<string, any>;
63
63
  prose: boolean;
64
64
  tag: string;
@@ -1,7 +1,7 @@
1
1
  import { defineNitroPlugin } from "nitropack/runtime";
2
2
  import { getContentRuntimeConfig } from "../runtime-config.js";
3
+ import { GINKO_SITEMAP_SOURCE_NAME, resolveContentSitemapSource } from "../../utils/sitemap-source.js";
3
4
  const LEGACY_NUXT_CONTENT_V2_SOURCE = "@nuxt/content@v2:urls";
4
- const GINKO_SOURCE = "@lupinum/ginko-content:urls";
5
5
  export default defineNitroPlugin((nitro) => {
6
6
  nitro.hooks.hook("sitemap:sources", (ctx) => {
7
7
  const runtimeConfig = getContentRuntimeConfig();
@@ -10,13 +10,13 @@ export default defineNitroPlugin((nitro) => {
10
10
  if (!sitemap || !apiBaseURL) {
11
11
  return;
12
12
  }
13
- const fetch = `${apiBaseURL}${sitemap.path || "/sitemap"}`;
13
+ const fetch = resolveContentSitemapSource(apiBaseURL, sitemap.path || "/sitemap");
14
14
  ctx.sources = ctx.sources.filter((source) => {
15
- return source.context?.name !== LEGACY_NUXT_CONTENT_V2_SOURCE && source.context?.name !== GINKO_SOURCE;
15
+ return source.context?.name !== LEGACY_NUXT_CONTENT_V2_SOURCE && source.context?.name !== GINKO_SITEMAP_SOURCE_NAME;
16
16
  });
17
17
  ctx.sources.push({
18
18
  context: {
19
- name: GINKO_SOURCE
19
+ name: GINKO_SITEMAP_SOURCE_NAME
20
20
  },
21
21
  fetch,
22
22
  sourceType: "app"
@@ -0,0 +1 @@
1
+ export * from './sitemap-source.js'
@@ -0,0 +1,2 @@
1
+ export declare const GINKO_SITEMAP_SOURCE_NAME = "@lupinum/ginko-content:urls";
2
+ export declare function resolveContentSitemapSource(apiBaseURL: string, sitemapPath?: string): string;
@@ -0,0 +1,6 @@
1
+ export const GINKO_SITEMAP_SOURCE_NAME = "@lupinum/ginko-content:urls";
2
+ export function resolveContentSitemapSource(apiBaseURL, sitemapPath = "/sitemap") {
3
+ const base = apiBaseURL.replace(/\/$/, "");
4
+ const path = sitemapPath.startsWith("/") ? sitemapPath : `/${sitemapPath}`;
5
+ return `${base}${path}`;
6
+ }
@@ -132,8 +132,7 @@ export interface ContentCollectionConfig<TSchema extends ZodType | undefined = Z
132
132
  cms?: ContentCmsCollectionConfig;
133
133
  }
134
134
  export type ContentCollectionKind = 'page' | 'data';
135
- export type BuiltinContentProviderName = 'filesystem' | 'cms' | 'ginko-cms' | 'ginko';
136
- export type ContentProviderName = BuiltinContentProviderName | (string & {});
135
+ export type ContentProviderName = 'filesystem' | (string & {});
137
136
  export type DefineCollectionOptions<TSchema extends ZodType | undefined = ZodType | undefined> = Omit<ContentCollectionConfig<TSchema>, 'source' | 'exclude'>;
138
137
  export interface DefineCollectionObject<TSchema extends ZodType | undefined = ZodType | undefined> extends DefineCollectionOptions<TSchema> {
139
138
  /**
@@ -153,14 +152,13 @@ export interface DefineCollectionObject<TSchema extends ZodType | undefined = Zo
153
152
  */
154
153
  export interface ContentConfig<TCollections extends Record<string, ContentCollectionConfig> = Record<string, ContentCollectionConfig>> {
155
154
  /**
156
- * Content backing implementation. `filesystem` is the default. `cms`,
157
- * `ginko-cms`, and `ginko` resolve to the official Ginko CMS provider when
158
- * `@lupinum/ginko-cms` is installed.
155
+ * Content backing implementation. `filesystem` is the default. Provider
156
+ * modules can register named implementations, for example `cms`.
159
157
  */
160
158
  provider?: ContentProviderName;
161
159
  /**
162
- * External provider modules keyed by provider name. Built-in providers do not
163
- * need to be registered here.
160
+ * External provider modules keyed by provider name. First-party provider
161
+ * modules register themselves, so app configs usually do not need this.
164
162
  */
165
163
  providers?: Record<string, string>;
166
164
  /**
@@ -245,13 +243,13 @@ export declare function defineCollection<const Name extends string, const TConfi
245
243
  * ```ts
246
244
  * import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
247
245
  *
246
+ * export const docs = defineCollection('docs', {
247
+ * type: 'page',
248
+ * source: 'docs/*.md'
249
+ * })
250
+ *
248
251
  * export default defineContentConfig({
249
- * collections: {
250
- * docs: defineCollection({
251
- * type: 'page',
252
- * source: 'docs/*.md'
253
- * })
254
- * }
252
+ * collections: { docs }
255
253
  * })
256
254
  * ```
257
255
  */
@@ -265,9 +263,9 @@ export declare function defineContentConfig<TCollections extends Record<string,
265
263
  * @example
266
264
  * ```ts
267
265
  * import { z } from 'zod'
268
- * import { defineCollection, reference } from '@lupinum/ginko-content/config'
266
+ * import { defineCollection, defineContentConfig, reference } from '@lupinum/ginko-content/config'
269
267
  *
270
- * export default defineCollection({
268
+ * export const blog = defineCollection('blog', {
271
269
  * type: 'page',
272
270
  * source: 'blog/*.md',
273
271
  * schema: z.object({
@@ -275,6 +273,10 @@ export declare function defineContentConfig<TCollections extends Record<string,
275
273
  * related: z.array(reference('blog')).default([])
276
274
  * })
277
275
  * })
276
+ *
277
+ * export default defineContentConfig({
278
+ * collections: { blog }
279
+ * })
278
280
  * ```
279
281
  */
280
282
  export declare function reference(collection?: string): ContentReferenceSchema;
@@ -386,12 +386,13 @@ export interface ModuleOptions {
386
386
  /**
387
387
  * Backing implementation for public content reads.
388
388
  *
389
- * `filesystem` is the default. `cms`, `ginko-cms`, and `ginko` resolve to the
390
- * official Ginko CMS provider when `@lupinum/ginko-cms` is installed.
389
+ * `filesystem` is the default. Provider modules can register named
390
+ * implementations, for example `cms`.
391
391
  */
392
392
  provider?: ContentProviderName;
393
393
  /**
394
- * External provider modules keyed by provider name.
394
+ * External provider modules keyed by provider name. First-party provider
395
+ * modules register themselves, so app configs usually do not need this.
395
396
  */
396
397
  providers?: Record<string, string>;
397
398
  /**
@@ -434,6 +434,7 @@ export interface ContentSitemapImage {
434
434
  export interface ContentSitemapEntry {
435
435
  loc: string;
436
436
  _sitemap?: string;
437
+ lastmod?: string;
437
438
  alternatives?: ContentSitemapAlternative[];
438
439
  images?: ContentSitemapImage[];
439
440
  }
package/dist/types.d.mts CHANGED
@@ -8,4 +8,4 @@ export { type defineCollection, type defineContentConfig, type reference } from
8
8
 
9
9
  export { default } from './module.mjs'
10
10
 
11
- export { type BacklinkFields, type BacklinkSource, type BacklinksOptions, type BacklinksResult, type BuiltinContentProviderName, type CollectionQueryBuilder, type CollectionQueryOperator, type CollectionQueryValue, type CollectionSchema, type ContentCacheArtifact, type ContentCmsCollectionConfig, type ContentCmsFieldConfig, type ContentCmsFieldType, type ContentCmsRelationConfig, type ContentCollectionConfig, type ContentCollectionHandle, type ContentCollectionI18nConfig, type ContentCollectionI18nMap, type ContentCollectionItem, type ContentCollectionItemSurroundingsOptions, type ContentCollectionKind, type ContentCollectionMap, type ContentCollectionName, type ContentCollectionNavigationOptions, type ContentCollectionPageOptions, type ContentCollectionRouteConfig, type ContentCollectionRouteMetaOptions, type ContentCollectionSearchSectionsOptions, type ContentCollectionSource, type ContentCollectionSourceObject, type ContentConfig, type ContentContext, type ContentI18nOptions, type ContentLocaleEntry, type ContentLocaleRoute, type ContentManifest, type ContentMiniSearchOptions, type ContentNavigationItem, type ContentPageResult, type ContentPreviewOptions, type ContentProviderName, type ContentProviderSearchRequest, type ContentQueryBuilder, type ContentQueryBuilderParams, type ContentQueryBuilderWhere, type ContentQueryFetcher, type ContentQueryRequest, type ContentQuerySortFields, type ContentQuerySortOptions, type ContentQuerySortParams, type ContentReferenceSchema, type ContentResolvedMeta, type ContentRevalidateOptions, type ContentRouteMeta, type ContentSearchEngine, type ContentSearchIndexRecord, type ContentSearchOptions, type ContentSearchPublicRuntimeConfig, type ContentSearchResult, type ContentSearchSection, type ContentSelector, type ContentSeoImage, type ContentSeoMeta, type ContentSitemapAlternative, type ContentSitemapAssertOptions, type ContentSitemapAssertSitemapOptions, type ContentSitemapEntry, type ContentSitemapImage, type ContentSitemapOptions, type ContentTransformer, type ContentTreeItem, type ContentVariant, type DefineCollectionObject, type DefineCollectionOptions, type DocumentFromHandle, type LocaleFallback, type LocalePathEntry, type LocalizedDoc, type ManifestVariant, type ManyOptions, type MarkdownNode, type MarkdownOptions, type MarkdownParsedContent, type MarkdownPluginDescriptor, type MarkdownPluginOptions, type MarkdownRoot, type ModuleHooks, type ModuleOptions, type MountOptions, type NavItem, type NeighborsOptions, type NeighborsResult, type OneOptions, type PaginationOptions, type PaginationResult, type ParseContentOptions, type ParsedContent, type ParsedContentInternalMeta, type ParsedContentMeta, type PopulateFromOptions, type PopulateSpec, type PopulatedDocument, type QueryGroupBuilder, type QueryGroupFunction, type QueryMatchOperator, type QueryOperators, type QueryOrderDirection, type QueryOrderOptions, type QueryWhere, type ResolutionEnvelope, type ResolveContentReferenceOptions, type ResolveOneOptions, type ResolveOneResult, type ResolvedContentI18nOptions, type ResolvedMarkdownPlugin, type ResolvedVariant, type SortDirection, type SortSpec, type StrictParsedContent, type StrictParsedContentMeta, type Toc, type TocLink, type TransformContentOptions, type TransformContentSource, type TreeOptions, type VariantsOptions } from './module.mjs'
11
+ export { type BacklinkFields, type BacklinkSource, type BacklinksOptions, type BacklinksResult, type CollectionQueryBuilder, type CollectionQueryOperator, type CollectionQueryValue, type CollectionSchema, type ContentCacheArtifact, type ContentCmsCollectionConfig, type ContentCmsFieldConfig, type ContentCmsFieldType, type ContentCmsRelationConfig, type ContentCollectionConfig, type ContentCollectionHandle, type ContentCollectionI18nConfig, type ContentCollectionI18nMap, type ContentCollectionItem, type ContentCollectionItemSurroundingsOptions, type ContentCollectionKind, type ContentCollectionMap, type ContentCollectionName, type ContentCollectionNavigationOptions, type ContentCollectionPageOptions, type ContentCollectionRouteConfig, type ContentCollectionRouteMetaOptions, type ContentCollectionSearchSectionsOptions, type ContentCollectionSource, type ContentCollectionSourceObject, type ContentConfig, type ContentContext, type ContentI18nOptions, type ContentLocaleEntry, type ContentLocaleRoute, type ContentManifest, type ContentMiniSearchOptions, type ContentNavigationItem, type ContentPageResult, type ContentPreviewOptions, type ContentProviderName, type ContentProviderSearchRequest, type ContentQueryBuilder, type ContentQueryBuilderParams, type ContentQueryBuilderWhere, type ContentQueryFetcher, type ContentQueryRequest, type ContentQuerySortFields, type ContentQuerySortOptions, type ContentQuerySortParams, type ContentReferenceSchema, type ContentResolvedMeta, type ContentRevalidateOptions, type ContentRouteMeta, type ContentSearchEngine, type ContentSearchIndexRecord, type ContentSearchOptions, type ContentSearchPublicRuntimeConfig, type ContentSearchResult, type ContentSearchSection, type ContentSelector, type ContentSeoImage, type ContentSeoMeta, type ContentSitemapAlternative, type ContentSitemapAssertOptions, type ContentSitemapAssertSitemapOptions, type ContentSitemapEntry, type ContentSitemapImage, type ContentSitemapOptions, type ContentTransformer, type ContentTreeItem, type ContentVariant, type DefineCollectionObject, type DefineCollectionOptions, type DocumentFromHandle, type LocaleFallback, type LocalePathEntry, type LocalizedDoc, type ManifestVariant, type ManyOptions, type MarkdownNode, type MarkdownOptions, type MarkdownParsedContent, type MarkdownPluginDescriptor, type MarkdownPluginOptions, type MarkdownRoot, type ModuleHooks, type ModuleOptions, type MountOptions, type NavItem, type NeighborsOptions, type NeighborsResult, type OneOptions, type PaginationOptions, type PaginationResult, type ParseContentOptions, type ParsedContent, type ParsedContentInternalMeta, type ParsedContentMeta, type PopulateFromOptions, type PopulateSpec, type PopulatedDocument, type QueryGroupBuilder, type QueryGroupFunction, type QueryMatchOperator, type QueryOperators, type QueryOrderDirection, type QueryOrderOptions, type QueryWhere, type ResolutionEnvelope, type ResolveContentReferenceOptions, type ResolveOneOptions, type ResolveOneResult, type ResolvedContentI18nOptions, type ResolvedMarkdownPlugin, type ResolvedVariant, type SortDirection, type SortSpec, type StrictParsedContent, type StrictParsedContentMeta, type Toc, type TocLink, type TransformContentOptions, type TransformContentSource, type TreeOptions, type VariantsOptions } from './module.mjs'
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "@lupinum/ginko-content",
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "contributions": {
6
6
  "html": {
7
7
  "description-markup": "markdown",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Collection-first content engine for Nuxt",
5
5
  "homepage": "https://ginko-content.nuxt.dev",
6
6
  "bugs": {