@cedros/data-react 0.1.7 → 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/dist/docsContract.d.ts +56 -0
- package/dist/docsContract.js +136 -0
- package/dist/react/docs.d.ts +14 -0
- package/dist/react/docs.js +207 -0
- 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/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.js +13 -14
- 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 -11
- package/dist/site-templates/contentIndex.js +10 -0
- 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 +23 -3
- package/dist/site-templates/docsNavigation.js +178 -75
- 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
|
@@ -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,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/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/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, };
|
|
@@ -6,10 +6,12 @@ import { MarkdownContent } from "./MarkdownContent.js";
|
|
|
6
6
|
import { BlogSearchInput, BookmarkButton, FilterDimensionChips } from "./blogControls.js";
|
|
7
7
|
import { TipWidget } from "./tipControls.js";
|
|
8
8
|
import { ContentPaywall } from "./paywallControls.js";
|
|
9
|
+
import { BlogIndexControls } from "./blogTemplateUi.js";
|
|
9
10
|
export function BlogIndexTemplate({ siteTitle, navigation, posts, title = "Blog", description = "", basePath = "/blog", query = "", category = "", tag = "", sort = "newest", page = 1, pageSize = 8, categories, tags, filterDimensions, activeFilters, onFilterChange, onSearch, bookmarkedSlugs, onBookmarkToggle }) {
|
|
10
11
|
const normalizedFilters = collectFilterValues(posts);
|
|
11
12
|
const resolvedCategories = categories ?? normalizedFilters.categories;
|
|
12
13
|
const resolvedTags = tags ?? normalizedFilters.tags;
|
|
14
|
+
const totalPosts = posts.length;
|
|
13
15
|
const result = prepareBlogIndex(posts, {
|
|
14
16
|
query,
|
|
15
17
|
category,
|
|
@@ -20,22 +22,24 @@ export function BlogIndexTemplate({ siteTitle, navigation, posts, title = "Blog"
|
|
|
20
22
|
dimensions: activeFilters
|
|
21
23
|
});
|
|
22
24
|
const useClientControls = !!(filterDimensions || onSearch);
|
|
23
|
-
return (
|
|
25
|
+
return (_jsx(SiteLayout, { siteTitle: siteTitle, navigation: navigation, children: _jsxs("section", { className: "cedros-site__blog-page", children: [_jsxs("section", { className: "cedros-site__card cedros-site__blog-hero", children: [_jsxs("div", { className: "cedros-site__blog-hero-copy", children: [_jsx("span", { className: "cedros-site__pill cedros-site__pill--strong", children: "Journal" }), _jsx("h1", { className: "cedros-site__title cedros-site__blog-hero-title", children: title }), description && _jsx("p", { className: "cedros-site__subtitle cedros-site__blog-hero-subtitle", children: description })] }), _jsxs("dl", { className: "cedros-site__hero-stats", children: [_jsxs("div", { className: "cedros-site__hero-stat", children: [_jsx("dt", { children: "Posts" }), _jsx("dd", { children: totalPosts })] }), _jsxs("div", { className: "cedros-site__hero-stat", children: [_jsx("dt", { children: "Categories" }), _jsx("dd", { children: resolvedCategories.length })] }), _jsxs("div", { className: "cedros-site__hero-stat", children: [_jsx("dt", { children: "Topics" }), _jsx("dd", { children: resolvedTags.length })] })] })] }), useClientControls ? (_jsxs("section", { className: "cedros-site__card cedros-site__blog-controls-panel", children: [_jsxs("div", { className: "cedros-site__controls-header", children: [_jsxs("div", { children: [_jsx("p", { className: "cedros-site__eyebrow", children: "Discover" }), _jsx("h2", { className: "cedros-site__section-title", children: "Search and explore the latest posts" })] }), _jsx("p", { className: "cedros-site__entry-meta", children: "Use search and topic chips to narrow down the editorial feed." })] }), onSearch && _jsx(BlogSearchInput, { defaultValue: query, onSearch: onSearch }), filterDimensions && onFilterChange && (_jsx(FilterDimensionChips, { dimensions: filterDimensions, active: activeFilters ?? {}, onChange: onFilterChange }))] })) : (_jsx(BlogIndexControls, { 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("p", { className: "cedros-site__eyebrow", children: "No matches" }), _jsx("h2", { className: "cedros-site__section-title", children: "No posts found" }), _jsx("p", { className: "cedros-site__subtitle", children: "Adjust filters or search terms to find matching articles." })] })), result.totalItems > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cedros-site__section-heading", children: [_jsxs("div", { children: [_jsx("p", { className: "cedros-site__eyebrow", children: "Latest posts" }), _jsx("h2", { className: "cedros-site__section-title", children: "News, analysis, and product updates" })] }), _jsxs("p", { className: "cedros-site__entry-meta", children: [result.totalItems, " matching posts"] })] }), _jsx("section", { className: "cedros-site__content-grid cedros-site__content-grid--blog", children: result.items.map((post) => {
|
|
26
|
+
const cardMetadata = [post.publishedAt, post.author, readingTime(post.readingMinutes)]
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join(" • ");
|
|
29
|
+
return (_jsxs("article", { className: "cedros-site__card cedros-site__entry-card cedros-site__blog-card", children: [_jsxs("div", { className: "cedros-site__blog-card-top", children: [_jsx("div", { className: "cedros-site__blog-card-taxonomy", children: post.category && (_jsx("a", { href: buildContentListHref(basePath, { q: query, category: post.category, tag, sort }), className: "cedros-site__pill", children: post.category })) }), bookmarkedSlugs && onBookmarkToggle && (_jsx(BookmarkButton, { slug: post.slug, isBookmarked: bookmarkedSlugs.has(post.slug), onToggle: onBookmarkToggle }))] }), _jsx("h2", { className: "cedros-site__entry-title cedros-site__entry-title--blog", children: _jsx("a", { href: `${basePath}/${post.slug}`, children: post.title }) }), post.excerpt && _jsx("p", { className: "cedros-site__subtitle", children: post.excerpt }), post.tags && post.tags.length > 0 && (_jsx("div", { className: "cedros-site__tag-list", children: post.tags.map((entryTag) => (_jsx("a", { href: buildContentListHref(basePath, { q: query, category, tag: entryTag, sort }), className: "cedros-site__pill", children: entryTag }, entryTag))) })), _jsxs("div", { className: "cedros-site__blog-card-footer", children: [cardMetadata && _jsx("p", { className: "cedros-site__entry-meta", children: cardMetadata }), _jsx("a", { href: `${basePath}/${post.slug}`, className: "cedros-site__button cedros-site__button--ghost", children: "Read post" })] })] }, post.slug));
|
|
30
|
+
}) }), _jsx(ContentPagination, { basePath: basePath, page: result.page, totalPages: result.totalPages, query: { q: query, category, tag, sort } })] }))] }) }));
|
|
24
31
|
}
|
|
25
32
|
export function BlogPostTemplate({ siteTitle, navigation, title, bodyMarkdown, bodyHtml, allowUnsafeHtmlFallback = false, publishedAt, updatedAt, author, category, tags = [], readingMinutes, canonicalUrl, breadcrumbs = [], relatedPosts = [], basePath = "/blog", isBookmarked, onBookmarkToggle, slug, tipping, paywall }) {
|
|
26
33
|
const metadata = [publishedAt, updatedAt ? `Updated ${updatedAt}` : "", author, readingTime(readingMinutes)].filter(Boolean);
|
|
27
34
|
const showBookmark = isBookmarked !== undefined && onBookmarkToggle && slug;
|
|
28
|
-
return (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
function BlogIndexControls({ basePath, query, category, tag, sort, categories, tags }) {
|
|
38
|
-
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 blog posts" })] }), _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: "newest", children: "Newest" }), _jsx("option", { value: "oldest", children: "Oldest" }), _jsx("option", { value: "title-asc", children: "Title A-Z" }), _jsx("option", { value: "title-desc", children: "Title Z-A" })] })] }), _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" })] })] }));
|
|
35
|
+
return (_jsx(SiteLayout, { siteTitle: siteTitle, navigation: navigation, children: _jsxs("section", { className: "cedros-site__blog-page cedros-site__blog-page--article", children: [_jsxs("article", { className: "cedros-site__card cedros-site__article cedros-site__blog-article", children: [_jsxs("div", { className: "cedros-site__article-top", children: [_jsx(Breadcrumbs, { trail: breadcrumbs }), _jsxs("div", { className: "cedros-site__blog-article-actions", children: [canonicalUrl && (_jsx("a", { href: canonicalUrl, className: "cedros-site__button cedros-site__button--ghost", children: "Canonical URL" })), showBookmark && (_jsx(BookmarkButton, { slug: slug, isBookmarked: isBookmarked, onToggle: onBookmarkToggle }))] })] }), _jsxs("div", { className: "cedros-site__article-header", children: [_jsx("span", { className: "cedros-site__pill cedros-site__pill--strong", children: "Blog" }), _jsx("h1", { className: "cedros-site__title", children: title }), metadata.length > 0 && (_jsx("div", { className: "cedros-site__article-meta-list", children: metadata.map((entry) => (_jsx("span", { className: "cedros-site__meta-chip", children: entry }, entry))) })), (category || tags.length > 0) && (_jsxs("div", { className: "cedros-site__blog-taxonomy", children: [category && (_jsx("a", { href: buildContentListHref(basePath, { category }), className: "cedros-site__pill", children: category })), tags.map((tag) => (_jsx("a", { href: buildContentListHref(basePath, { tag }), className: "cedros-site__pill", children: tag }, tag)))] }))] }), _jsx("div", { className: "cedros-site__article-body", children: renderPaywallOrContent({
|
|
36
|
+
paywall,
|
|
37
|
+
bodyMarkdown,
|
|
38
|
+
bodyHtml,
|
|
39
|
+
allowUnsafeHtmlFallback
|
|
40
|
+
}) })] }), tipping?.enabled && (_jsxs("section", { className: "cedros-site__card cedros-site__blog-support-card", children: [_jsxs("div", { className: "cedros-site__blog-support-header", children: [_jsx("p", { className: "cedros-site__eyebrow", children: "Support" }), _jsx("h2", { className: "cedros-site__section-title", children: "Tip the author or support the publication" })] }), _jsx(TipWidget, { dataServerUrl: tipping.dataServerUrl, recipient: tipping.allowPerPostRecipient && tipping.recipientOverride
|
|
41
|
+
? tipping.recipientOverride
|
|
42
|
+
: tipping.recipient, currencies: tipping.currencies, presets: tipping.presets, label: tipping.label, description: tipping.description, senderAddress: tipping.senderAddress, signTransaction: tipping.signTransaction })] })), relatedPosts.length > 0 && (_jsxs("section", { className: "cedros-site__card cedros-site__blog-related", children: [_jsxs("div", { className: "cedros-site__blog-related-header", children: [_jsx("p", { className: "cedros-site__eyebrow", children: "Continue reading" }), _jsx("h2", { className: "cedros-site__section-title", children: "Related posts" })] }), _jsx("div", { className: "cedros-site__blog-related-grid", children: relatedPosts.map((post) => (_jsxs("article", { className: "cedros-site__card cedros-site__blog-related-card", children: [_jsx("h3", { className: "cedros-site__entry-title cedros-site__entry-title--blog", children: _jsx("a", { href: `${basePath}/${post.slug}`, children: post.title }) }), post.excerpt && _jsx("p", { className: "cedros-site__subtitle", children: post.excerpt }), _jsx("div", { className: "cedros-site__blog-card-footer", children: _jsx("a", { href: `${basePath}/${post.slug}`, className: "cedros-site__button cedros-site__button--ghost", children: "Read post" }) })] }, post.slug))) })] }))] }) }));
|
|
39
43
|
}
|
|
40
44
|
function readingTime(minutes) {
|
|
41
45
|
if (!minutes) {
|
|
@@ -11,4 +11,4 @@ export interface DocsSidebarProps {
|
|
|
11
11
|
* Desktop: always visible, sticky.
|
|
12
12
|
* Mobile: hidden behind a hamburger button; overlay closes on link click.
|
|
13
13
|
*/
|
|
14
|
-
export declare function DocsSidebar({ title, basePath, searchQuery, sections
|
|
14
|
+
export declare function DocsSidebar({ title, basePath, searchQuery, sections }: DocsSidebarProps): React.JSX.Element;
|
|
@@ -7,7 +7,7 @@ import { useState } from "react";
|
|
|
7
7
|
* Desktop: always visible, sticky.
|
|
8
8
|
* Mobile: hidden behind a hamburger button; overlay closes on link click.
|
|
9
9
|
*/
|
|
10
|
-
export function DocsSidebar({ title, basePath, searchQuery, sections
|
|
10
|
+
export function DocsSidebar({ title, basePath, searchQuery, sections }) {
|
|
11
11
|
const [open, setOpen] = useState(false);
|
|
12
|
-
return (_jsxs(_Fragment, { children: [
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "cedros-site__sidebar-toggle", "aria-label": "Toggle sidebar", "aria-expanded": open, onClick: () => setOpen((prev) => !prev), children: [_jsxs("span", { className: "cedros-site__sidebar-toggle-copy", children: [_jsx("span", { className: "cedros-site__sidebar-toggle-label", children: title }), _jsxs("span", { className: "cedros-site__sidebar-toggle-meta", children: [sections.length, " sections"] })] }), _jsx("span", { className: "cedros-site__sidebar-toggle-state", children: open ? "Close" : "Open" })] }), open && (_jsx("div", { className: "cedros-site__sidebar-overlay", onClick: () => setOpen(false), role: "presentation" })), _jsxs("aside", { className: `cedros-site__card cedros-site__docs-sidebar${open ? " cedros-site__docs-sidebar--open" : ""}`, children: [_jsxs("div", { className: "cedros-site__docs-sidebar-top", children: [_jsxs("div", { children: [_jsx("p", { className: "cedros-site__eyebrow", children: "Navigation" }), _jsx("h2", { className: "cedros-site__docs-sidebar-title", children: title })] }), _jsx("button", { type: "button", className: "cedros-site__docs-sidebar-close", "aria-label": "Close sidebar", onClick: () => setOpen(false), children: "Close" })] }), _jsx("p", { className: "cedros-site__docs-sidebar-meta", children: "Browse sections, search directly, or return to the full index." }), _jsx("form", { method: "get", action: basePath, className: "cedros-site__docs-search", children: _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Search docs" }), _jsx("input", { type: "search", name: "q", defaultValue: searchQuery, placeholder: "Search docs..." })] }) }), _jsx("a", { href: basePath, className: "cedros-site__docs-root-link", children: "Browse all documentation" }), sections.length === 0 && (_jsx("p", { className: "cedros-site__entry-meta", children: "No sections available yet." })), sections.map((section) => (_jsxs("details", { className: "cedros-site__docs-section", open: section.collapsed !== true, children: [_jsx("summary", { className: "cedros-site__docs-section-title", children: section.label }), _jsx("nav", { className: "cedros-site__docs-section-links", "aria-label": section.label, children: section.items.map((item) => (_jsxs("a", { href: item.href, className: `cedros-site__docs-item cedros-site__docs-item--depth-${item.depth ?? 0}${item.isActive ? " cedros-site__docs-item--active" : ""}`, "aria-current": item.isActive ? "page" : undefined, onClick: () => setOpen(false), children: [_jsx("span", { children: item.label }), item.badge && (_jsx("span", { className: "cedros-site__docs-item-badge", children: item.badge }))] }, item.key))) })] }, section.key)))] })] }));
|
|
13
13
|
}
|