@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +177 -0
  3. package/dist/admin/api.d.ts +19 -0
  4. package/dist/admin/api.js +108 -0
  5. package/dist/admin/components.d.ts +36 -0
  6. package/dist/admin/components.js +22 -0
  7. package/dist/admin/history.d.ts +17 -0
  8. package/dist/admin/history.js +103 -0
  9. package/dist/admin/icons.d.ts +15 -0
  10. package/dist/admin/icons.js +18 -0
  11. package/dist/admin/index.d.ts +13 -0
  12. package/dist/admin/index.js +12 -0
  13. package/dist/admin/permissions.d.ts +4 -0
  14. package/dist/admin/permissions.js +45 -0
  15. package/dist/admin/plugin.d.ts +4 -0
  16. package/dist/admin/plugin.js +180 -0
  17. package/dist/admin/primitives/ConfirmDialog.d.ts +14 -0
  18. package/dist/admin/primitives/ConfirmDialog.js +7 -0
  19. package/dist/admin/primitives/DataTable.d.ts +14 -0
  20. package/dist/admin/primitives/DataTable.js +7 -0
  21. package/dist/admin/primitives/DiffViewer.d.ts +11 -0
  22. package/dist/admin/primitives/DiffViewer.js +8 -0
  23. package/dist/admin/primitives/FormFieldRow.d.ts +23 -0
  24. package/dist/admin/primitives/FormFieldRow.js +16 -0
  25. package/dist/admin/primitives/JsonCodeEditor.d.ts +10 -0
  26. package/dist/admin/primitives/JsonCodeEditor.js +42 -0
  27. package/dist/admin/primitives/Pagination.d.ts +8 -0
  28. package/dist/admin/primitives/Pagination.js +8 -0
  29. package/dist/admin/primitives/Toolbar.d.ts +23 -0
  30. package/dist/admin/primitives/Toolbar.js +10 -0
  31. package/dist/admin/primitives/alerts.d.ts +21 -0
  32. package/dist/admin/primitives/alerts.js +44 -0
  33. package/dist/admin/sectionIds.d.ts +20 -0
  34. package/dist/admin/sectionIds.js +33 -0
  35. package/dist/admin/sections/CollectionsSection.d.ts +2 -0
  36. package/dist/admin/sections/CollectionsSection.js +125 -0
  37. package/dist/admin/sections/ContractVerifySection.d.ts +11 -0
  38. package/dist/admin/sections/ContractVerifySection.js +98 -0
  39. package/dist/admin/sections/CustomDataSection.d.ts +2 -0
  40. package/dist/admin/sections/CustomDataSection.js +256 -0
  41. package/dist/admin/sections/DataOpsSection.d.ts +26 -0
  42. package/dist/admin/sections/DataOpsSection.js +245 -0
  43. package/dist/admin/sections/HistorySection.d.ts +2 -0
  44. package/dist/admin/sections/HistorySection.js +26 -0
  45. package/dist/admin/sections/MonetizationSection.d.ts +2 -0
  46. package/dist/admin/sections/MonetizationSection.js +140 -0
  47. package/dist/admin/sections/NavigationSection.d.ts +13 -0
  48. package/dist/admin/sections/NavigationSection.js +195 -0
  49. package/dist/admin/sections/PagesSection.d.ts +2 -0
  50. package/dist/admin/sections/PagesSection.js +157 -0
  51. package/dist/admin/sections/SchemaDesignerSection.d.ts +2 -0
  52. package/dist/admin/sections/SchemaDesignerSection.js +167 -0
  53. package/dist/admin/sections/SiteSettingsSection.d.ts +12 -0
  54. package/dist/admin/sections/SiteSettingsSection.js +122 -0
  55. package/dist/admin/sections/TippingSection.d.ts +2 -0
  56. package/dist/admin/sections/TippingSection.js +178 -0
  57. package/dist/admin/sections/media/MediaDetail.d.ts +12 -0
  58. package/dist/admin/sections/media/MediaDetail.js +74 -0
  59. package/dist/admin/sections/media/MediaGrid.d.ts +14 -0
  60. package/dist/admin/sections/media/MediaGrid.js +22 -0
  61. package/dist/admin/sections/media/MediaSection.d.ts +2 -0
  62. package/dist/admin/sections/media/MediaSection.js +97 -0
  63. package/dist/admin/sections/media/MediaUploader.d.ts +7 -0
  64. package/dist/admin/sections/media/MediaUploader.js +72 -0
  65. package/dist/admin/sections/media/types.d.ts +33 -0
  66. package/dist/admin/sections/media/types.js +1 -0
  67. package/dist/admin/styles.css +533 -0
  68. package/dist/admin/types.d.ts +85 -0
  69. package/dist/admin/types.js +1 -0
  70. package/dist/index.d.ts +4 -0
  71. package/dist/index.js +3 -0
  72. package/dist/react/CmsContent.d.ts +20 -0
  73. package/dist/react/CmsContent.js +31 -0
  74. package/dist/react/entries.d.ts +9 -0
  75. package/dist/react/entries.js +25 -0
  76. package/dist/react/fetch.d.ts +11 -0
  77. package/dist/react/fetch.js +32 -0
  78. package/dist/react/index.d.ts +10 -0
  79. package/dist/react/index.js +9 -0
  80. package/dist/react/metadata.d.ts +44 -0
  81. package/dist/react/metadata.js +142 -0
  82. package/dist/react/sanitize.d.ts +17 -0
  83. package/dist/react/sanitize.js +326 -0
  84. package/dist/react/server.d.ts +14 -0
  85. package/dist/react/server.js +13 -0
  86. package/dist/react/sitemap.d.ts +28 -0
  87. package/dist/react/sitemap.js +91 -0
  88. package/dist/react/slugs.d.ts +27 -0
  89. package/dist/react/slugs.js +52 -0
  90. package/dist/react/types.d.ts +85 -0
  91. package/dist/react/types.js +1 -0
  92. package/dist/react/visitor.d.ts +7 -0
  93. package/dist/react/visitor.js +18 -0
  94. package/dist/site-templates/BlogTemplates.d.ts +95 -0
  95. package/dist/site-templates/BlogTemplates.js +64 -0
  96. package/dist/site-templates/ContactPageTemplate.d.ts +14 -0
  97. package/dist/site-templates/ContactPageTemplate.js +5 -0
  98. package/dist/site-templates/DashboardOverviewTemplate.d.ts +29 -0
  99. package/dist/site-templates/DashboardOverviewTemplate.js +17 -0
  100. package/dist/site-templates/DashboardShell.d.ts +28 -0
  101. package/dist/site-templates/DashboardShell.js +10 -0
  102. package/dist/site-templates/DocsSidebar.d.ts +14 -0
  103. package/dist/site-templates/DocsSidebar.js +13 -0
  104. package/dist/site-templates/DocsTemplates.d.ts +60 -0
  105. package/dist/site-templates/DocsTemplates.js +47 -0
  106. package/dist/site-templates/HomePageTemplate.d.ts +15 -0
  107. package/dist/site-templates/HomePageTemplate.js +10 -0
  108. package/dist/site-templates/LegalPageTemplate.d.ts +12 -0
  109. package/dist/site-templates/LegalPageTemplate.js +6 -0
  110. package/dist/site-templates/MarkdownContent.d.ts +7 -0
  111. package/dist/site-templates/MarkdownContent.js +24 -0
  112. package/dist/site-templates/NotFoundTemplate.d.ts +9 -0
  113. package/dist/site-templates/NotFoundTemplate.js +5 -0
  114. package/dist/site-templates/SiteFooter.d.ts +13 -0
  115. package/dist/site-templates/SiteFooter.js +4 -0
  116. package/dist/site-templates/SiteLayout.d.ts +14 -0
  117. package/dist/site-templates/SiteLayout.js +6 -0
  118. package/dist/site-templates/TopNav.d.ts +10 -0
  119. package/dist/site-templates/TopNav.js +8 -0
  120. package/dist/site-templates/blogControls.d.ts +19 -0
  121. package/dist/site-templates/blogControls.js +37 -0
  122. package/dist/site-templates/codeBlock.d.ts +9 -0
  123. package/dist/site-templates/codeBlock.js +31 -0
  124. package/dist/site-templates/content-styles.css +410 -0
  125. package/dist/site-templates/contentIndex.d.ts +65 -0
  126. package/dist/site-templates/contentIndex.js +181 -0
  127. package/dist/site-templates/contentUi.d.ts +14 -0
  128. package/dist/site-templates/contentUi.js +24 -0
  129. package/dist/site-templates/docs-styles.css +259 -0
  130. package/dist/site-templates/docsNavigation.d.ts +18 -0
  131. package/dist/site-templates/docsNavigation.js +50 -0
  132. package/dist/site-templates/index.d.ts +28 -0
  133. package/dist/site-templates/index.js +25 -0
  134. package/dist/site-templates/monetization-styles.css +154 -0
  135. package/dist/site-templates/paywallControls.d.ts +22 -0
  136. package/dist/site-templates/paywallControls.js +9 -0
  137. package/dist/site-templates/routing.d.ts +12 -0
  138. package/dist/site-templates/routing.js +36 -0
  139. package/dist/site-templates/solanaAtaSetup.d.ts +11 -0
  140. package/dist/site-templates/solanaAtaSetup.js +38 -0
  141. package/dist/site-templates/solanaMicropayments.d.ts +65 -0
  142. package/dist/site-templates/solanaMicropayments.js +115 -0
  143. package/dist/site-templates/styles.css +332 -0
  144. package/dist/site-templates/tipControls.d.ts +24 -0
  145. package/dist/site-templates/tipControls.js +43 -0
  146. package/dist/site-templates/tocExtractor.d.ts +16 -0
  147. package/dist/site-templates/tocExtractor.js +58 -0
  148. package/dist/site-templates/tocScrollSpy.d.ts +16 -0
  149. package/dist/site-templates/tocScrollSpy.js +37 -0
  150. package/dist/templates.d.ts +8 -0
  151. package/dist/templates.js +20 -0
  152. 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, "&amp;")
200
+ .replace(/"/g, "&quot;")
201
+ .replace(/</g, "&lt;")
202
+ .replace(/>/g, "&gt;");
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: ![alt](src)
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, "&amp;")
324
+ .replace(/</g, "&lt;")
325
+ .replace(/>/g, "&gt;");
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[]>;