seeemess 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Preview: <%= it.title || 'Untitled' %></title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Playfair+Display:wght@400;700&family=Playfair+Display+SC&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="/css/index.css">
9
+ <style>
10
+ .preview-banner{background:#333;color:#fff;padding:0.5rem 1rem;text-align:center;font-size:0.85rem;position:sticky;top:0;z-index:100;}
11
+ .byline::first-letter{font-size:inherit;float:none;padding:0;line-height:inherit;}
12
+ .post-content>p:first-child::first-letter{font-family:var(--font-headline);font-size:4rem;float:left;line-height:0.75;padding-top:8px;padding-right:12px;font-weight:400;color:var(--ink-color);}
13
+ .post-image-wrapper{position:relative;display:inline-block;width:100%;}
14
+ .post-image-wrapper .post-image{display:block;width:100%;}
15
+ .image-caption,.image-credit{position:absolute;bottom:0.5rem;padding:0.25rem 0.5rem;background:rgba(40,40,40,0.85);color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:0.75rem;font-style:normal;max-width:60%;line-height:1.3;}
16
+ .image-caption{left:0.5rem;}
17
+ .image-credit{right:0.5rem;text-align:right;}
18
+ .post-content ul{list-style:none;padding-left:1.5em;margin:1.25rem 0;}
19
+ .post-content ul li{position:relative;margin-bottom:0.5em;padding-left:0.25em;}
20
+ .post-content ul li::before{content:"✦";position:absolute;left:-1.25em;color:var(--ink-color-light);font-size:0.65em;top:0.35em;}
21
+ .post-content ol{padding-left:1.5em;margin:1.25rem 0;}
22
+ .post-content ol li{margin-bottom:0.5em;padding-left:0.25em;}
23
+ .post-content ul ul li::before{content:"◆";font-size:0.5em;top:0.45em;}
24
+ .post-content ul ul ul li::before{content:"•";font-size:0.7em;top:0.3em;}
25
+ .post-content img{cursor:pointer;transition:opacity 0.2s ease;}
26
+ .post-content img:hover{opacity:0.9;}
27
+ .photo-gallery{margin:2rem 0;padding:1.5rem;border-top:3px double var(--ink-color);border-bottom:3px double var(--ink-color);}
28
+ .gallery-title{text-align:center;font-family:var(--font-headline);letter-spacing:0.1em;text-transform:uppercase;margin:0 0 1rem 0;font-size:1rem;}
29
+ .gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:0.75rem;}
30
+ .gallery-item{display:block;overflow:hidden;margin:0;cursor:pointer;aspect-ratio:4/3;}
31
+ .gallery-item img{width:100%;height:100%;object-fit:cover;transition:transform 0.2s;}
32
+ .gallery-item:hover img{transform:scale(1.05);}
33
+ .lightbox-image-wrapper{position:relative;display:inline-block;}
34
+ .lightbox-image-wrapper img{max-width:90vw;max-height:80vh;display:block;}
35
+ .lightbox-img-caption,.lightbox-img-credit{position:absolute;bottom:0.5rem;padding:0.25rem 0.5rem;background:rgba(40,40,40,0.85);color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:0.85rem;max-width:60%;line-height:1.3;}
36
+ .lightbox-img-caption{left:0.5rem;}
37
+ .lightbox-img-credit{right:0.5rem;text-align:right;}
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div class="preview-banner">📄 Preview Mode - This is how your post will appear</div>
42
+ <main>
43
+ <h1><%= it.title || 'Untitled Post' %></h1>
44
+ <% if (it.image) { %>
45
+ <figure class="post-figure">
46
+ <div class="post-image-wrapper">
47
+ <img src="<%= it.imageUrlPath %>/<%= it.image %>" alt="<%= it.imageCaption || it.title || '' %>" class="post-image">
48
+ <% if (it.imageCaption) { %><span class="image-caption"><%= it.imageCaption %></span><% } %>
49
+ <% if (it.imageCredit) { %><span class="image-credit">Photo: <%= it.imageCredit %></span><% } %>
50
+ </div>
51
+ <figcaption class="post-date"><time><%= it.formattedDate %></time><%~ it.tagsHtml %></figcaption>
52
+ </figure>
53
+ <% } else { %>
54
+ <p class="post-date"><time><%= it.formattedDate %></time><%~ it.tagsHtml %></p>
55
+ <% } %>
56
+ <% if (it.author) { %>
57
+ <p class="byline"><span class="byline-author">By <%= it.author %></span></p>
58
+ <% } %>
59
+ <div class="post-content"><%~ it.htmlBody %></div>
60
+ <%~ it.galleryHtml %>
61
+ <div class="ornament-divider" aria-hidden="true"><span class="ornament-line"></span><span class="ornament-symbol">❧</span><span class="ornament-line"></span></div>
62
+ </main>
63
+
64
+ <% if (it.images.length > 0 || it.gallery.length > 0) { %>
65
+ <script id="contentImagesData" type="application/json"><%~ JSON.stringify({ images: it.images, gallery: it.gallery }) %></script>
66
+ <% } %>
67
+
68
+ <!-- Content Image Lightbox -->
69
+ <div class="lightbox-overlay" id="contentLightbox" onclick="closeContentLightbox(event)">
70
+ <button class="lightbox-close" onclick="closeContentLightbox(event)" aria-label="Close">&times;</button>
71
+ <button class="lightbox-nav lightbox-prev" id="contentLightboxPrev" onclick="navigateContentLightbox(-1, event)" aria-label="Previous">&#10094;</button>
72
+ <div class="lightbox-content" onclick="event.stopPropagation()">
73
+ <div class="lightbox-image-wrapper">
74
+ <img id="contentLightboxImg" src="" alt="">
75
+ <span class="lightbox-img-caption" id="contentLightboxCaption"></span>
76
+ <span class="lightbox-img-credit" id="contentLightboxCredit"></span>
77
+ </div>
78
+ <div class="lightbox-actions">
79
+ <span class="lightbox-counter" id="contentLightboxCounter"></span>
80
+ </div>
81
+ </div>
82
+ <button class="lightbox-nav lightbox-next" id="contentLightboxNext" onclick="navigateContentLightbox(1, event)" aria-label="Next">&#10095;</button>
83
+ </div>
84
+
85
+ <!-- Gallery Lightbox -->
86
+ <div class="lightbox-overlay" id="galleryLightbox" onclick="closeGalleryLightbox(event)">
87
+ <button class="lightbox-close" onclick="closeGalleryLightbox(event)" aria-label="Close">&times;</button>
88
+ <button class="lightbox-nav lightbox-prev" id="galleryLightboxPrev" onclick="navigateGalleryLightbox(-1, event)" aria-label="Previous">&#10094;</button>
89
+ <div class="lightbox-content" onclick="event.stopPropagation()">
90
+ <div class="lightbox-image-wrapper">
91
+ <img id="galleryLightboxImg" src="" alt="">
92
+ <span class="lightbox-img-caption" id="galleryLightboxCaption"></span>
93
+ <span class="lightbox-img-credit" id="galleryLightboxCredit"></span>
94
+ </div>
95
+ <div class="lightbox-actions">
96
+ <span class="lightbox-counter" id="galleryLightboxCounter"></span>
97
+ </div>
98
+ </div>
99
+ <button class="lightbox-nav lightbox-next" id="galleryLightboxNext" onclick="navigateGalleryLightbox(1, event)" aria-label="Next">&#10095;</button>
100
+ </div>
101
+
102
+ <script>
103
+ (function() {
104
+ let contentCurrentIndex = 0;
105
+ let contentImages = [];
106
+
107
+ let imageMetadata = {};
108
+ const metaScript = document.getElementById('contentImagesData');
109
+ if (metaScript) {
110
+ try {
111
+ const data = JSON.parse(metaScript.textContent);
112
+ (data.gallery || []).forEach(item => {
113
+ const filename = (item.src || item).split('/').pop();
114
+ imageMetadata[filename] = typeof item === 'string' ? { src: item } : item;
115
+ });
116
+ (data.images || []).forEach(item => {
117
+ const filename = item.src.split('/').pop();
118
+ const existing = imageMetadata[filename] || {};
119
+ imageMetadata[filename] = {
120
+ src: item.src,
121
+ caption: item.caption || existing.caption || '',
122
+ credit: item.credit || existing.credit || ''
123
+ };
124
+ });
125
+ } catch (e) {}
126
+ }
127
+
128
+ function initContentLightbox() {
129
+ const postContent = document.querySelector('.post-content');
130
+ if (!postContent) return;
131
+
132
+ const imgs = postContent.querySelectorAll('img');
133
+ contentImages = Array.from(imgs).map(img => {
134
+ let src = img.src;
135
+ const fullSrc = src.replace(/-(?:400|600|800|thumb)(\.[^.]+)$/, '$1');
136
+ const filename = fullSrc.split('/').pop();
137
+ const meta = imageMetadata[filename] || {};
138
+ return {
139
+ src: fullSrc,
140
+ caption: meta.caption || img.alt || '',
141
+ credit: meta.credit || ''
142
+ };
143
+ });
144
+
145
+ imgs.forEach((img, index) => {
146
+ img.style.cursor = 'pointer';
147
+ img.addEventListener('click', function(e) {
148
+ e.preventDefault();
149
+ openContentLightbox(index);
150
+ });
151
+ });
152
+ }
153
+
154
+ window.openContentLightbox = function(index) {
155
+ if (contentImages.length === 0) return;
156
+ contentCurrentIndex = index;
157
+ updateContentLightbox();
158
+ document.getElementById('contentLightbox').classList.add('active');
159
+ document.body.style.overflow = 'hidden';
160
+ };
161
+
162
+ window.closeContentLightbox = function(e) {
163
+ if (e && e.target !== e.currentTarget && !e.target.classList.contains('lightbox-close')) return;
164
+ document.getElementById('contentLightbox').classList.remove('active');
165
+ document.body.style.overflow = '';
166
+ };
167
+
168
+ window.navigateContentLightbox = function(dir, e) {
169
+ if (e) e.stopPropagation();
170
+ contentCurrentIndex += dir;
171
+ if (contentCurrentIndex < 0) contentCurrentIndex = contentImages.length - 1;
172
+ if (contentCurrentIndex >= contentImages.length) contentCurrentIndex = 0;
173
+ updateContentLightbox();
174
+ };
175
+
176
+ function updateContentLightbox() {
177
+ const item = contentImages[contentCurrentIndex];
178
+ if (!item) return;
179
+ document.getElementById('contentLightboxImg').src = item.src;
180
+ document.getElementById('contentLightboxImg').alt = item.caption || 'Image';
181
+ const captionEl = document.getElementById('contentLightboxCaption');
182
+ const creditEl = document.getElementById('contentLightboxCredit');
183
+ captionEl.textContent = item.caption || '';
184
+ captionEl.style.display = item.caption ? 'block' : 'none';
185
+ creditEl.textContent = item.credit ? 'Photo: ' + item.credit : '';
186
+ creditEl.style.display = item.credit ? 'block' : 'none';
187
+ document.getElementById('contentLightboxCounter').textContent = (contentCurrentIndex + 1) + ' / ' + contentImages.length;
188
+ document.getElementById('contentLightboxPrev').style.display = contentImages.length > 1 ? 'flex' : 'none';
189
+ document.getElementById('contentLightboxNext').style.display = contentImages.length > 1 ? 'flex' : 'none';
190
+ }
191
+
192
+ document.addEventListener('keydown', function(e) {
193
+ const lightbox = document.getElementById('contentLightbox');
194
+ if (!lightbox || !lightbox.classList.contains('active')) return;
195
+ if (e.key === 'Escape') closeContentLightbox({target: lightbox, currentTarget: lightbox});
196
+ if (e.key === 'ArrowLeft') navigateContentLightbox(-1);
197
+ if (e.key === 'ArrowRight') navigateContentLightbox(1);
198
+ });
199
+
200
+ initContentLightbox();
201
+ })();
202
+
203
+ (function() {
204
+ let galleryCurrentIndex = 0;
205
+ let galleryItems = [];
206
+
207
+ function initGalleryLightbox() {
208
+ const grid = document.getElementById('galleryGrid');
209
+ if (!grid) return;
210
+ galleryItems = Array.from(grid.querySelectorAll('.gallery-item')).map(el => ({
211
+ src: el.dataset.src,
212
+ caption: el.dataset.caption || '',
213
+ credit: el.dataset.credit || ''
214
+ }));
215
+ }
216
+
217
+ window.openGalleryLightbox = function(index) {
218
+ initGalleryLightbox();
219
+ if (galleryItems.length === 0) return;
220
+ galleryCurrentIndex = index;
221
+ updateGalleryLightbox();
222
+ document.getElementById('galleryLightbox').classList.add('active');
223
+ document.body.style.overflow = 'hidden';
224
+ };
225
+
226
+ window.closeGalleryLightbox = function(e) {
227
+ if (e && e.target !== e.currentTarget && !e.target.classList.contains('lightbox-close')) return;
228
+ document.getElementById('galleryLightbox').classList.remove('active');
229
+ document.body.style.overflow = '';
230
+ };
231
+
232
+ window.navigateGalleryLightbox = function(dir, e) {
233
+ if (e) e.stopPropagation();
234
+ galleryCurrentIndex += dir;
235
+ if (galleryCurrentIndex < 0) galleryCurrentIndex = galleryItems.length - 1;
236
+ if (galleryCurrentIndex >= galleryItems.length) galleryCurrentIndex = 0;
237
+ updateGalleryLightbox();
238
+ };
239
+
240
+ function updateGalleryLightbox() {
241
+ const item = galleryItems[galleryCurrentIndex];
242
+ if (!item) return;
243
+ document.getElementById('galleryLightboxImg').src = item.src;
244
+ document.getElementById('galleryLightboxImg').alt = item.caption || 'Gallery image';
245
+ const captionEl = document.getElementById('galleryLightboxCaption');
246
+ const creditEl = document.getElementById('galleryLightboxCredit');
247
+ captionEl.textContent = item.caption || '';
248
+ captionEl.style.display = item.caption ? 'block' : 'none';
249
+ creditEl.textContent = item.credit ? 'Photo: ' + item.credit : '';
250
+ creditEl.style.display = item.credit ? 'block' : 'none';
251
+ document.getElementById('galleryLightboxCounter').textContent = (galleryCurrentIndex + 1) + ' / ' + galleryItems.length;
252
+ document.getElementById('galleryLightboxPrev').style.display = galleryItems.length > 1 ? 'flex' : 'none';
253
+ document.getElementById('galleryLightboxNext').style.display = galleryItems.length > 1 ? 'flex' : 'none';
254
+ }
255
+
256
+ document.addEventListener('keydown', function(e) {
257
+ const lightbox = document.getElementById('galleryLightbox');
258
+ if (!lightbox || !lightbox.classList.contains('active')) return;
259
+ if (e.key === 'Escape') closeGalleryLightbox({target: lightbox, currentTarget: lightbox});
260
+ if (e.key === 'ArrowLeft') navigateGalleryLightbox(-1);
261
+ if (e.key === 'ArrowRight') navigateGalleryLightbox(1);
262
+ });
263
+ })();
264
+ </script>
265
+ </body>
266
+ </html>
@@ -0,0 +1,16 @@
1
+ import { execSync } from 'child_process';
2
+ import { getCmsRoot, getBranchWords } from '../config.js';
3
+
4
+ export function randomWord() {
5
+ const words = getBranchWords();
6
+ return words[Math.floor(Math.random() * words.length)];
7
+ }
8
+
9
+ export function runGit(cmd, cwd = getCmsRoot()) {
10
+ console.log('Running:', cmd);
11
+ return execSync(cmd, { cwd, encoding: 'utf-8' });
12
+ }
13
+
14
+ export function generateBranchName() {
15
+ return `publish-${randomWord()}-${randomWord()}-${Date.now()}`;
16
+ }
@@ -0,0 +1,137 @@
1
+ import markdownIt from 'markdown-it';
2
+ import markdownItAttrs from 'markdown-it-attrs';
3
+
4
+ // Configure markdown-it with attrs support
5
+ export const md = markdownIt({
6
+ html: true,
7
+ breaks: true,
8
+ linkify: true,
9
+ typographer: true
10
+ }).use(markdownItAttrs);
11
+
12
+ export function parseFrontmatter(content) {
13
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
14
+ if (!match) return {};
15
+ const fm = {};
16
+ const lines = match[1].split('\n');
17
+ let i = 0;
18
+ while (i < lines.length) {
19
+ const line = lines[i];
20
+ const colonIdx = line.indexOf(':');
21
+ if (colonIdx > 0 && !line.startsWith(' ') && !line.startsWith('\t')) {
22
+ const key = line.slice(0, colonIdx).trim();
23
+ let value = line.slice(colonIdx + 1).trim();
24
+
25
+ // Check if this is a multi-line YAML array (gallery with objects)
26
+ if (value === '' && i + 1 < lines.length && lines[i + 1].trim().startsWith('- ')) {
27
+ const items = [];
28
+ i++;
29
+ let currentItem = null;
30
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t') || lines[i].trim().startsWith('- '))) {
31
+ const itemLine = lines[i].trim();
32
+ if (itemLine.startsWith('- ')) {
33
+ if (currentItem) items.push(currentItem);
34
+ currentItem = {};
35
+ const firstProp = itemLine.slice(2).trim();
36
+ if (firstProp) {
37
+ const propColon = firstProp.indexOf(':');
38
+ if (propColon > 0) {
39
+ const propKey = firstProp.slice(0, propColon).trim();
40
+ let propVal = firstProp.slice(propColon + 1).trim();
41
+ if (propVal.startsWith('"') && propVal.endsWith('"')) {
42
+ propVal = propVal.slice(1, -1).replace(/\\"/g, '"');
43
+ }
44
+ currentItem[propKey] = propVal;
45
+ }
46
+ }
47
+ } else if (currentItem) {
48
+ const propColon = itemLine.indexOf(':');
49
+ if (propColon > 0) {
50
+ const propKey = itemLine.slice(0, propColon).trim();
51
+ let propVal = itemLine.slice(propColon + 1).trim();
52
+ if (propVal.startsWith('"') && propVal.endsWith('"')) {
53
+ propVal = propVal.slice(1, -1).replace(/\\"/g, '"');
54
+ }
55
+ currentItem[propKey] = propVal;
56
+ }
57
+ }
58
+ i++;
59
+ }
60
+ if (currentItem) items.push(currentItem);
61
+ fm[key] = items;
62
+ continue;
63
+ }
64
+
65
+ // Handle quoted strings
66
+ if (value.startsWith('"') && value.endsWith('"')) {
67
+ value = value.slice(1, -1).replace(/\\"/g, '"');
68
+ }
69
+ // Handle inline arrays
70
+ else if (value.startsWith('[')) {
71
+ value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^["']|["']$/g, ''));
72
+ }
73
+ // Handle booleans
74
+ else if (value === 'true') {
75
+ value = true;
76
+ } else if (value === 'false') {
77
+ value = false;
78
+ }
79
+ fm[key] = value;
80
+ }
81
+ i++;
82
+ }
83
+ return fm;
84
+ }
85
+
86
+ export function parseFrontmatterAndBody(content) {
87
+ const match = content.match(/^---\n([\s\S]*?)\n---\n*([\s\S]*)/);
88
+ if (!match) return { frontmatter: {}, body: content };
89
+ return { frontmatter: parseFrontmatter(content), body: match[2].trim() };
90
+ }
91
+
92
+ export function buildMarkdown(fm, body) {
93
+ const tagArray = typeof fm.tags === 'string'
94
+ ? fm.tags.split(',').map(t => t.trim()).filter(Boolean)
95
+ : (fm.tags || []);
96
+
97
+ const galleryArray = Array.isArray(fm.gallery) ? fm.gallery : [];
98
+ const imagesArray = Array.isArray(fm.images) ? fm.images : [];
99
+
100
+ const lines = [
101
+ '---',
102
+ 'title: "' + (fm.title || '').replace(/"/g, '\\"') + '"',
103
+ 'synopsis: "' + (fm.synopsis || '').replace(/"/g, '\\"') + '"',
104
+ 'date: ' + fm.date,
105
+ 'tags: [' + tagArray.map(t => '"' + t + '"').join(', ') + ']',
106
+ ];
107
+ if (fm.image) lines.push('image: ' + fm.image);
108
+ if (fm.imageCaption) lines.push('imageCaption: "' + fm.imageCaption.replace(/"/g, '\\"') + '"');
109
+ if (fm.imageCredit) lines.push('imageCredit: "' + fm.imageCredit.replace(/"/g, '\\"') + '"');
110
+ if (fm.author) lines.push('author: "' + fm.author.replace(/"/g, '\\"') + '"');
111
+ if (imagesArray.length > 0) {
112
+ const imagesYaml = imagesArray.map(item => {
113
+ let yaml = ' - src: "' + (item.src || '') + '"';
114
+ if (item.caption) yaml += '\n caption: "' + item.caption.replace(/"/g, '\\"') + '"';
115
+ if (item.credit) yaml += '\n credit: "' + item.credit.replace(/"/g, '\\"') + '"';
116
+ return yaml;
117
+ }).join('\n');
118
+ lines.push('images:\n' + imagesYaml);
119
+ }
120
+ if (galleryArray.length > 0) {
121
+ const galleryYaml = galleryArray.map(item => {
122
+ if (typeof item === 'string') {
123
+ return ' - src: "' + item + '"';
124
+ }
125
+ let yaml = ' - src: "' + (item.src || '') + '"';
126
+ if (item.caption) yaml += '\n caption: "' + item.caption.replace(/"/g, '\\"') + '"';
127
+ if (item.credit) yaml += '\n credit: "' + item.credit.replace(/"/g, '\\"') + '"';
128
+ return yaml;
129
+ }).join('\n');
130
+ lines.push('gallery:\n' + galleryYaml);
131
+ }
132
+ if (fm.showGallery) lines.push('showGallery: true');
133
+ if (fm.lede === true) lines.push('lede: true');
134
+ if (fm.status) lines.push('status: ' + fm.status);
135
+ lines.push('---');
136
+ return lines.join('\n') + '\n\n' + body;
137
+ }
package/index.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * SeeEmEss - A simple CMS built on Eleventy
3
+ *
4
+ * @example
5
+ * // In your eleventy.config.js:
6
+ * import filters from 'seeemess/filters';
7
+ * export default function(eleventyConfig) {
8
+ * filters(eleventyConfig);
9
+ * }
10
+ *
11
+ * @example
12
+ * // In your admin/start.js:
13
+ * import { startAdminServer, createConfig } from 'seeemess';
14
+ *
15
+ * startAdminServer({
16
+ * sections: [
17
+ * { id: 'posts', name: 'Blog Posts', folder: 'posts', tag: 'post' }
18
+ * ]
19
+ * });
20
+ */
21
+
22
+ // Admin server and configuration
23
+ export { startAdminServer, createConfig } from './admin/server.js';
24
+
25
+ // Eleventy filters
26
+ export { default as filters } from './_config/filters.js';
27
+
28
+ // Re-export config utilities for advanced usage
29
+ export { getConfig, getContentDir, getPublicDir, getSections } from './admin/config.js';
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "seeemess",
3
+ "version": "1.0.0",
4
+ "description": "A simple CMS framework built on Eleventy with an admin interface for managing blog content.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./filters": "./_config/filters.js",
10
+ "./admin": "./admin/server.js"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test",
14
+ "test:coverage": "node --test --experimental-test-coverage"
15
+ },
16
+ "keywords": [
17
+ "cms",
18
+ "eleventy",
19
+ "11ty",
20
+ "blog",
21
+ "admin",
22
+ "content-management"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git://github.com/user/seeemess.git"
27
+ },
28
+ "author": "",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "eta": "^3.5.0",
35
+ "express": "^4.22.1",
36
+ "luxon": "^3.6.1",
37
+ "markdown-it": "^14.1.0",
38
+ "markdown-it-attrs": "^4.3.1",
39
+ "sharp": "^0.34.5",
40
+ "zod": "^3.25.67",
41
+ "zod-validation-error": "^3.5.2"
42
+ },
43
+ "peerDependencies": {
44
+ "@11ty/eleventy": ">=3.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@11ty/eleventy": {
48
+ "optional": true
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,157 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ // We need to reset the module state between tests, so we use dynamic imports
5
+ async function freshImport() {
6
+ // Clear the module from cache to reset singleton state
7
+ const modulePath = new URL('../admin/config.js', import.meta.url).href;
8
+
9
+ // Create a unique import each time by adding a cache-busting query
10
+ const uniqueUrl = `${modulePath}?t=${Date.now()}-${Math.random()}`;
11
+ return import(uniqueUrl);
12
+ }
13
+
14
+ describe('createConfig', () => {
15
+ it('returns config with defaults', async () => {
16
+ const { createConfig } = await freshImport();
17
+ const config = createConfig();
18
+
19
+ assert.ok(config.CMS_ROOT);
20
+ assert.ok(config.CONTENT_DIR);
21
+ assert.ok(config.PUBLIC_DIR);
22
+ assert.strictEqual(config.IMAGE_URL_PATH, '/uploads');
23
+ assert.strictEqual(config.PORT, 3000);
24
+ assert.deepStrictEqual(config.SECTIONS, []);
25
+ assert.ok(Array.isArray(config.IMAGE_SIZES));
26
+ assert.ok(Array.isArray(config.BRANCH_WORDS));
27
+ });
28
+
29
+ it('merges provided options', async () => {
30
+ const { createConfig } = await freshImport();
31
+ const sections = [{ id: 'blog', name: 'Blog', folder: 'blog', tag: 'blog' }];
32
+ const config = createConfig({
33
+ cmsRoot: '/custom/root',
34
+ port: 4000,
35
+ sections
36
+ });
37
+
38
+ assert.strictEqual(config.CMS_ROOT, '/custom/root');
39
+ assert.strictEqual(config.PORT, 4000);
40
+ assert.deepStrictEqual(config.SECTIONS, sections);
41
+ });
42
+
43
+ it('normalizes imageUrlPath - adds leading slash', async () => {
44
+ const { createConfig } = await freshImport();
45
+ const config = createConfig({ imageUrlPath: 'images' });
46
+ assert.strictEqual(config.IMAGE_URL_PATH, '/images');
47
+ });
48
+
49
+ it('normalizes imageUrlPath - removes trailing slash', async () => {
50
+ const { createConfig } = await freshImport();
51
+ const config = createConfig({ imageUrlPath: '/uploads/' });
52
+ assert.strictEqual(config.IMAGE_URL_PATH, '/uploads');
53
+ });
54
+
55
+ it('normalizes imageUrlPath - handles both issues', async () => {
56
+ const { createConfig } = await freshImport();
57
+ const config = createConfig({ imageUrlPath: 'assets/images/' });
58
+ assert.strictEqual(config.IMAGE_URL_PATH, '/assets/images');
59
+ });
60
+
61
+ it('accepts custom image sizes', async () => {
62
+ const { createConfig } = await freshImport();
63
+ const customSizes = [{ suffix: '-sm', width: 200 }];
64
+ const config = createConfig({ imageSizes: customSizes });
65
+ assert.deepStrictEqual(config.IMAGE_SIZES, customSizes);
66
+ });
67
+
68
+ it('accepts custom branch words', async () => {
69
+ const { createConfig } = await freshImport();
70
+ const customWords = ['foo', 'bar', 'baz'];
71
+ const config = createConfig({ branchWords: customWords });
72
+ assert.deepStrictEqual(config.BRANCH_WORDS, customWords);
73
+ });
74
+
75
+ it('resolves previewTemplate relative to cmsRoot', async () => {
76
+ const { createConfig } = await freshImport();
77
+ const config = createConfig({
78
+ cmsRoot: '/project',
79
+ previewTemplate: 'templates/preview.eta'
80
+ });
81
+ assert.strictEqual(config.PREVIEW_TEMPLATE, '/project/templates/preview.eta');
82
+ });
83
+ });
84
+
85
+ describe('getConfig', () => {
86
+ it('throws if not initialized', async () => {
87
+ const { getConfig } = await freshImport();
88
+ assert.throws(
89
+ () => getConfig(),
90
+ { message: 'Config not initialized. Call createConfig() first.' }
91
+ );
92
+ });
93
+
94
+ it('returns config after createConfig called', async () => {
95
+ const { createConfig, getConfig } = await freshImport();
96
+ const created = createConfig({ port: 5000 });
97
+ const retrieved = getConfig();
98
+
99
+ assert.strictEqual(retrieved, created);
100
+ assert.strictEqual(retrieved.PORT, 5000);
101
+ });
102
+ });
103
+
104
+ describe('individual getters', () => {
105
+ it('getCmsRoot returns CMS_ROOT', async () => {
106
+ const { createConfig, getCmsRoot } = await freshImport();
107
+ createConfig({ cmsRoot: '/test/root' });
108
+ assert.strictEqual(getCmsRoot(), '/test/root');
109
+ });
110
+
111
+ it('getContentDir returns CONTENT_DIR', async () => {
112
+ const { createConfig, getContentDir } = await freshImport();
113
+ createConfig({ contentDir: '/custom/content' });
114
+ assert.strictEqual(getContentDir(), '/custom/content');
115
+ });
116
+
117
+ it('getPublicDir returns PUBLIC_DIR', async () => {
118
+ const { createConfig, getPublicDir } = await freshImport();
119
+ createConfig({ publicDir: '/custom/public' });
120
+ assert.strictEqual(getPublicDir(), '/custom/public');
121
+ });
122
+
123
+ it('getImageUrlPath returns IMAGE_URL_PATH', async () => {
124
+ const { createConfig, getImageUrlPath } = await freshImport();
125
+ createConfig({ imageUrlPath: '/media' });
126
+ assert.strictEqual(getImageUrlPath(), '/media');
127
+ });
128
+
129
+ it('getPort returns PORT', async () => {
130
+ const { createConfig, getPort } = await freshImport();
131
+ createConfig({ port: 8080 });
132
+ assert.strictEqual(getPort(), 8080);
133
+ });
134
+
135
+ it('getSections returns SECTIONS', async () => {
136
+ const { createConfig, getSections } = await freshImport();
137
+ const sections = [{ id: 'test' }];
138
+ createConfig({ sections });
139
+ assert.deepStrictEqual(getSections(), sections);
140
+ });
141
+
142
+ it('getImageSizes returns IMAGE_SIZES', async () => {
143
+ const { createConfig, getImageSizes } = await freshImport();
144
+ createConfig();
145
+ const sizes = getImageSizes();
146
+ assert.ok(Array.isArray(sizes));
147
+ assert.ok(sizes.length > 0);
148
+ });
149
+
150
+ it('getBranchWords returns BRANCH_WORDS', async () => {
151
+ const { createConfig, getBranchWords } = await freshImport();
152
+ createConfig();
153
+ const words = getBranchWords();
154
+ assert.ok(Array.isArray(words));
155
+ assert.ok(words.length > 0);
156
+ });
157
+ });