source: Klonkt/scripts/migrate-posts-to-html.js@ e679bce

main
Last change on this file since e679bce was 8453812, checked in by roboburr <roboburr@…>, 3 months ago

Branding: 100% PrutCMS/PrutFolio-free — Klonkt as original product

All fork/lineage references (README, package description, server/comments,
PrutFolio-as-noun -> Klonkt-site) removed. Plus a small, mysterious wink
at a certain Bart in CircleFederation.js. Internal package name (prutfolio)
+ PWA id + server paths left untouched (stability).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@…>

  • Property mode set to 100644
File size: 3.1 KB
Line 
1/**
2 * P58 — One-shot migration: convert markdown post.content to sanitized HTML.
3 *
4 * Run on the server AFTER deploying P58:
5 * node scripts/migrate-posts-to-html.js
6 *
7 * Idempotent: posts whose content already starts with '<' are detected as
8 * HTML and skipped. Markdown posts are rendered via MarkdownService, then
9 * sanitized via HtmlSanitizerService, then written back. Original markdown
10 * is preserved in posts.content_legacy_md so we can roll back if needed.
11 *
12 * Re-running the script after the column is added is safe — the second run
13 * will see HTML content and skip everything.
14 */
15
16import db from '../src/config/database.js';
17import MarkdownService from '../src/services/MarkdownService.js';
18import HtmlSanitizerService from '../src/services/HtmlSanitizerService.js';
19
20function ensureBackupColumn() {
21 // Try to add the legacy_md backup column; SQLite throws if it exists.
22 try {
23 db.exec(`ALTER TABLE posts ADD COLUMN content_legacy_md TEXT`);
24 console.log(' + added posts.content_legacy_md column');
25 } catch (e) {
26 if (!/duplicate column/i.test(e.message)) throw e;
27 }
28}
29
30function looksLikeHtml(s) {
31 if (!s) return false;
32 // Heuristic: starts with a tag, OR contains common block-level tags. Doesn't
33 // need to be airtight — false positives just mean we skip a post that's
34 // already mostly HTML, which is fine.
35 const t = s.trimStart();
36 return t.startsWith('<') || /<\/?(p|h[1-6]|ul|ol|li|blockquote|figure|img|div)\b/i.test(s);
37}
38
39function migrateOne(post) {
40 if (!post.content || !post.content.trim()) {
41 return { id: post.id, action: 'empty' };
42 }
43 if (looksLikeHtml(post.content)) {
44 return { id: post.id, action: 'skipped-already-html' };
45 }
46 if (post.content_legacy_md) {
47 // Already migrated (legacy backup exists) but content somehow markdown again?
48 return { id: post.id, action: 'skipped-already-backed-up' };
49 }
50
51 const renderedHtml = MarkdownService.render(post.content);
52 const cleanHtml = HtmlSanitizerService.sanitize(renderedHtml);
53
54 db.prepare(`
55 UPDATE posts SET content = ?, content_legacy_md = ?, updated_at = updated_at
56 WHERE id = ?
57 `).run(cleanHtml, post.content, post.id);
58
59 return { id: post.id, action: 'migrated', from: post.content.length, to: cleanHtml.length };
60}
61
62function main() {
63 console.log('Klonkt — markdown → HTML post migration\n');
64 ensureBackupColumn();
65
66 const posts = db.prepare(`SELECT id, slug, content, content_legacy_md FROM posts`).all();
67 console.log(` found ${posts.length} posts\n`);
68
69 const stats = { migrated: 0, skipped: 0, empty: 0 };
70 for (const p of posts) {
71 const r = migrateOne(p);
72 const tag = r.action.startsWith('skipped') ? 'skipped' :
73 r.action === 'empty' ? 'empty' : 'migrated';
74 stats[tag]++;
75 const detail = r.action === 'migrated' ? ` (${r.from} md → ${r.to} html chars)` : ` [${r.action}]`;
76 console.log(` - ${p.slug}${detail}`);
77 }
78
79 console.log(`\nDone. migrated=${stats.migrated} skipped=${stats.skipped} empty=${stats.empty}`);
80 console.log('Backup column posts.content_legacy_md retains original markdown.');
81}
82
83main();
Note: See TracBrowser for help on using the repository browser.