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 +7 -1
- package/admin/routes/posts.js +10 -4
- package/admin/routes/publish.js +8 -7
- package/admin/server.js +2 -2
- package/admin/templates/admin.eta +23 -0
- package/admin/utils/markdown.js +60 -1
- package/package.json +1 -1
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;
|
package/admin/routes/posts.js
CHANGED
|
@@ -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
|
});
|
package/admin/routes/publish.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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';
|
package/admin/utils/markdown.js
CHANGED
|
@@ -89,7 +89,61 @@ export function parseFrontmatterAndBody(content) {
|
|
|
89
89
|
return { frontmatter: parseFrontmatter(content), body: match[2].trim() };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
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
|
}
|