@lupinum/ginko-content 0.3.0 → 0.3.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.
@@ -18,10 +18,9 @@
18
18
  * page resolver when the request arrives as a localized URL.
19
19
  * - **`byRef`**: normalized ref string → canonical key. This is how
20
20
  * markdown links such as `[Ada]($authors.ada)` find their target without a scan.
21
- * - **`referenceTargets`**: the `buildReferenceTargets` map every shape
22
- * the user might plausibly write to point at a document (canonical
23
- * name, locale-prefixed path, short slug) pre-resolved to a canonical
24
- * key.
21
+ * - **`referenceTargets`**: every unambiguous shape the user might write
22
+ * (canonical name, locale-prefixed path, short slug) pre-resolved to its
23
+ * canonical key and owning collection.
25
24
  *
26
25
  * The graph is rebuilt from scratch on every request in dev; in production
27
26
  * it is memoized per-request via `memoizeRuntimeValue(event, 'graph', ...)`.
@@ -32,6 +31,10 @@ import type { ContentVariantIdentity, ResolvedVariant } from '../../types/runtim
32
31
  export interface ContentGraphVariant extends ContentVariantIdentity {
33
32
  document: ParsedContent;
34
33
  }
34
+ export interface ContentGraphReferenceTarget {
35
+ readonly canonicalKey: string;
36
+ readonly collection: string;
37
+ }
35
38
  export interface ContentGraph {
36
39
  documents: ParsedContent[];
37
40
  /** id → document. Keys include the locale suffix; one entry per source file. */
@@ -52,8 +55,8 @@ export interface ContentGraph {
52
55
  byRef: Record<string, string>;
53
56
  /** navigation path → `{ [locale]: document }`. Drives `.navigation.yml` merging. */
54
57
  byNavigationPath: Record<string, Record<string, ParsedContent>>;
55
- /** Every user-writable reference shape pre-resolved to a canonical key. */
56
- referenceTargets: Map<string, string>;
58
+ /** Every unambiguous user-writable reference shape with its collection scope intact. */
59
+ referenceTargets: Map<string, ContentGraphReferenceTarget>;
57
60
  /** Exact collection-scoped user-writable reference targets. */
58
61
  referenceTargetsByCollection: Record<string, Map<string, string>>;
59
62
  }
@@ -134,6 +137,12 @@ export declare const resolveGraphRouteVariant: (graph: ContentGraph, routePath:
134
137
  * leak through.
135
138
  */
136
139
  export declare const resolveGraphCanonicalKey: (graph: ContentGraph, identity: string, collection?: string) => string | null;
140
+ /**
141
+ * Resolve a user-authored reference without discarding the collection that
142
+ * made the identity unambiguous. Canonical keys are collection-scoped, so
143
+ * callers that subsequently resolve a locale variant must carry both values.
144
+ */
145
+ export declare const resolveGraphReferenceTarget: (graph: ContentGraph, identity: string, collection?: string) => ContentGraphReferenceTarget | null;
137
146
  export declare const resolveGraphCollectionLocales: (graph: ContentGraph, identity: string, collection?: string) => ContentLocaleEntry[];
138
147
  export declare const selectGraphDocuments: (graph: ContentGraph, options?: {
139
148
  collection?: string;
@@ -100,7 +100,10 @@ export const buildContentGraph = (documents, options = {}) => {
100
100
  }
101
101
  const referenceTargets = /* @__PURE__ */ new Map();
102
102
  for (const [identity, collections] of referenceTargetCollections) {
103
- if (collections.length === 1) referenceTargets.set(identity, referenceTargetsByCollection[collections[0]].get(identity));
103
+ if (collections.length !== 1) continue;
104
+ const collection = collections[0];
105
+ const canonicalKey = referenceTargetsByCollection[collection].get(identity);
106
+ if (canonicalKey) referenceTargets.set(identity, { canonicalKey, collection });
104
107
  }
105
108
  return {
106
109
  documents,
@@ -180,9 +183,24 @@ export const resolveGraphCanonicalKey = (graph, identity, collection) => {
180
183
  if (collection && !(graph.byCollection[collection] || []).length) {
181
184
  return null;
182
185
  }
183
- const targetCanonicalKey = collection ? graph.referenceTargetsByCollection[collection]?.get(normalizedIdentity) : graph.referenceTargets.get(normalizedIdentity);
186
+ const targetCanonicalKey = collection ? graph.referenceTargetsByCollection[collection]?.get(normalizedIdentity) : graph.referenceTargets.get(normalizedIdentity)?.canonicalKey;
184
187
  return targetCanonicalKey && hasCollectionVariant(targetCanonicalKey) ? targetCanonicalKey : null;
185
188
  };
189
+ export const resolveGraphReferenceTarget = (graph, identity, collection) => {
190
+ const normalizedIdentity = normalizeReferenceValue(identity);
191
+ if (!normalizedIdentity) return null;
192
+ if (collection) {
193
+ const canonicalKey = resolveGraphCanonicalKey(graph, normalizedIdentity, collection);
194
+ return canonicalKey ? { canonicalKey, collection } : null;
195
+ }
196
+ const canonicalVariants = graph.byCanonical[normalizedIdentity];
197
+ if (canonicalVariants) {
198
+ const targetCollection = Object.values(canonicalVariants)[0]?.document.collection || "content";
199
+ return { canonicalKey: normalizedIdentity, collection: targetCollection };
200
+ }
201
+ const target = graph.referenceTargets.get(normalizedIdentity);
202
+ return target && getGraphCanonicalVariants(graph, target.canonicalKey, target.collection) ? target : null;
203
+ };
186
204
  export const resolveGraphCollectionLocales = (graph, identity, collection) => {
187
205
  const canonicalKey = resolveGraphCanonicalKey(graph, identity, collection);
188
206
  if (!canonicalKey) {
@@ -1,4 +1,4 @@
1
- import { resolveGraphCanonicalKey } from "../../core/content/graph.js";
1
+ import { getGraphCanonicalVariants, resolveGraphReferenceTarget } from "../../core/content/graph.js";
2
2
  import { parseRefLink } from "../../core/references/resolve.js";
3
3
  const normalizePath = (path) => {
4
4
  const normalized = path.replace(/\/{2,}/g, "/").replace(/\/$/, "");
@@ -98,11 +98,11 @@ export const validateContentLinks = async (documents, options) => {
98
98
  }
99
99
  continue;
100
100
  }
101
- const canonicalKey = resolveGraphCanonicalKey(options.graph, parsedRef.ref);
102
- const variants = canonicalKey ? options.graph.byCanonical[canonicalKey] : void 0;
101
+ const target = resolveGraphReferenceTarget(options.graph, parsedRef.ref);
102
+ const variants = target ? getGraphCanonicalVariants(options.graph, target.canonicalKey, target.collection) : void 0;
103
103
  const variant = variants?.[document.locale || ""] || variants?.[options.defaultLocale || ""] || Object.values(variants || {})[0];
104
104
  const targetDocument2 = variant ? options.graph.byId[variant.contentId] : void 0;
105
- const targetRoute = targetDocument2 ? routeByIdentity.get(identityKey(targetDocument2.collection, canonicalKey || "", variant?.locale)) : void 0;
105
+ const targetRoute = target && targetDocument2 ? routeByIdentity.get(identityKey(target.collection, target.canonicalKey, variant?.locale)) : void 0;
106
106
  if (!targetRoute) {
107
107
  addBroken(sourceFile, authoredValue);
108
108
  continue;
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "configKey": "content",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.4.7 <5"
package/dist/module.mjs CHANGED
@@ -29,7 +29,7 @@ import { createRouterMatcher } from 'vue-router';
29
29
  import { globby } from 'globby';
30
30
 
31
31
  const name = "@lupinum/ginko-content";
32
- const version = "0.3.0";
32
+ const version = "0.3.2";
33
33
  const peerDependencies = {
34
34
  nuxt: ">=4.4.7 <5"};
35
35
 
@@ -1,4 +1,9 @@
1
- import { fetchContentApi, getContentApiFetcher, getPreviewToken } from "./utils.js";
1
+ import {
2
+ createPrerenderPathAdder,
3
+ fetchContentApi,
4
+ getContentApiFetcher,
5
+ getPreviewToken
6
+ } from "./utils.js";
2
7
  import { getContentRuntime } from "./runtime.js";
3
8
  import {
4
9
  backlinks as backlinksWithContext,
@@ -13,13 +18,14 @@ export const createClientContentQueryContext = () => {
13
18
  const runtime = getContentRuntime();
14
19
  const fetcher = getContentApiFetcher();
15
20
  const previewToken = getPreviewToken() ?? null;
21
+ const addPrerenderPath = createPrerenderPathAdder();
16
22
  return {
17
23
  runtime,
18
24
  transport: async (endpoint, params) => {
19
25
  return await fetchContentApi(
20
26
  endpoint,
21
27
  params,
22
- { fetcher, runtime, previewToken }
28
+ { fetcher, runtime, previewToken, addPrerenderPath }
23
29
  );
24
30
  }
25
31
  };
@@ -7,17 +7,17 @@ interface ContentRuntimeShape {
7
7
  }
8
8
  export declare const withContentBase: (url: string) => string;
9
9
  export declare const navigationDisabled: () => never;
10
- export declare const addPrerenderPath: (path: string) => void;
10
+ export declare const createPrerenderPathAdder: () => ((path: string) => void) | undefined;
11
11
  export type ContentApiEndpoint = 'query' | 'navigation';
12
12
  export type ContentApiFetcher = (request: string, init?: Record<string, unknown>) => Promise<unknown>;
13
13
  export declare const getPreviewToken: () => any;
14
14
  export declare const getContentApiFetcher: (fetcher?: ContentApiFetcher) => ContentApiFetcher;
15
- export declare const buildContentApiPath: (endpoint: ContentApiEndpoint, params: ContentProviderQueryInput, runtime?: ContentRuntimeShape) => string;
15
+ export declare const buildContentApiPath: (endpoint: ContentApiEndpoint, params: ContentProviderQueryInput, runtime: ContentRuntimeShape) => string;
16
16
  export declare const isHtmlFallbackResponse: (data: unknown) => data is string;
17
- export declare function fetchContentApi<T>(endpoint: ContentApiEndpoint, params: ContentProviderQueryInput, options?: {
18
- fetcher?: ContentApiFetcher;
19
- runtime?: ContentRuntimeShape;
20
- notFoundMessage?: string;
21
- previewToken?: string | null;
17
+ export declare function fetchContentApi<T>(endpoint: ContentApiEndpoint, params: ContentProviderQueryInput, options: {
18
+ fetcher: ContentApiFetcher;
19
+ runtime: ContentRuntimeShape;
20
+ previewToken: string | null;
21
+ addPrerenderPath?: (path: string) => void;
22
22
  }): Promise<T>;
23
23
  export {};
@@ -1,6 +1,6 @@
1
1
  import { withBase } from "ufo";
2
2
  import { hash } from "ohash";
3
- import { useRequestEvent, useRequestFetch } from "#imports";
3
+ import { tryUseNuxtApp, useRequestEvent, useRequestFetch } from "#imports";
4
4
  import { encodeQueryParams } from "../../utils/query.js";
5
5
  import { useContentPreview } from "./preview.js";
6
6
  import { getContentRuntime } from "./runtime.js";
@@ -11,13 +11,6 @@ export const navigationDisabled = () => {
11
11
  console.warn("Learn more in the Ginko navigation documentation.");
12
12
  throw new Error("Navigation is only accessible when you enable it in module options.");
13
13
  };
14
- const lookupRequestEvent = () => {
15
- try {
16
- return useRequestEvent();
17
- } catch {
18
- return void 0;
19
- }
20
- };
21
14
  const addPathToEvent = (event, path) => {
22
15
  event.node.res.setHeader(
23
16
  "x-nitro-prerender",
@@ -27,16 +20,12 @@ const addPathToEvent = (event, path) => {
27
20
  ].filter(Boolean).join(",")
28
21
  );
29
22
  };
30
- const createPrerenderPathAdder = () => {
31
- const event = lookupRequestEvent();
23
+ export const createPrerenderPathAdder = () => {
24
+ if (import.meta.dev || !import.meta.server) return void 0;
25
+ const nuxtApp = tryUseNuxtApp();
26
+ const event = nuxtApp ? useRequestEvent(nuxtApp) : void 0;
32
27
  return event ? (path) => addPathToEvent(event, path) : void 0;
33
28
  };
34
- export const addPrerenderPath = (path) => {
35
- const event = lookupRequestEvent();
36
- if (event) {
37
- addPathToEvent(event, path);
38
- }
39
- };
40
29
  export const getPreviewToken = () => useContentPreview().getPreviewToken();
41
30
  export const getContentApiFetcher = (fetcher) => {
42
31
  if (fetcher) {
@@ -48,32 +37,26 @@ export const getContentApiFetcher = (fetcher) => {
48
37
  return $fetch;
49
38
  };
50
39
  export const buildContentApiPath = (endpoint, params, runtime) => {
51
- const content = runtime || readContentRuntime();
52
40
  const encodedParams = encodeQueryParams(params);
53
- const requestKey = import.meta.dev ? "_" : `${hash(params)}.${content.integrity}`;
54
- return withBase(`/${endpoint}/${requestKey}/${encodedParams}.json`, content.api.baseURL);
41
+ const requestKey = import.meta.dev ? "_" : `${hash(params)}.${runtime.integrity}`;
42
+ return withBase(`/${endpoint}/${requestKey}/${encodedParams}.json`, runtime.api.baseURL);
55
43
  };
56
44
  export const isHtmlFallbackResponse = (data) => {
57
45
  return typeof data === "string" && data.startsWith("<!DOCTYPE html>");
58
46
  };
59
- export async function fetchContentApi(endpoint, params, options = {}) {
47
+ export async function fetchContentApi(endpoint, params, options) {
60
48
  const apiPath = buildContentApiPath(endpoint, params, options.runtime);
61
- const previewToken = options.previewToken === void 0 ? getPreviewToken() : options.previewToken;
62
- const addPrerenderPathOnSuccess = !import.meta.dev && import.meta.server ? createPrerenderPathAdder() : void 0;
63
- const fetcher = getContentApiFetcher(options.fetcher);
64
- const data = await fetcher(apiPath, {
49
+ const data = await options.fetcher(apiPath, {
65
50
  method: "GET",
66
51
  responseType: "json",
67
- ...previewToken ? { headers: { "x-nuxt-content-preview": previewToken } } : {}
52
+ ...options.previewToken ? { headers: { "x-nuxt-content-preview": options.previewToken } } : {}
68
53
  });
69
54
  if (isHtmlFallbackResponse(data)) {
70
- throw new Error(options.notFoundMessage || "Not found");
55
+ throw new Error("Not found");
71
56
  }
72
57
  if (data === void 0 || data === null) {
73
58
  throw new TypeError("Invalid content API response: expected a non-empty JSON body.");
74
59
  }
75
- if (!import.meta.dev && import.meta.server) {
76
- addPrerenderPathOnSuccess?.(apiPath);
77
- }
60
+ options.addPrerenderPath?.(apiPath);
78
61
  return data;
79
62
  }
@@ -19,6 +19,7 @@ export declare const getContentGraph: (event: H3Event) => Promise<ContentGraph>;
19
19
  export declare function resolveVariant(event: H3Event, canonicalKey: string, requestedLocale?: string, options?: {
20
20
  fallback?: string[];
21
21
  exact?: boolean;
22
+ collection?: string;
22
23
  }): Promise<import("../types/runtime").ResolvedVariant | null>;
23
24
  export declare function resolveRouteVariant(event: H3Event, routePath: string, requestedLocale?: string, options?: {
24
25
  fallback?: string[];
@@ -40,6 +40,7 @@ export async function resolveVariant(event, canonicalKey, requestedLocale, optio
40
40
  locales: config.locales,
41
41
  fallback: options.fallback,
42
42
  exact: options.exact,
43
+ collection: options.collection,
43
44
  localeFallback: config.localeFallback
44
45
  });
45
46
  }
@@ -1,8 +1,9 @@
1
+ import { resolveGraphReferenceTarget } from "../core/content/graph.js";
1
2
  import { collectMarkdownRefLinks, parseRefLink } from "../core/references/resolve.js";
2
3
  import { projectContentRoute } from "../features/localization/route-projector.js";
3
4
  import { resolveRuntimeCollectionLocalePolicy } from "../features/localization/config.js";
4
5
  import { contentConfig } from "./driver.js";
5
- import { getContentGraph, resolveCanonicalKey, resolveVariant } from "./graph.js";
6
+ import { getContentGraph, resolveVariant } from "./graph.js";
6
7
  const isConfiguredQuickLink = (href) => {
7
8
  const parsed = parseRefLink(href);
8
9
  if (!parsed) {
@@ -33,8 +34,8 @@ const resolveDocumentRefLinks = async (event, content, requestedLocale) => {
33
34
  if (!parsed) {
34
35
  return null;
35
36
  }
36
- const canonicalKey = await resolveCanonicalKey(event, parsed.ref);
37
- if (!canonicalKey) {
37
+ const target = resolveGraphReferenceTarget(graph, parsed.ref);
38
+ if (!target) {
38
39
  if (isConfiguredQuickLink(href)) {
39
40
  return [href, href];
40
41
  }
@@ -45,7 +46,9 @@ const resolveDocumentRefLinks = async (event, content, requestedLocale) => {
45
46
  }
46
47
  return [href, href];
47
48
  }
48
- const variant = await resolveVariant(event, canonicalKey, requestedLocale);
49
+ const variant = await resolveVariant(event, target.canonicalKey, requestedLocale, {
50
+ collection: target.collection
51
+ });
49
52
  if (!variant?.path) {
50
53
  if (import.meta.dev) {
51
54
  console.warn(
@@ -60,10 +63,9 @@ const resolveDocumentRefLinks = async (event, content, requestedLocale) => {
60
63
  );
61
64
  }
62
65
  const routeLocale = variant.fallback && requestedLocale ? requestedLocale : variant.resolvedLocale;
63
- const targetCollection = graph.byId[variant.contentId]?.collection;
64
- const targetPolicy = targetCollection ? resolveRuntimeCollectionLocalePolicy(targetCollection, config) : void 0;
66
+ const targetPolicy = resolveRuntimeCollectionLocalePolicy(target.collection, config);
65
67
  if (!targetPolicy) {
66
- throw new Error(`Missing resolved locale policy for content collection "${targetCollection || ""}".`);
68
+ throw new Error(`Missing resolved locale policy for content collection "${target.collection}".`);
67
69
  }
68
70
  return [
69
71
  href,
@@ -51,6 +51,7 @@ declare module '#imports' {
51
51
  export const useFetch: typeof import('#app').useFetch
52
52
  export const useHead: typeof import('#app').useHead
53
53
  export const useNuxtApp: typeof import('#app').useNuxtApp
54
+ export const tryUseNuxtApp: typeof import('#app').tryUseNuxtApp
54
55
  export const useCookie: typeof import('#app').useCookie
55
56
  export const useRequestEvent: typeof import('#app').useRequestEvent
56
57
  export const useRequestFetch: typeof import('#app').useRequestFetch
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "@lupinum/ginko-content",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
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.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Collection-first content engine for Nuxt",
5
5
  "homepage": "https://github.com/lupinum-dev/ginko-content",
6
6
  "bugs": {