@lupinum/ginko-content 0.1.0 → 0.1.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.
Files changed (42) hide show
  1. package/README.md +93 -47
  2. package/compatibility.json +5 -5
  3. package/dist/cli.mjs +184 -29
  4. package/dist/core/references/schema.d.ts +1 -0
  5. package/dist/core/references/schema.js +33 -10
  6. package/dist/integrations/nitro/context.d.ts +4 -8
  7. package/dist/module.d.mts +90 -30
  8. package/dist/module.json +1 -1
  9. package/dist/module.mjs +241 -41
  10. package/dist/public/client.d.ts +4 -2
  11. package/dist/public/client.js +2 -0
  12. package/dist/runtime/app/components/ContentRenderer.vue +9 -5
  13. package/dist/runtime/app/components/internal/ContentRendererMarkdown.d.vue.ts +1 -1
  14. package/dist/runtime/app/components/internal/ContentRendererMarkdown.vue.d.ts +1 -1
  15. package/dist/runtime/app/composables/head.d.ts +27 -3
  16. package/dist/runtime/app/composables/head.js +82 -78
  17. package/dist/runtime/app/composables/query-api.d.ts +9 -10
  18. package/dist/runtime/app/composables/search.d.ts +4 -4
  19. package/dist/runtime/app/composables/search.js +11 -5
  20. package/dist/runtime/app/composables/use-content.d.ts +35 -15
  21. package/dist/runtime/app/composables/use-content.js +83 -13
  22. package/dist/runtime/query/unified.d.ts +1 -0
  23. package/dist/runtime/query/unified.js +93 -6
  24. package/dist/runtime/server/api/search.d.ts +2 -1
  25. package/dist/runtime/server/api/search.js +6 -2
  26. package/dist/runtime/server/pagefind.js +1 -0
  27. package/dist/runtime/server/plugins/sitemap.js +4 -4
  28. package/dist/runtime/server/search.d.ts +11 -0
  29. package/dist/runtime/server/search.js +24 -7
  30. package/dist/runtime/shared/search.js +3 -2
  31. package/dist/runtime/utils/sitemap-source +1 -0
  32. package/dist/runtime/utils/sitemap-source.d.ts +2 -0
  33. package/dist/runtime/utils/sitemap-source.js +6 -0
  34. package/dist/testing/provider-fixture.js +1 -0
  35. package/dist/types/config.d.ts +42 -20
  36. package/dist/types/config.js +26 -2
  37. package/dist/types/module.d.ts +39 -6
  38. package/dist/types/query.d.ts +20 -11
  39. package/dist/types/search.d.ts +3 -1
  40. package/dist/types.d.mts +2 -2
  41. package/dist/web-types.json +1 -1
  42. package/package.json +4 -4
@@ -4,95 +4,99 @@ function isHeadImageObject(image) {
4
4
  return typeof image === "object" && image !== null;
5
5
  }
6
6
  export const useContentHead = (_content, to = useRoute()) => {
7
- const content = unref(_content);
8
7
  const config = useRuntimeConfig();
9
- const appConfig = config.app;
10
8
  const applyHead = (head) => useHead(head);
11
- const refreshHead = (data = content) => {
12
- if (!to.path || !data) {
13
- return;
14
- }
15
- const head = Object.assign({}, data?.head || {});
16
- head.meta = [...head.meta || []];
17
- head.link = [...head.link || []];
18
- const title = head.title || data?.title;
19
- if (title) {
20
- head.title = title;
21
- if (import.meta.server && !head.meta.some((m) => m.property === "og:title")) {
22
- head.meta.push({
23
- property: "og:title",
24
- content: title
25
- });
26
- }
9
+ const refreshHead = (data = unref(_content)) => {
10
+ const head = resolveContentHead(data, to, config);
11
+ if (!head) return;
12
+ if (import.meta.client) {
13
+ nextTick(() => applyHead(head));
14
+ } else {
15
+ applyHead(head);
27
16
  }
28
- const siteUrl = typeof config.public.siteUrl === "string" ? withoutTrailingSlash(config.public.siteUrl) : void 0;
29
- if (import.meta.server && siteUrl) {
30
- const url = withoutTrailingSlash(joinURL(siteUrl, appConfig.baseURL, to.fullPath));
31
- if (!head.meta.some((m) => m.property === "og:url")) {
32
- head.meta.push({
33
- property: "og:url",
34
- content: url
35
- });
36
- }
37
- if (!head.link.some((m) => m.rel === "canonical")) {
38
- head.link.push({
39
- rel: "canonical",
40
- href: url
41
- });
42
- }
17
+ };
18
+ watch(() => unref(_content), refreshHead, { immediate: true });
19
+ };
20
+ export const resolveContentHead = (data, to, config) => {
21
+ if (!to.path || !data) {
22
+ return null;
23
+ }
24
+ const head = Object.assign({}, data?.head || data?.seo || {});
25
+ const appConfig = config.app;
26
+ head.meta = [...head.meta || []];
27
+ head.link = [...head.link || []];
28
+ const title = head.title || data?.title;
29
+ if (title) {
30
+ head.title = title;
31
+ if (import.meta.server && !head.meta.some((m) => m.property === "og:title")) {
32
+ head.meta.push({
33
+ property: "og:title",
34
+ content: title
35
+ });
43
36
  }
44
- const description = head?.description || data?.description;
45
- if (description && head.meta.filter((m) => m.name === "description").length === 0) {
37
+ }
38
+ const siteUrl = typeof config.public.siteUrl === "string" ? withoutTrailingSlash(config.public.siteUrl) : void 0;
39
+ if (import.meta.server && siteUrl) {
40
+ const url = withoutTrailingSlash(joinURL(siteUrl, appConfig.baseURL, to.fullPath));
41
+ if (!head.meta.some((m) => m.property === "og:url")) {
46
42
  head.meta.push({
47
- name: "description",
48
- content: description
43
+ property: "og:url",
44
+ content: url
45
+ });
46
+ }
47
+ if (!head.link.some((m) => m.rel === "canonical")) {
48
+ head.link.push({
49
+ rel: "canonical",
50
+ href: url
49
51
  });
50
52
  }
51
- if (import.meta.server && description && !head.meta.some((m) => m.property === "og:description")) {
53
+ }
54
+ const description = head?.description || data?.description;
55
+ if (description && head.meta.filter((m) => m.name === "description").length === 0) {
56
+ head.meta.push({
57
+ name: "description",
58
+ content: description
59
+ });
60
+ }
61
+ if (import.meta.server && description && !head.meta.some((m) => m.property === "og:description")) {
62
+ head.meta.push({
63
+ property: "og:description",
64
+ content: description
65
+ });
66
+ }
67
+ const image = head?.image || data?.image;
68
+ if (import.meta.server && image && head.meta.filter((m) => m.property === "og:image").length === 0) {
69
+ if (typeof image === "string") {
52
70
  head.meta.push({
53
- property: "og:description",
54
- content: description
71
+ property: "og:image",
72
+ content: siteUrl && !hasProtocol(image) ? new URL(joinURL(appConfig.baseURL, image), siteUrl).href : image
55
73
  });
56
74
  }
57
- const image = head?.image || data?.image;
58
- if (import.meta.server && image && head.meta.filter((m) => m.property === "og:image").length === 0) {
59
- if (typeof image === "string") {
60
- head.meta.push({
61
- property: "og:image",
62
- content: siteUrl && !hasProtocol(image) ? new URL(joinURL(appConfig.baseURL, image), siteUrl).href : image
63
- });
64
- }
65
- if (isHeadImageObject(image)) {
66
- const imageKeys = [
67
- "src",
68
- "secure_url",
69
- "type",
70
- "width",
71
- "height",
72
- "alt"
73
- ];
74
- for (const key of imageKeys) {
75
- if (key === "src" && image.src) {
76
- const isAbsoluteURL = hasProtocol(image.src);
77
- const imageURL = isAbsoluteURL ? image.src : joinURL(appConfig.baseURL, image.src ?? "/");
78
- head.meta.push({
79
- property: "og:image",
80
- content: siteUrl && !isAbsoluteURL ? new URL(imageURL, siteUrl).href : imageURL
81
- });
82
- } else if (image[key]) {
83
- head.meta.push({
84
- property: `og:image:${key}`,
85
- content: image[key]
86
- });
87
- }
75
+ if (isHeadImageObject(image)) {
76
+ const imageKeys = [
77
+ "src",
78
+ "secure_url",
79
+ "type",
80
+ "width",
81
+ "height",
82
+ "alt"
83
+ ];
84
+ for (const key of imageKeys) {
85
+ if (key === "src" && image.src) {
86
+ const isAbsoluteURL = hasProtocol(image.src);
87
+ const imageURL = isAbsoluteURL ? image.src : joinURL(appConfig.baseURL, image.src ?? "/");
88
+ head.meta.push({
89
+ property: "og:image",
90
+ content: siteUrl && !isAbsoluteURL ? new URL(imageURL, siteUrl).href : imageURL
91
+ });
92
+ } else if (image[key]) {
93
+ head.meta.push({
94
+ property: `og:image:${key}`,
95
+ content: image[key]
96
+ });
88
97
  }
89
98
  }
90
99
  }
91
- if (import.meta.client) {
92
- nextTick(() => applyHead(head));
93
- } else {
94
- applyHead(head);
95
- }
96
- };
97
- watch(() => unref(_content), refreshHead, { immediate: true });
100
+ }
101
+ return head;
98
102
  };
@@ -1,26 +1,25 @@
1
1
  import type { ParsedContent } from '../../../types/content';
2
- import type { ContentCollectionHandle } from '../../../types/config';
3
- import type { ContentVariant, ContentTreeItem, BacklinksOptions, BacklinksResult, BacklinkSource, DocumentFromHandle, ManyOptions, LocalizedDoc, NeighborsOptions, NeighborsResult, OneOptions, PopulateSpec, PopulateFromOptions, PopulatedDocument, PaginationOptions, PaginationResult, ResolveOneOptions, ResolveOneResult, TreeOptions, VariantsOptions } from '../../../types/query';
2
+ import type { ContentCollectionTarget, ContentVariant, ContentTreeItem, BacklinksOptions, BacklinksResult, BacklinkSource, DocumentFromHandle, ManyOptions, LocalizedDoc, NeighborsOptions, NeighborsResult, OneOptions, PopulateSpec, PopulateFromOptions, PopulatedDocument, PaginationOptions, PaginationResult, ResolveOneOptions, ResolveOneResult, TreeOptions, VariantsOptions } from '../../../types/query';
4
3
  import { type ContentQueryContext } from '../../query/unified';
5
4
  export declare const createClientContentQueryContext: () => ContentQueryContext;
6
- export declare function resolveOne<const H extends ContentCollectionHandle | string, O extends ResolveOneOptions<H, PopulateSpec | undefined>>(handle: H, options: O): Promise<ResolveOneResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
7
- export declare function one<const H extends ContentCollectionHandle | string, O extends OneOptions<H, PopulateSpec | undefined>>(handle: H, options: O): Promise<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>> | null>;
8
- export declare function many<const H extends ContentCollectionHandle | string, O extends ManyOptions<H, PopulateSpec | undefined>>(handle: H, options?: O & ManyOptions<H, PopulateSpec | undefined>): Promise<Array<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>>;
9
- export declare function paginate<const H extends ContentCollectionHandle | string, O extends PaginationOptions<H, PopulateSpec | undefined>>(handle: H, options: O): Promise<PaginationResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
10
- export declare function backlinks<const Target extends ContentCollectionHandle | string, const Source extends BacklinkSource | ReadonlyArray<BacklinkSource>, P extends PopulateSpec | undefined = undefined>(handle: Target, options: BacklinksOptions<Target, Source, P>): Promise<BacklinksResult<Source, P>>;
11
- export declare function variants<H extends ContentCollectionHandle | string>(handle: H, options: VariantsOptions<H>): Promise<Array<ContentVariant<H extends {
5
+ export declare function resolveOne<const H extends ContentCollectionTarget, O extends ResolveOneOptions<H, PopulateSpec | undefined>>(handle: H, options: O): Promise<ResolveOneResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
6
+ export declare function one<const H extends ContentCollectionTarget, O extends OneOptions<H, PopulateSpec | undefined>>(handle: H, options: O): Promise<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>> | null>;
7
+ export declare function many<const H extends ContentCollectionTarget, O extends ManyOptions<H, PopulateSpec | undefined>>(handle: H, options?: O & ManyOptions<H, PopulateSpec | undefined>): Promise<Array<LocalizedDoc<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>>;
8
+ export declare function paginate<const H extends ContentCollectionTarget, O extends PaginationOptions<H, PopulateSpec | undefined>>(handle: H, options: O): Promise<PaginationResult<PopulatedDocument<DocumentFromHandle<H>, PopulateFromOptions<O>>>>;
9
+ export declare function backlinks<const Target extends ContentCollectionTarget, const Source extends BacklinkSource | ReadonlyArray<BacklinkSource>, P extends PopulateSpec | undefined = undefined>(handle: Target, options: BacklinksOptions<Target, Source, P>): Promise<BacklinksResult<Source, P>>;
10
+ export declare function variants<H extends ContentCollectionTarget>(handle: H, options: VariantsOptions<H>): Promise<Array<ContentVariant<H extends {
12
11
  __schema: {
13
12
  _output: infer O;
14
13
  };
15
14
  } ? O & ParsedContent : ParsedContent>>>;
16
- export declare function tree<H extends ContentCollectionHandle | string, Fields extends ReadonlyArray<string> | undefined = undefined>(handle: H, options?: Omit<TreeOptions<H>, 'fields'> & {
15
+ export declare function tree<H extends ContentCollectionTarget, Fields extends ReadonlyArray<string> | undefined = undefined>(handle: H, options?: Omit<TreeOptions<H>, 'fields'> & {
17
16
  fields?: Fields;
18
17
  }): Promise<ContentTreeItem<H extends {
19
18
  __schema: {
20
19
  _output: infer O;
21
20
  };
22
21
  } ? O & ParsedContent : ParsedContent, Fields>[]>;
23
- export declare function neighbors<H extends ContentCollectionHandle | string>(handle: H, options: NeighborsOptions<H>): Promise<NeighborsResult<H extends {
22
+ export declare function neighbors<H extends ContentCollectionTarget>(handle: H, options: NeighborsOptions<H>): Promise<NeighborsResult<H extends {
24
23
  __schema: {
25
24
  _output: infer O;
26
25
  };
@@ -1,9 +1,9 @@
1
1
  import type { ComputedRef, Ref } from 'vue';
2
2
  import type { MaybeRefOrGetter } from '#imports';
3
3
  import { ref } from '#imports';
4
- import type { ContentCollectionMap } from '@lupinum/ginko-content';
4
+ import type { ContentCollectionHandle } from '../../../types/config';
5
5
  import type { ContentNavigationItem } from '../../../types/content';
6
- import type { ContentSearchSection } from '../../../types/query';
6
+ import type { ContentCollectionMap, ContentCollectionStringName, ContentSearchSection } from '../../../types/query';
7
7
  import type { ContentSearchResult } from '../../../types/search';
8
8
  export interface UseContentSearchResultsOptions {
9
9
  locale?: MaybeRefOrGetter<string | undefined>;
@@ -52,7 +52,7 @@ export interface UseContentSearchDataResult {
52
52
  navigation: ComputedRef<ContentNavigationItem[]>;
53
53
  searchTerm: ReturnType<typeof ref<string>>;
54
54
  }
55
- export declare function useContentSearchData<K extends keyof ContentCollectionMap & string>(collection: K, options?: UseContentSearchDataOptions): Promise<UseContentSearchDataResult>;
56
- export declare function useContentSearchData(collection: string, options?: UseContentSearchDataOptions): Promise<UseContentSearchDataResult>;
55
+ export declare function useContentSearchData<K extends keyof ContentCollectionMap & string>(collection: ContentCollectionHandle<K> | K, options?: UseContentSearchDataOptions): Promise<UseContentSearchDataResult>;
56
+ export declare function useContentSearchData(collection: ContentCollectionStringName | ContentCollectionHandle, options?: UseContentSearchDataOptions): Promise<UseContentSearchDataResult>;
57
57
  export declare const useContentSearchResults: (search: MaybeRefOrGetter<string>, options?: UseContentSearchResultsOptions) => Promise<UseContentSearchResultsResult>;
58
58
  export declare const useContentSearch: (options?: UseContentSearchOptions) => Promise<UseContentSearchResult>;
@@ -13,7 +13,7 @@ const defaultSearchConfig = {
13
13
  engine: "minisearch",
14
14
  minisearch: {
15
15
  fields: ["title", "content", "headings"],
16
- storeFields: ["path", "title", "excerpt", "anchor", "locale"],
16
+ storeFields: ["path", "title", "excerpt", "anchor", "locale", "collection"],
17
17
  boost: {
18
18
  title: 4,
19
19
  headings: 2,
@@ -95,14 +95,16 @@ const loadSearchSections = async (collection, options) => {
95
95
  }
96
96
  });
97
97
  };
98
+ const collectionName = (collection) => typeof collection === "string" ? collection : collection.name;
98
99
  export async function useContentSearchData(collection, options = {}) {
100
+ const name = collectionName(collection);
99
101
  const searchTerm = ref("");
100
102
  const locale = computed(() => toValue(options.locale));
101
- const payload = await useAsyncData(computed(() => `content-search-data:${collection}:${locale.value || "default"}`), async () => {
103
+ const payload = await useAsyncData(computed(() => `content-search-data:${name}:${locale.value || "default"}`), async () => {
102
104
  const locale2 = toValue(options.locale);
103
105
  const [navigation, files2] = await Promise.all([
104
- tree(collection, locale2 ? { locale: locale2 } : {}),
105
- loadSearchSections(collection, options)
106
+ tree(name, locale2 ? { locale: locale2 } : {}),
107
+ loadSearchSections(name, options)
106
108
  ]);
107
109
  return {
108
110
  files: files2,
@@ -239,7 +241,10 @@ const useCmsSearch = async (search, apiBaseURL, options) => {
239
241
  });
240
242
  const { data, pending, error } = await useFetch(requestUrl);
241
243
  return {
242
- results: computed(() => data.value || []),
244
+ results: computed(() => (data.value || []).map((result) => ({
245
+ ...result,
246
+ collection: typeof result.collection === "string" ? result.collection : ""
247
+ }))),
243
248
  pending: computed(() => pending.value),
244
249
  error: computed(() => error.value)
245
250
  };
@@ -292,6 +297,7 @@ const usePagefindSearch = (search, pagefindUrl, options) => {
292
297
  const [path = "", anchor] = String(data?.url || "").split("#");
293
298
  return {
294
299
  path,
300
+ collection: typeof data?.meta?.collection === "string" ? data.meta.collection : "",
295
301
  title: data?.meta?.title || path,
296
302
  excerpt: data?.excerpt || "",
297
303
  score: result.score,
@@ -1,7 +1,6 @@
1
1
  import type { ComputedRef, MaybeRefOrGetter } from 'vue';
2
2
  import { createError } from '#imports';
3
- import type { ContentCollectionHandle } from '../../../types/config';
4
- import type { BacklinksOptions, BacklinksResult, BacklinkSource, ContentVariant, ContentTreeItem, ContentResolvedMeta, DocumentFromHandle, LocalePathEntry, LocaleFallback, ManyOptions, LocalizedDoc, NeighborsOptions, NeighborsResult, OneOptions, PopulateSpec, PopulateFromOptions, PopulatedDocument, PaginationOptions, PaginationResult, ResolveOneOptions, ResolveOneResult, SortSpec, TreeOptions, VariantsOptions } from '../../../types/query';
3
+ import type { BacklinksOptions, BacklinksResult, BacklinkSource, ContentCollectionTarget, ContentVariant, ContentTreeItem, ContentResolvedMeta, DocumentFromHandle, LocalePathEntry, LocaleFallback, ManyOptions, LocalizedDoc, NeighborsOptions, NeighborsResult, OneOptions, PopulateSpec, PopulateFromOptions, PopulatedDocument, PaginationOptions, PaginationResult, ResolveOneOptions, ResolveOneResult, SortSpec, TreeOptions, VariantsOptions } from '../../../types/query';
5
4
  type ReactiveLeaf<T> = T | MaybeRefOrGetter<T>;
6
5
  type ReactiveValue<V> = V extends ((...args: never[]) => unknown) | readonly unknown[] ? ReactiveLeaf<V> : V extends object | undefined ? ReactiveLeaf<V> | {
7
6
  [P in keyof NonNullable<V>]?: ReactiveLeaf<NonNullable<V>[P]>;
@@ -22,7 +21,7 @@ interface UseContentOneReturn<T> {
22
21
  * `options` changes. Caching follows Nuxt's standard `useAsyncData` semantics
23
22
  * keyed by `${collection}:${stableHash(options)}`.
24
23
  */
25
- export declare function useContentOne<const H extends ContentCollectionHandle | string, O extends OneOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentOneReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
24
+ export declare function useContentOne<const H extends ContentCollectionTarget, O extends OneOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentOneReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
26
25
  type ContentPageSurroundOptions<H> = boolean | Omit<NeighborsOptions<H>, 'by' | 'locale' | 'fallback'>;
27
26
  type ContentPageOneOptions<H, P extends PopulateSpec | undefined> = Omit<OneOptions<H, P>, 'by' | 'locale' | 'fallback'> & {
28
27
  /**
@@ -37,7 +36,7 @@ export type UseContentPageOptions<H = unknown, P extends PopulateSpec | undefine
37
36
  /**
38
37
  * Load previous/next items alongside the page.
39
38
  *
40
- * Pass `true` for the default neighbor query, or an options object to choose
39
+ * Pass `true` for the default surround query, or an options object to choose
41
40
  * fields. The page query remains one request unless this is enabled.
42
41
  */
43
42
  surround?: ContentPageSurroundOptions<H>;
@@ -48,8 +47,11 @@ export type UseContentPageOptions<H = unknown, P extends PopulateSpec | undefine
48
47
  notFound?: false | (() => Parameters<typeof createError>[0]);
49
48
  };
50
49
  interface UseContentPageReturn<T> {
50
+ data: ComputedRef<LocalizedDoc<T> | undefined>;
51
51
  page: ComputedRef<LocalizedDoc<T> | undefined>;
52
- surround: ComputedRef<Array<ContentTreeItem<T> | null>>;
52
+ previous: ComputedRef<ContentTreeItem<T> | null>;
53
+ next: ComputedRef<ContentTreeItem<T> | null>;
54
+ surround: ComputedRef<Array<ContentTreeItem<T>>>;
53
55
  pending: ComputedRef<boolean>;
54
56
  status: ComputedRef<string>;
55
57
  error: ComputedRef<unknown>;
@@ -60,7 +62,7 @@ interface UseContentPageReturn<T> {
60
62
  * components; the explicit `useContentOne` selector remains the lower-level
61
63
  * primitive for custom reads.
62
64
  */
63
- export declare function useContentPage<const H extends ContentCollectionHandle | string, P extends PopulateSpec | undefined = undefined>(handle: H, options?: UseContentPageOptions<H, P>): Promise<UseContentPageReturn<PopulatedDocument<DocFromHandle<H>, P>>>;
65
+ export declare function useContentPage<const H extends ContentCollectionTarget, P extends PopulateSpec | undefined = undefined>(handle: H, options?: UseContentPageOptions<H, P>): Promise<UseContentPageReturn<PopulatedDocument<DocFromHandle<H>, P>>>;
64
66
  interface UseContentResolveOneReturn<T> {
65
67
  doc: ComputedRef<LocalizedDoc<T> | null>;
66
68
  explain: ComputedRef<ResolveOneResult<T>['explain'] | null>;
@@ -71,7 +73,7 @@ interface UseContentResolveOneReturn<T> {
71
73
  /**
72
74
  * Reactively resolve a single document with a first-class diagnostic envelope.
73
75
  */
74
- export declare function useContentResolveOne<const H extends ContentCollectionHandle | string, O extends ResolveOneOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentResolveOneReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
76
+ export declare function useContentResolveOne<const H extends ContentCollectionTarget, O extends ResolveOneOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentResolveOneReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
75
77
  interface UseContentManyReturn<T> {
76
78
  data: ComputedRef<Array<LocalizedDoc<T>>>;
77
79
  status: ComputedRef<string>;
@@ -95,11 +97,11 @@ interface UseContentPaginationReturn<T> {
95
97
  /**
96
98
  * Reactively resolve a list of documents.
97
99
  */
98
- export declare function useContentMany<const H extends ContentCollectionHandle | string, O extends ManyOptions<H, PopulateSpec | undefined>>(handle: H, options?: Reactive<O>): Promise<UseContentManyReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
100
+ export declare function useContentMany<const H extends ContentCollectionTarget, O extends ManyOptions<H, PopulateSpec | undefined>>(handle: H, options?: Reactive<O>): Promise<UseContentManyReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
99
101
  /**
100
102
  * Reactively resolve one page of documents plus total/page navigation metadata.
101
103
  */
102
- export declare function useContentPagination<const H extends ContentCollectionHandle | string, O extends PaginationOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentPaginationReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
104
+ export declare function useContentPagination<const H extends ContentCollectionTarget, O extends PaginationOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentPaginationReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
103
105
  interface UseContentBacklinksReturn<T> {
104
106
  data: ComputedRef<T[]>;
105
107
  status: ComputedRef<string>;
@@ -109,7 +111,7 @@ interface UseContentBacklinksReturn<T> {
109
111
  /**
110
112
  * Reactively resolve source documents that reference a target document.
111
113
  */
112
- export declare function useContentBacklinks<const Target extends ContentCollectionHandle | string, const Source extends BacklinkSource | ReadonlyArray<BacklinkSource>, P extends PopulateSpec | undefined = undefined>(handle: Target, options: Reactive<BacklinksOptions<Target, Source, P>> & {
114
+ export declare function useContentBacklinks<const Target extends ContentCollectionTarget, const Source extends BacklinkSource | ReadonlyArray<BacklinkSource>, P extends PopulateSpec | undefined = undefined>(handle: Target, options: Reactive<BacklinksOptions<Target, Source, P>> & {
113
115
  from: ReactiveValue<Source>;
114
116
  }): Promise<UseContentBacklinksReturn<BacklinksResult<Source, P>[number]>>;
115
117
  interface UseContentVariantsReturn<T> {
@@ -123,17 +125,35 @@ interface UseContentVariantsReturn<T> {
123
125
  * locale switchers. Returns one entry per configured locale, with a
124
126
  * `translated` flag.
125
127
  */
126
- export declare function useContentVariants<H extends ContentCollectionHandle | string>(handle: H, options: Reactive<VariantsOptions<H>>): Promise<UseContentVariantsReturn<DocFromHandle<H>>>;
128
+ export declare function useContentVariants<H extends ContentCollectionTarget>(handle: H, options: Reactive<VariantsOptions<H>>): Promise<UseContentVariantsReturn<DocFromHandle<H>>>;
127
129
  interface UseContentTreeReturn<T> {
128
130
  data: ComputedRef<T[]>;
129
131
  status: ComputedRef<string>;
130
132
  error: ComputedRef<unknown>;
131
133
  refresh: () => Promise<void>;
132
134
  }
135
+ export type ContentNavigationNode<T> = Omit<ContentTreeItem<T>, 'children'> & {
136
+ id: string;
137
+ path: string;
138
+ title: string;
139
+ children: Array<ContentNavigationNode<T>>;
140
+ };
141
+ interface UseContentNavigationReturn<T> {
142
+ data: ComputedRef<Array<ContentNavigationNode<T>>>;
143
+ firstPage: ComputedRef<ContentNavigationNode<T> | null>;
144
+ paths: ComputedRef<Set<string>>;
145
+ status: ComputedRef<string>;
146
+ error: ComputedRef<unknown>;
147
+ refresh: () => Promise<void>;
148
+ }
133
149
  /**
134
150
  * Reactively resolve the navigation tree for a collection.
135
151
  */
136
- export declare function useContentTree<H extends ContentCollectionHandle | string>(handle: H, options?: Reactive<TreeOptions<H>>): Promise<UseContentTreeReturn<ContentTreeItem<DocFromHandle<H>>>>;
152
+ export declare function useContentTree<H extends ContentCollectionTarget>(handle: H, options?: Reactive<TreeOptions<H>>): Promise<UseContentTreeReturn<ContentTreeItem<DocFromHandle<H>>>>;
153
+ /**
154
+ * Reactively resolve normalized navigation for sidebars and active-state UI.
155
+ */
156
+ export declare function useContentNavigation<H extends ContentCollectionTarget>(handle: H, options?: Reactive<TreeOptions<H>>): Promise<UseContentNavigationReturn<DocFromHandle<H>>>;
137
157
  interface UseContentNeighborsReturn<T> {
138
158
  data: ComputedRef<NeighborsResult<T>>;
139
159
  pending: ComputedRef<boolean>;
@@ -144,7 +164,7 @@ interface UseContentNeighborsReturn<T> {
144
164
  /**
145
165
  * Reactively resolve previous/next surroundings for a document.
146
166
  */
147
- export declare function useContentNeighbors<H extends ContentCollectionHandle | string>(handle: H, options: Reactive<NeighborsOptions<H>>): Promise<UseContentNeighborsReturn<DocFromHandle<H>>>;
167
+ export declare function useContentNeighbors<H extends ContentCollectionTarget>(handle: H, options: Reactive<NeighborsOptions<H>>): Promise<UseContentNeighborsReturn<DocFromHandle<H>>>;
148
168
  interface UseContentLocaleSwitchReturn<T> {
149
169
  /**
150
170
  * Per-locale path map of the resolved document, keyed by locale code.
@@ -184,5 +204,5 @@ interface UseContentLocaleSwitchReturn<T> {
184
204
  * </script>
185
205
  * ```
186
206
  */
187
- export declare function useContentLocaleSwitch<const H extends ContentCollectionHandle | string, O extends OneOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentLocaleSwitchReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
188
- export type { ManyOptions, OneOptions, BacklinksOptions, BacklinksResult, LocalizedDoc, ContentResolvedMeta, DocumentFromHandle, NeighborsOptions, NeighborsResult, PaginationOptions, PaginationResult, PopulateSpec, PopulateFromOptions, PopulatedDocument, ResolveOneOptions, ResolveOneResult, SortSpec, TreeOptions, VariantsOptions, ContentVariant };
207
+ export declare function useContentLocaleSwitch<const H extends ContentCollectionTarget, O extends OneOptions<H, PopulateSpec | undefined>>(handle: H, options: Reactive<O>): Promise<UseContentLocaleSwitchReturn<PopulatedDocument<DocFromHandle<H>, PopulateFromOptions<O>>>>;
208
+ export type { ManyOptions, OneOptions, BacklinksOptions, BacklinksResult, LocalizedDoc, ContentResolvedMeta, ContentCollectionTarget, DocumentFromHandle, NeighborsOptions, NeighborsResult, PaginationOptions, PaginationResult, PopulateSpec, PopulateFromOptions, PopulatedDocument, ResolveOneOptions, ResolveOneResult, SortSpec, TreeOptions, VariantsOptions, ContentVariant };
@@ -2,6 +2,7 @@ import { computed, shallowRef, toValue, watch } from "vue";
2
2
  import { hash } from "ohash";
3
3
  import { createError, useAsyncData, useRoute } from "#imports";
4
4
  import { createClientContentQueryContext } from "./query-api.js";
5
+ import { resolveCollectionI18n } from "../../../features/localization/path";
5
6
  import {
6
7
  backlinks as backlinksWithContext,
7
8
  many as manyWithContext,
@@ -12,7 +13,9 @@ import {
12
13
  tree as treeWithContext,
13
14
  variants as variantsWithContext
14
15
  } from "../../query/unified.js";
16
+ import { resolveActiveLocale } from "./locale.js";
15
17
  import { useContentRoute } from "./route.js";
18
+ import { getContentRuntime } from "./runtime.js";
16
19
  const unwrapDeep = (value, seen = /* @__PURE__ */ new WeakSet()) => {
17
20
  if (value === null || value === void 0) return value;
18
21
  if (typeof value === "function") return unwrapDeep(toValue(value), seen);
@@ -35,6 +38,7 @@ const resolveOptions = (reactive) => {
35
38
  return unwrapDeep(reactive);
36
39
  };
37
40
  const stableKey = (prefix, name, options) => `${prefix}:${name}:${hash(options)}`;
41
+ const contentCollectionName = (handle) => typeof handle === "string" ? handle : handle.name;
38
42
  export async function useContentOne(handle, options) {
39
43
  const resolved = computed(() => resolveOptions(options));
40
44
  const key = computed(() => stableKey("content-one", typeof handle === "string" ? handle : handle.name, resolved.value));
@@ -61,7 +65,7 @@ const normalizePageNotFound = (notFound) => {
61
65
  if (notFound === false) return void 0;
62
66
  return createError(notFound ? notFound() : defaultPageNotFound());
63
67
  };
64
- const normalizeSurround = (surround) => {
68
+ const normalizePageSurround = (surround) => {
65
69
  if (!surround) return void 0;
66
70
  return surround === true ? {} : surround;
67
71
  };
@@ -88,6 +92,18 @@ export async function useContentPage(handle, options = {}) {
88
92
  const isBrowser = import.meta.client && typeof window !== "undefined";
89
93
  const { notFound, surround, ...oneOptions } = options;
90
94
  const routeSelector = { route: () => normalizeRoutePath(route.path) };
95
+ const runtime = getContentRuntime();
96
+ const activeLocale = computed(() => {
97
+ const explicitLocale = toValue(oneOptions.locale);
98
+ if (explicitLocale) {
99
+ return explicitLocale;
100
+ }
101
+ const { locales, defaultLocale } = resolveCollectionI18n(contentCollectionName(handle), runtime);
102
+ if (!locales.length && !defaultLocale) {
103
+ return void 0;
104
+ }
105
+ return resolveActiveLocale(locales, defaultLocale);
106
+ });
91
107
  const rawPage = shallowRef(null);
92
108
  const resolvingRoute = shallowRef(false);
93
109
  const sawRouteQueryPending = shallowRef(false);
@@ -98,12 +114,13 @@ export async function useContentPage(handle, options = {}) {
98
114
  useContentRoute(page);
99
115
  const pageResultPromise = useContentOne(handle, {
100
116
  ...oneOptions,
117
+ locale: () => activeLocale.value,
101
118
  by: routeSelector
102
119
  });
103
- const surroundOptions = normalizeSurround(surround);
104
- const neighborResultPromise = surroundOptions ? useContentNeighbors(handle, {
120
+ const surroundOptions = normalizePageSurround(surround);
121
+ const surroundResultPromise = surroundOptions ? useContentNeighbors(handle, {
105
122
  ...surroundOptions,
106
- ..."locale" in oneOptions ? { locale: oneOptions.locale } : {},
123
+ locale: () => activeLocale.value,
107
124
  ..."fallback" in oneOptions ? { fallback: oneOptions.fallback } : {},
108
125
  by: routeSelector
109
126
  }) : void 0;
@@ -135,30 +152,40 @@ export async function useContentPage(handle, options = {}) {
135
152
  }
136
153
  }
137
154
  });
138
- const neighborResult = neighborResultPromise ? await neighborResultPromise : void 0;
139
- const pending = computed(() => pageResult.pending.value || Boolean(neighborResult?.pending.value));
155
+ const surroundResult = surroundResultPromise ? await surroundResultPromise : void 0;
156
+ const pending = computed(() => pageResult.pending.value || Boolean(surroundResult?.pending.value));
140
157
  const pageError = computed(() => {
141
158
  if (pageResult.error.value) return pageResult.error.value;
142
- if (neighborResult?.error.value) return neighborResult.error.value;
159
+ if (surroundResult?.error.value) return surroundResult.error.value;
143
160
  if (pending.value || resolvingRoute.value || page.value || notFound === false) return void 0;
144
161
  return normalizePageNotFound(notFound);
145
162
  });
146
163
  if (!isBrowser && pageError.value) {
147
164
  throw pageError.value;
148
165
  }
166
+ const previous = computed(() => {
167
+ if (!page.value || !surroundResult) return null;
168
+ return surroundResult.data.value.prev;
169
+ });
170
+ const next = computed(() => {
171
+ if (!page.value || !surroundResult) return null;
172
+ return surroundResult.data.value.next;
173
+ });
174
+ const pageSurround = computed(() => {
175
+ return [previous.value, next.value].filter((item) => Boolean(item));
176
+ });
149
177
  return {
178
+ data: page,
150
179
  page,
151
- surround: computed(() => {
152
- if (!page.value || !neighborResult) return [];
153
- const neighbors = neighborResult.data.value;
154
- return [neighbors.prev, neighbors.next];
155
- }),
180
+ surround: pageSurround,
181
+ previous,
182
+ next,
156
183
  pending,
157
184
  status: computed(() => pageResult.status.value),
158
185
  error: pageError,
159
186
  refresh: async () => {
160
187
  await pageResult.refresh();
161
- if (neighborResult) await neighborResult.refresh();
188
+ if (surroundResult) await surroundResult.refresh();
162
189
  }
163
190
  };
164
191
  }
@@ -263,6 +290,37 @@ export async function useContentVariants(handle, options) {
263
290
  refresh: () => asyncData.refresh()
264
291
  };
265
292
  }
293
+ const navigationNodeId = (item) => {
294
+ const id = item.id ?? item._id ?? item._canonicalKey ?? item.path ?? item._path ?? item.title;
295
+ return typeof id === "string" && id.length ? id : item.title;
296
+ };
297
+ const normalizeContentNavigation = (items = []) => {
298
+ return items.map((item) => {
299
+ const source = item;
300
+ return {
301
+ ...item,
302
+ id: navigationNodeId(source),
303
+ path: item.path,
304
+ title: item.title,
305
+ children: normalizeContentNavigation(item.children || [])
306
+ };
307
+ });
308
+ };
309
+ const findFirstContentNavigationPage = (items) => {
310
+ for (const item of items) {
311
+ if (item.path) return item;
312
+ const child = findFirstContentNavigationPage(item.children);
313
+ if (child) return child;
314
+ }
315
+ return null;
316
+ };
317
+ const collectContentNavigationPaths = (items, paths = /* @__PURE__ */ new Set()) => {
318
+ for (const item of items) {
319
+ if (item.path) paths.add(item.path);
320
+ collectContentNavigationPaths(item.children, paths);
321
+ }
322
+ return paths;
323
+ };
266
324
  export async function useContentTree(handle, options = {}) {
267
325
  const resolved = computed(() => resolveOptions(options));
268
326
  const key = computed(() => stableKey("content-tree", typeof handle === "string" ? handle : handle.name, resolved.value));
@@ -279,6 +337,18 @@ export async function useContentTree(handle, options = {}) {
279
337
  refresh: () => asyncData.refresh()
280
338
  };
281
339
  }
340
+ export async function useContentNavigation(handle, options = {}) {
341
+ const tree = await useContentTree(handle, options);
342
+ const data = computed(() => normalizeContentNavigation(tree.data.value));
343
+ return {
344
+ data,
345
+ firstPage: computed(() => findFirstContentNavigationPage(data.value)),
346
+ paths: computed(() => collectContentNavigationPaths(data.value)),
347
+ status: tree.status,
348
+ error: tree.error,
349
+ refresh: tree.refresh
350
+ };
351
+ }
282
352
  export async function useContentNeighbors(handle, options) {
283
353
  const resolved = computed(() => resolveOptions(options));
284
354
  const key = computed(() => stableKey("content-neighbors", typeof handle === "string" ? handle : handle.name, resolved.value));
@@ -27,6 +27,7 @@ export interface RuntimeContentConfig {
27
27
  defaultLocale?: string;
28
28
  };
29
29
  route?: string | Record<string, string>;
30
+ references?: Record<string, string[]>;
30
31
  }>;
31
32
  }
32
33
  export type ContentQueryEndpoint = 'query' | 'navigation';