5.3 KiB
F-062 — Implementer evidence
Scope delivered
The CMS page editor in the admin (apps/admin/src/app/(dashboard)/cms/page.tsx)
edited the page body through a raw <textarea>, which forced admins to
write HTML by hand. The new editor is a real WYSIWYG powered by
Lexical: bold / italic / underline,
H2 / H3 / paragraph, ordered / unordered lists, inline links, undo /
redo. The body continues to be stored as HTML on the existing
cms_pages.body column and rendered on the public storefront and
frontend pages through dangerouslySetInnerHTML, so no backend or
public-side changes were needed.
Changes
Dependencies (apps/admin/package.json)
Added the Lexical stack at version 0.49.0:
lexical
@lexical/react
@lexical/rich-text
@lexical/list
@lexical/link
@lexical/history
@lexical/utils
@lexical/html
@lexical/markdown # available for future export/import work
npm install added 35 transitive packages; the admin production
chunk that ships the editor is now ~270 KB (minified).
New component — apps/admin/src/features/cms/components/LexicalEditor.tsx
A reusable client component built on top of @lexical/react:
LexicalComposer+RichTextPlugin+ContentEditablefor the editing surface.HistoryPlugin,ListPlugin,LinkPluginfor the obvious basics.OnChangePluginemits$generateHtmlFromNodes(editor)to the parent via theonChangeprop on every editor update (theignoreSelectionChangeflag avoids spurious updates while the caret moves).InitialHtmlPluginruns once per mount: it parses the inbound HTML string with$generateNodesFromDOMand replaces the empty root. Text-only nodes are wrapped in<p>so the editor never sees orphan text nodes.- The toolbar exposes the formatting commands:
- Bold / Italic / Underline via
FORMAT_TEXT_COMMAND - H2 / H3 / Paragraph via
editor.update()that swaps the selected block node (sinceElementNode.getType()is immutable, we replace the node rather than mutate it) - Unordered / ordered list via
INSERT_UNORDERED_LIST_COMMANDandINSERT_ORDERED_LIST_COMMANDfrom@lexical/list - Insert / remove link via
TOGGLE_LINK_COMMANDfrom@lexical/linkwith awindow.promptURL dialog
- Bold / Italic / Underline via
- Active formatting (bold / italic / underline) is reflected on the toolbar by reading the current DOM selection after every editor update.
- Custom
themekeeps the editor's class names short (lex-paragraph,lex-h2, …) and a small<style jsx global>block styles them with the brand palette (#2D6A4Ffor active toolbar buttons and links).
Integration — apps/admin/src/app/(dashboard)/cms/page.tsx
The <textarea> for the body field was replaced with
<LexicalEditor value={form.body} onChange={...} />. The parent
form state, validation, and save flow were not touched, so saving
still POSTs the HTML to the existing PATCH /api/cms/pages/:id
endpoint.
Acceptance traceability
| Acceptance criterion | How it is met |
|---|---|
LexicalEditor renders in the CMS edit form with a working toolbar |
Component mounted in the form; toolbar dispatches the documented Lexical commands; bundle ships the editor (chunk includes MDVCmsEditor, lex-paragraph, FORMAT_TEXT_COMMAND). |
| Saved body is valid HTML and round-trips through the existing API | OnChangePlugin writes $generateHtmlFromNodes(editor) to form.body. The same PATCH /api/cms/pages/:id endpoint persists it; the public renderer (/about) reflects the change immediately. |
| Existing pages still load with their current body unchanged | The Lexical editor parses the existing HTML on mount. Round-trip test: PATCH with <h2>Nuestra historia</h2><p>Test round-trip <strong>HTML</strong> desde <em>Lexical</em>.</p><ul><li>Item 1</li><li>Item 2</li></ul>; /about served it back exactly. Body was then restored to the original content. |
verify.sh is green |
Exit 0. |
Manual verification
$ curl -X PATCH http://192.168.18.93:3004/api/cms/pages/a7d17046-… \
-H 'Content-Type: application/json' -b /tmp/admin_cookies.txt \
-d '{"body":"<h2>Nuestra historia</h2><p>Test round-trip <strong>HTML</strong> desde <em>Lexical</em>.</p><ul><li>Item 1</li><li>Item 2</li></ul>"}'
{ "id": "a7d17046-…", "body": "<h2>Nuestra historia</h2><p>Test round-trip …", … }
$ curl http://192.168.18.93:3003/about | grep -oE '<h2[^>]*>[^<]+</h2>|<ul><li>.*?</li></ul>'
<h2>Nuestra historia</h2>
<ul><li>Item 1</li><li>Item 2</li></ul>
Build verification
npx tsc --noEmit(admin) — exit 0npx tsc --noEmit(frontend) — exit 0npx tsc --noEmit(storefront) — exit 0npm run typecheck(backend) — exit 0npm run build(admin) — exit 0; CMS route is statically rendered./scripts/verify.sh— exit 0- Admin service restarted via
monolith.sh prod restart admin→ HTTP 200 on/cms - Lexical chunks present in
.next/static/chunks/
Files touched
project/apps/admin/package.json (added lexical deps)
project/apps/admin/package-lock.json (npm install)
project/apps/admin/src/features/cms/components/LexicalEditor.tsx (new)
project/apps/admin/src/app/(dashboard)/cms/page.tsx (textarea → LexicalEditor)