@cedros/data-react 0.1.0
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/CHANGELOG.md +24 -0
- package/README.md +177 -0
- package/dist/admin/api.d.ts +19 -0
- package/dist/admin/api.js +108 -0
- package/dist/admin/components.d.ts +36 -0
- package/dist/admin/components.js +22 -0
- package/dist/admin/history.d.ts +17 -0
- package/dist/admin/history.js +103 -0
- package/dist/admin/icons.d.ts +15 -0
- package/dist/admin/icons.js +18 -0
- package/dist/admin/index.d.ts +13 -0
- package/dist/admin/index.js +12 -0
- package/dist/admin/permissions.d.ts +4 -0
- package/dist/admin/permissions.js +45 -0
- package/dist/admin/plugin.d.ts +4 -0
- package/dist/admin/plugin.js +180 -0
- package/dist/admin/primitives/ConfirmDialog.d.ts +14 -0
- package/dist/admin/primitives/ConfirmDialog.js +7 -0
- package/dist/admin/primitives/DataTable.d.ts +14 -0
- package/dist/admin/primitives/DataTable.js +7 -0
- package/dist/admin/primitives/DiffViewer.d.ts +11 -0
- package/dist/admin/primitives/DiffViewer.js +8 -0
- package/dist/admin/primitives/FormFieldRow.d.ts +23 -0
- package/dist/admin/primitives/FormFieldRow.js +16 -0
- package/dist/admin/primitives/JsonCodeEditor.d.ts +10 -0
- package/dist/admin/primitives/JsonCodeEditor.js +42 -0
- package/dist/admin/primitives/Pagination.d.ts +8 -0
- package/dist/admin/primitives/Pagination.js +8 -0
- package/dist/admin/primitives/Toolbar.d.ts +23 -0
- package/dist/admin/primitives/Toolbar.js +10 -0
- package/dist/admin/primitives/alerts.d.ts +21 -0
- package/dist/admin/primitives/alerts.js +44 -0
- package/dist/admin/sectionIds.d.ts +20 -0
- package/dist/admin/sectionIds.js +33 -0
- package/dist/admin/sections/CollectionsSection.d.ts +2 -0
- package/dist/admin/sections/CollectionsSection.js +125 -0
- package/dist/admin/sections/ContractVerifySection.d.ts +11 -0
- package/dist/admin/sections/ContractVerifySection.js +98 -0
- package/dist/admin/sections/CustomDataSection.d.ts +2 -0
- package/dist/admin/sections/CustomDataSection.js +256 -0
- package/dist/admin/sections/DataOpsSection.d.ts +26 -0
- package/dist/admin/sections/DataOpsSection.js +245 -0
- package/dist/admin/sections/HistorySection.d.ts +2 -0
- package/dist/admin/sections/HistorySection.js +26 -0
- package/dist/admin/sections/MonetizationSection.d.ts +2 -0
- package/dist/admin/sections/MonetizationSection.js +140 -0
- package/dist/admin/sections/NavigationSection.d.ts +13 -0
- package/dist/admin/sections/NavigationSection.js +195 -0
- package/dist/admin/sections/PagesSection.d.ts +2 -0
- package/dist/admin/sections/PagesSection.js +157 -0
- package/dist/admin/sections/SchemaDesignerSection.d.ts +2 -0
- package/dist/admin/sections/SchemaDesignerSection.js +167 -0
- package/dist/admin/sections/SiteSettingsSection.d.ts +12 -0
- package/dist/admin/sections/SiteSettingsSection.js +122 -0
- package/dist/admin/sections/TippingSection.d.ts +2 -0
- package/dist/admin/sections/TippingSection.js +178 -0
- package/dist/admin/sections/media/MediaDetail.d.ts +12 -0
- package/dist/admin/sections/media/MediaDetail.js +74 -0
- package/dist/admin/sections/media/MediaGrid.d.ts +14 -0
- package/dist/admin/sections/media/MediaGrid.js +22 -0
- package/dist/admin/sections/media/MediaSection.d.ts +2 -0
- package/dist/admin/sections/media/MediaSection.js +97 -0
- package/dist/admin/sections/media/MediaUploader.d.ts +7 -0
- package/dist/admin/sections/media/MediaUploader.js +72 -0
- package/dist/admin/sections/media/types.d.ts +33 -0
- package/dist/admin/sections/media/types.js +1 -0
- package/dist/admin/styles.css +533 -0
- package/dist/admin/types.d.ts +85 -0
- package/dist/admin/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/react/CmsContent.d.ts +20 -0
- package/dist/react/CmsContent.js +31 -0
- package/dist/react/entries.d.ts +9 -0
- package/dist/react/entries.js +25 -0
- package/dist/react/fetch.d.ts +11 -0
- package/dist/react/fetch.js +32 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +9 -0
- package/dist/react/metadata.d.ts +44 -0
- package/dist/react/metadata.js +142 -0
- package/dist/react/sanitize.d.ts +17 -0
- package/dist/react/sanitize.js +326 -0
- package/dist/react/server.d.ts +14 -0
- package/dist/react/server.js +13 -0
- package/dist/react/sitemap.d.ts +28 -0
- package/dist/react/sitemap.js +91 -0
- package/dist/react/slugs.d.ts +27 -0
- package/dist/react/slugs.js +52 -0
- package/dist/react/types.d.ts +85 -0
- package/dist/react/types.js +1 -0
- package/dist/react/visitor.d.ts +7 -0
- package/dist/react/visitor.js +18 -0
- package/dist/site-templates/BlogTemplates.d.ts +95 -0
- package/dist/site-templates/BlogTemplates.js +64 -0
- package/dist/site-templates/ContactPageTemplate.d.ts +14 -0
- package/dist/site-templates/ContactPageTemplate.js +5 -0
- package/dist/site-templates/DashboardOverviewTemplate.d.ts +29 -0
- package/dist/site-templates/DashboardOverviewTemplate.js +17 -0
- package/dist/site-templates/DashboardShell.d.ts +28 -0
- package/dist/site-templates/DashboardShell.js +10 -0
- package/dist/site-templates/DocsSidebar.d.ts +14 -0
- package/dist/site-templates/DocsSidebar.js +13 -0
- package/dist/site-templates/DocsTemplates.d.ts +60 -0
- package/dist/site-templates/DocsTemplates.js +47 -0
- package/dist/site-templates/HomePageTemplate.d.ts +15 -0
- package/dist/site-templates/HomePageTemplate.js +10 -0
- package/dist/site-templates/LegalPageTemplate.d.ts +12 -0
- package/dist/site-templates/LegalPageTemplate.js +6 -0
- package/dist/site-templates/MarkdownContent.d.ts +7 -0
- package/dist/site-templates/MarkdownContent.js +24 -0
- package/dist/site-templates/NotFoundTemplate.d.ts +9 -0
- package/dist/site-templates/NotFoundTemplate.js +5 -0
- package/dist/site-templates/SiteFooter.d.ts +13 -0
- package/dist/site-templates/SiteFooter.js +4 -0
- package/dist/site-templates/SiteLayout.d.ts +14 -0
- package/dist/site-templates/SiteLayout.js +6 -0
- package/dist/site-templates/TopNav.d.ts +10 -0
- package/dist/site-templates/TopNav.js +8 -0
- package/dist/site-templates/blogControls.d.ts +19 -0
- package/dist/site-templates/blogControls.js +37 -0
- package/dist/site-templates/codeBlock.d.ts +9 -0
- package/dist/site-templates/codeBlock.js +31 -0
- package/dist/site-templates/content-styles.css +410 -0
- package/dist/site-templates/contentIndex.d.ts +65 -0
- package/dist/site-templates/contentIndex.js +181 -0
- package/dist/site-templates/contentUi.d.ts +14 -0
- package/dist/site-templates/contentUi.js +24 -0
- package/dist/site-templates/docs-styles.css +259 -0
- package/dist/site-templates/docsNavigation.d.ts +18 -0
- package/dist/site-templates/docsNavigation.js +50 -0
- package/dist/site-templates/index.d.ts +28 -0
- package/dist/site-templates/index.js +25 -0
- package/dist/site-templates/monetization-styles.css +154 -0
- package/dist/site-templates/paywallControls.d.ts +22 -0
- package/dist/site-templates/paywallControls.js +9 -0
- package/dist/site-templates/routing.d.ts +12 -0
- package/dist/site-templates/routing.js +36 -0
- package/dist/site-templates/solanaAtaSetup.d.ts +11 -0
- package/dist/site-templates/solanaAtaSetup.js +38 -0
- package/dist/site-templates/solanaMicropayments.d.ts +65 -0
- package/dist/site-templates/solanaMicropayments.js +115 -0
- package/dist/site-templates/styles.css +332 -0
- package/dist/site-templates/tipControls.d.ts +24 -0
- package/dist/site-templates/tipControls.js +43 -0
- package/dist/site-templates/tocExtractor.d.ts +16 -0
- package/dist/site-templates/tocExtractor.js +58 -0
- package/dist/site-templates/tocScrollSpy.d.ts +16 -0
- package/dist/site-templates/tocScrollSpy.js +37 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +20 -0
- package/package.json +58 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServerFetchOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the cedros-data server URL from options or environment.
|
|
4
|
+
* Throws if no URL is available.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveServerUrl(options?: ServerFetchOptions): string;
|
|
7
|
+
/** Internal fetch wrapper for server-side calls to cedros-data. */
|
|
8
|
+
export declare function fetchJson<T>(serverUrl: string, path: string, init?: {
|
|
9
|
+
method?: string;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the cedros-data server URL from options or environment.
|
|
3
|
+
* Throws if no URL is available.
|
|
4
|
+
*/
|
|
5
|
+
export function resolveServerUrl(options) {
|
|
6
|
+
const url = options?.serverUrl ??
|
|
7
|
+
(typeof process !== "undefined"
|
|
8
|
+
? process.env.CEDROS_DATA_URL ?? process.env.NEXT_PUBLIC_BACKEND_API_URL
|
|
9
|
+
: undefined);
|
|
10
|
+
if (!url) {
|
|
11
|
+
throw new Error("@cedros/data-react: no serverUrl provided and neither CEDROS_DATA_URL nor " +
|
|
12
|
+
"NEXT_PUBLIC_BACKEND_API_URL environment variable is set");
|
|
13
|
+
}
|
|
14
|
+
return url.replace(/\/+$/, "");
|
|
15
|
+
}
|
|
16
|
+
/** Internal fetch wrapper for server-side calls to cedros-data. */
|
|
17
|
+
export async function fetchJson(serverUrl, path, init) {
|
|
18
|
+
const headers = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
};
|
|
21
|
+
const response = await fetch(`${serverUrl}${path}`, {
|
|
22
|
+
method: init?.method ?? "GET",
|
|
23
|
+
headers,
|
|
24
|
+
body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
throw new Error(`@cedros/data-react: ${init?.method ?? "GET"} ${path} failed ` +
|
|
29
|
+
`(${response.status}): ${text}`);
|
|
30
|
+
}
|
|
31
|
+
return (await response.json());
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cedros/data-react — client-safe exports.
|
|
3
|
+
*
|
|
4
|
+
* These exports are safe to use in React Client Components and
|
|
5
|
+
* do not import any server-only modules.
|
|
6
|
+
*/
|
|
7
|
+
export { CmsContent, type CmsContentProps } from "./CmsContent.js";
|
|
8
|
+
export { getOrCreateVisitorId } from "./visitor.js";
|
|
9
|
+
export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
|
|
10
|
+
export type { CmsPageRecord, SiteDataRecord, SanitizeOptions, ContentType, } from "./types.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cedros/data-react — client-safe exports.
|
|
3
|
+
*
|
|
4
|
+
* These exports are safe to use in React Client Components and
|
|
5
|
+
* do not import any server-only modules.
|
|
6
|
+
*/
|
|
7
|
+
export { CmsContent } from "./CmsContent.js";
|
|
8
|
+
export { getOrCreateVisitorId } from "./visitor.js";
|
|
9
|
+
export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BuildPageMetadataOptions, CmsPageRecord, ServerFetchOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Metadata object compatible with Next.js `generateMetadata` return type.
|
|
4
|
+
* Kept as a plain object so this module doesn't import `next` as a dependency.
|
|
5
|
+
*/
|
|
6
|
+
export interface PageMetadata {
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
openGraph?: {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
images?: Array<{
|
|
13
|
+
url: string;
|
|
14
|
+
}>;
|
|
15
|
+
url?: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
};
|
|
18
|
+
twitter?: {
|
|
19
|
+
card?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
};
|
|
23
|
+
alternates?: {
|
|
24
|
+
canonical?: string;
|
|
25
|
+
};
|
|
26
|
+
other?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds a Next.js-compatible Metadata object from a CMS page record.
|
|
30
|
+
*
|
|
31
|
+
* @param page - CMS page record from the entries API
|
|
32
|
+
* @param options - Optional site data, origin, path, and mode
|
|
33
|
+
* @returns Metadata object suitable for Next.js `generateMetadata`
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildPageMetadata(page: CmsPageRecord, options?: BuildPageMetadataOptions): PageMetadata;
|
|
36
|
+
/**
|
|
37
|
+
* Convenience wrapper that fetches a page by slug, then builds metadata.
|
|
38
|
+
*
|
|
39
|
+
* Uses the cedros-data entries API to query the "pages" collection by
|
|
40
|
+
* entry_key (slug), then delegates to `buildPageMetadata`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function generatePageMetadata(slug: string, options?: BuildPageMetadataOptions & ServerFetchOptions & {
|
|
43
|
+
collection?: string;
|
|
44
|
+
}): Promise<PageMetadata>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { fetchJson, resolveServerUrl } from "./fetch.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a Next.js-compatible Metadata object from a CMS page record.
|
|
4
|
+
*
|
|
5
|
+
* @param page - CMS page record from the entries API
|
|
6
|
+
* @param options - Optional site data, origin, path, and mode
|
|
7
|
+
* @returns Metadata object suitable for Next.js `generateMetadata`
|
|
8
|
+
*/
|
|
9
|
+
export function buildPageMetadata(page, options) {
|
|
10
|
+
const p = page.payload;
|
|
11
|
+
const site = options?.siteData;
|
|
12
|
+
const origin = options?.origin?.replace(/\/+$/, "") ?? "";
|
|
13
|
+
const path = options?.path ?? p.route ?? "";
|
|
14
|
+
const mode = options?.mode ?? "page";
|
|
15
|
+
const siteName = site?.siteName ?? site?.siteTitle ?? "";
|
|
16
|
+
const title = p.title ?? siteName ?? undefined;
|
|
17
|
+
const description = p.description ?? site?.defaultDescription ?? undefined;
|
|
18
|
+
const ogTitle = p.ogTitle ?? p.title ?? undefined;
|
|
19
|
+
const ogDescription = p.ogDescription ?? p.description ?? undefined;
|
|
20
|
+
const ogImages = buildOgImages(p.ogImage, site?.defaultOgImage);
|
|
21
|
+
const pageUrl = origin ? `${origin}${path}` : undefined;
|
|
22
|
+
const ogType = mode === "blog" ? "article" : "website";
|
|
23
|
+
const twitterTitle = p.twitterTitle ?? p.ogTitle ?? p.title ?? undefined;
|
|
24
|
+
const twitterDescription = p.twitterDescription ?? p.ogDescription ?? p.description ?? undefined;
|
|
25
|
+
const canonical = resolveCanonical(origin, path, mode, p.slug);
|
|
26
|
+
const metadata = {
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
openGraph: {
|
|
30
|
+
title: ogTitle,
|
|
31
|
+
description: ogDescription,
|
|
32
|
+
images: ogImages,
|
|
33
|
+
url: pageUrl,
|
|
34
|
+
type: ogType,
|
|
35
|
+
},
|
|
36
|
+
twitter: {
|
|
37
|
+
card: "summary_large_image",
|
|
38
|
+
title: twitterTitle,
|
|
39
|
+
description: twitterDescription,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
if (canonical) {
|
|
43
|
+
metadata.alternates = { canonical };
|
|
44
|
+
}
|
|
45
|
+
// For blog posts, add JSON-LD structured data as a serialized string
|
|
46
|
+
if (mode === "blog") {
|
|
47
|
+
metadata.other = {
|
|
48
|
+
"script:ld+json": buildBlogJsonLd(page, origin, path),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return metadata;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convenience wrapper that fetches a page by slug, then builds metadata.
|
|
55
|
+
*
|
|
56
|
+
* Uses the cedros-data entries API to query the "pages" collection by
|
|
57
|
+
* entry_key (slug), then delegates to `buildPageMetadata`.
|
|
58
|
+
*/
|
|
59
|
+
export async function generatePageMetadata(slug, options) {
|
|
60
|
+
const serverUrl = resolveServerUrl(options);
|
|
61
|
+
const collection = options?.collection ?? "pages";
|
|
62
|
+
const entries = await fetchJson(serverUrl, "/entries/query", {
|
|
63
|
+
method: "POST",
|
|
64
|
+
body: {
|
|
65
|
+
collection_name: collection,
|
|
66
|
+
entry_keys: [slug],
|
|
67
|
+
limit: 1,
|
|
68
|
+
offset: 0,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (entries.length === 0) {
|
|
72
|
+
return { title: slug };
|
|
73
|
+
}
|
|
74
|
+
let siteData = options?.siteData;
|
|
75
|
+
if (!siteData) {
|
|
76
|
+
siteData = await fetchSiteData(serverUrl);
|
|
77
|
+
}
|
|
78
|
+
return buildPageMetadata(entries[0], {
|
|
79
|
+
...options,
|
|
80
|
+
siteData,
|
|
81
|
+
path: options?.path ?? `/${slug}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function fetchSiteData(serverUrl) {
|
|
85
|
+
try {
|
|
86
|
+
const entries = await fetchJson(serverUrl, "/entries/query", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: {
|
|
89
|
+
collection_name: "site_settings",
|
|
90
|
+
entry_keys: ["global"],
|
|
91
|
+
limit: 1,
|
|
92
|
+
offset: 0,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (entries.length > 0) {
|
|
96
|
+
return entries[0].payload;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Site settings may not exist; fall through
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function buildOgImages(pageImage, defaultImage) {
|
|
105
|
+
if (pageImage)
|
|
106
|
+
return [{ url: pageImage }];
|
|
107
|
+
if (defaultImage)
|
|
108
|
+
return [{ url: defaultImage }];
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
function resolveCanonical(origin, path, mode, slug) {
|
|
112
|
+
if (!origin)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (mode === "docs" && slug) {
|
|
115
|
+
return `${origin}/blog/${slug}`;
|
|
116
|
+
}
|
|
117
|
+
return `${origin}${path}`;
|
|
118
|
+
}
|
|
119
|
+
function buildBlogJsonLd(page, origin, path) {
|
|
120
|
+
const p = page.payload;
|
|
121
|
+
const jsonLd = {
|
|
122
|
+
"@context": "https://schema.org",
|
|
123
|
+
"@type": "BlogPosting",
|
|
124
|
+
headline: p.title,
|
|
125
|
+
description: p.description,
|
|
126
|
+
datePublished: p.publishedAt,
|
|
127
|
+
dateModified: p.updatedAt ?? page.updated_at,
|
|
128
|
+
};
|
|
129
|
+
if (origin) {
|
|
130
|
+
jsonLd.url = `${origin}${path}`;
|
|
131
|
+
}
|
|
132
|
+
if (p.author) {
|
|
133
|
+
jsonLd.author = { "@type": "Person", name: p.author };
|
|
134
|
+
}
|
|
135
|
+
if (p.ogImage) {
|
|
136
|
+
jsonLd.image = p.ogImage;
|
|
137
|
+
}
|
|
138
|
+
if (p.wordCount) {
|
|
139
|
+
jsonLd.wordCount = p.wordCount;
|
|
140
|
+
}
|
|
141
|
+
return JSON.stringify(jsonLd);
|
|
142
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SanitizeOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sanitizes CMS HTML by stripping dangerous tags, attributes, and protocols.
|
|
4
|
+
*
|
|
5
|
+
* Uses a regex-based approach that processes tags iteratively. This is not a
|
|
6
|
+
* full HTML parser, but is sufficient for CMS content where the input is
|
|
7
|
+
* author-controlled (not arbitrary user input from the public internet).
|
|
8
|
+
*/
|
|
9
|
+
export declare function sanitizeCmsHtml(html: string, options?: SanitizeOptions): string;
|
|
10
|
+
/**
|
|
11
|
+
* Converts a basic markdown string to HTML.
|
|
12
|
+
*
|
|
13
|
+
* Handles headings, bold, italic, links, images, code blocks, inline code,
|
|
14
|
+
* unordered/ordered lists, blockquotes, horizontal rules, and paragraphs.
|
|
15
|
+
* For rich markdown processing, use a full library like react-markdown.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderCmsMarkdown(markdown: string): string;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/** Safe HTML tags for CMS prose content. */
|
|
2
|
+
const DEFAULT_ALLOWED_TAGS = new Set([
|
|
3
|
+
"a",
|
|
4
|
+
"abbr",
|
|
5
|
+
"address",
|
|
6
|
+
"article",
|
|
7
|
+
"aside",
|
|
8
|
+
"b",
|
|
9
|
+
"bdi",
|
|
10
|
+
"bdo",
|
|
11
|
+
"blockquote",
|
|
12
|
+
"br",
|
|
13
|
+
"caption",
|
|
14
|
+
"cite",
|
|
15
|
+
"code",
|
|
16
|
+
"col",
|
|
17
|
+
"colgroup",
|
|
18
|
+
"dd",
|
|
19
|
+
"del",
|
|
20
|
+
"details",
|
|
21
|
+
"dfn",
|
|
22
|
+
"div",
|
|
23
|
+
"dl",
|
|
24
|
+
"dt",
|
|
25
|
+
"em",
|
|
26
|
+
"figcaption",
|
|
27
|
+
"figure",
|
|
28
|
+
"footer",
|
|
29
|
+
"h1",
|
|
30
|
+
"h2",
|
|
31
|
+
"h3",
|
|
32
|
+
"h4",
|
|
33
|
+
"h5",
|
|
34
|
+
"h6",
|
|
35
|
+
"header",
|
|
36
|
+
"hr",
|
|
37
|
+
"i",
|
|
38
|
+
"img",
|
|
39
|
+
"ins",
|
|
40
|
+
"kbd",
|
|
41
|
+
"li",
|
|
42
|
+
"main",
|
|
43
|
+
"mark",
|
|
44
|
+
"nav",
|
|
45
|
+
"ol",
|
|
46
|
+
"p",
|
|
47
|
+
"picture",
|
|
48
|
+
"pre",
|
|
49
|
+
"q",
|
|
50
|
+
"rp",
|
|
51
|
+
"rt",
|
|
52
|
+
"ruby",
|
|
53
|
+
"s",
|
|
54
|
+
"samp",
|
|
55
|
+
"section",
|
|
56
|
+
"small",
|
|
57
|
+
"source",
|
|
58
|
+
"span",
|
|
59
|
+
"strong",
|
|
60
|
+
"sub",
|
|
61
|
+
"summary",
|
|
62
|
+
"sup",
|
|
63
|
+
"table",
|
|
64
|
+
"tbody",
|
|
65
|
+
"td",
|
|
66
|
+
"tfoot",
|
|
67
|
+
"th",
|
|
68
|
+
"thead",
|
|
69
|
+
"time",
|
|
70
|
+
"tr",
|
|
71
|
+
"u",
|
|
72
|
+
"ul",
|
|
73
|
+
"var",
|
|
74
|
+
"wbr",
|
|
75
|
+
]);
|
|
76
|
+
const DEFAULT_ALLOWED_ATTRIBUTES = {
|
|
77
|
+
a: new Set(["href", "title", "target", "rel", "id"]),
|
|
78
|
+
img: new Set(["src", "alt", "title", "width", "height", "loading"]),
|
|
79
|
+
source: new Set(["src", "srcset", "type", "media"]),
|
|
80
|
+
td: new Set(["colspan", "rowspan"]),
|
|
81
|
+
th: new Set(["colspan", "rowspan", "scope"]),
|
|
82
|
+
ol: new Set(["start", "type"]),
|
|
83
|
+
time: new Set(["datetime"]),
|
|
84
|
+
blockquote: new Set(["cite"]),
|
|
85
|
+
q: new Set(["cite"]),
|
|
86
|
+
col: new Set(["span"]),
|
|
87
|
+
colgroup: new Set(["span"]),
|
|
88
|
+
details: new Set(["open"]),
|
|
89
|
+
"*": new Set(["class", "id", "lang", "dir"]),
|
|
90
|
+
};
|
|
91
|
+
const DEFAULT_ALLOWED_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
92
|
+
const EVENT_HANDLER_PATTERN = /^on/i;
|
|
93
|
+
/**
|
|
94
|
+
* Sanitizes CMS HTML by stripping dangerous tags, attributes, and protocols.
|
|
95
|
+
*
|
|
96
|
+
* Uses a regex-based approach that processes tags iteratively. This is not a
|
|
97
|
+
* full HTML parser, but is sufficient for CMS content where the input is
|
|
98
|
+
* author-controlled (not arbitrary user input from the public internet).
|
|
99
|
+
*/
|
|
100
|
+
export function sanitizeCmsHtml(html, options) {
|
|
101
|
+
if (!html)
|
|
102
|
+
return "";
|
|
103
|
+
const allowedTags = options?.allowedTags
|
|
104
|
+
? new Set(options.allowedTags)
|
|
105
|
+
: DEFAULT_ALLOWED_TAGS;
|
|
106
|
+
const allowedAttributes = options?.allowedAttributes
|
|
107
|
+
? toAttributeSets(options.allowedAttributes)
|
|
108
|
+
: DEFAULT_ALLOWED_ATTRIBUTES;
|
|
109
|
+
const allowedProtocols = options?.allowedProtocols
|
|
110
|
+
? new Set(options.allowedProtocols)
|
|
111
|
+
: DEFAULT_ALLOWED_PROTOCOLS;
|
|
112
|
+
const linkTarget = options?.linkTarget;
|
|
113
|
+
let result = html;
|
|
114
|
+
// Strip dangerous tags and their content entirely
|
|
115
|
+
result = result.replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, "");
|
|
116
|
+
result = result.replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, "");
|
|
117
|
+
result = result.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript\s*>/gi, "");
|
|
118
|
+
// Strip comments
|
|
119
|
+
result = result.replace(/<!--[\s\S]*?-->/g, "");
|
|
120
|
+
// Process tags
|
|
121
|
+
result = result.replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)?\/?>/g, (match, tagName, attrs) => {
|
|
122
|
+
const tag = tagName.toLowerCase();
|
|
123
|
+
if (!allowedTags.has(tag)) {
|
|
124
|
+
return "";
|
|
125
|
+
}
|
|
126
|
+
const isClosing = match.startsWith("</");
|
|
127
|
+
if (isClosing) {
|
|
128
|
+
return `</${tag}>`;
|
|
129
|
+
}
|
|
130
|
+
const isSelfClosing = match.endsWith("/>");
|
|
131
|
+
const sanitizedAttrs = sanitizeAttributes(tag, attrs ?? "", allowedAttributes, allowedProtocols, linkTarget);
|
|
132
|
+
const attrStr = sanitizedAttrs ? ` ${sanitizedAttrs}` : "";
|
|
133
|
+
return isSelfClosing ? `<${tag}${attrStr} />` : `<${tag}${attrStr}>`;
|
|
134
|
+
});
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function sanitizeAttributes(tag, attrString, allowedAttributes, allowedProtocols, linkTarget) {
|
|
138
|
+
const tagAttrs = allowedAttributes[tag];
|
|
139
|
+
const globalAttrs = allowedAttributes["*"];
|
|
140
|
+
const attrs = [];
|
|
141
|
+
const attrRegex = /([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
142
|
+
let match;
|
|
143
|
+
let hasRel = false;
|
|
144
|
+
let hasTarget = false;
|
|
145
|
+
while ((match = attrRegex.exec(attrString)) !== null) {
|
|
146
|
+
const name = match[1].toLowerCase();
|
|
147
|
+
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
148
|
+
// Block event handlers
|
|
149
|
+
if (EVENT_HANDLER_PATTERN.test(name))
|
|
150
|
+
continue;
|
|
151
|
+
// Check if attribute is allowed
|
|
152
|
+
if (!tagAttrs?.has(name) && !globalAttrs?.has(name))
|
|
153
|
+
continue;
|
|
154
|
+
// Validate URL attributes
|
|
155
|
+
if ((name === "href" || name === "src" || name === "srcset") && value) {
|
|
156
|
+
if (!isAllowedUrl(value, allowedProtocols))
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
attrs.push(`${name}="${escapeAttrValue(value)}"`);
|
|
160
|
+
if (name === "rel")
|
|
161
|
+
hasRel = true;
|
|
162
|
+
if (name === "target")
|
|
163
|
+
hasTarget = true;
|
|
164
|
+
}
|
|
165
|
+
// For <a> tags: add rel="noopener noreferrer" to external links
|
|
166
|
+
if (tag === "a") {
|
|
167
|
+
const hrefAttr = attrs.find((a) => a.startsWith("href="));
|
|
168
|
+
const href = hrefAttr
|
|
169
|
+
? hrefAttr.slice(6, -1)
|
|
170
|
+
: "";
|
|
171
|
+
const isExternal = href.startsWith("http://") || href.startsWith("https://");
|
|
172
|
+
if (isExternal) {
|
|
173
|
+
if (!hasRel) {
|
|
174
|
+
attrs.push('rel="noopener noreferrer"');
|
|
175
|
+
}
|
|
176
|
+
if (!hasTarget && linkTarget) {
|
|
177
|
+
attrs.push(`target="${escapeAttrValue(linkTarget)}"`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return attrs.join(" ");
|
|
182
|
+
}
|
|
183
|
+
function isAllowedUrl(url, allowedProtocols) {
|
|
184
|
+
const trimmed = url.trim();
|
|
185
|
+
if (!trimmed)
|
|
186
|
+
return true;
|
|
187
|
+
// Relative URLs are always OK
|
|
188
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
// Check protocol
|
|
192
|
+
const protocolMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*:)/);
|
|
193
|
+
if (!protocolMatch)
|
|
194
|
+
return true; // No protocol = relative
|
|
195
|
+
return allowedProtocols.has(protocolMatch[1].toLowerCase());
|
|
196
|
+
}
|
|
197
|
+
function escapeAttrValue(value) {
|
|
198
|
+
return value
|
|
199
|
+
.replace(/&/g, "&")
|
|
200
|
+
.replace(/"/g, """)
|
|
201
|
+
.replace(/</g, "<")
|
|
202
|
+
.replace(/>/g, ">");
|
|
203
|
+
}
|
|
204
|
+
function toAttributeSets(input) {
|
|
205
|
+
const result = {};
|
|
206
|
+
for (const [key, values] of Object.entries(input)) {
|
|
207
|
+
result[key] = new Set(values);
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Converts a basic markdown string to HTML.
|
|
213
|
+
*
|
|
214
|
+
* Handles headings, bold, italic, links, images, code blocks, inline code,
|
|
215
|
+
* unordered/ordered lists, blockquotes, horizontal rules, and paragraphs.
|
|
216
|
+
* For rich markdown processing, use a full library like react-markdown.
|
|
217
|
+
*/
|
|
218
|
+
export function renderCmsMarkdown(markdown) {
|
|
219
|
+
if (!markdown)
|
|
220
|
+
return "";
|
|
221
|
+
let html = markdown;
|
|
222
|
+
// Fenced code blocks (before other processing)
|
|
223
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, _lang, code) => `<pre><code>${escapeHtml(code.trimEnd())}</code></pre>`);
|
|
224
|
+
// Process line-by-line for block elements
|
|
225
|
+
const lines = html.split("\n");
|
|
226
|
+
const output = [];
|
|
227
|
+
let inList = null;
|
|
228
|
+
for (let i = 0; i < lines.length; i++) {
|
|
229
|
+
const line = lines[i];
|
|
230
|
+
// Skip lines inside code blocks (already processed)
|
|
231
|
+
if (line.startsWith("<pre><code>") || line.endsWith("</code></pre>")) {
|
|
232
|
+
output.push(line);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Headings
|
|
236
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
237
|
+
if (headingMatch) {
|
|
238
|
+
if (inList) {
|
|
239
|
+
output.push(`</${inList}>`);
|
|
240
|
+
inList = null;
|
|
241
|
+
}
|
|
242
|
+
const level = headingMatch[1].length;
|
|
243
|
+
output.push(`<h${level}>${processInline(headingMatch[2])}</h${level}>`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// Horizontal rule
|
|
247
|
+
if (/^---+$/.test(line.trim())) {
|
|
248
|
+
if (inList) {
|
|
249
|
+
output.push(`</${inList}>`);
|
|
250
|
+
inList = null;
|
|
251
|
+
}
|
|
252
|
+
output.push("<hr />");
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Blockquote
|
|
256
|
+
if (line.startsWith("> ")) {
|
|
257
|
+
if (inList) {
|
|
258
|
+
output.push(`</${inList}>`);
|
|
259
|
+
inList = null;
|
|
260
|
+
}
|
|
261
|
+
output.push(`<blockquote><p>${processInline(line.slice(2))}</p></blockquote>`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Unordered list
|
|
265
|
+
const ulMatch = line.match(/^[-*+]\s+(.+)$/);
|
|
266
|
+
if (ulMatch) {
|
|
267
|
+
if (inList !== "ul") {
|
|
268
|
+
if (inList)
|
|
269
|
+
output.push(`</${inList}>`);
|
|
270
|
+
output.push("<ul>");
|
|
271
|
+
inList = "ul";
|
|
272
|
+
}
|
|
273
|
+
output.push(`<li>${processInline(ulMatch[1])}</li>`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
// Ordered list
|
|
277
|
+
const olMatch = line.match(/^\d+\.\s+(.+)$/);
|
|
278
|
+
if (olMatch) {
|
|
279
|
+
if (inList !== "ol") {
|
|
280
|
+
if (inList)
|
|
281
|
+
output.push(`</${inList}>`);
|
|
282
|
+
output.push("<ol>");
|
|
283
|
+
inList = "ol";
|
|
284
|
+
}
|
|
285
|
+
output.push(`<li>${processInline(olMatch[1])}</li>`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// Close list if we hit a non-list line
|
|
289
|
+
if (inList) {
|
|
290
|
+
output.push(`</${inList}>`);
|
|
291
|
+
inList = null;
|
|
292
|
+
}
|
|
293
|
+
// Empty line
|
|
294
|
+
if (!line.trim()) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Paragraph
|
|
298
|
+
output.push(`<p>${processInline(line)}</p>`);
|
|
299
|
+
}
|
|
300
|
+
if (inList) {
|
|
301
|
+
output.push(`</${inList}>`);
|
|
302
|
+
}
|
|
303
|
+
return output.join("\n");
|
|
304
|
+
}
|
|
305
|
+
function processInline(text) {
|
|
306
|
+
let result = escapeHtml(text);
|
|
307
|
+
// Images: 
|
|
308
|
+
result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />');
|
|
309
|
+
// Links: [text](href)
|
|
310
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
311
|
+
// Bold: **text** or __text__
|
|
312
|
+
result = result.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
313
|
+
result = result.replace(/__(.+?)__/g, "<strong>$1</strong>");
|
|
314
|
+
// Italic: *text* or _text_
|
|
315
|
+
result = result.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
|
316
|
+
result = result.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<em>$1</em>");
|
|
317
|
+
// Inline code: `code`
|
|
318
|
+
result = result.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
function escapeHtml(text) {
|
|
322
|
+
return text
|
|
323
|
+
.replace(/&/g, "&")
|
|
324
|
+
.replace(/</g, "<")
|
|
325
|
+
.replace(/>/g, ">");
|
|
326
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cedros/data-react/server — server-only exports.
|
|
3
|
+
*
|
|
4
|
+
* These functions call the cedros-data backend directly and should
|
|
5
|
+
* only be used in React Server Components, route handlers, or
|
|
6
|
+
* Next.js data-fetching functions (generateMetadata, generateStaticParams).
|
|
7
|
+
*
|
|
8
|
+
* Do not import this module in client components.
|
|
9
|
+
*/
|
|
10
|
+
export { buildPageMetadata, generatePageMetadata, type PageMetadata, } from "./metadata.js";
|
|
11
|
+
export { loadSitemapEntries } from "./sitemap.js";
|
|
12
|
+
export { fetchBlogPost } from "./entries.js";
|
|
13
|
+
export { listBlogSlugs, listContentSlugs, listLearnPathIds, } from "./slugs.js";
|
|
14
|
+
export type { BuildPageMetadataOptions, CmsPageRecord, MeteredReadsInfo, SiteDataRecord, SitemapEntry, ServerFetchOptions, ContentType, } from "./types.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cedros/data-react/server — server-only exports.
|
|
3
|
+
*
|
|
4
|
+
* These functions call the cedros-data backend directly and should
|
|
5
|
+
* only be used in React Server Components, route handlers, or
|
|
6
|
+
* Next.js data-fetching functions (generateMetadata, generateStaticParams).
|
|
7
|
+
*
|
|
8
|
+
* Do not import this module in client components.
|
|
9
|
+
*/
|
|
10
|
+
export { buildPageMetadata, generatePageMetadata, } from "./metadata.js";
|
|
11
|
+
export { loadSitemapEntries } from "./sitemap.js";
|
|
12
|
+
export { fetchBlogPost } from "./entries.js";
|
|
13
|
+
export { listBlogSlugs, listContentSlugs, listLearnPathIds, } from "./slugs.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ContentType, ServerFetchOptions, SitemapEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Loads all routable content slugs from cedros-data for sitemap generation.
|
|
4
|
+
*
|
|
5
|
+
* Queries each content type collection and returns a flat list of sitemap
|
|
6
|
+
* entries with slug, lastModified, changeFrequency, and priority.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // app/sitemap.ts
|
|
11
|
+
* import { loadSitemapEntries } from '@cedros/data-react/server';
|
|
12
|
+
*
|
|
13
|
+
* export default async function sitemap() {
|
|
14
|
+
* const entries = await loadSitemapEntries({
|
|
15
|
+
* serverUrl: process.env.NEXT_PUBLIC_BACKEND_API_URL,
|
|
16
|
+
* });
|
|
17
|
+
* return entries.map((e) => ({
|
|
18
|
+
* url: `${process.env.NEXT_PUBLIC_ORIGIN}/${e.slug}`,
|
|
19
|
+
* lastModified: e.lastModified,
|
|
20
|
+
* changeFrequency: e.changeFrequency,
|
|
21
|
+
* priority: e.priority,
|
|
22
|
+
* }));
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadSitemapEntries(options?: ServerFetchOptions & {
|
|
27
|
+
contentTypes?: ContentType[];
|
|
28
|
+
}): Promise<SitemapEntry[]>;
|