cms-renderer 0.0.0 → 0.1.1

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 (51) hide show
  1. package/dist/chunk-HVKFEZBT.js +116 -0
  2. package/dist/chunk-HVKFEZBT.js.map +1 -0
  3. package/dist/chunk-JHKDRASN.js +39 -0
  4. package/dist/chunk-JHKDRASN.js.map +1 -0
  5. package/dist/chunk-RPM73PQZ.js +17 -0
  6. package/dist/chunk-RPM73PQZ.js.map +1 -0
  7. package/dist/lib/block-renderer.d.ts +32 -0
  8. package/dist/lib/block-renderer.js +7 -0
  9. package/dist/lib/block-renderer.js.map +1 -0
  10. package/dist/lib/cms-api.d.ts +25 -0
  11. package/dist/lib/cms-api.js +7 -0
  12. package/dist/lib/cms-api.js.map +1 -0
  13. package/dist/lib/data-utils.d.ts +218 -0
  14. package/dist/lib/data-utils.js +247 -0
  15. package/dist/lib/data-utils.js.map +1 -0
  16. package/dist/lib/image/lazy-load.d.ts +75 -0
  17. package/dist/lib/image/lazy-load.js +83 -0
  18. package/dist/lib/image/lazy-load.js.map +1 -0
  19. package/dist/lib/markdown-utils.d.ts +172 -0
  20. package/dist/lib/markdown-utils.js +137 -0
  21. package/dist/lib/markdown-utils.js.map +1 -0
  22. package/dist/lib/renderer.d.ts +40 -0
  23. package/dist/lib/renderer.js +371 -0
  24. package/dist/lib/renderer.js.map +1 -0
  25. package/{lib/result.ts → dist/lib/result.d.ts} +32 -146
  26. package/dist/lib/result.js +37 -0
  27. package/dist/lib/result.js.map +1 -0
  28. package/dist/lib/schema.d.ts +15 -0
  29. package/dist/lib/schema.js +35 -0
  30. package/dist/lib/schema.js.map +1 -0
  31. package/{lib/trpc.ts → dist/lib/trpc.d.ts} +6 -4
  32. package/dist/lib/trpc.js +7 -0
  33. package/dist/lib/trpc.js.map +1 -0
  34. package/dist/lib/types.d.ts +163 -0
  35. package/dist/lib/types.js +1 -0
  36. package/dist/lib/types.js.map +1 -0
  37. package/package.json +50 -11
  38. package/.turbo/turbo-check-types.log +0 -2
  39. package/lib/__tests__/enrich-block-images.test.ts +0 -394
  40. package/lib/block-renderer.tsx +0 -60
  41. package/lib/cms-api.ts +0 -86
  42. package/lib/data-utils.ts +0 -572
  43. package/lib/image/lazy-load.ts +0 -209
  44. package/lib/markdown-utils.ts +0 -368
  45. package/lib/renderer.tsx +0 -189
  46. package/lib/schema.ts +0 -74
  47. package/lib/types.ts +0 -201
  48. package/next.config.ts +0 -39
  49. package/postcss.config.mjs +0 -5
  50. package/tsconfig.json +0 -12
  51. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,247 @@
1
+ import {
2
+ err,
3
+ ok
4
+ } from "../chunk-HVKFEZBT.js";
5
+
6
+ // lib/data-utils.ts
7
+ var FetchErrorCode = {
8
+ INVALID_SLUG: "INVALID_SLUG",
9
+ NOT_FOUND: "NOT_FOUND",
10
+ NETWORK_ERROR: "NETWORK_ERROR",
11
+ TIMEOUT: "TIMEOUT",
12
+ PARSE_ERROR: "PARSE_ERROR",
13
+ SERVER_ERROR: "SERVER_ERROR",
14
+ RETRY_EXHAUSTED: "RETRY_EXHAUSTED"
15
+ };
16
+ var DEFAULT_TIMEOUT = 1e4;
17
+ var DEFAULT_RETRIES = 3;
18
+ var DEFAULT_RETRY_DELAY = 1e3;
19
+ var TRANSIENT_STATUS_CODES = [408, 429, 500, 502, 503, 504];
20
+ var mockPages = {
21
+ demo: {
22
+ slug: "demo",
23
+ title: "Demo Page",
24
+ blocks: [
25
+ {
26
+ id: "mock-header-1",
27
+ type: "header",
28
+ content: {
29
+ headline: "Welcome",
30
+ alignment: "center"
31
+ }
32
+ },
33
+ {
34
+ id: "mock-article-1",
35
+ type: "article",
36
+ content: {
37
+ headline: "Getting Started",
38
+ body: "## Introduction\n\nThis is a demo article."
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ about: {
44
+ slug: "about",
45
+ title: "About Us",
46
+ blocks: [
47
+ {
48
+ id: "mock-header-2",
49
+ type: "header",
50
+ content: {
51
+ headline: "About Our Company",
52
+ alignment: "left"
53
+ }
54
+ }
55
+ ]
56
+ }
57
+ };
58
+ function createFetchError(code, message, options = {}) {
59
+ return { code, message, ...options };
60
+ }
61
+ function delay(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
64
+ function exponentialBackoff(baseDelay, attempt) {
65
+ const jitter = Math.random() * 100;
66
+ return baseDelay * 2 ** attempt + jitter;
67
+ }
68
+ function isTransientError(error) {
69
+ if (error instanceof Error) {
70
+ if (error.name === "TypeError" && error.message.includes("fetch")) {
71
+ return true;
72
+ }
73
+ for (const code of TRANSIENT_STATUS_CODES) {
74
+ if (error.message.includes(String(code))) {
75
+ return true;
76
+ }
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+ async function simulateFetch(slug, signal) {
82
+ await delay(50 + Math.random() * 100);
83
+ if (signal?.aborted) {
84
+ throw new Error("Request aborted");
85
+ }
86
+ const page = mockPages[slug];
87
+ if (!page) {
88
+ const error = new Error(`Page not found: ${slug}`);
89
+ error.name = "NotFoundError";
90
+ throw error;
91
+ }
92
+ return page;
93
+ }
94
+ async function fetchPageV1(slug) {
95
+ return simulateFetch(slug);
96
+ }
97
+ async function fetchPageV2(slug) {
98
+ try {
99
+ if (!slug || typeof slug !== "string") {
100
+ return null;
101
+ }
102
+ return await simulateFetch(slug);
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ async function fetchPageV3(slug, options = {}) {
108
+ const {
109
+ timeout = DEFAULT_TIMEOUT,
110
+ retries = DEFAULT_RETRIES,
111
+ retryDelay = DEFAULT_RETRY_DELAY,
112
+ signal: externalSignal
113
+ } = options;
114
+ if (slug === null || slug === void 0) {
115
+ return err(
116
+ createFetchError(
117
+ FetchErrorCode.INVALID_SLUG,
118
+ `Slug must be a string, received ${slug === null ? "null" : "undefined"}`
119
+ )
120
+ );
121
+ }
122
+ if (typeof slug !== "string") {
123
+ return err(
124
+ createFetchError(
125
+ FetchErrorCode.INVALID_SLUG,
126
+ `Slug must be a string, received ${typeof slug}`
127
+ )
128
+ );
129
+ }
130
+ const trimmedSlug = slug.trim();
131
+ if (trimmedSlug.length === 0) {
132
+ return err(createFetchError(FetchErrorCode.INVALID_SLUG, "Slug cannot be empty"));
133
+ }
134
+ const controller = new AbortController();
135
+ const { signal } = controller;
136
+ if (externalSignal) {
137
+ externalSignal.addEventListener("abort", () => controller.abort());
138
+ }
139
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
140
+ let lastError;
141
+ let attempt = 0;
142
+ try {
143
+ while (attempt <= retries) {
144
+ try {
145
+ const page = await simulateFetch(trimmedSlug, signal);
146
+ clearTimeout(timeoutId);
147
+ return ok(page);
148
+ } catch (error) {
149
+ lastError = error instanceof Error ? error : new Error(String(error));
150
+ if (signal.aborted) {
151
+ clearTimeout(timeoutId);
152
+ return err(
153
+ createFetchError(FetchErrorCode.TIMEOUT, `Request timed out after ${timeout}ms`, {
154
+ cause: lastError
155
+ })
156
+ );
157
+ }
158
+ if (lastError.name === "NotFoundError") {
159
+ clearTimeout(timeoutId);
160
+ return err(
161
+ createFetchError(FetchErrorCode.NOT_FOUND, `Page "${trimmedSlug}" not found`, {
162
+ cause: lastError
163
+ })
164
+ );
165
+ }
166
+ if (attempt < retries && isTransientError(lastError)) {
167
+ attempt++;
168
+ const backoff = exponentialBackoff(retryDelay, attempt);
169
+ if (process.env.NODE_ENV === "development") {
170
+ console.warn(
171
+ `[fetchPageV3] Retry ${attempt}/${retries} for "${trimmedSlug}" after ${Math.round(backoff)}ms`
172
+ );
173
+ }
174
+ await delay(backoff);
175
+ continue;
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ clearTimeout(timeoutId);
181
+ if (attempt >= retries) {
182
+ return err(
183
+ createFetchError(FetchErrorCode.RETRY_EXHAUSTED, `Failed after ${retries} retry attempts`, {
184
+ cause: lastError,
185
+ retryCount: attempt
186
+ })
187
+ );
188
+ }
189
+ return err(
190
+ createFetchError(
191
+ FetchErrorCode.NETWORK_ERROR,
192
+ lastError?.message ?? "Unknown network error",
193
+ { cause: lastError }
194
+ )
195
+ );
196
+ } catch (error) {
197
+ clearTimeout(timeoutId);
198
+ const cause = error instanceof Error ? error : new Error(String(error));
199
+ return err(
200
+ createFetchError(FetchErrorCode.SERVER_ERROR, `Unexpected error: ${cause.message}`, { cause })
201
+ );
202
+ }
203
+ }
204
+ var fetchPage = fetchPageV3;
205
+ function isValidSlug(slug) {
206
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
207
+ }
208
+ function normalizeSlug(input) {
209
+ return input.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
210
+ }
211
+ async function prefetchPages(slugs, options) {
212
+ return Promise.all(slugs.map((slug) => fetchPageV3(slug, options)));
213
+ }
214
+ async function fetchPageWithSWR(slug, cache, options = {}) {
215
+ const { maxAge = 6e4 } = options;
216
+ const cached = cache.get(slug);
217
+ const now = Date.now();
218
+ if (cached && now - cached.timestamp < maxAge) {
219
+ return ok(cached.data);
220
+ }
221
+ const result = await fetchPageV3(slug, options);
222
+ if (result.ok) {
223
+ cache.set(slug, { data: result.value, timestamp: now });
224
+ }
225
+ if (!result.ok && cached) {
226
+ if (process.env.NODE_ENV === "development") {
227
+ console.warn(
228
+ `[fetchPageWithSWR] Returning stale cache for "${slug}" after fetch failure:`,
229
+ result.error.message
230
+ );
231
+ }
232
+ return ok(cached.data);
233
+ }
234
+ return result;
235
+ }
236
+ export {
237
+ FetchErrorCode,
238
+ fetchPage,
239
+ fetchPageV1,
240
+ fetchPageV2,
241
+ fetchPageV3,
242
+ fetchPageWithSWR,
243
+ isValidSlug,
244
+ normalizeSlug,
245
+ prefetchPages
246
+ };
247
+ //# sourceMappingURL=data-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../lib/data-utils.ts"],"sourcesContent":["/**\n * Data Fetching Utilities\n *\n * Three implementations showing the progression from naive to production-ready:\n * - v1 (Naive): Direct await, crashes on any error\n * - v2 (Defensive): Try-catch with null fallback\n * - v3 (Robust): Result type, retry logic, timeout support\n *\n * The robust version (v3) is exported as the default `fetchPage`.\n */\n\nimport { err, ok, type Result } from './result';\nimport type { BlockData } from './types';\n\n// -----------------------------------------------------------------------------\n// Types\n// -----------------------------------------------------------------------------\n\n/**\n * Page structure returned by the tRPC API.\n */\nexport interface Page {\n slug: string;\n title: string;\n blocks: BlockData[];\n}\n\n/**\n * Error codes for data fetching failures.\n */\nexport const FetchErrorCode = {\n INVALID_SLUG: 'INVALID_SLUG',\n NOT_FOUND: 'NOT_FOUND',\n NETWORK_ERROR: 'NETWORK_ERROR',\n TIMEOUT: 'TIMEOUT',\n PARSE_ERROR: 'PARSE_ERROR',\n SERVER_ERROR: 'SERVER_ERROR',\n RETRY_EXHAUSTED: 'RETRY_EXHAUSTED',\n} as const;\n\nexport type FetchErrorCode = (typeof FetchErrorCode)[keyof typeof FetchErrorCode];\n\n/**\n * Structured error for data fetching failures.\n */\nexport interface FetchError {\n code: FetchErrorCode;\n message: string;\n cause?: Error;\n retryCount?: number;\n}\n\n/**\n * Options for robust data fetching.\n */\nexport interface FetchOptions {\n /**\n * Timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number;\n\n /**\n * Number of retry attempts for transient failures.\n * @default 3\n */\n retries?: number;\n\n /**\n * Base delay between retries in milliseconds.\n * Uses exponential backoff: delay * 2^attempt\n * @default 1000 (1 second)\n */\n retryDelay?: number;\n\n /**\n * AbortSignal for cancellation support.\n */\n signal?: AbortSignal;\n}\n\n// -----------------------------------------------------------------------------\n// Constants\n// -----------------------------------------------------------------------------\n\nconst DEFAULT_TIMEOUT = 10_000; // 10 seconds\nconst DEFAULT_RETRIES = 3;\nconst DEFAULT_RETRY_DELAY = 1_000; // 1 second\n\n/**\n * HTTP status codes that indicate transient failures (should retry).\n */\nconst TRANSIENT_STATUS_CODES = [408, 429, 500, 502, 503, 504] as const;\n\n// -----------------------------------------------------------------------------\n// Mock Data Store (for demonstration)\n// -----------------------------------------------------------------------------\n\n/**\n * Simulated data store.\n * In production, this would be replaced with actual tRPC/API calls.\n */\nconst mockPages: Record<string, Page> = {\n demo: {\n slug: 'demo',\n title: 'Demo Page',\n blocks: [\n {\n id: 'mock-header-1',\n type: 'header',\n content: {\n headline: 'Welcome',\n alignment: 'center',\n },\n },\n {\n id: 'mock-article-1',\n type: 'article',\n content: {\n headline: 'Getting Started',\n body: '## Introduction\\n\\nThis is a demo article.',\n },\n },\n ],\n },\n about: {\n slug: 'about',\n title: 'About Us',\n blocks: [\n {\n id: 'mock-header-2',\n type: 'header',\n content: {\n headline: 'About Our Company',\n alignment: 'left',\n },\n },\n ],\n },\n};\n\n// -----------------------------------------------------------------------------\n// Helper Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Creates a FetchError with the given code and message.\n */\nfunction createFetchError(\n code: FetchErrorCode,\n message: string,\n options: { cause?: Error; retryCount?: number } = {}\n): FetchError {\n return { code, message, ...options };\n}\n\n/**\n * Delays execution for the specified duration.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Calculates exponential backoff delay.\n */\nfunction exponentialBackoff(baseDelay: number, attempt: number): number {\n // Add jitter to prevent thundering herd\n const jitter = Math.random() * 100;\n return baseDelay * 2 ** attempt + jitter;\n}\n\n/**\n * Checks if an error represents a transient failure that should be retried.\n */\nfunction isTransientError(error: unknown): boolean {\n if (error instanceof Error) {\n // Network errors\n if (error.name === 'TypeError' && error.message.includes('fetch')) {\n return true;\n }\n // Check for HTTP status codes in error message\n for (const code of TRANSIENT_STATUS_CODES) {\n if (error.message.includes(String(code))) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Simulates a network request with configurable behavior.\n * In production, this would be an actual fetch/tRPC call.\n */\nasync function simulateFetch(slug: string, signal?: AbortSignal): Promise<Page> {\n // Simulate network latency\n await delay(50 + Math.random() * 100);\n\n // Check for cancellation\n if (signal?.aborted) {\n throw new Error('Request aborted');\n }\n\n const page = mockPages[slug];\n if (!page) {\n const error = new Error(`Page not found: ${slug}`);\n error.name = 'NotFoundError';\n throw error;\n }\n\n return page;\n}\n\n// -----------------------------------------------------------------------------\n// V1: Naive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Naive data fetcher - crashes on any error.\n *\n * DO NOT USE IN PRODUCTION. This is for demonstration only.\n *\n * Problems:\n * - No input validation\n * - No error handling\n * - Will crash the entire app on network failure\n * - No timeout protection\n * - No retry logic for transient failures\n *\n * @example\n * ```ts\n * // This throws if slug is invalid or network fails\n * const page = await fetchPageV1('demo');\n * ```\n */\nexport async function fetchPageV1(slug: string): Promise<Page> {\n // Direct await - any error crashes the caller\n return simulateFetch(slug);\n}\n\n// -----------------------------------------------------------------------------\n// V2: Defensive Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Defensive data fetcher - catches errors but loses context.\n *\n * Better than v1 but still problematic:\n * - Returns null on any error (loses error details)\n * - Caller can't distinguish between 404 and network failure\n * - No retry logic for transient failures\n * - No timeout protection\n *\n * @example\n * ```ts\n * const page = await fetchPageV2(slug);\n * if (!page) {\n * // What went wrong? 404? Network? Timeout? We don't know.\n * return <NotFound />;\n * }\n * ```\n */\nexport async function fetchPageV2(slug: string): Promise<Page | null> {\n try {\n // Basic validation\n if (!slug || typeof slug !== 'string') {\n return null;\n }\n\n return await simulateFetch(slug);\n } catch {\n // All errors become null - we lose valuable context\n return null;\n }\n}\n\n// -----------------------------------------------------------------------------\n// V3: Robust Implementation\n// -----------------------------------------------------------------------------\n\n/**\n * Robust data fetcher - production-ready with full error context.\n *\n * Features:\n * - Input validation with specific error codes\n * - Result type for explicit error handling\n * - Automatic retry with exponential backoff\n * - Timeout support via AbortController\n * - Distinguishes between error types (404 vs network vs timeout)\n * - Preserves error context for debugging\n *\n * @param slug - Page slug to fetch\n * @param options - Fetch options (timeout, retries, etc.)\n * @returns Result containing the page data or a structured error\n *\n * @example\n * ```ts\n * const result = await fetchPageV3('demo', {\n * timeout: 5000,\n * retries: 3,\n * });\n *\n * if (!result.ok) {\n * switch (result.error.code) {\n * case 'NOT_FOUND':\n * return <NotFoundPage slug={slug} />;\n * case 'TIMEOUT':\n * return <TimeoutError onRetry={handleRetry} />;\n * case 'NETWORK_ERROR':\n * return <NetworkError message={result.error.message} />;\n * case 'RETRY_EXHAUSTED':\n * return <RetryExhausted attempts={result.error.retryCount} />;\n * default:\n * return <GenericError />;\n * }\n * }\n *\n * return <PageRenderer page={result.value} />;\n * ```\n */\nexport async function fetchPageV3(\n slug: string,\n options: FetchOptions = {}\n): Promise<Result<Page, FetchError>> {\n const {\n timeout = DEFAULT_TIMEOUT,\n retries = DEFAULT_RETRIES,\n retryDelay = DEFAULT_RETRY_DELAY,\n signal: externalSignal,\n } = options;\n\n // 1. Validate input\n if (slug === null || slug === undefined) {\n return err(\n createFetchError(\n FetchErrorCode.INVALID_SLUG,\n `Slug must be a string, received ${slug === null ? 'null' : 'undefined'}`\n )\n );\n }\n\n if (typeof slug !== 'string') {\n return err(\n createFetchError(\n FetchErrorCode.INVALID_SLUG,\n `Slug must be a string, received ${typeof slug}`\n )\n );\n }\n\n const trimmedSlug = slug.trim();\n if (trimmedSlug.length === 0) {\n return err(createFetchError(FetchErrorCode.INVALID_SLUG, 'Slug cannot be empty'));\n }\n\n // 2. Create abort controller for timeout\n const controller = new AbortController();\n const { signal } = controller;\n\n // Link external signal if provided\n if (externalSignal) {\n externalSignal.addEventListener('abort', () => controller.abort());\n }\n\n // 3. Set up timeout\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n // 4. Attempt fetch with retry logic\n let lastError: Error | undefined;\n let attempt = 0;\n\n try {\n while (attempt <= retries) {\n try {\n const page = await simulateFetch(trimmedSlug, signal);\n clearTimeout(timeoutId);\n return ok(page);\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Check for abort/timeout\n if (signal.aborted) {\n clearTimeout(timeoutId);\n return err(\n createFetchError(FetchErrorCode.TIMEOUT, `Request timed out after ${timeout}ms`, {\n cause: lastError,\n })\n );\n }\n\n // Check for 404 (not retryable)\n if (lastError.name === 'NotFoundError') {\n clearTimeout(timeoutId);\n return err(\n createFetchError(FetchErrorCode.NOT_FOUND, `Page \"${trimmedSlug}\" not found`, {\n cause: lastError,\n })\n );\n }\n\n // Check if we should retry\n if (attempt < retries && isTransientError(lastError)) {\n attempt++;\n const backoff = exponentialBackoff(retryDelay, attempt);\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[fetchPageV3] Retry ${attempt}/${retries} for \"${trimmedSlug}\" after ${Math.round(backoff)}ms`\n );\n }\n await delay(backoff);\n continue;\n }\n\n // No more retries or non-transient error\n break;\n }\n }\n\n // 5. All retries exhausted\n clearTimeout(timeoutId);\n\n if (attempt >= retries) {\n return err(\n createFetchError(FetchErrorCode.RETRY_EXHAUSTED, `Failed after ${retries} retry attempts`, {\n cause: lastError,\n retryCount: attempt,\n })\n );\n }\n\n // Non-retryable error\n return err(\n createFetchError(\n FetchErrorCode.NETWORK_ERROR,\n lastError?.message ?? 'Unknown network error',\n { cause: lastError }\n )\n );\n } catch (error) {\n clearTimeout(timeoutId);\n const cause = error instanceof Error ? error : new Error(String(error));\n return err(\n createFetchError(FetchErrorCode.SERVER_ERROR, `Unexpected error: ${cause.message}`, { cause })\n );\n }\n}\n\n// -----------------------------------------------------------------------------\n// Default Export\n// -----------------------------------------------------------------------------\n\n/**\n * Production-ready page fetcher.\n *\n * This is an alias for `fetchPageV3` - the robust implementation\n * with validation, retry logic, and Result-based error handling.\n *\n * @example\n * ```ts\n * import { fetchPage } from '@/lib/data-utils';\n *\n * const result = await fetchPage('demo');\n * if (!result.ok) {\n * // Handle error with full context\n * console.error(`[${result.error.code}] ${result.error.message}`);\n * return null;\n * }\n * return result.value;\n * ```\n */\nexport const fetchPage = fetchPageV3;\n\n// -----------------------------------------------------------------------------\n// Utility Functions\n// -----------------------------------------------------------------------------\n\n/**\n * Validates a page slug format.\n *\n * @param slug - Slug to validate\n * @returns true if slug is valid format\n */\nexport function isValidSlug(slug: string): boolean {\n // Slugs should be lowercase, alphanumeric with hyphens\n return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);\n}\n\n/**\n * Normalizes a slug (lowercase, trim, replace spaces with hyphens).\n *\n * @param input - Raw input to normalize\n * @returns Normalized slug\n */\nexport function normalizeSlug(input: string): string {\n return input\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n}\n\n/**\n * Prefetches multiple pages in parallel with Result types.\n *\n * @param slugs - Array of page slugs to fetch\n * @param options - Fetch options applied to all requests\n * @returns Array of Results for each page\n *\n * @example\n * ```ts\n * const results = await prefetchPages(['home', 'about', 'blog']);\n * const errors = results.filter(r => !r.ok);\n * if (errors.length > 0) {\n * console.warn(`${errors.length} pages failed to load`);\n * }\n * ```\n */\nexport async function prefetchPages(\n slugs: string[],\n options?: FetchOptions\n): Promise<Result<Page, FetchError>[]> {\n return Promise.all(slugs.map((slug) => fetchPageV3(slug, options)));\n}\n\n/**\n * Fetches a page with stale-while-revalidate semantics.\n * Returns cached data immediately if available, then updates in background.\n *\n * @param slug - Page slug to fetch\n * @param cache - Cache storage (e.g., Map, localStorage wrapper)\n * @param options - Fetch options\n * @returns Cached data or fresh fetch result\n */\nexport async function fetchPageWithSWR(\n slug: string,\n cache: Map<string, { data: Page; timestamp: number }>,\n options: FetchOptions & { maxAge?: number } = {}\n): Promise<Result<Page, FetchError>> {\n const { maxAge = 60_000 } = options; // 1 minute default\n\n const cached = cache.get(slug);\n const now = Date.now();\n\n // Return fresh cache immediately\n if (cached && now - cached.timestamp < maxAge) {\n return ok(cached.data);\n }\n\n // Fetch fresh data\n const result = await fetchPageV3(slug, options);\n\n // Update cache on success\n if (result.ok) {\n cache.set(slug, { data: result.value, timestamp: now });\n }\n\n // If fetch failed but we have stale cache, return it with warning\n if (!result.ok && cached) {\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n `[fetchPageWithSWR] Returning stale cache for \"${slug}\" after fetch failure:`,\n result.error.message\n );\n }\n return ok(cached.data);\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Lazy Loading Utilities
3
+ *
4
+ * Reusable IntersectionObserver-based lazy loading with
5
+ * connection-aware quality adjustment.
6
+ *
7
+ * This module provides:
8
+ * - useLazyLoad: Hook for deferred loading when elements enter viewport
9
+ * - getConnectionAwareQuality: Adjusts image quality based on network speed
10
+ */
11
+ interface LazyLoadOptions {
12
+ /** Root margin for early loading (default: "200px") */
13
+ rootMargin?: string;
14
+ /** Threshold for intersection (default: 0) */
15
+ threshold?: number;
16
+ /** Callback when element enters viewport */
17
+ onEnter?: () => void;
18
+ }
19
+ interface LazyLoadResult {
20
+ /** Callback ref to attach to the element */
21
+ ref: (node: HTMLElement | null) => void;
22
+ /** Whether the element has entered the viewport */
23
+ inView: boolean;
24
+ }
25
+ /**
26
+ * IntersectionObserver-based lazy loading hook.
27
+ *
28
+ * Triggers when an element approaches the viewport. Once triggered,
29
+ * the observer disconnects (one-shot behavior).
30
+ *
31
+ * @param options - Configuration options
32
+ * @returns Object with ref callback and inView state
33
+ *
34
+ * @example
35
+ * ```tsx
36
+ * function LazyComponent() {
37
+ * const { ref, inView } = useLazyLoad({ rootMargin: '200px' });
38
+ *
39
+ * return (
40
+ * <div ref={ref}>
41
+ * {inView && <ExpensiveContent />}
42
+ * </div>
43
+ * );
44
+ * }
45
+ * ```
46
+ */
47
+ declare function useLazyLoad(options?: LazyLoadOptions): LazyLoadResult;
48
+ /**
49
+ * Returns appropriate image quality based on network conditions.
50
+ *
51
+ * Uses the Network Information API when available to detect:
52
+ * - Data saver mode (returns lowest quality)
53
+ * - Effective connection type (slow-2g, 2g, 3g, 4g)
54
+ *
55
+ * Falls back to default quality (80) when:
56
+ * - Running on server (SSR)
57
+ * - Network Information API not supported
58
+ *
59
+ * @returns Quality value between 30-80
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const quality = getConnectionAwareQuality();
64
+ * const imageUrl = `${baseUrl}?q=${quality}`;
65
+ * ```
66
+ */
67
+ declare function getConnectionAwareQuality(): number;
68
+ /**
69
+ * Check if the user prefers reduced data usage.
70
+ *
71
+ * @returns true if Save-Data is enabled or connection is slow
72
+ */
73
+ declare function prefersReducedData(): boolean;
74
+
75
+ export { type LazyLoadOptions, type LazyLoadResult, getConnectionAwareQuality, prefersReducedData, useLazyLoad };
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ // lib/image/lazy-load.ts
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ function useLazyLoad(options = {}) {
6
+ const { rootMargin = "200px", threshold = 0, onEnter } = options;
7
+ const [inView, setInView] = useState(false);
8
+ const observerRef = useRef(null);
9
+ const ref = useCallback(
10
+ (node) => {
11
+ if (observerRef.current) {
12
+ observerRef.current.disconnect();
13
+ observerRef.current = null;
14
+ }
15
+ if (!node) return;
16
+ observerRef.current = new IntersectionObserver(
17
+ ([entry]) => {
18
+ if (entry?.isIntersecting) {
19
+ setInView(true);
20
+ onEnter?.();
21
+ observerRef.current?.disconnect();
22
+ observerRef.current = null;
23
+ }
24
+ },
25
+ { rootMargin, threshold }
26
+ );
27
+ observerRef.current.observe(node);
28
+ },
29
+ [rootMargin, threshold, onEnter]
30
+ );
31
+ useEffect(() => {
32
+ return () => {
33
+ if (observerRef.current) {
34
+ observerRef.current.disconnect();
35
+ observerRef.current = null;
36
+ }
37
+ };
38
+ }, []);
39
+ return { ref, inView };
40
+ }
41
+ var QUALITY_PRESETS = {
42
+ "slow-2g": 30,
43
+ "2g": 50,
44
+ "3g": 65,
45
+ "4g": 80,
46
+ saveData: 40,
47
+ default: 80
48
+ };
49
+ function getConnectionAwareQuality() {
50
+ if (typeof navigator === "undefined") {
51
+ return QUALITY_PRESETS.default;
52
+ }
53
+ const nav = navigator;
54
+ const connection = nav.connection;
55
+ if (!connection) {
56
+ return QUALITY_PRESETS.default;
57
+ }
58
+ if (connection.saveData) {
59
+ return QUALITY_PRESETS.saveData;
60
+ }
61
+ const effectiveType = connection.effectiveType;
62
+ if (effectiveType && effectiveType in QUALITY_PRESETS) {
63
+ return QUALITY_PRESETS[effectiveType];
64
+ }
65
+ return QUALITY_PRESETS.default;
66
+ }
67
+ function prefersReducedData() {
68
+ if (typeof navigator === "undefined") {
69
+ return false;
70
+ }
71
+ const nav = navigator;
72
+ const connection = nav.connection;
73
+ if (!connection) {
74
+ return false;
75
+ }
76
+ return connection.saveData === true || connection.effectiveType === "slow-2g" || connection.effectiveType === "2g";
77
+ }
78
+ export {
79
+ getConnectionAwareQuality,
80
+ prefersReducedData,
81
+ useLazyLoad
82
+ };
83
+ //# sourceMappingURL=lazy-load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../lib/image/lazy-load.ts"],"sourcesContent":["/**\n * Lazy Loading Utilities\n *\n * Reusable IntersectionObserver-based lazy loading with\n * connection-aware quality adjustment.\n *\n * This module provides:\n * - useLazyLoad: Hook for deferred loading when elements enter viewport\n * - getConnectionAwareQuality: Adjusts image quality based on network speed\n */\n\n'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface LazyLoadOptions {\n /** Root margin for early loading (default: \"200px\") */\n rootMargin?: string;\n /** Threshold for intersection (default: 0) */\n threshold?: number;\n /** Callback when element enters viewport */\n onEnter?: () => void;\n}\n\nexport interface LazyLoadResult {\n /** Callback ref to attach to the element */\n ref: (node: HTMLElement | null) => void;\n /** Whether the element has entered the viewport */\n inView: boolean;\n}\n\n// =============================================================================\n// useLazyLoad Hook\n// =============================================================================\n\n/**\n * IntersectionObserver-based lazy loading hook.\n *\n * Triggers when an element approaches the viewport. Once triggered,\n * the observer disconnects (one-shot behavior).\n *\n * @param options - Configuration options\n * @returns Object with ref callback and inView state\n *\n * @example\n * ```tsx\n * function LazyComponent() {\n * const { ref, inView } = useLazyLoad({ rootMargin: '200px' });\n *\n * return (\n * <div ref={ref}>\n * {inView && <ExpensiveContent />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useLazyLoad(options: LazyLoadOptions = {}): LazyLoadResult {\n const { rootMargin = '200px', threshold = 0, onEnter } = options;\n const [inView, setInView] = useState(false);\n const observerRef = useRef<IntersectionObserver | null>(null);\n\n const ref = useCallback(\n (node: HTMLElement | null) => {\n // Disconnect previous observer\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n\n // No node to observe\n if (!node) return;\n\n // Create new observer\n observerRef.current = new IntersectionObserver(\n ([entry]) => {\n if (entry?.isIntersecting) {\n setInView(true);\n onEnter?.();\n // Disconnect after first intersection (one-shot)\n observerRef.current?.disconnect();\n observerRef.current = null;\n }\n },\n { rootMargin, threshold }\n );\n\n observerRef.current.observe(node);\n },\n [rootMargin, threshold, onEnter]\n );\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (observerRef.current) {\n observerRef.current.disconnect();\n observerRef.current = null;\n }\n };\n }, []);\n\n return { ref, inView };\n}\n\n// =============================================================================\n// Connection-Aware Quality\n// =============================================================================\n\n/**\n * Network Information API types (not in standard TypeScript lib).\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API\n */\ninterface NetworkInformation {\n effectiveType?: 'slow-2g' | '2g' | '3g' | '4g';\n downlink?: number;\n saveData?: boolean;\n}\n\ninterface NavigatorWithConnection extends Navigator {\n connection?: NetworkInformation;\n}\n\n/**\n * Quality presets based on connection type.\n */\nconst QUALITY_PRESETS = {\n 'slow-2g': 30,\n '2g': 50,\n '3g': 65,\n '4g': 80,\n saveData: 40,\n default: 80,\n} as const;\n\n/**\n * Returns appropriate image quality based on network conditions.\n *\n * Uses the Network Information API when available to detect:\n * - Data saver mode (returns lowest quality)\n * - Effective connection type (slow-2g, 2g, 3g, 4g)\n *\n * Falls back to default quality (80) when:\n * - Running on server (SSR)\n * - Network Information API not supported\n *\n * @returns Quality value between 30-80\n *\n * @example\n * ```typescript\n * const quality = getConnectionAwareQuality();\n * const imageUrl = `${baseUrl}?q=${quality}`;\n * ```\n */\nexport function getConnectionAwareQuality(): number {\n // SSR fallback\n if (typeof navigator === 'undefined') {\n return QUALITY_PRESETS.default;\n }\n\n const nav = navigator as NavigatorWithConnection;\n const connection = nav.connection;\n\n // No Network Information API support\n if (!connection) {\n return QUALITY_PRESETS.default;\n }\n\n // User has enabled data saver - respect their preference\n if (connection.saveData) {\n return QUALITY_PRESETS.saveData;\n }\n\n // Adjust based on effective connection type\n const effectiveType = connection.effectiveType;\n if (effectiveType && effectiveType in QUALITY_PRESETS) {\n return QUALITY_PRESETS[effectiveType];\n }\n\n return QUALITY_PRESETS.default;\n}\n\n/**\n * Check if the user prefers reduced data usage.\n *\n * @returns true if Save-Data is enabled or connection is slow\n */\nexport function prefersReducedData(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n\n const nav = navigator as NavigatorWithConnection;\n const connection = nav.connection;\n\n if (!connection) {\n return false;\n }\n\n return (\n connection.saveData === true ||\n connection.effectiveType === 'slow-2g' ||\n connection.effectiveType === '2g'\n );\n}\n"],"mappings":";;;AAaA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAgDlD,SAAS,YAAY,UAA2B,CAAC,GAAmB;AACzE,QAAM,EAAE,aAAa,SAAS,YAAY,GAAG,QAAQ,IAAI;AACzD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,cAAc,OAAoC,IAAI;AAE5D,QAAM,MAAM;AAAA,IACV,CAAC,SAA6B;AAE5B,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ,WAAW;AAC/B,oBAAY,UAAU;AAAA,MACxB;AAGA,UAAI,CAAC,KAAM;AAGX,kBAAY,UAAU,IAAI;AAAA,QACxB,CAAC,CAAC,KAAK,MAAM;AACX,cAAI,OAAO,gBAAgB;AACzB,sBAAU,IAAI;AACd,sBAAU;AAEV,wBAAY,SAAS,WAAW;AAChC,wBAAY,UAAU;AAAA,UACxB;AAAA,QACF;AAAA,QACA,EAAE,YAAY,UAAU;AAAA,MAC1B;AAEA,kBAAY,QAAQ,QAAQ,IAAI;AAAA,IAClC;AAAA,IACA,CAAC,YAAY,WAAW,OAAO;AAAA,EACjC;AAGA,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ,WAAW;AAC/B,oBAAY,UAAU;AAAA,MACxB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,KAAK,OAAO;AACvB;AAuBA,IAAM,kBAAkB;AAAA,EACtB,WAAW;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV,SAAS;AACX;AAqBO,SAAS,4BAAoC;AAElD,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO,gBAAgB;AAAA,EACzB;AAEA,QAAM,MAAM;AACZ,QAAM,aAAa,IAAI;AAGvB,MAAI,CAAC,YAAY;AACf,WAAO,gBAAgB;AAAA,EACzB;AAGA,MAAI,WAAW,UAAU;AACvB,WAAO,gBAAgB;AAAA,EACzB;AAGA,QAAM,gBAAgB,WAAW;AACjC,MAAI,iBAAiB,iBAAiB,iBAAiB;AACrD,WAAO,gBAAgB,aAAa;AAAA,EACtC;AAEA,SAAO,gBAAgB;AACzB;AAOO,SAAS,qBAA8B;AAC5C,MAAI,OAAO,cAAc,aAAa;AACpC,WAAO;AAAA,EACT;AAEA,QAAM,MAAM;AACZ,QAAM,aAAa,IAAI;AAEvB,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,SACE,WAAW,aAAa,QACxB,WAAW,kBAAkB,aAC7B,WAAW,kBAAkB;AAEjC;","names":[]}
@@ -0,0 +1,172 @@
1
+ import { MDTree } from 'md4w';
2
+ import { Result } from './result.js';
3
+
4
+ /**
5
+ * Markdown Parsing Utilities
6
+ *
7
+ * Three implementations showing the progression from naive to production-ready:
8
+ * - v1 (Naive): Direct call, crashes on bad input
9
+ * - v2 (Defensive): Try-catch with null fallback
10
+ * - v3 (Robust): Result type, validation, sanitization
11
+ *
12
+ * The robust version (v3) is exported as the default `parseMarkdown`.
13
+ */
14
+
15
+ /**
16
+ * Error codes for markdown parsing failures.
17
+ */
18
+ declare const ParseErrorCode: {
19
+ readonly INVALID_INPUT: "INVALID_INPUT";
20
+ readonly EMPTY_CONTENT: "EMPTY_CONTENT";
21
+ readonly PARSE_FAILED: "PARSE_FAILED";
22
+ readonly CONTENT_TOO_LONG: "CONTENT_TOO_LONG";
23
+ readonly NESTED_TOO_DEEP: "NESTED_TOO_DEEP";
24
+ };
25
+ type ParseErrorCode = (typeof ParseErrorCode)[keyof typeof ParseErrorCode];
26
+ /**
27
+ * Structured error for markdown parsing failures.
28
+ */
29
+ interface ParseError {
30
+ code: ParseErrorCode;
31
+ message: string;
32
+ cause?: Error;
33
+ }
34
+ /**
35
+ * Options for robust markdown parsing.
36
+ */
37
+ interface ParseOptions {
38
+ /**
39
+ * Maximum content length in characters.
40
+ * @default 500000 (500KB of text, ~100K words)
41
+ */
42
+ maxLength?: number;
43
+ /**
44
+ * Whether to sanitize XSS attempts in the output.
45
+ * @default true
46
+ */
47
+ sanitize?: boolean;
48
+ /**
49
+ * Whether to allow empty content.
50
+ * @default false
51
+ */
52
+ allowEmpty?: boolean;
53
+ }
54
+ /**
55
+ * Naive markdown parser - crashes on bad input.
56
+ *
57
+ * DO NOT USE IN PRODUCTION. This is for demonstration only.
58
+ *
59
+ * Problems:
60
+ * - No input validation
61
+ * - No error handling
62
+ * - Will crash the entire app on malformed input
63
+ * - No protection against XSS
64
+ * - No content limits
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // This crashes if content is null, undefined, or malformed
69
+ * const ast = parseMarkdownV1(userInput);
70
+ * ```
71
+ */
72
+ declare function parseMarkdownV1(content: string): MDTree;
73
+ /**
74
+ * Defensive markdown parser - catches errors but loses context.
75
+ *
76
+ * Better than v1 but still problematic:
77
+ * - Returns null on any error (loses error details)
78
+ * - Caller can't distinguish between empty content and parse failure
79
+ * - Still no XSS protection
80
+ * - Still no content limits
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const ast = parseMarkdownV2(userInput);
85
+ * if (!ast) {
86
+ * // What went wrong? We don't know.
87
+ * return <ErrorFallback />;
88
+ * }
89
+ * ```
90
+ */
91
+ declare function parseMarkdownV2(content: string): MDTree | null;
92
+ /**
93
+ * Robust markdown parser - production-ready with full error context.
94
+ *
95
+ * Features:
96
+ * - Input validation with specific error codes
97
+ * - Result type for explicit error handling
98
+ * - XSS pattern detection and optional sanitization
99
+ * - Content length limits
100
+ * - Preserves error context for debugging
101
+ *
102
+ * @param content - Markdown string to parse
103
+ * @param options - Parsing options
104
+ * @returns Result containing the AST or a structured error
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const result = parseMarkdownV3(userInput);
109
+ *
110
+ * if (!result.ok) {
111
+ * switch (result.error.code) {
112
+ * case 'INVALID_INPUT':
113
+ * return <InvalidInputError message={result.error.message} />;
114
+ * case 'CONTENT_TOO_LONG':
115
+ * return <ContentTooLongError />;
116
+ * case 'PARSE_FAILED':
117
+ * console.error('Parse error:', result.error.cause);
118
+ * return <ParseError />;
119
+ * default:
120
+ * return <GenericError />;
121
+ * }
122
+ * }
123
+ *
124
+ * return <MarkdownRenderer ast={result.value} />;
125
+ * ```
126
+ */
127
+ declare function parseMarkdownV3(content: string, options?: ParseOptions): Result<MDTree, ParseError>;
128
+ /**
129
+ * Production-ready markdown parser.
130
+ *
131
+ * This is an alias for `parseMarkdownV3` - the robust implementation
132
+ * with validation, sanitization, and Result-based error handling.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * import { parseMarkdown } from '@/lib/markdown-utils';
137
+ *
138
+ * const result = parseMarkdown(content);
139
+ * if (!result.ok) {
140
+ * // Handle error with full context
141
+ * console.error(`[${result.error.code}] ${result.error.message}`);
142
+ * return null;
143
+ * }
144
+ * return result.value;
145
+ * ```
146
+ */
147
+ declare const parseMarkdown: typeof parseMarkdownV3;
148
+ /**
149
+ * Checks if content is safe markdown (no XSS patterns detected).
150
+ *
151
+ * @param content - Markdown string to check
152
+ * @returns true if no XSS patterns detected
153
+ */
154
+ declare function isSafeMarkdown(content: string): boolean;
155
+ /**
156
+ * Estimates the word count of markdown content.
157
+ * Useful for content length limits and reading time estimates.
158
+ *
159
+ * @param content - Markdown string to count
160
+ * @returns Estimated word count
161
+ */
162
+ declare function estimateWordCount(content: string): number;
163
+ /**
164
+ * Estimates reading time for markdown content.
165
+ *
166
+ * @param content - Markdown string
167
+ * @param wordsPerMinute - Reading speed (default 200 WPM)
168
+ * @returns Reading time in minutes
169
+ */
170
+ declare function estimateReadingTime(content: string, wordsPerMinute?: number): number;
171
+
172
+ export { type ParseError, ParseErrorCode, type ParseOptions, estimateReadingTime, estimateWordCount, isSafeMarkdown, parseMarkdown, parseMarkdownV1, parseMarkdownV2, parseMarkdownV3 };