@cedros/data-react 0.1.6 → 0.1.8
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/docsContract.d.ts +56 -0
- package/dist/docsContract.js +136 -0
- package/dist/react/contentCollections.d.ts +4 -0
- package/dist/react/contentCollections.js +48 -0
- package/dist/react/docs.d.ts +14 -0
- package/dist/react/docs.js +207 -0
- package/dist/react/entries.js +6 -7
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +1 -0
- package/dist/react/server.d.ts +2 -1
- package/dist/react/server.js +1 -0
- package/dist/react/sitemap.js +7 -20
- package/dist/react/slugs.js +7 -20
- package/dist/react/types.d.ts +17 -0
- package/dist/site-templates/BlogTemplates.js +16 -12
- package/dist/site-templates/DocsSidebar.d.ts +1 -1
- package/dist/site-templates/DocsSidebar.js +2 -2
- package/dist/site-templates/DocsTemplates.d.ts +9 -8
- package/dist/site-templates/DocsTemplates.js +32 -18
- package/dist/site-templates/SiteFooter.js +1 -1
- package/dist/site-templates/SiteLayout.js +1 -1
- package/dist/site-templates/TopNav.js +1 -1
- package/dist/site-templates/blog-styles.css +259 -0
- package/dist/site-templates/blogControls.js +2 -2
- package/dist/site-templates/blogTemplateUi.d.ts +10 -0
- package/dist/site-templates/blogTemplateUi.js +4 -0
- package/dist/site-templates/content-styles.css +127 -309
- package/dist/site-templates/contentIndex.d.ts +4 -7
- package/dist/site-templates/contentIndex.js +25 -1
- package/dist/site-templates/contentUi.js +4 -3
- package/dist/site-templates/dashboard-styles.css +109 -0
- package/dist/site-templates/docs-layout.css +372 -0
- package/dist/site-templates/docs-styles.css +288 -96
- package/dist/site-templates/docsNavigation.d.ts +50 -1
- package/dist/site-templates/docsNavigation.js +242 -8
- package/dist/site-templates/docsTemplateShell.d.ts +8 -0
- package/dist/site-templates/docsTemplateShell.js +14 -0
- package/dist/site-templates/index.d.ts +2 -1
- package/dist/site-templates/index.js +2 -1
- package/dist/site-templates/styles.css +283 -201
- 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,56 @@
|
|
|
1
|
+
export interface DocsTocHeading {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
depth?: 2 | 3 | 4;
|
|
5
|
+
}
|
|
6
|
+
export interface DocsBreadcrumb {
|
|
7
|
+
label: string;
|
|
8
|
+
href: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DocsEntryRecord {
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
category?: string;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
lastUpdated?: string;
|
|
17
|
+
bodyMarkdown?: string;
|
|
18
|
+
bodyHtml?: string;
|
|
19
|
+
bodyText?: string;
|
|
20
|
+
searchText?: string;
|
|
21
|
+
status?: string;
|
|
22
|
+
productSlug?: string;
|
|
23
|
+
productLabel?: string;
|
|
24
|
+
productOrder?: number;
|
|
25
|
+
sectionKey?: string;
|
|
26
|
+
sectionLabel?: string;
|
|
27
|
+
sectionOrder?: number;
|
|
28
|
+
order?: number;
|
|
29
|
+
readingMinutes?: number;
|
|
30
|
+
toc?: DocsTocHeading[];
|
|
31
|
+
breadcrumbs?: DocsBreadcrumb[];
|
|
32
|
+
editHref?: string;
|
|
33
|
+
sourcePath?: string;
|
|
34
|
+
sourceHref?: string;
|
|
35
|
+
navLabel?: string;
|
|
36
|
+
navOrder?: number;
|
|
37
|
+
aliases?: string[];
|
|
38
|
+
badge?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface DocsNormalizationOptions {
|
|
41
|
+
aliases?: Record<string, string>;
|
|
42
|
+
basePath?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface DocsFilterOptions extends DocsNormalizationOptions {
|
|
45
|
+
productSlug?: string;
|
|
46
|
+
sectionKey?: string;
|
|
47
|
+
includeDrafts?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface SortDocsEntriesOptions {
|
|
50
|
+
preserveSourceOrder?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function normalizeDocsSlug(value: string, options?: DocsNormalizationOptions): string;
|
|
53
|
+
export declare function normalizeDocsHref(href: string, options?: DocsNormalizationOptions): string;
|
|
54
|
+
export declare function matchesDocsEntryFilter(entry: DocsEntryRecord, options?: DocsFilterOptions): boolean;
|
|
55
|
+
export declare function compareDocsEntriesByOrder(left: DocsEntryRecord, right: DocsEntryRecord): number;
|
|
56
|
+
export declare function sortDocsEntries<T extends DocsEntryRecord>(entries: T[], options?: SortDocsEntriesOptions): T[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const DEFAULT_DOCS_BASE_PATH = "/docs";
|
|
2
|
+
const ABSOLUTE_URL_PATTERN = /^[a-z][a-z\d+\-.]*:\/\//iu;
|
|
3
|
+
export function normalizeDocsSlug(value, options = {}) {
|
|
4
|
+
const { pathname } = splitHref(value);
|
|
5
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
6
|
+
let relativePath = trimSlashes(pathname);
|
|
7
|
+
if (!relativePath) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
const normalizedBase = trimSlashes(basePath);
|
|
11
|
+
if (normalizedBase) {
|
|
12
|
+
if (relativePath === normalizedBase) {
|
|
13
|
+
relativePath = "";
|
|
14
|
+
}
|
|
15
|
+
else if (relativePath.startsWith(`${normalizedBase}/`)) {
|
|
16
|
+
relativePath = relativePath.slice(normalizedBase.length + 1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const segments = relativePath
|
|
20
|
+
.split("/")
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map(slugifySegment);
|
|
23
|
+
if (segments.length === 0) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
const aliasMap = normalizeAliasMap(options.aliases);
|
|
27
|
+
if (segments[0] && aliasMap[segments[0]]) {
|
|
28
|
+
segments[0] = aliasMap[segments[0]];
|
|
29
|
+
}
|
|
30
|
+
return segments.join("/");
|
|
31
|
+
}
|
|
32
|
+
export function normalizeDocsHref(href, options = {}) {
|
|
33
|
+
const { origin, suffix } = splitHref(href);
|
|
34
|
+
const slug = normalizeDocsSlug(href, options);
|
|
35
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
36
|
+
const path = slug ? `${basePath}/${slug}`.replace(/\/+/gu, "/") : basePath;
|
|
37
|
+
if (origin) {
|
|
38
|
+
return `${origin}${path}${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
return `${path}${suffix}`;
|
|
41
|
+
}
|
|
42
|
+
export function matchesDocsEntryFilter(entry, options = {}) {
|
|
43
|
+
if (!options.includeDrafts && entry.status?.trim().toLowerCase() === "draft") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (options.productSlug) {
|
|
47
|
+
const entryProduct = normalizeDocsSlug(entry.productSlug ?? "", options);
|
|
48
|
+
const expectedProduct = normalizeDocsSlug(options.productSlug, options);
|
|
49
|
+
if (!entryProduct || entryProduct !== expectedProduct) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (options.sectionKey) {
|
|
54
|
+
const entrySection = normalizeFragment(entry.sectionKey);
|
|
55
|
+
const expectedSection = normalizeFragment(options.sectionKey);
|
|
56
|
+
if (!entrySection || entrySection !== expectedSection) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
export function compareDocsEntriesByOrder(left, right) {
|
|
63
|
+
return (compareOptionalNumber(left.productOrder, right.productOrder) ||
|
|
64
|
+
compareOptionalText(left.productSlug, right.productSlug) ||
|
|
65
|
+
compareOptionalNumber(left.sectionOrder, right.sectionOrder) ||
|
|
66
|
+
compareOptionalText(left.sectionKey, right.sectionKey) ||
|
|
67
|
+
compareOptionalNumber(left.order, right.order) ||
|
|
68
|
+
compareOptionalNumber(left.navOrder, right.navOrder));
|
|
69
|
+
}
|
|
70
|
+
export function sortDocsEntries(entries, options = {}) {
|
|
71
|
+
const preserveSourceOrder = options.preserveSourceOrder !== false;
|
|
72
|
+
return entries
|
|
73
|
+
.map((entry, index) => ({ entry, index }))
|
|
74
|
+
.sort((left, right) => {
|
|
75
|
+
const byOrder = compareDocsEntriesByOrder(left.entry, right.entry);
|
|
76
|
+
if (byOrder !== 0) {
|
|
77
|
+
return byOrder;
|
|
78
|
+
}
|
|
79
|
+
if (preserveSourceOrder) {
|
|
80
|
+
return left.index - right.index;
|
|
81
|
+
}
|
|
82
|
+
return (compareOptionalText(left.entry.navLabel ?? left.entry.title, right.entry.navLabel ?? right.entry.title) ||
|
|
83
|
+
compareOptionalText(left.entry.slug, right.entry.slug) ||
|
|
84
|
+
left.index - right.index);
|
|
85
|
+
})
|
|
86
|
+
.map(({ entry }) => entry);
|
|
87
|
+
}
|
|
88
|
+
function splitHref(value) {
|
|
89
|
+
const [withoutHash, hash = ""] = value.split("#", 2);
|
|
90
|
+
const [withoutQuery, query = ""] = withoutHash.split("?", 2);
|
|
91
|
+
const suffix = `${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`;
|
|
92
|
+
if (ABSOLUTE_URL_PATTERN.test(withoutQuery)) {
|
|
93
|
+
const url = new URL(withoutQuery);
|
|
94
|
+
return {
|
|
95
|
+
origin: url.origin,
|
|
96
|
+
pathname: url.pathname,
|
|
97
|
+
suffix,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
origin: "",
|
|
102
|
+
pathname: withoutQuery,
|
|
103
|
+
suffix,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function normalizeBasePath(basePath = DEFAULT_DOCS_BASE_PATH) {
|
|
107
|
+
const trimmed = `/${trimSlashes(basePath)}`;
|
|
108
|
+
return trimmed === "/" ? DEFAULT_DOCS_BASE_PATH : trimmed;
|
|
109
|
+
}
|
|
110
|
+
function normalizeAliasMap(aliases) {
|
|
111
|
+
if (!aliases) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
return Object.fromEntries(Object.entries(aliases)
|
|
115
|
+
.map(([from, to]) => [slugifySegment(from), slugifySegment(to)])
|
|
116
|
+
.filter(([from, to]) => from && to));
|
|
117
|
+
}
|
|
118
|
+
function trimSlashes(value) {
|
|
119
|
+
return value.trim().replace(/^\/+|\/+$/gu, "");
|
|
120
|
+
}
|
|
121
|
+
function slugifySegment(value) {
|
|
122
|
+
return value
|
|
123
|
+
.trim()
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
126
|
+
.replace(/^-+|-+$/gu, "");
|
|
127
|
+
}
|
|
128
|
+
function normalizeFragment(value) {
|
|
129
|
+
return slugifySegment(value ?? "");
|
|
130
|
+
}
|
|
131
|
+
function compareOptionalNumber(left, right) {
|
|
132
|
+
return (left ?? Number.MAX_SAFE_INTEGER) - (right ?? Number.MAX_SAFE_INTEGER);
|
|
133
|
+
}
|
|
134
|
+
function compareOptionalText(left, right) {
|
|
135
|
+
return (left ?? "").localeCompare(right ?? "");
|
|
136
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DocsEntryRecord } from "../docsContract.js";
|
|
2
|
+
import type { DocsFetchOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Loads the normalized docs corpus from cedros-data and applies product/section filters.
|
|
5
|
+
*
|
|
6
|
+
* Returns docs in package ordering, excluding drafts unless `includeDrafts` is true.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fetchDocsIndex(options?: DocsFetchOptions): Promise<DocsEntryRecord[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a single docs article by canonical slug, current route, or any declared alias.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` when the requested doc does not exist after normalization.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fetchDocsEntry(slugOrPath: string, options?: DocsFetchOptions): Promise<DocsEntryRecord | null>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { fetchDocEntry as findDocEntry } from "../site-templates/docsNavigation.js";
|
|
2
|
+
import { matchesDocsEntryFilter, normalizeDocsHref, normalizeDocsSlug, sortDocsEntries, } from "../docsContract.js";
|
|
3
|
+
import { resolveApiKey, resolveServerUrl } from "./fetch.js";
|
|
4
|
+
import { fetchJson } from "./fetch.js";
|
|
5
|
+
const DEFAULT_DOCS_COLLECTION = "docs";
|
|
6
|
+
const DOCS_BATCH_SIZE = 1_000;
|
|
7
|
+
/**
|
|
8
|
+
* Loads the normalized docs corpus from cedros-data and applies product/section filters.
|
|
9
|
+
*
|
|
10
|
+
* Returns docs in package ordering, excluding drafts unless `includeDrafts` is true.
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchDocsIndex(options = {}) {
|
|
13
|
+
const serverUrl = resolveServerUrl(options);
|
|
14
|
+
const apiKey = resolveApiKey(options);
|
|
15
|
+
const collectionName = options.collectionName ?? DEFAULT_DOCS_COLLECTION;
|
|
16
|
+
const records = await fetchAllDocsRecords(serverUrl, collectionName, apiKey);
|
|
17
|
+
const docs = sortDocsEntries(records
|
|
18
|
+
.map((record) => normalizeDocsEntry(record, options))
|
|
19
|
+
.filter((entry) => matchesDocsEntryFilter(entry, options)));
|
|
20
|
+
const offset = Math.max(0, Math.floor(options.offset ?? 0));
|
|
21
|
+
const end = options.limit === undefined
|
|
22
|
+
? undefined
|
|
23
|
+
: offset + Math.max(0, Math.floor(options.limit));
|
|
24
|
+
return docs.slice(offset, end);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves a single docs article by canonical slug, current route, or any declared alias.
|
|
28
|
+
*
|
|
29
|
+
* Returns `null` when the requested doc does not exist after normalization.
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchDocsEntry(slugOrPath, options = {}) {
|
|
32
|
+
const docs = await fetchDocsIndex({
|
|
33
|
+
...options,
|
|
34
|
+
limit: undefined,
|
|
35
|
+
offset: undefined,
|
|
36
|
+
});
|
|
37
|
+
return findDocEntry(docs, slugOrPath, options) ?? null;
|
|
38
|
+
}
|
|
39
|
+
async function fetchAllDocsRecords(serverUrl, collectionName, apiKey) {
|
|
40
|
+
const records = [];
|
|
41
|
+
let offset = 0;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const batch = await fetchJson(serverUrl, "/entries/query", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: {
|
|
46
|
+
collection_name: collectionName,
|
|
47
|
+
limit: DOCS_BATCH_SIZE,
|
|
48
|
+
offset,
|
|
49
|
+
},
|
|
50
|
+
apiKey,
|
|
51
|
+
});
|
|
52
|
+
records.push(...batch);
|
|
53
|
+
if (batch.length < DOCS_BATCH_SIZE) {
|
|
54
|
+
return records;
|
|
55
|
+
}
|
|
56
|
+
offset += batch.length;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeDocsEntry(record, options) {
|
|
60
|
+
const payload = record.payload;
|
|
61
|
+
const bodyMarkdown = readStringAny(payload, [
|
|
62
|
+
"bodyMarkdown",
|
|
63
|
+
"body_markdown",
|
|
64
|
+
"markdown",
|
|
65
|
+
"body",
|
|
66
|
+
]);
|
|
67
|
+
const slug = normalizeDocsSlug(readStringAny(payload, ["slug", "routeSlug", "path", "route"]) ??
|
|
68
|
+
record.entry_key, options) || "index";
|
|
69
|
+
return {
|
|
70
|
+
slug,
|
|
71
|
+
title: readStringAny(payload, ["title", "navTitle"]) ?? humanizeDocsSlug(slug),
|
|
72
|
+
description: readStringAny(payload, ["description"]) ??
|
|
73
|
+
inferDescription(bodyMarkdown),
|
|
74
|
+
category: readStringAny(payload, ["category"]),
|
|
75
|
+
tags: readStringArray(payload, "tags"),
|
|
76
|
+
lastUpdated: readStringAny(payload, [
|
|
77
|
+
"lastUpdated",
|
|
78
|
+
"updatedAt",
|
|
79
|
+
"updated_at",
|
|
80
|
+
"publishedAt",
|
|
81
|
+
]) ?? record.updated_at,
|
|
82
|
+
bodyMarkdown,
|
|
83
|
+
bodyHtml: readStringAny(payload, ["bodyHtml", "body_html"]),
|
|
84
|
+
bodyText: readStringAny(payload, ["bodyText", "body_text"]),
|
|
85
|
+
searchText: readStringAny(payload, ["searchText", "search_text"]),
|
|
86
|
+
status: readStringAny(payload, ["status"]),
|
|
87
|
+
productSlug: normalizeDocsSlug(readStringAny(payload, ["productSlug", "productKey", "product"]) ?? "", options) || undefined,
|
|
88
|
+
productLabel: readStringAny(payload, ["productLabel", "productTitle"]),
|
|
89
|
+
productOrder: readNumberAny(payload, ["productOrder"]),
|
|
90
|
+
sectionKey: normalizeSectionKey(readStringAny(payload, ["sectionKey", "section", "sidebarSection"])),
|
|
91
|
+
sectionLabel: readStringAny(payload, [
|
|
92
|
+
"sectionLabel",
|
|
93
|
+
"sectionTitle",
|
|
94
|
+
"section",
|
|
95
|
+
"sidebarSection",
|
|
96
|
+
"category",
|
|
97
|
+
]),
|
|
98
|
+
sectionOrder: readNumberAny(payload, ["sectionOrder"]),
|
|
99
|
+
order: readNumberAny(payload, ["order", "navOrder"]),
|
|
100
|
+
readingMinutes: readNumberAny(payload, ["readingMinutes"]),
|
|
101
|
+
toc: readToc(payload),
|
|
102
|
+
breadcrumbs: readBreadcrumbs(payload, options),
|
|
103
|
+
editHref: readStringAny(payload, ["editHref", "editUrl", "sourceUrl"]),
|
|
104
|
+
sourcePath: readStringAny(payload, ["sourcePath"]),
|
|
105
|
+
sourceHref: readStringAny(payload, ["sourceHref", "sourceUrl"]),
|
|
106
|
+
navLabel: readStringAny(payload, ["navLabel", "navTitle"]),
|
|
107
|
+
navOrder: readNumberAny(payload, ["navOrder", "order"]),
|
|
108
|
+
aliases: readStringArray(payload, "aliases")
|
|
109
|
+
.map((value) => normalizeDocsSlug(value, options))
|
|
110
|
+
.filter(Boolean),
|
|
111
|
+
badge: readStringAny(payload, ["badge"]),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function readBreadcrumbs(payload, options) {
|
|
115
|
+
const raw = payload.breadcrumbs;
|
|
116
|
+
if (!Array.isArray(raw)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const breadcrumbs = raw
|
|
120
|
+
.map((value) => asObject(value))
|
|
121
|
+
.filter((entry) => entry !== undefined)
|
|
122
|
+
.map((entry) => {
|
|
123
|
+
const label = readStringAny(entry, ["label", "title"]);
|
|
124
|
+
const href = readStringAny(entry, ["href", "url", "path"]);
|
|
125
|
+
if (!label || !href) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
label,
|
|
130
|
+
href: normalizeDocsHref(href, options),
|
|
131
|
+
};
|
|
132
|
+
})
|
|
133
|
+
.filter((entry) => entry !== undefined);
|
|
134
|
+
return breadcrumbs.length > 0 ? breadcrumbs : undefined;
|
|
135
|
+
}
|
|
136
|
+
function readToc(payload) {
|
|
137
|
+
const raw = payload.toc;
|
|
138
|
+
if (!Array.isArray(raw)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const headings = raw
|
|
142
|
+
.map((value) => asObject(value))
|
|
143
|
+
.filter((entry) => entry !== undefined)
|
|
144
|
+
.map((entry) => {
|
|
145
|
+
const id = readString(entry, "id");
|
|
146
|
+
const label = readString(entry, "label");
|
|
147
|
+
if (!id || !label) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const depth = readNumber(entry, "depth");
|
|
151
|
+
const heading = {
|
|
152
|
+
id,
|
|
153
|
+
label,
|
|
154
|
+
depth: depth === 2 || depth === 3 || depth === 4
|
|
155
|
+
? depth
|
|
156
|
+
: undefined,
|
|
157
|
+
};
|
|
158
|
+
return heading;
|
|
159
|
+
})
|
|
160
|
+
.filter((entry) => entry !== undefined);
|
|
161
|
+
return headings.length > 0 ? headings : undefined;
|
|
162
|
+
}
|
|
163
|
+
function inferDescription(markdown) {
|
|
164
|
+
const paragraph = markdown
|
|
165
|
+
?.split(/\n{2,}/u)
|
|
166
|
+
.map((segment) => segment.replace(/^#+\s+/gmu, "").trim())
|
|
167
|
+
.find((segment) => segment && !segment.startsWith("```"));
|
|
168
|
+
return paragraph || undefined;
|
|
169
|
+
}
|
|
170
|
+
function humanizeDocsSlug(slug) {
|
|
171
|
+
const leaf = slug.split("/").pop() ?? slug;
|
|
172
|
+
return leaf.replace(/[-_]+/gu, " ").replace(/\b\w/gu, (char) => char.toUpperCase());
|
|
173
|
+
}
|
|
174
|
+
function normalizeSectionKey(value) {
|
|
175
|
+
return value
|
|
176
|
+
?.trim()
|
|
177
|
+
.toLowerCase()
|
|
178
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
179
|
+
.replace(/^-+|-+$/gu, "") || undefined;
|
|
180
|
+
}
|
|
181
|
+
function asObject(value) {
|
|
182
|
+
return value !== null && typeof value === "object" ? value : undefined;
|
|
183
|
+
}
|
|
184
|
+
function readString(payload, key) {
|
|
185
|
+
const value = payload[key];
|
|
186
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
187
|
+
}
|
|
188
|
+
function readStringAny(payload, keys) {
|
|
189
|
+
return keys.map((key) => readString(payload, key)).find(Boolean);
|
|
190
|
+
}
|
|
191
|
+
function readNumber(payload, key) {
|
|
192
|
+
const value = payload[key];
|
|
193
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
194
|
+
}
|
|
195
|
+
function readNumberAny(payload, keys) {
|
|
196
|
+
return keys.map((key) => readNumber(payload, key)).find((value) => value !== undefined);
|
|
197
|
+
}
|
|
198
|
+
function readStringArray(payload, key) {
|
|
199
|
+
const value = payload[key];
|
|
200
|
+
if (typeof value === "string" && value.trim()) {
|
|
201
|
+
return [value.trim()];
|
|
202
|
+
}
|
|
203
|
+
if (!Array.isArray(value)) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
207
|
+
}
|
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/index.d.ts
CHANGED
|
@@ -9,4 +9,5 @@ export type { ThemeMode, CedrosDataThemeOverrides, CedrosDataThemeValue, } from
|
|
|
9
9
|
export { CmsContent, type CmsContentProps } from "./CmsContent.js";
|
|
10
10
|
export { getOrCreateVisitorId } from "./visitor.js";
|
|
11
11
|
export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
|
|
12
|
-
export
|
|
12
|
+
export { normalizeDocsHref, normalizeDocsSlug } from "../docsContract.js";
|
|
13
|
+
export type { CmsPageRecord, PageMetadataInput, SiteDataRecord, SanitizeOptions, ContentType, DocsBreadcrumb, DocsEntryRecord, DocsFetchOptions, DocsNormalizationOptions, DocsTocHeading, MeteredReadsInfo, } from "./types.js";
|
package/dist/react/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { CedrosDataProvider, useCedrosDataTheme, useCedrosDataThemeOptional, } f
|
|
|
8
8
|
export { CmsContent } from "./CmsContent.js";
|
|
9
9
|
export { getOrCreateVisitorId } from "./visitor.js";
|
|
10
10
|
export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
|
|
11
|
+
export { normalizeDocsHref, normalizeDocsSlug } from "../docsContract.js";
|
package/dist/react/server.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Do not import this module in client components.
|
|
9
9
|
*/
|
|
10
10
|
export { buildPageMetadata, generatePageMetadata, type PageMetadata, } from "./metadata.js";
|
|
11
|
+
export { fetchDocsEntry, fetchDocsIndex } from "./docs.js";
|
|
11
12
|
export { loadSitemapEntries } from "./sitemap.js";
|
|
12
13
|
export { fetchBlogPost } from "./entries.js";
|
|
13
14
|
export { listBlogSlugs, listContentSlugs, listLearnPathIds, } from "./slugs.js";
|
|
14
|
-
export type { BuildPageMetadataOptions, CmsPageRecord, MeteredReadsInfo, PageMetadataInput, SiteDataRecord, SitemapEntry, ServerFetchOptions, ContentType, } from "./types.js";
|
|
15
|
+
export type { BuildPageMetadataOptions, CmsPageRecord, MeteredReadsInfo, PageMetadataInput, SiteDataRecord, SitemapEntry, ServerFetchOptions, DocsBreadcrumb, DocsEntryRecord, DocsFetchOptions, DocsNormalizationOptions, DocsTocHeading, ContentType, } from "./types.js";
|
package/dist/react/server.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Do not import this module in client components.
|
|
9
9
|
*/
|
|
10
10
|
export { buildPageMetadata, generatePageMetadata, } from "./metadata.js";
|
|
11
|
+
export { fetchDocsEntry, fetchDocsIndex } from "./docs.js";
|
|
11
12
|
export { loadSitemapEntries } from "./sitemap.js";
|
|
12
13
|
export { fetchBlogPost } from "./entries.js";
|
|
13
14
|
export { listBlogSlugs, listContentSlugs, listLearnPathIds, } from "./slugs.js";
|
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
|
/**
|
package/dist/react/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DocsBreadcrumb, DocsEntryRecord, DocsNormalizationOptions, DocsTocHeading } from "../docsContract.js";
|
|
1
2
|
/** Content type identifiers used across cedros-data. */
|
|
2
3
|
export type ContentType = "page" | "blog" | "docs" | "learn" | "project" | "airdrop";
|
|
3
4
|
/**
|
|
@@ -97,3 +98,19 @@ export interface ServerFetchOptions {
|
|
|
97
98
|
/** Visitor ID for metered-reads tracking. Use `getOrCreateVisitorId()` on the client. */
|
|
98
99
|
visitorId?: string;
|
|
99
100
|
}
|
|
101
|
+
/** Options for loading docs entries from the cedros-data backend. */
|
|
102
|
+
export interface DocsFetchOptions extends ServerFetchOptions, DocsNormalizationOptions {
|
|
103
|
+
/** Override the backend collection. Defaults to "docs". */
|
|
104
|
+
collectionName?: string;
|
|
105
|
+
/** Restrict results to a single product slug. */
|
|
106
|
+
productSlug?: string;
|
|
107
|
+
/** Restrict results to a single section key. */
|
|
108
|
+
sectionKey?: string;
|
|
109
|
+
/** Include draft docs. Defaults to false. */
|
|
110
|
+
includeDrafts?: boolean;
|
|
111
|
+
/** Final limit applied after docs are normalized and filtered. */
|
|
112
|
+
limit?: number;
|
|
113
|
+
/** Final offset applied after docs are normalized and filtered. */
|
|
114
|
+
offset?: number;
|
|
115
|
+
}
|
|
116
|
+
export type { DocsBreadcrumb, DocsEntryRecord, DocsNormalizationOptions, DocsTocHeading, };
|