Skip to content

feat(news): replace django-ckeditor (CKEditor 4) with django-prose-editor (#1269)#1344

Merged
jonfroehlich merged 1 commit into
masterfrom
1269-replace-ckeditor-with-django-prose-editor
Jun 18, 2026
Merged

feat(news): replace django-ckeditor (CKEditor 4) with django-prose-editor (#1269)#1344
jonfroehlich merged 1 commit into
masterfrom
1269-replace-ckeditor-with-django-prose-editor

Conversation

@jonfroehlich

@jonfroehlich jonfroehlich commented Jun 18, 2026

Copy link
Copy Markdown
Member

Closes #1269. Replaces django-ckeditor (CKEditor 4 — EOL, unpatched XSS, the most fragile dependency blocking the Django 6.1 LTS upgrade) with django-prose-editor (ProseMirror-based, MIT) for the News editor — the only model using a rich-text field.

Why django-prose-editor

MIT-licensed with no editor-vendor licensing/branding regime (unlike CKEditor 5 / TinyMCE, which both re-introduce license-key churn); lightweight; the django-ckeditor maintainer himself migrated to it. Markdown editors were ruled out because News.content is stored as raw HTML (a Markdown switch would mean a lossy HTML→MD conversion of every post).

Migration is near-lossless (validated)

Ran 51 real production posts through prose-editor's actual nh3 sanitizer (extension-derived allowlist). The only markup dropped across the entire corpus was inline style on <img> (mostly width:100%); all text, links, lists, blockquotes, headings, code, and images survive. No tables/iframes/embeds exist in any post. The field swap is TEXT→TEXT (no schema change); content is sanitized on save.

What changed

  • News.contentProseEditorField(extensions=…, sanitize=True).
  • In-body image upload: prose-editor has no native upload, so a staff-only picker view (website/views/news_image_upload.py) is wired to the Figure pickerUrl via the CKEditor-4 filebrowser callback protocol. Reuses media/uploads/ + get_ckeditor_image_filename, validates with validate_image_upload (rejects SVG/HTML), and collects alt text at upload time.
  • "Edit HTML" source view enabled (adds nothing to the sanitize allowlist, so source edits are still cleaned on save).
  • Responsive images by default: prose-editor drops inline img dimensions on save, and the idempotent normalize_news_image_styles command (wired into docker-entrypoint.sh) strips them from legacy rows so the existing .news-item-content img CSS governs sizing. Eliminates the old manual "set width:100%, erase height in source" ritual.
  • Admin editor given a usable min-height (news_admin.css).
  • Removed ckeditor/ckeditor_uploader from INSTALLED_APPS, the CKEDITOR_* settings, the ckeditor/ URL include, and the dependency.

Migration shim (temporary; cleanup tracked in #1317)

Gitignored, per-environment historical migrations (00010003) import ckeditor_uploader.fields at load time, and the entrypoint re-imports all migrations on every start. Removing the package would crash startup everywhere, and we can't edit prod's migration files. A small in-repo ckeditor_uploader/ shim (a stub RichTextUploadingField(TextField)) keeps that import resolving — mirroring the existing image_cropping in-repo module. It can be deleted once the migrations-in-Git cleanup (#1317) lands; commented there.

Testing

  • 296 tests pass including new regression tests (website/tests/test_news_prose_editor.py): sanitize fidelity, upload endpoint auth/callback/rejection, admin widget rendering, normalizer idempotency.
  • Verified on a clean image rebuild: startup runs makemigrations/migrate cleanly (shim + AlterField), the normalizer runs, the admin renders the editor with working upload + source view.
  • Pa11y: no new violations introduced (pre-existing site-wide color-contrast debt unchanged; the News editor is admin-only/behind login and not scanned).

Screenshot of new editor

image

Deploy note

Intentionally no SemVer tag in this PR — merging to master deploys to -test only. Will tag for prod after auditing news pages on -test. Version bumped to 2.13.0.

🤖 Generated with Claude Code

CKEditor 4 (bundled by django-ckeditor) is EOL with unpatched XSS and was the
single most fragile dependency blocking the Django 6.1 LTS upgrade. Replace it
with django-prose-editor (ProseMirror, MIT) for the News editor — the only model
using a rich-text field.

- News.content -> ProseEditorField(extensions=..., sanitize=True). Content stays
  raw HTML; a 51-post production audit showed migration is near-lossless (only
  inline img `style` is dropped). Sanitized on save via an nh3 allowlist derived
  from the enabled extensions. Field swap is TEXT->TEXT (no schema change).
- In-body image upload: prose-editor has no native upload, so add a staff-only
  picker view (website/views/news_image_upload.py) wired to the Figure pickerUrl
  via the CKEditor-4 filebrowser callback protocol. Reuses media/uploads/ +
  get_ckeditor_image_filename and validate_image_upload (rejects SVG/HTML).
  Collects alt text at upload time.
- Enable the HTML extension ("Edit HTML" source view); give the admin editor a
  usable min-height (news_admin.css).
- Responsive-by-default images: prose-editor drops inline img dims on save, and
  the idempotent normalize_news_image_styles command (wired into the entrypoint)
  strips them from legacy rows so the existing .news-item-content img CSS governs
  sizing. Eliminates the old manual "set width:100%, erase height" ritual.
- Remove ckeditor/ckeditor_uploader from INSTALLED_APPS, the CKEDITOR_* settings,
  the ckeditor/ URL include, and the dependency.
- Add a small in-repo ckeditor_uploader shim so gitignored, per-environment
  historical migrations (0001-0003) that `import ckeditor_uploader.fields` keep
  loading after the package is removed (mirrors the image_cropping in-repo
  module). Cleanup tracked in #1317.
- Add regression tests (sanitize fidelity, upload auth/callback/rejection, admin
  widget rendering, normalizer idempotency). Bump version to 2.13.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonfroehlich jonfroehlich merged commit 5b60bd3 into master Jun 18, 2026
3 checks passed
@jonfroehlich jonfroehlich deleted the 1269-replace-ckeditor-with-django-prose-editor branch June 18, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace django-ckeditor (CKEditor 4 is EOL with known XSS issues)

1 participant