seeemess 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/admin/config.js CHANGED
@@ -35,6 +35,8 @@ let currentConfig = null;
35
35
  * Supported types: 'text', 'textarea'
36
36
  * @param {Array} [options.imageSizes] Array of image size definitions: { suffix, width }
37
37
  * @param {Array} [options.branchWords] Array of words for git branch name generation
38
+ * @param {string} [options.siteUrl] Base URL for story links (e.g., 'https://example.com/blog'). If set, the story URL is shown on the edit screen.
39
+ * @param {string} [options.deployCommand] Git command to run after merging to main (defaults to 'git push origin main')
38
40
  * @returns {Object} Configuration object
39
41
  */
40
42
  export function createConfig(options = {}) {
@@ -56,7 +58,9 @@ export function createConfig(options = {}) {
56
58
  PORT: options.port || process.env.PORT || 3000,
57
59
  SECTIONS: options.sections || [],
58
60
  IMAGE_SIZES: options.imageSizes || DEFAULT_IMAGE_SIZES,
59
- BRANCH_WORDS: options.branchWords || DEFAULT_BRANCH_WORDS
61
+ BRANCH_WORDS: options.branchWords || DEFAULT_BRANCH_WORDS,
62
+ SITE_URL: options.siteUrl || '',
63
+ DEPLOY_COMMAND: options.deployCommand || 'git push origin main'
60
64
  };
61
65
 
62
66
  // Set as current config for routes to access
@@ -88,3 +92,5 @@ export const getPort = () => getConfig().PORT;
88
92
  export const getSections = () => getConfig().SECTIONS;
89
93
  export const getImageSizes = () => getConfig().IMAGE_SIZES;
90
94
  export const getBranchWords = () => getConfig().BRANCH_WORDS;
95
+ export const getSiteUrl = () => getConfig().SITE_URL;
96
+ export const getDeployCommand = () => getConfig().DEPLOY_COMMAND;
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  import fs from 'fs';
3
3
  import { join } from 'path';
4
4
  import { getContentDir, getSections } from '../config.js';
5
- import { parseFrontmatter, parseFrontmatterAndBody, buildMarkdown } from '../utils/markdown.js';
5
+ import { parseFrontmatter, parseFrontmatterAndBody, buildMarkdown, extractUnmanagedFrontmatter } from '../utils/markdown.js';
6
6
 
7
7
  const router = Router();
8
8
 
@@ -71,6 +71,7 @@ router.post('/set-lede/*', (req, res) => {
71
71
 
72
72
  if (itemSection === section && itemFm.lede) {
73
73
  const { frontmatter: fm, body: bd } = parseFrontmatterAndBody(itemContent);
74
+ const itemUnmanaged = extractUnmanagedFrontmatter(itemContent);
74
75
  delete fm.lede;
75
76
  const knownKeys = ['title','synopsis','date','tags','image','imageCaption','imageCredit','imageFocalX','imageFocalY','author','gallery','images','showGallery','lede','status'];
76
77
  const customFields = {};
@@ -91,7 +92,7 @@ router.post('/set-lede/*', (req, res) => {
91
92
  status: fm.status,
92
93
  lede: false,
93
94
  customFields
94
- }, bd);
95
+ }, bd, itemUnmanaged);
95
96
  fs.writeFileSync(itemFullPath, updatedContent);
96
97
  }
97
98
  }
@@ -101,6 +102,7 @@ router.post('/set-lede/*', (req, res) => {
101
102
  clearLedeInSection(getContentDir());
102
103
 
103
104
  // Set lede on the target post
105
+ const unmanagedYaml = extractUnmanagedFrontmatter(content);
104
106
  const knownKeys = ['title','synopsis','date','tags','image','imageCaption','imageCredit','imageFocalX','imageFocalY','author','gallery','images','showGallery','lede','status'];
105
107
  const customFields = {};
106
108
  Object.keys(frontmatter).forEach(k => { if (!knownKeys.includes(k)) customFields[k] = frontmatter[k]; });
@@ -120,7 +122,7 @@ router.post('/set-lede/*', (req, res) => {
120
122
  status: frontmatter.status,
121
123
  lede: true,
122
124
  customFields
123
- }, body);
125
+ }, body, unmanagedYaml);
124
126
  fs.writeFileSync(fullPath, updatedContent);
125
127
 
126
128
  res.json({ success: true, section });
@@ -163,8 +165,12 @@ router.put('/*', (req, res) => {
163
165
  const { title, synopsis, tags, image, imageCaption, imageCredit, imageFocalX, imageFocalY, body, date: clientDate, status, author, gallery, images, showGallery, lede, customFields } = req.body;
164
166
  if (!fs.existsSync(fullPath)) return res.status(404).json({ error: 'Post not found' });
165
167
 
168
+ // Preserve frontmatter blocks the admin doesn't manage (e.g. scorecard)
169
+ const existingContent = fs.readFileSync(fullPath, 'utf-8');
170
+ const unmanagedYaml = extractUnmanagedFrontmatter(existingContent);
171
+
166
172
  const date = clientDate ? clientDate.slice(0, 10) : new Date().toISOString().slice(0, 10);
167
- const fileContent = buildMarkdown({ title, synopsis, date, tags, image, imageCaption, imageCredit, imageFocalX, imageFocalY, author, gallery, images, showGallery, lede, status: status || 'draft', customFields }, body);
173
+ const fileContent = buildMarkdown({ title, synopsis, date, tags, image, imageCaption, imageCredit, imageFocalX, imageFocalY, author, gallery, images, showGallery, lede, status: status || 'draft', customFields }, body, unmanagedYaml);
168
174
  fs.writeFileSync(fullPath, fileContent);
169
175
  res.json({ success: true });
170
176
  });
@@ -1,9 +1,9 @@
1
1
  import { Router } from 'express';
2
2
  import fs from 'fs';
3
3
  import { join } from 'path';
4
- import { getCmsRoot, getContentDir } from '../config.js';
4
+ import { getCmsRoot, getContentDir, getDeployCommand } from '../config.js';
5
5
  import { runGit, randomWord } from '../utils/git.js';
6
- import { parseFrontmatterAndBody, buildMarkdown } from '../utils/markdown.js';
6
+ import { parseFrontmatterAndBody, buildMarkdown, extractUnmanagedFrontmatter } from '../utils/markdown.js';
7
7
  import { deletedImages, clearDeletedImages } from './images.js';
8
8
 
9
9
  const router = Router();
@@ -18,13 +18,14 @@ router.post('/*', (req, res) => {
18
18
  try {
19
19
  const content = fs.readFileSync(fullPath, 'utf-8');
20
20
  const { frontmatter, body } = parseFrontmatterAndBody(content);
21
+ const unmanagedYaml = extractUnmanagedFrontmatter(content);
21
22
 
22
23
  // Update status to published, preserving all existing frontmatter fields
23
24
  const updatedContent = buildMarkdown({
24
25
  ...frontmatter,
25
26
  tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.join(', ') : frontmatter.tags,
26
27
  status: 'published'
27
- }, body);
28
+ }, body, unmanagedYaml);
28
29
  fs.writeFileSync(fullPath, updatedContent);
29
30
 
30
31
  // Create branch name
@@ -58,8 +59,8 @@ router.post('/*', (req, res) => {
58
59
  // Stage all new files/directories in public
59
60
  runGit('git add public/');
60
61
 
61
- // Check if there are changes to commit
62
- const status = runGit('git status --porcelain');
62
+ // Check if there are staged changes to commit
63
+ const status = runGit('git diff --cached --name-only');
63
64
  if (!status.trim()) {
64
65
  // No changes - clean up and return
65
66
  runGit('git checkout main');
@@ -74,7 +75,7 @@ router.post('/*', (req, res) => {
74
75
  // Switch back to main and merge
75
76
  runGit('git checkout main');
76
77
  runGit('git merge --no-ff ' + branchName + ' -m "Merge branch ' + branchName + '"');
77
- runGit('git push origin main');
78
+ runGit(getDeployCommand());
78
79
  runGit('git branch -d ' + branchName);
79
80
 
80
81
  res.json({ success: true, branch: branchName, message: 'Published and pushed to main' });
@@ -138,7 +139,7 @@ export function publishChangesHandler(req, res) {
138
139
 
139
140
  runGit('git checkout main');
140
141
  runGit('git merge --no-ff ' + branchName + ' -m "Merge branch ' + branchName + '"');
141
- runGit('git push origin main');
142
+ runGit(getDeployCommand());
142
143
  runGit('git branch -d ' + branchName);
143
144
 
144
145
  clearDeletedImages();
package/admin/server.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { Eta } from 'eta';
5
5
 
6
6
  // Config
7
- import { createConfig, getConfig, getCmsRoot, getPublicDir, getImageUrlPath, getPreviewTemplate, getTitle, getPort, getSections } from './config.js';
7
+ import { createConfig, getConfig, getCmsRoot, getPublicDir, getImageUrlPath, getPreviewTemplate, getTitle, getPort, getSections, getSiteUrl } from './config.js';
8
8
 
9
9
  // Routes
10
10
  import postsRouter from './routes/posts.js';
@@ -54,7 +54,7 @@ export function startAdminServer(options = {}) {
54
54
 
55
55
  // Admin interface
56
56
  app.get('/', (req, res) => {
57
- res.send(eta.render('admin', { imageUrlPath, title: getTitle() }));
57
+ res.send(eta.render('admin', { imageUrlPath, title: getTitle(), siteUrl: getSiteUrl() }));
58
58
  });
59
59
 
60
60
  // API Routes
@@ -292,6 +292,10 @@
292
292
  <input type="hidden" id="postStatus" value="draft">
293
293
  <input type="hidden" id="postLede" value="false">
294
294
 
295
+ <div id="storyUrlRow" style="display:none; margin-bottom:0.5rem;">
296
+ <a id="storyUrl" href="#" target="_blank" style="font-size:0.85rem; color:#2563eb; word-break:break-all;"></a>
297
+ </div>
298
+
295
299
  <div>
296
300
  <label>Title</label>
297
301
  <input type="text" id="title" required>
@@ -826,15 +830,33 @@
826
830
  previewImage();
827
831
  updateFocalPointMarker();
828
832
  document.querySelectorAll('.post-list li').forEach(li => li.classList.toggle('active', li.dataset.path === path));
833
+ showStoryUrl(path);
829
834
  showStatus('');
830
835
  markFormClean(false);
831
836
  }
832
837
 
838
+ const SITE_URL = '<%= it.siteUrl || "" %>';
839
+
840
+ function showStoryUrl(path) {
841
+ const row = document.getElementById('storyUrlRow');
842
+ if (!SITE_URL || !path) {
843
+ row.style.display = 'none';
844
+ return;
845
+ }
846
+ const urlPath = path.replace(/\.md$/, '').replace(/\/index$/, '');
847
+ const url = SITE_URL.replace(/\/$/, '') + '/' + urlPath + '/';
848
+ const link = document.getElementById('storyUrl');
849
+ link.href = url;
850
+ link.textContent = url;
851
+ row.style.display = 'block';
852
+ }
853
+
833
854
  function newPost() {
834
855
  document.getElementById('postPath').value = '';
835
856
  document.getElementById('postStatus').value = 'draft';
836
857
  document.getElementById('postLede').value = 'false';
837
858
  document.getElementById('postForm').reset();
859
+ showStoryUrl('');
838
860
  // Set current date and time (format: YYYY-MM-DDTHH:MM)
839
861
  const now = new Date();
840
862
  document.getElementById('date').value = now.toISOString().slice(0, 16);
@@ -911,6 +933,7 @@
911
933
  markFormClean(true);
912
934
  await loadPosts();
913
935
  if (result.path) loadPost(result.path);
936
+ showStoryUrl(result.path || document.getElementById('postPath').value);
914
937
  } else {
915
938
  showStatus('Error: ' + (result.error || 'Unknown'), 'error');
916
939
  btn.textContent = 'Save Draft';
@@ -89,7 +89,61 @@ export function parseFrontmatterAndBody(content) {
89
89
  return { frontmatter: parseFrontmatter(content), body: match[2].trim() };
90
90
  }
91
91
 
92
- export function buildMarkdown(fm, body) {
92
+ /**
93
+ * Extract raw YAML blocks from frontmatter that the simple parser can't handle
94
+ * (e.g. deeply nested structures like scorecard). Returns them as a raw string
95
+ * that can be appended verbatim when rebuilding the file.
96
+ */
97
+ export function extractUnmanagedFrontmatter(content) {
98
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
99
+ if (!match) return '';
100
+
101
+ const managedKeys = ['title','synopsis','date','tags','image','imageCaption','imageCredit','imageFocalX','imageFocalY','author','gallery','images','showGallery','lede','status'];
102
+ const rawYaml = match[1];
103
+ const lines = rawYaml.split('\n');
104
+ const unmanagedBlocks = [];
105
+ let i = 0;
106
+
107
+ while (i < lines.length) {
108
+ const line = lines[i];
109
+ const colonIdx = line.indexOf(':');
110
+
111
+ // Skip blank lines
112
+ if (!line.trim()) { i++; continue; }
113
+
114
+ // Only process top-level keys (no leading whitespace)
115
+ if (colonIdx > 0 && !line.startsWith(' ') && !line.startsWith('\t')) {
116
+ const key = line.slice(0, colonIdx).trim();
117
+
118
+ if (managedKeys.includes(key)) {
119
+ // Skip this key and any indented continuation lines
120
+ i++;
121
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t'))) {
122
+ i++;
123
+ }
124
+ continue;
125
+ }
126
+
127
+ // Check if this unknown key has indented children (complex nested YAML)
128
+ const blockStart = i;
129
+ i++;
130
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t'))) {
131
+ i++;
132
+ }
133
+ // Only preserve as unmanaged if it has nested children
134
+ // (simple key: value pairs are handled by buildMarkdown's unknown-key loop)
135
+ if (i > blockStart + 1) {
136
+ unmanagedBlocks.push(lines.slice(blockStart, i).join('\n'));
137
+ }
138
+ } else {
139
+ i++;
140
+ }
141
+ }
142
+
143
+ return unmanagedBlocks.join('\n');
144
+ }
145
+
146
+ export function buildMarkdown(fm, body, unmanagedYaml) {
93
147
  const tagArray = typeof fm.tags === 'string'
94
148
  ? fm.tags.split(',').map(t => t.trim()).filter(Boolean)
95
149
  : (fm.tags || []);
@@ -158,6 +212,11 @@ export function buildMarkdown(fm, body) {
158
212
  }
159
213
  });
160
214
 
215
+ // Append any unmanaged frontmatter blocks (e.g. scorecard) verbatim
216
+ if (unmanagedYaml) {
217
+ lines.push(unmanagedYaml);
218
+ }
219
+
161
220
  lines.push('---');
162
221
  return lines.join('\n') + '\n\n' + body;
163
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seeemess",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "A simple CMS framework built on Eleventy with an admin interface for managing blog content.",
5
5
  "type": "module",
6
6
  "main": "index.js",