chore: retire legacy custom CMS (content-editor.js) (MITHOME-93)
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Canceled after 0s
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Canceled after 0s
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Canceled after 0s
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Canceled after 0s
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Canceled after 0s
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Canceled after 0s
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Canceled after 0s
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Canceled after 0s

Removes the standalone, git-push-based content editor that predates
Payload CMS: content-editor.js, its scripts/cms-*.js modules, its
scripts/test-content-editor-*.js + scripts/test-cms-publish.js test
suite, scripts/markdown-render.js (only used by the editor's guide
renderer), the proto-side test doubles (cms-editor-client.test.ts,
cms-editor-shortcuts.test.ts), and the editor's own user guide
(docs/felhasznaloi-utmutato.md).

Kept: proto/src/content/*.json (still the source for
migrate-content-to-payload.ts and test fixtures for Header/Footer,
per MITHOME-96), proto/src/content/schema.js + scripts/test-content-schema.js
(still validate those JSON files), and docs/content-editor-recovery.md
(historical incident record, not user-facing tool docs).

Safety net before deletion (per user request): added
proto/scripts/export-content-snapshot.ts, a reusable Payload Local API
exporter, and ran it to produce docs/backups/payload-content-snapshot-*.json
— a full hu/en snapshot of every Global + LegalPages + Partners document
at the moment of retirement. Also confirmed no data-loss risk otherwise:
.content-backups/ (the editor's own gitignored backup dir) tops out at
2026-08-23, well before today's fresh migration run, and every JSON
edit ever made through the editor already exists as its own git commit
("content: frissítve a CMS-ből").

Updated dangling references: pre-deploy-tests.sh and
.agent/steering/testing.md (dropped the CMS test block),
.agent/workflows/deploy.md (publish flow is now Payload draft/publish,
not git push), CLAUDE.md + .agent/AGENTS.md (dropped the /cms-feature
workflow, deleted alongside it), README.md (stack description),
.agent/steering/development-rules.md (the guide-maintenance rule no
longer has a guide to maintain).

Verified: tsc, lint, proto unit tests (51 passed), root
test-content-schema.js, plane-sync unit tests, production build all
green after the deletion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Do Siki
2026-09-11 21:48:50 +02:00
co-authored by Claude Sonnet 5
parent 21a0d73639
commit 12b2711168
40 changed files with 1316 additions and 4199 deletions
+70
View File
@@ -0,0 +1,70 @@
/**
* MITHOME-93 biztonsági lépés: a régi egyedi CMS (content-editor.js +
* scripts/cms-*.js) eltávolítása előtt exportálja a Payload jelenlegi
* (publikált) szöveges tartalmát egy olvasható JSON fájlba — mindkét
* locale-lal (hu, en) —, hogy git-committolt, ember által is átnézhető
* biztonsági mentés maradjon a leépítés pillanatáról.
*
* NEM helyettesíti a git history-t (a src/content/*.json fájlok minden
* korábbi szerkesztése megvan commit-onként), és nem helyettesíti a
* MongoDB-t (az az élő forrás) — ez egy plusz, könnyen olvasható
* pillanatkép a "mielőtt törlünk, mentsünk" elv jegyében.
*
* Futtatás (proto/ mappából, futó MongoDB-vel és beállított env-ekkel):
* node --env-file=.env.local --import tsx scripts/export-content-snapshot.ts
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { getPayload, type Payload } from 'payload'
import config from '../src/payload.config'
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
const outDir = path.resolve(scriptDir, '../../docs/backups')
const GLOBAL_SLUGS = ['home', 'about', 'services', 'contact', 'common'] as const
const LOCALES = ['hu', 'en'] as const
async function exportGlobals(payload: Payload) {
const result: Record<string, Record<string, unknown>> = {}
for (const slug of GLOBAL_SLUGS) {
result[slug] = {}
for (const locale of LOCALES) {
result[slug][locale] = await payload.findGlobal({ slug, locale })
}
}
return result
}
async function exportCollection(payload: Payload, collection: 'legal-pages' | 'partners') {
const result: Record<string, unknown> = {}
for (const locale of LOCALES) {
const { docs } = await payload.find({ collection, locale, limit: 1000 })
result[locale] = docs
}
return result
}
async function run() {
const payload = await getPayload({ config })
const snapshot = {
exportedAt: new Date().toISOString(),
reason: 'MITHOME-93 — régi CMS (content-editor.js) leépítése előtti biztonsági mentés',
globals: await exportGlobals(payload),
collections: {
legalPages: await exportCollection(payload, 'legal-pages'),
partners: await exportCollection(payload, 'partners'),
},
}
fs.mkdirSync(outDir, { recursive: true })
const filename = `payload-content-snapshot-${snapshot.exportedAt.replace(/[:.]/g, '-')}.json`
const outPath = path.join(outDir, filename)
fs.writeFileSync(outPath, JSON.stringify(snapshot, null, 2), 'utf8')
payload.logger.info(`Snapshot kiírva: ${outPath}`)
process.exit(0)
}
run()
@@ -1,91 +0,0 @@
/**
* Regression test for the Content Editor browser script: deleting an array
* item via its ❌ button must reindex the remaining items, otherwise collect()
* produces sparse arrays (null holes) that fail schema validation
* ("$.details.services[1].specs.items[0]: string érték szükséges").
*
* Runs the REAL scripts/cms-editor-client.js in jsdom and clicks the actual
* delete buttons — earlier coverage only exercised reindexItems() directly,
* which missed that the onclick handler removed the node BEFORE looking up
* its container (detached node → closest() === null → no reindex).
*/
import fs from 'fs'
import path from 'path'
const clientJs = fs.readFileSync(path.join(__dirname, '../../../scripts/cms-editor-client.js'), 'utf8')
const service = (n: number) => ({
id: `svc-${n}`,
title: `Szolgáltatás ${n}`,
description: `Leírás ${n}`,
icon: '🔧',
features: [`feature ${n}`],
ctaText: 'CTA',
})
const makeData = () => ({
details: {
title: 'Részletek',
subtitle: 'Alcím',
services: [
{ icon: 'a', title: 's0', description: 'd0', specs: { title: 't0', items: ['a0', 'b0', 'c0'] } },
{ icon: 'b', title: 's1', description: 'd1', specs: { title: 't1', items: ['a1', 'b1', 'c1'] } },
{ icon: 'c', title: 's2', description: 'd2', specs: { title: 't2', items: ['a2', 'b2', 'c2'] } },
],
},
})
function bootClient(data: unknown) {
;(global as any).DATA = data
;(global as any).FILE = 'services'
;(global as any).CSRF_TOKEN = 'test-token'
;(global as any).fetch = jest.fn()
document.body.innerHTML = '<div id="editor"></div>'
// sloppy-mode eval publishes the script's functions on the global object
;(0, eval)(clientJs)
}
function deleteButtonFor(dataPath: string): HTMLButtonElement {
const field = document.querySelector(`[data-path="${CSS.escape(dataPath)}"]`) as HTMLElement
expect(field).not.toBeNull()
const wrap = field.closest('.str-item') as HTMLElement
expect(wrap).not.toBeNull()
return wrap.querySelector('.btn-del') as HTMLButtonElement
}
afterEach(() => {
delete (global as any).DATA
delete (global as any).FILE
delete (global as any).CSRF_TOKEN
})
describe('Content Editor client delete/reindex', () => {
it('deleting a nested string array item keeps the remaining items dense', () => {
const data = makeData()
bootClient(data)
deleteButtonFor('details.services[1].specs.items[0]').click()
const collected = (global as any).collect()
expect(collected.details.services[1].specs.items).toEqual(['b1', 'c1'])
expect(collected.details.services[0].specs.items).toEqual(['a0', 'b0', 'c0'])
expect(collected.details.services[2].specs.items).toEqual(['a2', 'b2', 'c2'])
})
it('deleting an object card reindexes the outer array', () => {
const data = makeData()
bootClient(data)
const cardHeader = Array.from(document.querySelectorAll('.card-header'))
.find(h => h.textContent === 'details.services[1]') as HTMLElement
expect(cardHeader).not.toBeNull()
const card = cardHeader.closest('.obj-card') as HTMLElement
;(card.querySelector('.btn-del-card') as HTMLButtonElement).click()
const collected = (global as any).collect()
expect(collected.details.services).toHaveLength(2)
expect(collected.details.services[0].title).toBe('s0')
expect(collected.details.services[1].title).toBe('s2')
expect(collected.details.services[1].specs.items).toEqual(['a2', 'b2', 'c2'])
})
})
@@ -1,102 +0,0 @@
/**
* Regression tests for the Content Editor keyboard shortcuts (MITHOME-75).
* Runs the real scripts/cms-editor-client.js in jsdom, dispatches actual
* KeyboardEvents and asserts:
* 1. Ctrl+S calls save (fetch /save) and the browser save dialog is
* suppressed (preventDefault)
* 2. Ctrl+P calls publish
* 3. Ctrl+Shift+V opens the versions page in a new tab
* 4. '?' opens the shortcuts overlay, Esc closes it
* 5. plain 's' inside an input does NOT trigger save
*/
import fs from 'fs'
import path from 'path'
const clientJs = fs.readFileSync(path.join(__dirname, '../../../scripts/cms-editor-client.js'), 'utf8')
+ '\n' + fs.readFileSync(path.join(__dirname, '../../../scripts/cms-editor-shortcuts.js'), 'utf8')
const makeData = () => ({
hero: { title: 'T', subtitle: 'S', description: 'D' },
})
function press(target: Document | Element, init: KeyboardEventInit) {
const ev = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init })
target.dispatchEvent(ev)
return ev
}
const flush = () => new Promise(r => setTimeout(r, 0))
// Globals must exist BEFORE the eval — the client script boots immediately
// (render(DATA, …)). The eval runs ONCE: every eval would add another keydown
// listener to the shared jsdom document, and the toggle-style overlay handler
// would then open/close itself multiple times per keypress.
;(global as any).DATA = makeData()
;(global as any).FILE = 'home'
;(global as any).CSRF_TOKEN = 'boot'
;(global as any).CONTENT_HASH = 'x'.repeat(64)
;(global as any).fetch = jest.fn(async () => ({ status: 200, ok: true, json: async () => ({ ok: true }) }))
;(global as any).window = global
document.body.innerHTML = '<div id="editor"></div>'
;(0, eval)(clientJs)
describe('Content Editor keyboard shortcuts', () => {
let fetchCalls: Array<{ url: string; init?: RequestInit }>
beforeEach(() => {
fetchCalls = []
;(global as any).DATA = makeData()
;(global as any).FILE = 'home'
;(global as any).CSRF_TOKEN = 'test-csrf'
;(global as any).CONTENT_HASH = 'x'.repeat(64)
;(global as any).fetch = jest.fn(async (url: string, init?: RequestInit) => {
fetchCalls.push({ url, init })
return { status: 200, ok: true, json: async () => ({ ok: true, contentHash: 'y'.repeat(64) }) }
})
;(global as any).window = global
;(window as any).open = jest.fn()
document.body.innerHTML = '<div id="editor"></div><span id="saveStatus" style="display:none"></span><button id="publishBtn">pub</button>'
})
afterEach(() => {
delete (global as any).DATA
delete (global as any).FILE
delete (global as any).CSRF_TOKEN
delete (global as any).CONTENT_HASH
})
it('Ctrl+S saves via fetch and suppresses the browser save dialog', () => {
const ev = press(document, { key: 's', ctrlKey: true })
expect(ev.defaultPrevented).toBe(true)
expect(fetchCalls.length).toBeGreaterThanOrEqual(1)
expect(fetchCalls[0].url).toContain('/save?file=home')
})
it('Cmd+P publishes', async () => {
const ev = press(document, { key: 'p', metaKey: true })
expect(ev.defaultPrevented).toBe(true)
await flush() // publish awaits save() before its own fetch
expect(fetchCalls.some(c => c.url === '/publish')).toBe(true)
})
it('Ctrl+Shift+V opens the versions page in a new tab', () => {
const ev = press(document, { key: 'V', ctrlKey: true, shiftKey: true })
expect(ev.defaultPrevented).toBe(true)
expect((window as any).open).toHaveBeenCalledWith('/versions?file=home', '_blank')
})
it("'?' opens the shortcuts overlay and Esc closes it", () => {
press(document, { key: '?' })
expect(document.getElementById('shortcuts-overlay')).not.toBeNull()
press(document, { key: 'Escape' })
expect(document.getElementById('shortcuts-overlay')).toBeNull()
})
it('plain typing in an input never triggers save', () => {
const input = document.createElement('input')
document.body.appendChild(input)
const ev = press(input, { key: 's' })
expect(ev.defaultPrevented).toBe(false)
expect(fetchCalls.length).toBe(0)
})
})
+6 -2
View File
@@ -1,5 +1,9 @@
// Shared runtime schema for the content JSON files.
// Kept dependency-free so it can run in both Next.js and content-editor.js.
// Shared runtime schema for the content JSON files (proto/src/content/*.json).
// Kept dependency-free — used by src/content/index.ts (test fixtures for
// Header/Footer, MITHOME-96) and scripts/test-content-schema.js. The JSON
// files themselves remain the source for scripts/migrate-content-to-payload.ts.
// The standalone content-editor.js CMS that used to run this too was retired
// in MITHOME-93 (superseded by Payload CMS).
const string = { type: 'string' };
const boolean = { type: 'boolean' };
const array = items => ({ type: 'array', items });