@cedros/data-react 0.1.6 → 0.1.7
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 +7 -0
- package/dist/react/contentCollections.d.ts +4 -0
- package/dist/react/contentCollections.js +48 -0
- package/dist/react/entries.js +6 -7
- package/dist/react/sitemap.js +7 -20
- package/dist/react/slugs.js +7 -20
- package/dist/site-templates/DocsTemplates.d.ts +9 -8
- package/dist/site-templates/DocsTemplates.js +32 -17
- package/dist/site-templates/contentIndex.d.ts +4 -0
- package/dist/site-templates/contentIndex.js +15 -1
- package/dist/site-templates/docsNavigation.d.ts +30 -1
- package/dist/site-templates/docsNavigation.js +132 -1
- package/dist/site-templates/docsTemplateShell.d.ts +8 -0
- package/dist/site-templates/docsTemplateShell.js +14 -0
- package/dist/site-templates/index.d.ts +1 -1
- package/dist/site-templates/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,13 +217,18 @@ Tipping:
|
|
|
217
217
|
Docs templates:
|
|
218
218
|
- `DocsIndexTemplate`
|
|
219
219
|
- `DocArticleTemplate` (GitBook-style left nav + right TOC)
|
|
220
|
+
- both accept `headless` to skip the built-in `SiteLayout`, or `renderLayout={(content) => ...}` to wrap the docs UI in an existing app shell
|
|
220
221
|
|
|
221
222
|
Content rendering and helpers:
|
|
222
223
|
- `MarkdownContent`
|
|
223
224
|
- `Breadcrumbs`
|
|
224
225
|
- `ContentPagination`
|
|
225
226
|
- `withActiveRouteState`
|
|
227
|
+
- `fetchDocEntry`
|
|
226
228
|
- `buildDocsSidebarSections`
|
|
229
|
+
- `buildDocsTree`
|
|
230
|
+
- `buildHierarchicalDocsSidebarSections`
|
|
231
|
+
- `buildDocsPrevNext`
|
|
227
232
|
- `withActiveDocsSidebar`
|
|
228
233
|
- `prepareBlogIndex`
|
|
229
234
|
- `prepareDocsIndex`
|
|
@@ -319,6 +324,8 @@ Docs/blog templates default to `bodyMarkdown`.
|
|
|
319
324
|
|
|
320
325
|
This keeps markdown as the safe default and avoids unsafe HTML rendering by default.
|
|
321
326
|
|
|
327
|
+
Docs search helpers also inspect optional `bodyMarkdown`, `bodyText`, `bodyHtml`, and `searchText` fields on docs entries, so callers can opt into body/full-text matching without changing the query API.
|
|
328
|
+
|
|
322
329
|
## Server helpers and API key
|
|
323
330
|
|
|
324
331
|
`@cedros/data-react/server` provides Next.js data-fetching helpers that call cedros-data directly:
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ContentType } from "./types.js";
|
|
2
|
+
export declare function collectionNameForContentType(contentType: ContentType): string;
|
|
3
|
+
export declare function collectionNamesForContentType(contentType: ContentType): string[];
|
|
4
|
+
export declare function queryEntriesByContentType<T>(serverUrl: string, contentType: ContentType, buildBody: (collectionName: string) => Record<string, unknown>, apiKey?: string): Promise<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fetchJson } from "./fetch.js";
|
|
2
|
+
const CANONICAL_CONTENT_TYPE_COLLECTIONS = {
|
|
3
|
+
page: "pages",
|
|
4
|
+
blog: "blog",
|
|
5
|
+
docs: "docs",
|
|
6
|
+
learn: "learn",
|
|
7
|
+
project: "projects",
|
|
8
|
+
airdrop: "airdrops",
|
|
9
|
+
};
|
|
10
|
+
const CONTENT_TYPE_COLLECTION_ALIASES = {
|
|
11
|
+
blog: ["blogs"],
|
|
12
|
+
learn: ["courses"],
|
|
13
|
+
};
|
|
14
|
+
export function collectionNameForContentType(contentType) {
|
|
15
|
+
return CANONICAL_CONTENT_TYPE_COLLECTIONS[contentType];
|
|
16
|
+
}
|
|
17
|
+
export function collectionNamesForContentType(contentType) {
|
|
18
|
+
const canonicalName = collectionNameForContentType(contentType);
|
|
19
|
+
const aliases = CONTENT_TYPE_COLLECTION_ALIASES[contentType] ?? [];
|
|
20
|
+
return [canonicalName, ...aliases.filter((alias) => alias !== canonicalName)];
|
|
21
|
+
}
|
|
22
|
+
export async function queryEntriesByContentType(serverUrl, contentType, buildBody, apiKey) {
|
|
23
|
+
return queryEntriesByCollectionNames(serverUrl, collectionNamesForContentType(contentType), buildBody, apiKey);
|
|
24
|
+
}
|
|
25
|
+
async function queryEntriesByCollectionNames(serverUrl, collectionNames, buildBody, apiKey) {
|
|
26
|
+
let lastCollectionNotFoundError;
|
|
27
|
+
const lastCollectionName = collectionNames[collectionNames.length - 1];
|
|
28
|
+
for (const collectionName of collectionNames) {
|
|
29
|
+
try {
|
|
30
|
+
return await fetchJson(serverUrl, "/entries/query", {
|
|
31
|
+
method: "POST",
|
|
32
|
+
body: buildBody(collectionName),
|
|
33
|
+
apiKey,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (!isCollectionNotFoundError(error) || collectionName === lastCollectionName) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
lastCollectionNotFoundError = error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw (lastCollectionNotFoundError ??
|
|
44
|
+
new Error("@cedros/data-react: no collection names were configured"));
|
|
45
|
+
}
|
|
46
|
+
function isCollectionNotFoundError(error) {
|
|
47
|
+
return error instanceof Error && /collection not found/i.test(error.message);
|
|
48
|
+
}
|
package/dist/react/entries.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveApiKey, resolveServerUrl } from "./fetch.js";
|
|
2
|
+
import { queryEntriesByContentType } from "./contentCollections.js";
|
|
2
3
|
/**
|
|
3
4
|
* Fetches a single blog post by slug, optionally passing visitor_id for metered reads.
|
|
4
5
|
*
|
|
@@ -10,7 +11,6 @@ export async function fetchBlogPost(slug, options) {
|
|
|
10
11
|
const serverUrl = resolveServerUrl(options);
|
|
11
12
|
const apiKey = resolveApiKey(options);
|
|
12
13
|
const body = {
|
|
13
|
-
collection_name: "blog",
|
|
14
14
|
entry_keys: [slug],
|
|
15
15
|
limit: 1,
|
|
16
16
|
offset: 0,
|
|
@@ -18,10 +18,9 @@ export async function fetchBlogPost(slug, options) {
|
|
|
18
18
|
if (options?.visitorId) {
|
|
19
19
|
body.visitor_id = options.visitorId;
|
|
20
20
|
}
|
|
21
|
-
const entries = await
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
21
|
+
const entries = await queryEntriesByContentType(serverUrl, "blog", (collectionName) => ({
|
|
22
|
+
...body,
|
|
23
|
+
collection_name: collectionName,
|
|
24
|
+
}), apiKey);
|
|
26
25
|
return entries.length > 0 ? entries[0] : null;
|
|
27
26
|
}
|
package/dist/react/sitemap.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const CONTENT_TYPE_COLLECTIONS = {
|
|
4
|
-
page: "pages",
|
|
5
|
-
blog: "blog",
|
|
6
|
-
docs: "docs",
|
|
7
|
-
learn: "learn",
|
|
8
|
-
project: "projects",
|
|
9
|
-
airdrop: "airdrops",
|
|
10
|
-
};
|
|
1
|
+
import { resolveApiKey, resolveServerUrl } from "./fetch.js";
|
|
2
|
+
import { queryEntriesByContentType } from "./contentCollections.js";
|
|
11
3
|
/** Default change frequency and priority for each content type. */
|
|
12
4
|
const CONTENT_TYPE_DEFAULTS = {
|
|
13
5
|
page: { changeFrequency: "monthly", priority: 0.8 },
|
|
@@ -67,17 +59,12 @@ export async function loadSitemapEntries(options) {
|
|
|
67
59
|
return entries;
|
|
68
60
|
}
|
|
69
61
|
async function fetchCollectionSlugs(serverUrl, contentType, apiKey) {
|
|
70
|
-
const collectionName = CONTENT_TYPE_COLLECTIONS[contentType];
|
|
71
62
|
const defaults = CONTENT_TYPE_DEFAULTS[contentType];
|
|
72
|
-
const records = await
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
offset: 0,
|
|
78
|
-
},
|
|
79
|
-
apiKey,
|
|
80
|
-
});
|
|
63
|
+
const records = await queryEntriesByContentType(serverUrl, contentType, (collectionName) => ({
|
|
64
|
+
collection_name: collectionName,
|
|
65
|
+
limit: 1000,
|
|
66
|
+
offset: 0,
|
|
67
|
+
}), apiKey);
|
|
81
68
|
return records.map((record) => {
|
|
82
69
|
const slug = record.payload.slug ??
|
|
83
70
|
record.payload.route?.replace(/^\//, "") ??
|
package/dist/react/slugs.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const CONTENT_TYPE_COLLECTIONS = {
|
|
4
|
-
page: "pages",
|
|
5
|
-
blog: "blog",
|
|
6
|
-
docs: "docs",
|
|
7
|
-
learn: "learn",
|
|
8
|
-
project: "projects",
|
|
9
|
-
airdrop: "airdrops",
|
|
10
|
-
};
|
|
1
|
+
import { resolveApiKey, resolveServerUrl } from "./fetch.js";
|
|
2
|
+
import { queryEntriesByContentType } from "./contentCollections.js";
|
|
11
3
|
/**
|
|
12
4
|
* Returns all slugs for a given content type, for use with
|
|
13
5
|
* Next.js `generateStaticParams`.
|
|
@@ -26,16 +18,11 @@ const CONTENT_TYPE_COLLECTIONS = {
|
|
|
26
18
|
export async function listContentSlugs(contentType, options) {
|
|
27
19
|
const serverUrl = resolveServerUrl(options);
|
|
28
20
|
const apiKey = resolveApiKey(options);
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
limit: 1000,
|
|
35
|
-
offset: 0,
|
|
36
|
-
},
|
|
37
|
-
apiKey,
|
|
38
|
-
});
|
|
21
|
+
const records = await queryEntriesByContentType(serverUrl, contentType, (collectionName) => ({
|
|
22
|
+
collection_name: collectionName,
|
|
23
|
+
limit: 1000,
|
|
24
|
+
offset: 0,
|
|
25
|
+
}), apiKey);
|
|
39
26
|
return records.map((record) => record.payload.slug ?? record.entry_key);
|
|
40
27
|
}
|
|
41
28
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { SiteNavigationItem } from "./SiteLayout.js";
|
|
2
2
|
import { type DocsIndexEntry } from "./contentIndex.js";
|
|
3
3
|
import { type DocsSidebarSection } from "./docsNavigation.js";
|
|
4
|
+
import { type DocsTemplateShellOptions } from "./docsTemplateShell.js";
|
|
4
5
|
export interface DocsIndexItem extends DocsIndexEntry {
|
|
5
6
|
}
|
|
6
|
-
export interface DocsIndexTemplateProps {
|
|
7
|
-
siteTitle
|
|
8
|
-
navigation
|
|
7
|
+
export interface DocsIndexTemplateProps extends DocsTemplateShellOptions {
|
|
8
|
+
siteTitle?: string;
|
|
9
|
+
navigation?: SiteNavigationItem[];
|
|
9
10
|
docs: DocsIndexItem[];
|
|
10
11
|
title?: string;
|
|
11
12
|
description?: string;
|
|
@@ -22,10 +23,10 @@ export interface DocsIndexTemplateProps {
|
|
|
22
23
|
sidebarSections?: DocsSidebarSection[];
|
|
23
24
|
sidebarTitle?: string;
|
|
24
25
|
}
|
|
25
|
-
export declare function DocsIndexTemplate({ siteTitle, navigation, docs, title, description, basePath, currentPath, query, category, tag, sort, page, pageSize, categories, tags, sidebarSections, sidebarTitle }: DocsIndexTemplateProps): React.JSX.Element;
|
|
26
|
-
export interface DocArticleTemplateProps {
|
|
27
|
-
siteTitle
|
|
28
|
-
navigation
|
|
26
|
+
export declare function DocsIndexTemplate({ siteTitle, navigation, docs, title, description, basePath, currentPath, query, category, tag, sort, page, pageSize, categories, tags, sidebarSections, sidebarTitle, headless, renderLayout }: DocsIndexTemplateProps): React.JSX.Element;
|
|
27
|
+
export interface DocArticleTemplateProps extends DocsTemplateShellOptions {
|
|
28
|
+
siteTitle?: string;
|
|
29
|
+
navigation?: SiteNavigationItem[];
|
|
29
30
|
title: string;
|
|
30
31
|
bodyMarkdown?: string;
|
|
31
32
|
bodyHtml?: string;
|
|
@@ -57,4 +58,4 @@ export interface DocArticleTemplateProps {
|
|
|
57
58
|
};
|
|
58
59
|
editHref?: string;
|
|
59
60
|
}
|
|
60
|
-
export declare function DocArticleTemplate({ siteTitle, navigation, title, bodyMarkdown, bodyHtml, allowUnsafeHtmlFallback, lastUpdated, readingMinutes, basePath, currentPath, searchQuery, docs, sidebarSections, sidebarTitle, breadcrumbs, toc, previousDoc, nextDoc, editHref }: DocArticleTemplateProps): React.JSX.Element;
|
|
61
|
+
export declare function DocArticleTemplate({ siteTitle, navigation, title, bodyMarkdown, bodyHtml, allowUnsafeHtmlFallback, lastUpdated, readingMinutes, basePath, currentPath, searchQuery, docs, sidebarSections, sidebarTitle, breadcrumbs, toc, previousDoc, nextDoc, editHref, headless, renderLayout }: DocArticleTemplateProps): React.JSX.Element;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { SiteLayout } from "./SiteLayout.js";
|
|
3
2
|
import { buildContentListHref, collectFilterValues, prepareDocsIndex } from "./contentIndex.js";
|
|
4
3
|
import { Breadcrumbs, ContentPagination } from "./contentUi.js";
|
|
5
|
-
import { buildDocsSidebarSections, withActiveDocsSidebar } from "./docsNavigation.js";
|
|
4
|
+
import { buildHierarchicalDocsSidebarSections, buildDocsSidebarSections, withActiveDocsSidebar } from "./docsNavigation.js";
|
|
6
5
|
import { DocsSidebar } from "./DocsSidebar.js";
|
|
6
|
+
import { renderDocsTemplateShell } from "./docsTemplateShell.js";
|
|
7
7
|
import { MarkdownContent } from "./MarkdownContent.js";
|
|
8
8
|
import { TocScrollSpy } from "./tocScrollSpy.js";
|
|
9
|
-
export function DocsIndexTemplate({ siteTitle, navigation, docs, title = "Documentation", description = "", basePath = "/docs", currentPath, query = "", category = "", tag = "", sort = "title-asc", page = 1, pageSize = 10, categories, tags, sidebarSections, sidebarTitle = "Docs" }) {
|
|
9
|
+
export function DocsIndexTemplate({ siteTitle, navigation, docs, title = "Documentation", description = "", basePath = "/docs", currentPath, query = "", category = "", tag = "", sort = "title-asc", page = 1, pageSize = 10, categories, tags, sidebarSections, sidebarTitle = "Docs", headless = false, renderLayout }) {
|
|
10
10
|
const normalizedFilters = collectFilterValues(docs);
|
|
11
11
|
const resolvedCategories = categories ?? normalizedFilters.categories;
|
|
12
12
|
const resolvedTags = tags ?? normalizedFilters.tags;
|
|
13
13
|
const activePath = currentPath ?? basePath;
|
|
14
|
-
const resolvedSidebarSections = withActiveDocsSidebar(sidebarSections ??
|
|
14
|
+
const resolvedSidebarSections = withActiveDocsSidebar(sidebarSections ?? buildDefaultDocsSidebarSections(docs, basePath), activePath);
|
|
15
15
|
const result = prepareDocsIndex(docs, {
|
|
16
16
|
query,
|
|
17
17
|
category,
|
|
@@ -20,21 +20,31 @@ export function DocsIndexTemplate({ siteTitle, navigation, docs, title = "Docume
|
|
|
20
20
|
page,
|
|
21
21
|
pageSize
|
|
22
22
|
});
|
|
23
|
-
return (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
return renderDocsTemplateShell((_jsxs("section", { className: "cedros-site__docs-page", children: [_jsx(DocsSidebar, { title: sidebarTitle, basePath: basePath, searchQuery: query, sections: resolvedSidebarSections }), _jsxs("div", { className: "cedros-site__docs-main", children: [_jsxs("section", { className: "cedros-site__card", children: [_jsx("span", { className: "cedros-site__pill", children: "docs" }), _jsx("h1", { className: "cedros-site__title", style: { marginTop: "0.6rem" }, children: title }), description && _jsx("p", { className: "cedros-site__subtitle", children: description }), _jsxs("p", { className: "cedros-site__entry-meta", style: { marginTop: "0.6rem" }, children: [result.totalItems, " results"] })] }), _jsx(DocsIndexControls, { basePath: basePath, query: query, category: category, tag: tag, sort: sort, categories: resolvedCategories, tags: resolvedTags }), result.totalItems === 0 && (_jsxs("section", { className: "cedros-site__card cedros-site__empty-state", children: [_jsx("h2", { className: "cedros-site__title", style: { fontSize: "1.25rem" }, children: "No documentation pages found" }), _jsx("p", { className: "cedros-site__subtitle", style: { marginTop: "0.6rem" }, children: "Try a broader query or clear filters." })] })), result.totalItems > 0 && (_jsxs(_Fragment, { children: [_jsx("section", { className: "cedros-site__content-grid cedros-site__content-grid--docs", children: result.items.map((doc) => (_jsxs("article", { className: "cedros-site__card cedros-site__entry-card", children: [_jsx("h2", { className: "cedros-site__entry-title", children: _jsx("a", { href: `${basePath}/${doc.slug}`, children: doc.title }) }), doc.description && _jsx("p", { className: "cedros-site__subtitle", children: doc.description }), (doc.lastUpdated || doc.category) && (_jsx("p", { className: "cedros-site__entry-meta", children: [doc.category, doc.lastUpdated ? `Updated ${doc.lastUpdated}` : ""]
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join(" • ") })), doc.tags && doc.tags.length > 0 && (_jsx("div", { className: "cedros-site__tag-list", children: doc.tags.map((entryTag) => (_jsx("a", { href: buildContentListHref(basePath, {
|
|
26
|
+
q: query,
|
|
27
|
+
category,
|
|
28
|
+
tag: entryTag,
|
|
29
|
+
sort
|
|
30
|
+
}), className: "cedros-site__pill", children: entryTag }, entryTag))) }))] }, doc.slug))) }), _jsx(ContentPagination, { basePath: basePath, page: result.page, totalPages: result.totalPages, query: { q: query, category, tag, sort } })] }))] })] })), {
|
|
31
|
+
siteTitle,
|
|
32
|
+
navigation,
|
|
33
|
+
headless,
|
|
34
|
+
renderLayout
|
|
35
|
+
});
|
|
31
36
|
}
|
|
32
|
-
export function DocArticleTemplate({ siteTitle, navigation, title, bodyMarkdown, bodyHtml, allowUnsafeHtmlFallback = false, lastUpdated, readingMinutes, basePath = "/docs", currentPath, searchQuery = "", docs = [], sidebarSections, sidebarTitle = "Docs", breadcrumbs = [], toc = [], previousDoc, nextDoc, editHref }) {
|
|
37
|
+
export function DocArticleTemplate({ siteTitle, navigation, title, bodyMarkdown, bodyHtml, allowUnsafeHtmlFallback = false, lastUpdated, readingMinutes, basePath = "/docs", currentPath, searchQuery = "", docs = [], sidebarSections, sidebarTitle = "Docs", breadcrumbs = [], toc = [], previousDoc, nextDoc, editHref, headless = false, renderLayout }) {
|
|
33
38
|
const activePath = currentPath ?? breadcrumbs[breadcrumbs.length - 1]?.href;
|
|
34
|
-
const resolvedSidebarSections = withActiveDocsSidebar(sidebarSections ??
|
|
35
|
-
return (
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const resolvedSidebarSections = withActiveDocsSidebar(sidebarSections ?? buildDefaultDocsSidebarSections(docs, basePath), activePath);
|
|
40
|
+
return renderDocsTemplateShell((_jsxs("section", { className: "cedros-site__docs-page cedros-site__docs-page--article", children: [_jsx(DocsSidebar, { title: sidebarTitle, basePath: basePath, searchQuery: searchQuery, sections: resolvedSidebarSections }), _jsxs("article", { className: "cedros-site__card cedros-site__article cedros-site__docs-article", children: [_jsx(Breadcrumbs, { trail: breadcrumbs }), _jsxs("div", { className: "cedros-site__article-header", children: [_jsx("span", { className: "cedros-site__pill", children: "docs" }), _jsx("h1", { className: "cedros-site__title", children: title }), (lastUpdated || readingMinutes) && (_jsx("p", { className: "cedros-site__entry-meta", children: [lastUpdated ? `Last updated ${lastUpdated}` : "", readingTime(readingMinutes)]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(" • ") }))] }), _jsx(MarkdownContent, { bodyMarkdown: bodyMarkdown, bodyHtml: bodyHtml, allowUnsafeHtmlFallback: allowUnsafeHtmlFallback }), (previousDoc || nextDoc || editHref) && (_jsxs("footer", { className: "cedros-site__doc-footer", children: [previousDoc && (_jsxs("a", { href: previousDoc.href, className: "cedros-site__nav-link", children: ["\u2190 ", previousDoc.title] })), nextDoc && (_jsxs("a", { href: nextDoc.href, className: "cedros-site__nav-link", children: [nextDoc.title, " \u2192"] })), editHref && (_jsx("a", { href: editHref, className: "cedros-site__nav-link", children: "Suggest edit" }))] }))] }), toc.length > 0 && (_jsxs("aside", { className: "cedros-site__card cedros-site__toc", children: [_jsx("h2", { className: "cedros-site__toc-title", children: "On this page" }), _jsx(TocScrollSpy, { entries: toc })] }))] })), {
|
|
43
|
+
siteTitle,
|
|
44
|
+
navigation,
|
|
45
|
+
headless,
|
|
46
|
+
renderLayout
|
|
47
|
+
});
|
|
38
48
|
}
|
|
39
49
|
function DocsIndexControls({ basePath, query, category, tag, sort, categories, tags }) {
|
|
40
50
|
return (_jsxs("form", { method: "get", action: basePath, className: "cedros-site__controls cedros-site__card", children: [_jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Search" }), _jsx("input", { type: "search", name: "q", defaultValue: query, placeholder: "Search docs" })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Category" }), _jsxs("select", { name: "category", defaultValue: category, children: [_jsx("option", { value: "", children: "All" }), categories.map((entry) => (_jsx("option", { value: entry, children: entry }, entry)))] })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Tag" }), _jsxs("select", { name: "tag", defaultValue: tag, children: [_jsx("option", { value: "", children: "All" }), tags.map((entry) => (_jsx("option", { value: entry, children: entry }, entry)))] })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Sort" }), _jsxs("select", { name: "sort", defaultValue: sort, children: [_jsx("option", { value: "title-asc", children: "Title A-Z" }), _jsx("option", { value: "title-desc", children: "Title Z-A" }), _jsx("option", { value: "updated-desc", children: "Recently updated" }), _jsx("option", { value: "updated-asc", children: "Least recently updated" })] })] }), _jsxs("div", { className: "cedros-site__control-actions", children: [_jsx("button", { className: "cedros-site__nav-link", type: "submit", children: "Apply" }), _jsx("a", { className: "cedros-site__nav-link", href: basePath, children: "Clear" })] })] }));
|
|
@@ -45,3 +55,8 @@ function readingTime(minutes) {
|
|
|
45
55
|
}
|
|
46
56
|
return `${minutes} min read`;
|
|
47
57
|
}
|
|
58
|
+
function buildDefaultDocsSidebarSections(docs, basePath) {
|
|
59
|
+
return docs.some((doc) => doc.slug.includes("/"))
|
|
60
|
+
? buildHierarchicalDocsSidebarSections(docs, basePath)
|
|
61
|
+
: buildDocsSidebarSections(docs, basePath);
|
|
62
|
+
}
|
|
@@ -40,6 +40,10 @@ export interface DocsIndexEntry {
|
|
|
40
40
|
category?: string;
|
|
41
41
|
tags?: string[];
|
|
42
42
|
lastUpdated?: string;
|
|
43
|
+
bodyMarkdown?: string;
|
|
44
|
+
bodyHtml?: string;
|
|
45
|
+
bodyText?: string;
|
|
46
|
+
searchText?: string;
|
|
43
47
|
}
|
|
44
48
|
export interface DocsIndexQuery {
|
|
45
49
|
query?: string;
|
|
@@ -125,7 +125,7 @@ function matchesDocsQuery(entry, query) {
|
|
|
125
125
|
if (!normalizedQuery) {
|
|
126
126
|
return true;
|
|
127
127
|
}
|
|
128
|
-
const haystack =
|
|
128
|
+
const haystack = buildDocsSearchText(entry);
|
|
129
129
|
return haystack.includes(normalizedQuery);
|
|
130
130
|
}
|
|
131
131
|
function compareBlogEntries(left, right, sort) {
|
|
@@ -159,6 +159,20 @@ function toTimestamp(value) {
|
|
|
159
159
|
const parsed = Date.parse(value ?? "");
|
|
160
160
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
161
161
|
}
|
|
162
|
+
function buildDocsSearchText(entry) {
|
|
163
|
+
return [
|
|
164
|
+
entry.title,
|
|
165
|
+
entry.description,
|
|
166
|
+
entry.category,
|
|
167
|
+
...(entry.tags ?? []),
|
|
168
|
+
entry.searchText,
|
|
169
|
+
entry.bodyText,
|
|
170
|
+
entry.bodyMarkdown,
|
|
171
|
+
entry.bodyHtml
|
|
172
|
+
]
|
|
173
|
+
.join(" ")
|
|
174
|
+
.toLowerCase();
|
|
175
|
+
}
|
|
162
176
|
function paginate(items, pageInput, pageSizeInput) {
|
|
163
177
|
const pageSize = sanitizePositiveInt(pageSizeInput, 10);
|
|
164
178
|
const totalItems = items.length;
|
|
@@ -4,7 +4,7 @@ export interface DocsSidebarItem {
|
|
|
4
4
|
label: string;
|
|
5
5
|
href: string;
|
|
6
6
|
isActive?: boolean;
|
|
7
|
-
depth?:
|
|
7
|
+
depth?: number;
|
|
8
8
|
badge?: string;
|
|
9
9
|
}
|
|
10
10
|
export interface DocsSidebarSection {
|
|
@@ -14,5 +14,34 @@ export interface DocsSidebarSection {
|
|
|
14
14
|
/** When true, section renders collapsed by default in the sidebar. */
|
|
15
15
|
collapsed?: boolean;
|
|
16
16
|
}
|
|
17
|
+
export interface DocsTreeNode<T extends DocsIndexEntry = DocsIndexEntry> {
|
|
18
|
+
key: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
label: string;
|
|
21
|
+
href?: string;
|
|
22
|
+
entry?: T;
|
|
23
|
+
children: DocsTreeNode<T>[];
|
|
24
|
+
}
|
|
25
|
+
export interface DocsTreeSection<T extends DocsIndexEntry = DocsIndexEntry> {
|
|
26
|
+
key: string;
|
|
27
|
+
label: string;
|
|
28
|
+
nodes: DocsTreeNode<T>[];
|
|
29
|
+
}
|
|
30
|
+
export interface DocsPagerLink<T extends DocsIndexEntry = DocsIndexEntry> {
|
|
31
|
+
title: string;
|
|
32
|
+
href: string;
|
|
33
|
+
entry: T;
|
|
34
|
+
}
|
|
35
|
+
export interface DocsPrevNext<T extends DocsIndexEntry = DocsIndexEntry> {
|
|
36
|
+
previousDoc?: DocsPagerLink<T>;
|
|
37
|
+
nextDoc?: DocsPagerLink<T>;
|
|
38
|
+
}
|
|
39
|
+
export interface FetchDocEntryOptions {
|
|
40
|
+
basePath?: string;
|
|
41
|
+
}
|
|
17
42
|
export declare function buildDocsSidebarSections(docs: DocsIndexEntry[], basePath?: string): DocsSidebarSection[];
|
|
43
|
+
export declare function fetchDocEntry<T extends DocsIndexEntry>(docs: T[], slugOrPath: string, options?: FetchDocEntryOptions): T | undefined;
|
|
44
|
+
export declare function buildDocsTree<T extends DocsIndexEntry>(docs: T[], basePath?: string): DocsTreeSection<T>[];
|
|
45
|
+
export declare function buildHierarchicalDocsSidebarSections<T extends DocsIndexEntry>(docs: T[], basePath?: string): DocsSidebarSection[];
|
|
46
|
+
export declare function buildDocsPrevNext<T extends DocsIndexEntry>(docs: T[], currentSlugOrPath: string, options?: FetchDocEntryOptions): DocsPrevNext<T>;
|
|
18
47
|
export declare function withActiveDocsSidebar(sections: DocsSidebarSection[], currentPath?: string): DocsSidebarSection[];
|
|
@@ -8,7 +8,7 @@ export function buildDocsSidebarSections(docs, basePath = "/docs") {
|
|
|
8
8
|
current.items.push({
|
|
9
9
|
key: doc.slug,
|
|
10
10
|
label: doc.title,
|
|
11
|
-
href:
|
|
11
|
+
href: buildDocHref(basePath, doc.slug)
|
|
12
12
|
});
|
|
13
13
|
grouped.set(sectionKey, current);
|
|
14
14
|
});
|
|
@@ -20,6 +20,52 @@ export function buildDocsSidebarSections(docs, basePath = "/docs") {
|
|
|
20
20
|
}))
|
|
21
21
|
.sort((left, right) => left.label.localeCompare(right.label));
|
|
22
22
|
}
|
|
23
|
+
export function fetchDocEntry(docs, slugOrPath, options = {}) {
|
|
24
|
+
const normalizedLookup = normalizeDocLookup(slugOrPath, options.basePath);
|
|
25
|
+
return docs.find((doc) => {
|
|
26
|
+
const normalizedDocSlug = normalizeDocSlug(doc.slug);
|
|
27
|
+
return normalizedDocSlug === normalizedLookup || (!normalizedLookup && normalizedDocSlug === "index");
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function buildDocsTree(docs, basePath = "/docs") {
|
|
31
|
+
const sections = [];
|
|
32
|
+
const grouped = new Map();
|
|
33
|
+
docs.forEach((doc) => {
|
|
34
|
+
const sectionLabel = doc.category ?? "Overview";
|
|
35
|
+
const sectionKey = sectionLabel.toLowerCase().replace(/\s+/gu, "-");
|
|
36
|
+
const section = grouped.get(sectionKey) ?? { key: sectionKey, label: sectionLabel, nodes: [] };
|
|
37
|
+
if (!grouped.has(sectionKey)) {
|
|
38
|
+
grouped.set(sectionKey, section);
|
|
39
|
+
sections.push(section);
|
|
40
|
+
}
|
|
41
|
+
insertDocTreeNode(section.nodes, doc, basePath);
|
|
42
|
+
});
|
|
43
|
+
return sections;
|
|
44
|
+
}
|
|
45
|
+
export function buildHierarchicalDocsSidebarSections(docs, basePath = "/docs") {
|
|
46
|
+
return buildDocsTree(docs, basePath)
|
|
47
|
+
.map((section) => ({
|
|
48
|
+
key: section.key,
|
|
49
|
+
label: section.label,
|
|
50
|
+
items: flattenDocsTreeNodes(section.nodes)
|
|
51
|
+
}))
|
|
52
|
+
.filter((section) => section.items.length > 0);
|
|
53
|
+
}
|
|
54
|
+
export function buildDocsPrevNext(docs, currentSlugOrPath, options = {}) {
|
|
55
|
+
const current = fetchDocEntry(docs, currentSlugOrPath, options);
|
|
56
|
+
if (!current) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
const currentSlug = normalizeDocSlug(current.slug);
|
|
60
|
+
const currentIndex = docs.findIndex((doc) => normalizeDocSlug(doc.slug) === currentSlug);
|
|
61
|
+
if (currentIndex === -1) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
previousDoc: buildDocsPagerLink(docs[currentIndex - 1], options.basePath),
|
|
66
|
+
nextDoc: buildDocsPagerLink(docs[currentIndex + 1], options.basePath)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
23
69
|
export function withActiveDocsSidebar(sections, currentPath) {
|
|
24
70
|
if (!currentPath) {
|
|
25
71
|
return sections.map((section) => ({
|
|
@@ -48,3 +94,88 @@ export function withActiveDocsSidebar(sections, currentPath) {
|
|
|
48
94
|
})
|
|
49
95
|
}));
|
|
50
96
|
}
|
|
97
|
+
function buildDocHref(basePath, slug) {
|
|
98
|
+
const normalizedBasePath = normalizeRoutePath(basePath);
|
|
99
|
+
const normalizedSlug = normalizeDocSlug(slug);
|
|
100
|
+
if (!normalizedSlug) {
|
|
101
|
+
return normalizedBasePath;
|
|
102
|
+
}
|
|
103
|
+
if (normalizedBasePath === "/") {
|
|
104
|
+
return `/${normalizedSlug}`;
|
|
105
|
+
}
|
|
106
|
+
return `${normalizedBasePath}/${normalizedSlug}`;
|
|
107
|
+
}
|
|
108
|
+
function normalizeDocSlug(slug) {
|
|
109
|
+
return slug.trim().replace(/^\/+|\/+$/gu, "");
|
|
110
|
+
}
|
|
111
|
+
function normalizeDocLookup(value, basePath = "/docs") {
|
|
112
|
+
const normalizedValue = normalizeRoutePath(value).replace(/^\/+/u, "");
|
|
113
|
+
const normalizedBasePath = normalizeRoutePath(basePath).replace(/^\/+/u, "");
|
|
114
|
+
if (!normalizedBasePath) {
|
|
115
|
+
return normalizedValue;
|
|
116
|
+
}
|
|
117
|
+
if (normalizedValue === normalizedBasePath) {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
if (normalizedValue.startsWith(`${normalizedBasePath}/`)) {
|
|
121
|
+
return normalizedValue.slice(normalizedBasePath.length + 1);
|
|
122
|
+
}
|
|
123
|
+
return normalizedValue;
|
|
124
|
+
}
|
|
125
|
+
function insertDocTreeNode(nodes, doc, basePath) {
|
|
126
|
+
const segments = normalizedDocSegments(doc.slug);
|
|
127
|
+
let cursor = nodes;
|
|
128
|
+
segments.forEach((segment, index) => {
|
|
129
|
+
const slug = segments.slice(0, index + 1).join("/");
|
|
130
|
+
let node = cursor.find((candidate) => candidate.slug === slug);
|
|
131
|
+
if (!node) {
|
|
132
|
+
node = {
|
|
133
|
+
key: slug || "index",
|
|
134
|
+
slug,
|
|
135
|
+
label: humanizeDocSegment(segment),
|
|
136
|
+
children: []
|
|
137
|
+
};
|
|
138
|
+
cursor.push(node);
|
|
139
|
+
}
|
|
140
|
+
if (index === segments.length - 1) {
|
|
141
|
+
node.entry = doc;
|
|
142
|
+
node.href = buildDocHref(basePath, doc.slug);
|
|
143
|
+
node.label = doc.title;
|
|
144
|
+
}
|
|
145
|
+
cursor = node.children;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function normalizedDocSegments(slug) {
|
|
149
|
+
const normalized = normalizeDocSlug(slug);
|
|
150
|
+
return normalized ? normalized.split("/").filter(Boolean) : ["index"];
|
|
151
|
+
}
|
|
152
|
+
function flattenDocsTreeNodes(nodes, depth = 0) {
|
|
153
|
+
return nodes.flatMap((node) => {
|
|
154
|
+
const items = [];
|
|
155
|
+
if (node.entry && node.href) {
|
|
156
|
+
items.push({
|
|
157
|
+
key: node.key,
|
|
158
|
+
label: node.label,
|
|
159
|
+
href: node.href,
|
|
160
|
+
depth: Math.min(depth, 2)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
items.push(...flattenDocsTreeNodes(node.children, depth + 1));
|
|
164
|
+
return items;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function buildDocsPagerLink(doc, basePath = "/docs") {
|
|
168
|
+
if (!doc) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
title: doc.title,
|
|
173
|
+
href: buildDocHref(basePath, doc.slug),
|
|
174
|
+
entry: doc
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function humanizeDocSegment(segment) {
|
|
178
|
+
return segment
|
|
179
|
+
.replace(/[-_]+/gu, " ")
|
|
180
|
+
.replace(/\b\w/gu, (char) => char.toUpperCase());
|
|
181
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SiteNavigationItem } from "./SiteLayout.js";
|
|
2
|
+
export interface DocsTemplateShellOptions {
|
|
3
|
+
siteTitle?: string;
|
|
4
|
+
navigation?: SiteNavigationItem[];
|
|
5
|
+
headless?: boolean;
|
|
6
|
+
renderLayout?: (content: React.JSX.Element) => React.JSX.Element;
|
|
7
|
+
}
|
|
8
|
+
export declare function renderDocsTemplateShell(content: React.JSX.Element, options: DocsTemplateShellOptions): React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SiteLayout } from "./SiteLayout.js";
|
|
3
|
+
export function renderDocsTemplateShell(content, options) {
|
|
4
|
+
if (options.renderLayout) {
|
|
5
|
+
return options.renderLayout(content);
|
|
6
|
+
}
|
|
7
|
+
if (options.headless) {
|
|
8
|
+
return content;
|
|
9
|
+
}
|
|
10
|
+
if (options.siteTitle === undefined || options.navigation === undefined) {
|
|
11
|
+
throw new Error("Docs templates require `siteTitle` and `navigation` unless `headless` or `renderLayout` is provided.");
|
|
12
|
+
}
|
|
13
|
+
return (_jsx(SiteLayout, { siteTitle: options.siteTitle, navigation: options.navigation, children: content }));
|
|
14
|
+
}
|
|
@@ -5,7 +5,7 @@ export { DashboardShell, type DashboardShellProps, type DashboardNavItem } from
|
|
|
5
5
|
export { DashboardOverviewTemplate, type DashboardOverviewTemplateProps, type DashboardPanel, type DashboardStat } from "./DashboardOverviewTemplate.js";
|
|
6
6
|
export { isRouteActive, normalizeRoutePath, withActiveRouteState, type RouteAwareItem, type RouteMatcherOptions } from "./routing.js";
|
|
7
7
|
export { buildContentListHref, collectFilterValues, collectDimensionValues, matchesFilterDimensions, prepareBlogIndex, prepareDocsIndex, type BlogIndexEntry, type BlogIndexQuery, type DocsIndexEntry, type DocsIndexQuery, type FilterDimension, type FilterDimensionValues, type PaginationResult } from "./contentIndex.js";
|
|
8
|
-
export { buildDocsSidebarSections, withActiveDocsSidebar, type DocsSidebarItem, type DocsSidebarSection } from "./docsNavigation.js";
|
|
8
|
+
export { buildDocsSidebarSections, buildDocsTree, buildHierarchicalDocsSidebarSections, buildDocsPrevNext, fetchDocEntry, withActiveDocsSidebar, type DocsTreeNode, type DocsTreeSection, type DocsPagerLink, type DocsPrevNext, type FetchDocEntryOptions, type DocsSidebarItem, type DocsSidebarSection } from "./docsNavigation.js";
|
|
9
9
|
export { MarkdownContent, type MarkdownContentProps } from "./MarkdownContent.js";
|
|
10
10
|
export { extractTocFromMarkdown, type TocEntry } from "./tocExtractor.js";
|
|
11
11
|
export { DocsSidebar, type DocsSidebarProps } from "./DocsSidebar.js";
|
|
@@ -5,7 +5,7 @@ export { DashboardShell } from "./DashboardShell.js";
|
|
|
5
5
|
export { DashboardOverviewTemplate } from "./DashboardOverviewTemplate.js";
|
|
6
6
|
export { isRouteActive, normalizeRoutePath, withActiveRouteState } from "./routing.js";
|
|
7
7
|
export { buildContentListHref, collectFilterValues, collectDimensionValues, matchesFilterDimensions, prepareBlogIndex, prepareDocsIndex } from "./contentIndex.js";
|
|
8
|
-
export { buildDocsSidebarSections, withActiveDocsSidebar } from "./docsNavigation.js";
|
|
8
|
+
export { buildDocsSidebarSections, buildDocsTree, buildHierarchicalDocsSidebarSections, buildDocsPrevNext, fetchDocEntry, withActiveDocsSidebar } from "./docsNavigation.js";
|
|
9
9
|
export { MarkdownContent } from "./MarkdownContent.js";
|
|
10
10
|
export { extractTocFromMarkdown } from "./tocExtractor.js";
|
|
11
11
|
export { DocsSidebar } from "./DocsSidebar.js";
|