@autumnsgrove/groveengine 0.6.2 → 0.6.4

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 (79) hide show
  1. package/LICENSE +378 -0
  2. package/dist/auth/jwt.d.ts +10 -4
  3. package/dist/auth/jwt.js +18 -4
  4. package/dist/auth/session.d.ts +22 -15
  5. package/dist/auth/session.js +35 -16
  6. package/dist/components/admin/GutterManager.svelte +81 -139
  7. package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
  8. package/dist/components/admin/MarkdownEditor.svelte +80 -23
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
  10. package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
  11. package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
  12. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
  13. package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
  14. package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
  15. package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
  16. package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
  17. package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
  18. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
  19. package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
  20. package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
  21. package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
  22. package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
  23. package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
  24. package/dist/components/custom/ContentWithGutter.svelte +53 -23
  25. package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
  26. package/dist/components/custom/GutterItem.svelte +1 -1
  27. package/dist/components/custom/LeftGutter.svelte +43 -13
  28. package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
  29. package/dist/config/ai-models.js +1 -1
  30. package/dist/groveauth/client.js +11 -11
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +2 -2
  33. package/dist/server/logger.d.ts +74 -26
  34. package/dist/server/logger.js +133 -184
  35. package/dist/server/services/cache.js +1 -10
  36. package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
  37. package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
  38. package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
  39. package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
  40. package/dist/ui/components/charts/Sparkline.svelte +18 -7
  41. package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
  42. package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
  43. package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
  44. package/dist/ui/components/gallery/Lightbox.svelte +5 -2
  45. package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
  46. package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
  47. package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
  48. package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
  49. package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
  50. package/dist/ui/components/ui/Button.svelte +5 -0
  51. package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
  52. package/dist/ui/components/ui/Input.svelte +4 -0
  53. package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
  54. package/dist/ui/components/ui/Logo.svelte +86 -0
  55. package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
  56. package/dist/ui/components/ui/LogoLoader.svelte +71 -0
  57. package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
  58. package/dist/ui/components/ui/index.d.ts +2 -0
  59. package/dist/ui/components/ui/index.js +2 -0
  60. package/dist/ui/tailwind.preset.js +8 -8
  61. package/dist/utils/api.js +2 -1
  62. package/dist/utils/debounce.d.ts +4 -3
  63. package/dist/utils/debounce.js +10 -6
  64. package/dist/utils/gallery.d.ts +58 -32
  65. package/dist/utils/gallery.js +111 -129
  66. package/dist/utils/gutter.d.ts +47 -26
  67. package/dist/utils/gutter.js +116 -124
  68. package/dist/utils/imageProcessor.d.ts +66 -19
  69. package/dist/utils/imageProcessor.js +31 -10
  70. package/dist/utils/index.d.ts +11 -11
  71. package/dist/utils/index.js +4 -3
  72. package/dist/utils/json.js +1 -1
  73. package/dist/utils/markdown.d.ts +183 -103
  74. package/dist/utils/markdown.js +517 -678
  75. package/dist/utils/sanitize.d.ts +22 -12
  76. package/dist/utils/sanitize.js +268 -282
  77. package/dist/utils/validation.js +4 -3
  78. package/package.json +23 -23
  79. package/static/fonts/alagard.ttf +0 -0
@@ -1,29 +1,28 @@
1
1
  import { marked } from "marked";
2
2
  import matter from "gray-matter";
3
- import { sanitizeMarkdown } from './sanitize.js';
4
-
3
+ import { sanitizeMarkdown } from "./sanitize.js";
4
+ // ============================================================================
5
+ // Marked Configuration
6
+ // ============================================================================
5
7
  // Configure marked renderer for GitHub-style code blocks
6
8
  const renderer = new marked.Renderer();
7
9
  renderer.code = function (token) {
8
- // Handle both old (code, language) and new (token) API signatures
9
- const code = typeof token === "string" ? token : token.text;
10
- const language = typeof token === "string" ? arguments[1] : token.lang;
11
-
12
- const lang = language || "text";
13
-
14
- // Render markdown/md code blocks as formatted HTML (like GitHub)
15
- if (lang === "markdown" || lang === "md") {
16
- // Parse the markdown content and render it
17
- const renderedContent = marked.parse(code);
18
- // Escape the raw markdown for the copy button
19
- const escapedCode = code
20
- .replace(/&/g, "&")
21
- .replace(/</g, "&lt;")
22
- .replace(/>/g, "&gt;")
23
- .replace(/"/g, "&quot;")
24
- .replace(/'/g, "&#39;");
25
-
26
- return `<div class="rendered-markdown-block">
10
+ // Handle both old (code, language) and new (token) API signatures
11
+ const code = typeof token === "string" ? token : token.text;
12
+ const language = typeof token === "string" ? arguments[1] : token.lang;
13
+ const lang = language || "text";
14
+ // Render markdown/md code blocks as formatted HTML (like GitHub)
15
+ if (lang === "markdown" || lang === "md") {
16
+ // Parse the markdown content and render it
17
+ const renderedContent = marked.parse(code, { async: false });
18
+ // Escape the raw markdown for the copy button
19
+ const escapedCode = code
20
+ .replace(/&/g, "&amp;")
21
+ .replace(/</g, "&lt;")
22
+ .replace(/>/g, "&gt;")
23
+ .replace(/"/g, "&quot;")
24
+ .replace(/'/g, "&#39;");
25
+ return `<div class="rendered-markdown-block">
27
26
  <div class="rendered-markdown-header">
28
27
  <span class="rendered-markdown-label">Markdown</span>
29
28
  <button class="code-block-copy" aria-label="Copy markdown to clipboard" data-code="${escapedCode}">
@@ -38,16 +37,14 @@ renderer.code = function (token) {
38
37
  ${renderedContent}
39
38
  </div>
40
39
  </div>`;
41
- }
42
-
43
- const escapedCode = code
44
- .replace(/&/g, "&amp;")
45
- .replace(/</g, "&lt;")
46
- .replace(/>/g, "&gt;")
47
- .replace(/"/g, "&quot;")
48
- .replace(/'/g, "&#39;");
49
-
50
- return `<div class="code-block-wrapper">
40
+ }
41
+ const escapedCode = code
42
+ .replace(/&/g, "&amp;")
43
+ .replace(/</g, "&lt;")
44
+ .replace(/>/g, "&gt;")
45
+ .replace(/"/g, "&quot;")
46
+ .replace(/'/g, "&#39;");
47
+ return `<div class="code-block-wrapper">
51
48
  <div class="code-block-header">
52
49
  <span class="code-block-language">${lang}</span>
53
50
  <button class="code-block-copy" aria-label="Copy code to clipboard" data-code="${escapedCode}">
@@ -61,780 +58,622 @@ renderer.code = function (token) {
61
58
  <pre><code class="language-${lang}">${escapedCode}</code></pre>
62
59
  </div>`;
63
60
  };
64
-
65
61
  marked.setOptions({
66
- renderer: renderer,
67
- gfm: true,
68
- breaks: false,
62
+ renderer: renderer,
63
+ gfm: true,
64
+ breaks: false,
69
65
  });
70
-
66
+ // ============================================================================
67
+ // Utility Functions
68
+ // ============================================================================
71
69
  /**
72
70
  * Validates if a string is a valid URL
73
- * @param {string} urlString - The string to validate as a URL
74
- * @returns {boolean} True if the string is a valid URL, false otherwise
75
71
  */
76
72
  function isValidUrl(urlString) {
77
- try {
78
- const url = new URL(urlString);
79
- return url.protocol === "http:" || url.protocol === "https:";
80
- } catch {
81
- return false;
82
- }
73
+ try {
74
+ const url = new URL(urlString);
75
+ return url.protocol === "http:" || url.protocol === "https:";
76
+ }
77
+ catch {
78
+ return false;
79
+ }
83
80
  }
84
-
85
81
  /**
86
82
  * Extract headers from markdown content for table of contents
87
- * @param {string} markdown - The raw markdown content
88
- * @returns {Array} Array of header objects with level, text, and id
89
83
  */
90
84
  export function extractHeaders(markdown) {
91
- const headers = [];
92
-
93
- // Remove fenced code blocks before extracting headers
94
- // This prevents # comments inside code blocks from being treated as headers
95
- const markdownWithoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, "");
96
-
97
- const headerRegex = /^(#{1,6})\s+(.+)$/gm;
98
-
99
- let match;
100
- while ((match = headerRegex.exec(markdownWithoutCodeBlocks)) !== null) {
101
- const level = match[1].length;
102
- const text = match[2].trim();
103
- // Create a slug-style ID from the header text
104
- const id = text
105
- .toLowerCase()
106
- .replace(/[^\w\s-]/g, "")
107
- .replace(/\s+/g, "-")
108
- .replace(/-+/g, "-")
109
- .trim();
110
-
111
- headers.push({
112
- level,
113
- text,
114
- id,
115
- });
116
- }
117
-
118
- return headers;
85
+ const headers = [];
86
+ // Remove fenced code blocks before extracting headers
87
+ // This prevents # comments inside code blocks from being treated as headers
88
+ const markdownWithoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, "");
89
+ const headerRegex = /^(#{1,6})\s+(.+)$/gm;
90
+ let match;
91
+ while ((match = headerRegex.exec(markdownWithoutCodeBlocks)) !== null) {
92
+ const level = match[1].length;
93
+ const text = match[2].trim();
94
+ // Create a slug-style ID from the header text
95
+ const id = text
96
+ .toLowerCase()
97
+ .replace(/[^\w\s-]/g, "")
98
+ .replace(/\s+/g, "-")
99
+ .replace(/-+/g, "-")
100
+ .trim();
101
+ headers.push({
102
+ level,
103
+ text,
104
+ id,
105
+ });
106
+ }
107
+ return headers;
119
108
  }
120
-
121
109
  /**
122
110
  * Process anchor tags in HTML content
123
111
  * Converts <!-- anchor:tagname --> comments to identifiable span elements
124
- * @param {string} html - The HTML content
125
- * @returns {string} HTML with anchor markers converted to spans
126
112
  */
127
113
  export function processAnchorTags(html) {
128
- // Convert <!-- anchor:tagname --> to <span class="anchor-marker" data-anchor="tagname"></span>
129
- // Supports alphanumeric characters, underscores, and hyphens in tag names
130
- return html.replace(
131
- /<!--\s*anchor:([\w-]+)\s*-->/g,
132
- (match, tagname) =>
133
- `<span class="anchor-marker" data-anchor="${tagname}"></span>`,
134
- );
114
+ // Convert <!-- anchor:tagname --> to <span class="anchor-marker" data-anchor="tagname"></span>
115
+ // Supports alphanumeric characters, underscores, and hyphens in tag names
116
+ return html.replace(/<!--\s*anchor:([\w-]+)\s*-->/g, (_match, tagname) => `<span class="anchor-marker" data-anchor="${tagname}"></span>`);
135
117
  }
136
-
137
118
  /**
138
119
  * Parse markdown content and convert to HTML
139
- * @param {string} markdownContent - The raw markdown content (may include frontmatter)
140
- * @returns {Object} Object with data (frontmatter), content (HTML), headers, and raw markdown
141
120
  */
142
121
  export function parseMarkdownContent(markdownContent) {
143
- const { data, content: markdown } = matter(markdownContent);
144
-
145
- let htmlContent = marked.parse(markdown);
146
-
147
- // Process anchor tags in the HTML content
148
- htmlContent = processAnchorTags(htmlContent);
149
-
150
- // Extract headers for table of contents
151
- const headers = extractHeaders(markdown);
152
-
153
- return {
154
- data,
155
- content: htmlContent,
156
- headers,
157
- rawMarkdown: markdown,
158
- };
122
+ const { data, content: markdown } = matter(markdownContent);
123
+ let htmlContent = marked.parse(markdown, { async: false });
124
+ // Process anchor tags in the HTML content
125
+ htmlContent = processAnchorTags(htmlContent);
126
+ // Extract headers for table of contents
127
+ const headers = extractHeaders(markdown);
128
+ return {
129
+ data: data,
130
+ content: htmlContent,
131
+ headers,
132
+ rawMarkdown: markdown,
133
+ };
159
134
  }
160
-
161
135
  /**
162
136
  * Parse markdown content with sanitization (for user-facing pages like home, about, contact)
163
- * @param {string} markdownContent - The raw markdown content (may include frontmatter)
164
- * @returns {Object} Object with data (frontmatter), content (sanitized HTML), headers
165
137
  */
166
138
  export function parseMarkdownContentSanitized(markdownContent) {
167
- const { data, content: markdown } = matter(markdownContent);
168
- const htmlContent = sanitizeMarkdown(marked.parse(markdown));
169
- const headers = extractHeaders(markdown);
170
-
171
- return {
172
- data,
173
- content: htmlContent,
174
- headers,
175
- };
139
+ const { data, content: markdown } = matter(markdownContent);
140
+ const htmlContent = sanitizeMarkdown(marked.parse(markdown, { async: false }));
141
+ const headers = extractHeaders(markdown);
142
+ return {
143
+ data: data,
144
+ content: htmlContent,
145
+ headers,
146
+ };
176
147
  }
177
-
178
148
  /**
179
149
  * Get gutter content from provided modules
180
150
  * This is a utility function that processes gutter manifests, markdown, and images
181
- *
182
- * @param {string} slug - The page/post slug
183
- * @param {Object} manifestModules - The manifest modules (from import.meta.glob)
184
- * @param {Object} markdownModules - The markdown modules (from import.meta.glob)
185
- * @param {Object} imageModules - The image modules (from import.meta.glob)
186
- * @returns {Array} Array of gutter items with content and position info
187
151
  */
188
- export function processGutterContent(
189
- slug,
190
- manifestModules,
191
- markdownModules,
192
- imageModules,
193
- ) {
194
- // Find the manifest file for this page/post
195
- const manifestEntry = Object.entries(manifestModules).find(([filepath]) => {
196
- const parts = filepath.split("/");
197
- const folder = parts[parts.length - 3]; // Get the folder name
198
- return folder === slug;
199
- });
200
-
201
- if (!manifestEntry) {
202
- return [];
203
- }
204
-
205
- const manifest = manifestEntry[1].default || manifestEntry[1];
206
-
207
- if (!manifest.items || !Array.isArray(manifest.items)) {
208
- return [];
209
- }
210
-
211
- // Process each gutter item
212
- return manifest.items
213
- .map((item) => {
214
- if (item.type === "comment" || item.type === "markdown") {
215
- // Find the markdown content file
216
- const mdEntry = Object.entries(markdownModules).find(([filepath]) => {
217
- return filepath.includes(`/${slug}/gutter/${item.file}`);
218
- });
219
-
220
- if (mdEntry) {
221
- const markdownContent = mdEntry[1];
222
- const htmlContent = marked.parse(markdownContent);
223
-
224
- return {
225
- ...item,
226
- content: htmlContent,
227
- };
228
- }
229
- } else if (item.type === "photo" || item.type === "image") {
230
- // Check if file is an external URL
231
- if (item.file && isValidUrl(item.file)) {
232
- return {
233
- ...item,
234
- src: item.file,
235
- };
152
+ export function processGutterContent(slug, manifestModules, markdownModules, imageModules) {
153
+ // Find the manifest file for this page/post
154
+ const manifestEntry = Object.entries(manifestModules).find(([filepath]) => {
155
+ const parts = filepath.split("/");
156
+ const folder = parts[parts.length - 3]; // Get the folder name
157
+ return folder === slug;
158
+ });
159
+ if (!manifestEntry) {
160
+ return [];
161
+ }
162
+ const manifestData = manifestEntry[1];
163
+ const manifest = "default" in manifestData ? manifestData.default : manifestData;
164
+ if (!manifest.items || !Array.isArray(manifest.items)) {
165
+ return [];
166
+ }
167
+ // Process each gutter item
168
+ return manifest.items
169
+ .map((item) => {
170
+ // Destructure to separate images from other properties
171
+ // This ensures proper type handling when spreading
172
+ const { images: rawImages, ...baseItem } = item;
173
+ if (item.type === "comment" || item.type === "markdown") {
174
+ // Find the markdown content file
175
+ const mdEntry = Object.entries(markdownModules).find(([filepath]) => {
176
+ return filepath.includes(`/${slug}/gutter/${item.file}`);
177
+ });
178
+ if (mdEntry) {
179
+ const markdownContent = mdEntry[1];
180
+ const htmlContent = marked.parse(markdownContent, { async: false });
181
+ return {
182
+ ...baseItem,
183
+ content: htmlContent,
184
+ };
185
+ }
236
186
  }
237
-
238
- // Find the local image file
239
- const imgEntry = Object.entries(imageModules).find(([filepath]) => {
240
- return filepath.includes(`/${slug}/gutter/${item.file}`);
241
- });
242
-
243
- if (imgEntry) {
244
- return {
245
- ...item,
246
- src: imgEntry[1],
247
- };
187
+ else if (item.type === "photo" || item.type === "image") {
188
+ // Check if file is an external URL
189
+ if (item.file && isValidUrl(item.file)) {
190
+ return {
191
+ ...baseItem,
192
+ src: item.file,
193
+ };
194
+ }
195
+ // Find the local image file
196
+ const imgEntry = Object.entries(imageModules).find(([filepath]) => {
197
+ return filepath.includes(`/${slug}/gutter/${item.file}`);
198
+ });
199
+ if (imgEntry) {
200
+ return {
201
+ ...baseItem,
202
+ src: imgEntry[1],
203
+ };
204
+ }
248
205
  }
249
- } else if (item.type === "emoji") {
250
- // Emoji items can use URLs (local or CDN) or local files
251
- if (item.url) {
252
- // Direct URL (local path like /icons/instruction/mix.webp or CDN URL)
253
- return {
254
- ...item,
255
- src: item.url,
256
- };
257
- } else if (item.file) {
258
- // Local file in gutter directory
259
- const imgEntry = Object.entries(imageModules).find(([filepath]) => {
260
- return filepath.includes(`/${slug}/gutter/${item.file}`);
261
- });
262
-
263
- if (imgEntry) {
264
- return {
265
- ...item,
266
- src: imgEntry[1],
267
- };
268
- }
206
+ else if (item.type === "emoji") {
207
+ // Emoji items can use URLs (local or CDN) or local files
208
+ if (item.url) {
209
+ // Direct URL (local path like /icons/instruction/mix.webp or CDN URL)
210
+ return {
211
+ ...baseItem,
212
+ src: item.url,
213
+ };
214
+ }
215
+ else if (item.file) {
216
+ // Local file in gutter directory
217
+ const imgEntry = Object.entries(imageModules).find(([filepath]) => {
218
+ return filepath.includes(`/${slug}/gutter/${item.file}`);
219
+ });
220
+ if (imgEntry) {
221
+ return {
222
+ ...baseItem,
223
+ src: imgEntry[1],
224
+ };
225
+ }
226
+ }
227
+ return baseItem;
269
228
  }
270
- return item;
271
- } else if (item.type === "gallery") {
272
- /**
273
- * Process gallery items containing multiple images
274
- *
275
- * Galleries can contain:
276
- * - External URLs (validated for http/https protocol)
277
- * - Local files (resolved from the gutter directory)
278
- *
279
- * Images that fail to resolve (invalid URLs or missing files) are filtered out.
280
- * If all images fail to resolve, the entire gallery item is excluded.
281
- */
282
- const originalImageCount = (item.images || []).length;
283
- const images = (item.images || [])
284
- .map((img) => {
285
- // Check if it's an external URL
286
- if (img.url) {
287
- // Validate URL format to prevent malformed URLs from failing silently
288
- if (!isValidUrl(img.url)) {
289
- console.warn(
290
- `Invalid URL in gallery for "${slug}": ${img.url}`,
291
- );
229
+ else if (item.type === "gallery") {
230
+ /**
231
+ * Process gallery items containing multiple images
232
+ *
233
+ * Galleries can contain:
234
+ * - External URLs (validated for http/https protocol)
235
+ * - Local files (resolved from the gutter directory)
236
+ *
237
+ * Images that fail to resolve (invalid URLs or missing files) are filtered out.
238
+ * If all images fail to resolve, the entire gallery item is excluded.
239
+ */
240
+ const originalImageCount = (rawImages || []).length;
241
+ const images = (rawImages || [])
242
+ .map((img) => {
243
+ // Check if it's an external URL
244
+ if (img.url) {
245
+ // Validate URL format to prevent malformed URLs from failing silently
246
+ if (!isValidUrl(img.url)) {
247
+ console.warn(`Invalid URL in gallery for "${slug}": ${img.url}`);
248
+ return null;
249
+ }
250
+ return {
251
+ url: img.url,
252
+ alt: img.alt || "",
253
+ caption: img.caption || "",
254
+ };
255
+ }
256
+ // Otherwise, look for local file
257
+ if (img.file) {
258
+ const imgEntry = Object.entries(imageModules).find(([filepath]) => {
259
+ return filepath.includes(`/${slug}/gutter/${img.file}`);
260
+ });
261
+ if (imgEntry) {
262
+ return {
263
+ url: imgEntry[1],
264
+ alt: img.alt || "",
265
+ caption: img.caption || "",
266
+ };
267
+ }
268
+ else {
269
+ console.warn(`Local file not found in gallery for "${slug}": ${img.file}`);
270
+ }
271
+ }
292
272
  return null;
293
- }
294
- return {
295
- url: img.url,
296
- alt: img.alt || "",
297
- caption: img.caption || "",
298
- };
299
- }
300
-
301
- // Otherwise, look for local file
302
- if (img.file) {
303
- const imgEntry = Object.entries(imageModules).find(
304
- ([filepath]) => {
305
- return filepath.includes(`/${slug}/gutter/${img.file}`);
306
- },
307
- );
308
-
309
- if (imgEntry) {
273
+ })
274
+ .filter((img) => img !== null);
275
+ if (images.length > 0) {
310
276
  return {
311
- url: imgEntry[1],
312
- alt: img.alt || "",
313
- caption: img.caption || "",
277
+ ...baseItem,
278
+ images,
314
279
  };
315
- } else {
316
- console.warn(
317
- `Local file not found in gallery for "${slug}": ${img.file}`,
318
- );
319
- }
320
280
  }
321
-
322
- return null;
323
- })
324
- .filter(Boolean);
325
-
326
- if (images.length > 0) {
327
- return {
328
- ...item,
329
- images,
330
- };
331
- } else if (originalImageCount > 0) {
332
- // All images failed to resolve - log warning for debugging
333
- console.warn(
334
- `Gallery in "${slug}" has ${originalImageCount} image(s) defined but none could be resolved`,
335
- );
281
+ else if (originalImageCount > 0) {
282
+ // All images failed to resolve - log warning for debugging
283
+ console.warn(`Gallery in "${slug}" has ${originalImageCount} image(s) defined but none could be resolved`);
284
+ }
336
285
  }
337
- }
338
-
339
- return item;
286
+ return baseItem;
340
287
  })
341
- .filter(
342
- (item) =>
343
- item.content || item.src || item.images || item.type === "emoji",
344
- ); // Filter out items that weren't found
288
+ .filter((item) => item !== null && (!!item.content || !!item.src || !!item.images || item.type === "emoji"));
345
289
  }
346
-
347
290
  /**
348
291
  * Process a list of markdown files into post/recipe objects
349
- *
350
- * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
351
- * @returns {Array} Array of post/content objects with metadata and slug
352
292
  */
353
293
  export function processMarkdownModules(modules) {
354
- try {
355
- const items = Object.entries(modules)
356
- .map(([filepath, content]) => {
357
- try {
358
- // Extract slug from filepath: /path/to/Posts/example.md -> example
359
- const slug = filepath.split("/").pop().replace(".md", "");
360
- const { data } = matter(content);
361
-
362
- return {
363
- slug,
364
- title: data.title || "Untitled",
365
- date: data.date || new Date().toISOString(),
366
- tags: data.tags || [],
367
- description: data.description || "",
368
- };
369
- } catch (err) {
370
- console.error(`Error processing file ${filepath}:`, err);
371
- return null;
372
- }
373
- })
374
- .filter(Boolean)
375
- .sort((a, b) => new Date(b.date) - new Date(a.date));
376
-
377
- return items;
378
- } catch (err) {
379
- console.error("Error in processMarkdownModules:", err);
380
- return [];
381
- }
294
+ try {
295
+ const items = Object.entries(modules)
296
+ .map(([filepath, content]) => {
297
+ try {
298
+ // Extract slug from filepath: /path/to/Posts/example.md -> example
299
+ const filename = filepath.split("/").pop();
300
+ if (!filename)
301
+ return null;
302
+ const slug = filename.replace(".md", "");
303
+ const { data } = matter(content);
304
+ return {
305
+ slug,
306
+ title: data.title || "Untitled",
307
+ date: data.date || new Date().toISOString(),
308
+ tags: data.tags || [],
309
+ description: data.description || "",
310
+ };
311
+ }
312
+ catch (err) {
313
+ console.error(`Error processing file ${filepath}:`, err);
314
+ return null;
315
+ }
316
+ })
317
+ .filter((item) => item !== null)
318
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
319
+ return items;
320
+ }
321
+ catch (err) {
322
+ console.error("Error in processMarkdownModules:", err);
323
+ return [];
324
+ }
382
325
  }
383
-
384
326
  /**
385
327
  * Get a single item by slug from modules
386
- *
387
- * @param {string} slug - The item slug
388
- * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
389
- * @param {Object} options - Optional configuration
390
- * @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
391
- * @param {Object} options.sidecarModules - Sidecar/metadata modules (for recipes)
392
- * @returns {Object|null} Item object with content and metadata
393
328
  */
394
329
  export function getItemBySlug(slug, modules, options = {}) {
395
- // Find the matching module by slug
396
- const entry = Object.entries(modules).find(([filepath]) => {
397
- const fileSlug = filepath.split("/").pop().replace(".md", "");
398
- return fileSlug === slug;
399
- });
400
-
401
- if (!entry) {
402
- return null;
403
- }
404
-
405
- const rawContent = entry[1];
406
- const { data, content, headers } = parseMarkdownContent(rawContent);
407
-
408
- // Build the result object
409
- const result = {
410
- slug,
411
- title: data.title || "Untitled",
412
- date: data.date || new Date().toISOString(),
413
- tags: data.tags || [],
414
- description: data.description || "",
415
- content,
416
- headers,
417
- };
418
-
419
- // Process gutter content if provided
420
- if (options.gutterModules) {
421
- const { manifest, markdown, images } = options.gutterModules;
422
- result.gutterContent = processGutterContent(slug, manifest, markdown, images);
423
- }
424
-
425
- // Process sidecar/metadata if provided (for recipes)
426
- if (options.sidecarModules) {
427
- const sidecarEntry = Object.entries(options.sidecarModules).find(([filepath]) => {
428
- const parts = filepath.split("/");
429
- const folder = parts[parts.length - 3]; // Get the folder name
430
- return folder === slug;
431
- });
432
-
433
- if (sidecarEntry) {
434
- result.sidecar = sidecarEntry[1].default || sidecarEntry[1];
435
- }
436
- }
437
-
438
- return result;
439
- }
440
-
441
- /**
442
- * Get a page (home, about, contact) by filename from modules
443
- * Uses sanitization for security
444
- *
445
- * @param {string} filename - The filename to look for (e.g., "home.md", "about.md")
446
- * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
447
- * @param {Object} options - Optional configuration
448
- * @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
449
- * @param {string} options.slug - Override slug (defaults to filename without .md)
450
- * @returns {Object|null} Page object with content and metadata
451
- */
452
- export function getPageByFilename(filename, modules, options = {}) {
453
- try {
454
- // Find the matching file
330
+ // Find the matching module by slug
455
331
  const entry = Object.entries(modules).find(([filepath]) => {
456
- return filepath.includes(filename);
332
+ const filename = filepath.split("/").pop();
333
+ if (!filename)
334
+ return false;
335
+ const fileSlug = filename.replace(".md", "");
336
+ return fileSlug === slug;
457
337
  });
458
-
459
338
  if (!entry) {
460
- return null;
339
+ return null;
461
340
  }
462
-
463
341
  const rawContent = entry[1];
464
- const { data, content, headers } = parseMarkdownContentSanitized(rawContent);
465
- const slug = options.slug || filename.replace(".md", "");
466
-
342
+ const { data, content, headers } = parseMarkdownContent(rawContent);
467
343
  // Build the result object
468
344
  const result = {
469
- slug,
470
- title: data.title || slug.charAt(0).toUpperCase() + slug.slice(1),
471
- description: data.description || "",
472
- content,
473
- headers,
345
+ slug,
346
+ title: data.title || "Untitled",
347
+ date: data.date || new Date().toISOString(),
348
+ tags: data.tags || [],
349
+ description: data.description || "",
350
+ content,
351
+ headers,
474
352
  };
475
-
476
- // Add optional fields from frontmatter
477
- if (data.date) result.date = data.date;
478
- if (data.hero) result.hero = data.hero;
479
- if (data.galleries) result.galleries = data.galleries;
480
-
481
353
  // Process gutter content if provided
482
- if (options.gutterModules) {
483
- const { manifest, markdown, images } = options.gutterModules;
484
- result.gutterContent = processGutterContent(slug, manifest, markdown, images);
354
+ if (options.gutterModules?.manifest) {
355
+ const { manifest, markdown = {}, images = {} } = options.gutterModules;
356
+ result.gutterContent = processGutterContent(slug, manifest, markdown, images);
357
+ }
358
+ // Process sidecar/metadata if provided (for recipes)
359
+ if (options.sidecarModules) {
360
+ const sidecarEntry = Object.entries(options.sidecarModules).find(([filepath]) => {
361
+ const parts = filepath.split("/");
362
+ const folder = parts[parts.length - 3]; // Get the folder name
363
+ return folder === slug;
364
+ });
365
+ if (sidecarEntry) {
366
+ const sidecarData = sidecarEntry[1];
367
+ result.sidecar = typeof sidecarData === "object" && sidecarData !== null && "default" in sidecarData
368
+ ? sidecarData.default
369
+ : sidecarData;
370
+ }
485
371
  }
486
-
487
372
  return result;
488
- } catch (err) {
489
- console.error(`Error in getPageByFilename for ${filename}:`, err);
490
- return null;
491
- }
492
373
  }
493
-
374
+ /**
375
+ * Get a page (home, about, contact) by filename from modules
376
+ * Uses sanitization for security
377
+ */
378
+ export function getPageByFilename(filename, modules, options = {}) {
379
+ try {
380
+ // Find the matching file
381
+ const entry = Object.entries(modules).find(([filepath]) => {
382
+ return filepath.includes(filename);
383
+ });
384
+ if (!entry) {
385
+ return null;
386
+ }
387
+ const rawContent = entry[1];
388
+ const { data, content, headers } = parseMarkdownContentSanitized(rawContent);
389
+ const slug = options.slug || filename.replace(".md", "");
390
+ // Build the result object
391
+ const result = {
392
+ slug,
393
+ title: data.title || slug.charAt(0).toUpperCase() + slug.slice(1),
394
+ description: data.description || "",
395
+ content,
396
+ headers,
397
+ };
398
+ // Add optional fields from frontmatter
399
+ if (data.date)
400
+ result.date = data.date;
401
+ if (data.hero)
402
+ result.hero = data.hero;
403
+ if (data.galleries)
404
+ result.galleries = data.galleries;
405
+ // Process gutter content if provided
406
+ if (options.gutterModules?.manifest) {
407
+ const { manifest, markdown = {}, images = {} } = options.gutterModules;
408
+ result.gutterContent = processGutterContent(slug, manifest, markdown, images);
409
+ }
410
+ return result;
411
+ }
412
+ catch (err) {
413
+ console.error(`Error in getPageByFilename for ${filename}:`, err);
414
+ return null;
415
+ }
416
+ }
494
417
  /**
495
418
  * Get site configuration from a config module
496
- *
497
- * @param {Object} configModule - The config module from import.meta.glob
498
- * @returns {Object} Site configuration object
499
419
  */
500
420
  export function getSiteConfigFromModule(configModule) {
501
- const entry = Object.entries(configModule)[0];
502
- if (entry) {
503
- return entry[1].default || entry[1];
504
- }
505
- return {
506
- owner: { name: "Admin", email: "" },
507
- site: { title: "The Grove", description: "", copyright: "AutumnsGrove" },
508
- social: {},
509
- };
421
+ const entry = Object.entries(configModule)[0];
422
+ if (entry) {
423
+ const config = entry[1];
424
+ return "default" in config ? config.default : config;
425
+ }
426
+ return {
427
+ owner: { name: "Admin", email: "" },
428
+ site: { title: "The Grove", description: "", copyright: "AutumnsGrove" },
429
+ social: {},
430
+ };
510
431
  }
511
-
512
432
  /**
513
433
  * Create a configured content loader with all functions bound to the provided modules
514
434
  * This is the main factory function for creating a content loader in the consuming app
515
- *
516
- * @param {Object} config - Configuration object with all required modules
517
- * @param {Object} config.posts - Post modules from import.meta.glob
518
- * @param {Object} config.recipes - Recipe modules from import.meta.glob
519
- * @param {Object} config.about - About page modules from import.meta.glob
520
- * @param {Object} config.home - Home page modules from import.meta.glob
521
- * @param {Object} config.contact - Contact page modules from import.meta.glob
522
- * @param {Object} config.siteConfig - Site config module from import.meta.glob
523
- * @param {Object} config.postGutter - Post gutter modules { manifest, markdown, images }
524
- * @param {Object} config.recipeGutter - Recipe gutter modules { manifest, markdown, images }
525
- * @param {Object} config.recipeMetadata - Recipe metadata modules from import.meta.glob
526
- * @param {Object} config.aboutGutter - About gutter modules { manifest, markdown, images }
527
- * @param {Object} config.homeGutter - Home gutter modules { manifest, markdown, images }
528
- * @param {Object} config.contactGutter - Contact gutter modules { manifest, markdown, images }
529
- * @returns {Object} Object with all content loader functions
530
435
  */
531
436
  export function createContentLoader(config) {
532
- const {
533
- posts = {},
534
- recipes = {},
535
- about = {},
536
- home = {},
537
- contact = {},
538
- siteConfig = {},
539
- postGutter = {},
540
- recipeGutter = {},
541
- recipeMetadata = {},
542
- aboutGutter = {},
543
- homeGutter = {},
544
- contactGutter = {},
545
- } = config;
546
-
547
- return {
548
- /**
549
- * Get all posts with metadata
550
- */
551
- getAllPosts() {
552
- return processMarkdownModules(posts);
553
- },
554
-
555
- /**
556
- * Get all recipes with metadata
557
- */
558
- getAllRecipes() {
559
- return processMarkdownModules(recipes);
560
- },
561
-
562
- /**
563
- * Get the latest (most recent) post with full content
564
- */
565
- getLatestPost() {
566
- const allPosts = processMarkdownModules(posts);
567
- if (allPosts.length === 0) {
568
- return null;
569
- }
570
- return this.getPostBySlug(allPosts[0].slug);
571
- },
572
-
573
- /**
574
- * Get a single post by slug
575
- */
576
- getPostBySlug(slug) {
577
- return getItemBySlug(slug, posts, {
578
- gutterModules: postGutter.manifest ? postGutter : undefined,
579
- });
580
- },
581
-
582
- /**
583
- * Get a single recipe by slug
584
- */
585
- getRecipeBySlug(slug) {
586
- return getItemBySlug(slug, recipes, {
587
- gutterModules: recipeGutter.manifest ? recipeGutter : undefined,
588
- sidecarModules: recipeMetadata,
589
- });
590
- },
591
-
592
- /**
593
- * Get the home page content
594
- */
595
- getHomePage() {
596
- return getPageByFilename("home.md", home, {
597
- gutterModules: homeGutter.manifest ? homeGutter : undefined,
598
- slug: "home",
599
- });
600
- },
601
-
602
- /**
603
- * Get the about page content
604
- */
605
- getAboutPage() {
606
- return getPageByFilename("about.md", about, {
607
- gutterModules: aboutGutter.manifest ? aboutGutter : undefined,
608
- slug: "about",
609
- });
610
- },
611
-
612
- /**
613
- * Get the contact page content
614
- */
615
- getContactPage() {
616
- return getPageByFilename("contact.md", contact, {
617
- gutterModules: contactGutter.manifest ? contactGutter : undefined,
618
- slug: "contact",
619
- });
620
- },
621
-
622
- /**
623
- * Get the site configuration
624
- */
625
- getSiteConfig() {
626
- return getSiteConfigFromModule(siteConfig);
627
- },
628
-
629
- /**
630
- * Get gutter content for a post
631
- */
632
- getGutterContent(slug) {
633
- if (!postGutter.manifest) return [];
634
- return processGutterContent(
635
- slug,
636
- postGutter.manifest,
637
- postGutter.markdown || {},
638
- postGutter.images || {},
639
- );
640
- },
641
-
642
- /**
643
- * Get gutter content for a recipe
644
- */
645
- getRecipeGutterContent(slug) {
646
- if (!recipeGutter.manifest) return [];
647
- return processGutterContent(
648
- slug,
649
- recipeGutter.manifest,
650
- recipeGutter.markdown || {},
651
- recipeGutter.images || {},
652
- );
653
- },
654
-
655
- /**
656
- * Get gutter content for the home page
657
- */
658
- getHomeGutterContent(slug) {
659
- if (!homeGutter.manifest) return [];
660
- return processGutterContent(
661
- slug,
662
- homeGutter.manifest,
663
- homeGutter.markdown || {},
664
- homeGutter.images || {},
665
- );
666
- },
667
-
668
- /**
669
- * Get gutter content for the about page
670
- */
671
- getAboutGutterContent(slug) {
672
- if (!aboutGutter.manifest) return [];
673
- return processGutterContent(
674
- slug,
675
- aboutGutter.manifest,
676
- aboutGutter.markdown || {},
677
- aboutGutter.images || {},
678
- );
679
- },
680
-
681
- /**
682
- * Get gutter content for the contact page
683
- */
684
- getContactGutterContent(slug) {
685
- if (!contactGutter.manifest) return [];
686
- return processGutterContent(
687
- slug,
688
- contactGutter.manifest,
689
- contactGutter.markdown || {},
690
- contactGutter.images || {},
691
- );
692
- },
693
-
694
- /**
695
- * Get recipe sidecar/metadata by slug
696
- */
697
- getRecipeSidecar(slug) {
698
- const entry = Object.entries(recipeMetadata).find(([filepath]) => {
699
- const parts = filepath.split("/");
700
- const folder = parts[parts.length - 3];
701
- return folder === slug;
702
- });
703
-
704
- if (!entry) {
705
- return null;
706
- }
707
-
708
- return entry[1].default || entry[1];
709
- },
710
- };
437
+ const { posts = {}, recipes = {}, about = {}, home = {}, contact = {}, siteConfig = {}, postGutter = {}, recipeGutter = {}, recipeMetadata = {}, aboutGutter = {}, homeGutter = {}, contactGutter = {}, } = config;
438
+ const loader = {
439
+ /**
440
+ * Get all posts with metadata
441
+ */
442
+ getAllPosts() {
443
+ return processMarkdownModules(posts);
444
+ },
445
+ /**
446
+ * Get all recipes with metadata
447
+ */
448
+ getAllRecipes() {
449
+ return processMarkdownModules(recipes);
450
+ },
451
+ /**
452
+ * Get the latest (most recent) post with full content
453
+ */
454
+ getLatestPost() {
455
+ const allPosts = processMarkdownModules(posts);
456
+ if (allPosts.length === 0) {
457
+ return null;
458
+ }
459
+ return loader.getPostBySlug(allPosts[0].slug);
460
+ },
461
+ /**
462
+ * Get a single post by slug
463
+ */
464
+ getPostBySlug(slug) {
465
+ return getItemBySlug(slug, posts, {
466
+ gutterModules: postGutter.manifest ? postGutter : undefined,
467
+ });
468
+ },
469
+ /**
470
+ * Get a single recipe by slug
471
+ */
472
+ getRecipeBySlug(slug) {
473
+ return getItemBySlug(slug, recipes, {
474
+ gutterModules: recipeGutter.manifest ? recipeGutter : undefined,
475
+ sidecarModules: recipeMetadata,
476
+ });
477
+ },
478
+ /**
479
+ * Get the home page content
480
+ */
481
+ getHomePage() {
482
+ return getPageByFilename("home.md", home, {
483
+ gutterModules: homeGutter.manifest ? homeGutter : undefined,
484
+ slug: "home",
485
+ });
486
+ },
487
+ /**
488
+ * Get the about page content
489
+ */
490
+ getAboutPage() {
491
+ return getPageByFilename("about.md", about, {
492
+ gutterModules: aboutGutter.manifest ? aboutGutter : undefined,
493
+ slug: "about",
494
+ });
495
+ },
496
+ /**
497
+ * Get the contact page content
498
+ */
499
+ getContactPage() {
500
+ return getPageByFilename("contact.md", contact, {
501
+ gutterModules: contactGutter.manifest ? contactGutter : undefined,
502
+ slug: "contact",
503
+ });
504
+ },
505
+ /**
506
+ * Get the site configuration
507
+ */
508
+ getSiteConfig() {
509
+ return getSiteConfigFromModule(siteConfig);
510
+ },
511
+ /**
512
+ * Get gutter content for a post
513
+ */
514
+ getGutterContent(slug) {
515
+ if (!postGutter.manifest)
516
+ return [];
517
+ return processGutterContent(slug, postGutter.manifest, postGutter.markdown || {}, postGutter.images || {});
518
+ },
519
+ /**
520
+ * Get gutter content for a recipe
521
+ */
522
+ getRecipeGutterContent(slug) {
523
+ if (!recipeGutter.manifest)
524
+ return [];
525
+ return processGutterContent(slug, recipeGutter.manifest, recipeGutter.markdown || {}, recipeGutter.images || {});
526
+ },
527
+ /**
528
+ * Get gutter content for the home page
529
+ */
530
+ getHomeGutterContent(slug) {
531
+ if (!homeGutter.manifest)
532
+ return [];
533
+ return processGutterContent(slug, homeGutter.manifest, homeGutter.markdown || {}, homeGutter.images || {});
534
+ },
535
+ /**
536
+ * Get gutter content for the about page
537
+ */
538
+ getAboutGutterContent(slug) {
539
+ if (!aboutGutter.manifest)
540
+ return [];
541
+ return processGutterContent(slug, aboutGutter.manifest, aboutGutter.markdown || {}, aboutGutter.images || {});
542
+ },
543
+ /**
544
+ * Get gutter content for the contact page
545
+ */
546
+ getContactGutterContent(slug) {
547
+ if (!contactGutter.manifest)
548
+ return [];
549
+ return processGutterContent(slug, contactGutter.manifest, contactGutter.markdown || {}, contactGutter.images || {});
550
+ },
551
+ /**
552
+ * Get recipe sidecar/metadata by slug
553
+ */
554
+ getRecipeSidecar(slug) {
555
+ const entry = Object.entries(recipeMetadata).find(([filepath]) => {
556
+ const parts = filepath.split("/");
557
+ const folder = parts[parts.length - 3];
558
+ return folder === slug;
559
+ });
560
+ if (!entry) {
561
+ return null;
562
+ }
563
+ const data = entry[1];
564
+ return typeof data === "object" && data !== null && "default" in data
565
+ ? data.default
566
+ : data;
567
+ },
568
+ };
569
+ return loader;
711
570
  }
712
-
571
+ // ============================================================================
572
+ // Global Content Loader Registry
573
+ // ============================================================================
713
574
  /**
714
575
  * Registry for site-specific content loaders
715
576
  * Sites must register their content loaders using registerContentLoader()
716
577
  */
717
578
  let contentLoader = null;
718
-
719
579
  /**
720
580
  * Register a content loader for the site
721
581
  * This should be called by the consuming site to provide access to content
722
- * @param {Object} loader - Object with getAllPosts, getSiteConfig, getLatestPost functions
723
582
  */
724
583
  export function registerContentLoader(loader) {
725
- contentLoader = loader;
584
+ contentLoader = loader;
726
585
  }
727
-
728
586
  /**
729
587
  * Get all blog posts
730
- * @returns {Array} Array of post objects
731
588
  */
732
589
  export function getAllPosts() {
733
- if (!contentLoader || !contentLoader.getAllPosts) {
734
- console.warn('getAllPosts: No content loader registered. Call registerContentLoader() in your site.');
735
- return [];
736
- }
737
- return contentLoader.getAllPosts();
590
+ if (!contentLoader || !contentLoader.getAllPosts) {
591
+ console.warn("getAllPosts: No content loader registered. Call registerContentLoader() in your site.");
592
+ return [];
593
+ }
594
+ return contentLoader.getAllPosts();
738
595
  }
739
-
740
596
  /**
741
597
  * Get site configuration
742
- * @returns {Object} Site config object
743
598
  */
744
599
  export function getSiteConfig() {
745
- if (!contentLoader || !contentLoader.getSiteConfig) {
746
- console.warn('getSiteConfig: No content loader registered. Call registerContentLoader() in your site.');
747
- return {
748
- owner: { name: "Admin", email: "" },
749
- site: { title: "GroveEngine Site", description: "", copyright: "" },
750
- social: {},
751
- };
752
- }
753
- return contentLoader.getSiteConfig();
600
+ if (!contentLoader || !contentLoader.getSiteConfig) {
601
+ console.warn("getSiteConfig: No content loader registered. Call registerContentLoader() in your site.");
602
+ return {
603
+ owner: { name: "Admin", email: "" },
604
+ site: { title: "GroveEngine Site", description: "", copyright: "" },
605
+ social: {},
606
+ };
607
+ }
608
+ return contentLoader.getSiteConfig();
754
609
  }
755
-
756
610
  /**
757
611
  * Get the latest post
758
- * @returns {Object|null} Latest post or null
759
612
  */
760
613
  export function getLatestPost() {
761
- if (!contentLoader || !contentLoader.getLatestPost) {
762
- console.warn('getLatestPost: No content loader registered. Call registerContentLoader() in your site.');
763
- return null;
764
- }
765
- return contentLoader.getLatestPost();
614
+ if (!contentLoader || !contentLoader.getLatestPost) {
615
+ console.warn("getLatestPost: No content loader registered. Call registerContentLoader() in your site.");
616
+ return null;
617
+ }
618
+ return contentLoader.getLatestPost();
766
619
  }
767
-
768
620
  /**
769
621
  * Get home page content
770
- * @returns {Object|null} Home page data or null
771
622
  */
772
623
  export function getHomePage() {
773
- if (!contentLoader || !contentLoader.getHomePage) {
774
- console.warn('getHomePage: No content loader registered. Call registerContentLoader() in your site.');
775
- return null;
776
- }
777
- return contentLoader.getHomePage();
624
+ if (!contentLoader || !contentLoader.getHomePage) {
625
+ console.warn("getHomePage: No content loader registered. Call registerContentLoader() in your site.");
626
+ return null;
627
+ }
628
+ return contentLoader.getHomePage();
778
629
  }
779
-
780
630
  /**
781
631
  * Get a post by its slug
782
- * @param {string} slug - The post slug
783
- * @returns {Object|null} Post object or null
784
632
  */
785
633
  export function getPostBySlug(slug) {
786
- if (!contentLoader || !contentLoader.getPostBySlug) {
787
- console.warn('getPostBySlug: No content loader registered. Call registerContentLoader() in your site.');
788
- return null;
789
- }
790
- return contentLoader.getPostBySlug(slug);
634
+ if (!contentLoader || !contentLoader.getPostBySlug) {
635
+ console.warn("getPostBySlug: No content loader registered. Call registerContentLoader() in your site.");
636
+ return null;
637
+ }
638
+ return contentLoader.getPostBySlug(slug);
791
639
  }
792
-
793
640
  /**
794
641
  * Get about page content
795
- * @returns {Object|null} About page data or null
796
642
  */
797
643
  export function getAboutPage() {
798
- if (!contentLoader || !contentLoader.getAboutPage) {
799
- console.warn('getAboutPage: No content loader registered. Call registerContentLoader() in your site.');
800
- return null;
801
- }
802
- return contentLoader.getAboutPage();
644
+ if (!contentLoader || !contentLoader.getAboutPage) {
645
+ console.warn("getAboutPage: No content loader registered. Call registerContentLoader() in your site.");
646
+ return null;
647
+ }
648
+ return contentLoader.getAboutPage();
803
649
  }
804
-
805
650
  /**
806
651
  * Get contact page content
807
- * @returns {Object|null} Contact page data or null
808
652
  */
809
653
  export function getContactPage() {
810
- if (!contentLoader || !contentLoader.getContactPage) {
811
- console.warn('getContactPage: No content loader registered. Call registerContentLoader() in your site.');
812
- return null;
813
- }
814
- return contentLoader.getContactPage();
654
+ if (!contentLoader || !contentLoader.getContactPage) {
655
+ console.warn("getContactPage: No content loader registered. Call registerContentLoader() in your site.");
656
+ return null;
657
+ }
658
+ return contentLoader.getContactPage();
815
659
  }
816
-
817
660
  /**
818
661
  * Get all recipes
819
- * @returns {Array} Array of recipe objects
820
662
  */
821
663
  export function getAllRecipes() {
822
- if (!contentLoader || !contentLoader.getAllRecipes) {
823
- console.warn('getAllRecipes: No content loader registered. Call registerContentLoader() in your site.');
824
- return [];
825
- }
826
- return contentLoader.getAllRecipes();
664
+ if (!contentLoader || !contentLoader.getAllRecipes) {
665
+ console.warn("getAllRecipes: No content loader registered. Call registerContentLoader() in your site.");
666
+ return [];
667
+ }
668
+ return contentLoader.getAllRecipes();
827
669
  }
828
-
829
670
  /**
830
671
  * Get a recipe by its slug
831
- * @param {string} slug - The recipe slug
832
- * @returns {Object|null} Recipe object or null
833
672
  */
834
673
  export function getRecipeBySlug(slug) {
835
- if (!contentLoader || !contentLoader.getRecipeBySlug) {
836
- console.warn('getRecipeBySlug: No content loader registered. Call registerContentLoader() in your site.');
837
- return null;
838
- }
839
- return contentLoader.getRecipeBySlug(slug);
674
+ if (!contentLoader || !contentLoader.getRecipeBySlug) {
675
+ console.warn("getRecipeBySlug: No content loader registered. Call registerContentLoader() in your site.");
676
+ return null;
677
+ }
678
+ return contentLoader.getRecipeBySlug(slug);
840
679
  }