Notion‑Style Rich‑Text Editor – Implementation Requirements
This document lives next to the code and should be kept up‑to‑date whenever the editor implementation changes.
Problem Statement
- The existing
<MDXEditorComponent>
wraps @mdxeditor/editor giving us classic Markdown editing. - Authors want a Notion‑like, block‑based experience: slash‑commands, drag‑to‑re‑order blocks, task lists, tables, embeds, etc.
- The replacement must integrate cleanly with React Router loaders/actions, continue to autosave drafts, and work in both create‑new and edit flows.
Solution Overview
- Replace MDXEditor with TipTap + ProseMirror – an open‑source, headless, extensible editor used by Craft, GitLab, ChatGPT, and others.
- Keep a thin wrapper component (
NotionEditor.tsx
) that hides TipTap specifics and exposes a familiar API (value
,onChange
). - Persist JSON or Markdown (decision matrix below) in the existing
blog_posts.content
column; conversion helpers maintain backward compatibility.
Packages to Add
pnpm --filter web add @tiptap/react @tiptap/starter-kit @tiptap/extension-placeholder \
@tiptap/extension-link @tiptap/extension-task-list @tiptap/extension-task-item \
@tiptap/extension-underline @tiptap/extension-table @tiptap/extension-table-row \
@tiptap/extension-table-cell @tiptap/extension-table-header \
@tiptap/extension-code-block-lowlight lowlight classnames
Optional extras (future work): @hocuspocus/provider
for real‑time sync, tiptap-extension-command
for slash menu, tiptap-extension-placeholder
for AI.
File Layout
apps/web/app/components/rich-text/
NotionEditor.tsx <-- reusable TipTap wrapper (export default)
EditorBubbleMenu.tsx <-- inline formatting toolbar
EditorSlashMenu.tsx <-- block insertion menu
editor-extensions.ts <-- factory returning common extensions
serializer.ts <-- JSON ⇆ Markdown helpers (showdown / remark)
NOTE: keep the existing
mdx-editor/
folder until migration is complete; then deprecate.
Implementation Checklist
-
Create editor component ✅
- Use
useEditor
from@tiptap/react
. - Load
StarterKit
+ extra extensions (defined ineditor-extensions.ts
). - Expose props:
defaultContent
,readOnly
,onUpdate
(returns both JSON & Markdown). - Apply Tailwind classes for the Notion look (
prose
,focus:outline-none
, etc.).
- Use
-
BubbleMenu & SlashMenu ✅
- BubbleMenu: bold, italic, underline, link.
- SlashMenu:
/h1
,/h2
,/table
,/code
,/todo
,/image
. - Use Headless UI / Radix
Command
for a11y list.
-
Autosave Integration ✅
- In
triggerAutosave()
replaceconst currentContent = editor.getJSON();
- Submit as
JSON.stringify(currentContent)
.
- In
-
Route Changes (
posts/new
&posts/[id]
) ✅- Swap
<MDXEditorComponent>
→<NotionEditor>
. - Hidden
<input name="content" />
now holds the serialized payload. - Keep existing Sheet, dialogs, autosave logic.
- Swap
-
Database Decision ✅
Option | Pros | Cons |
---|---|---|
text column, store Markdown | No schema change | Lose block richness (tables, embeds) |
jsonb column, store TipTap JSON | Full fidelity, future‑proof | Requires migration & serialization on read for old posts |
Current plan: migrate to jsonb
but ship a read/write Markdown converter to avoid breaking the public blog immediately.
-
Migration Script ✅
- Drizzle migration adding a new
content_json jsonb
column (nullable). - Backfill:
content_json = markdownToTiptapJSON(content)
. - Eventually drop old
content
column.
- Drizzle migration adding a new
-
SSR & Build Considerations
- TipTap uses
window
; wrap hook inuseClient
guard or lazy‑import insideuseEffect
for SSR routes. - Continue to lazy‑load to keep bundle size small.
- TipTap uses
-
Accessibility & Keyboard Shortcuts
Cmd/Ctrl + b/i/u
for marks.Cmd/Ctrl + s
→ opens Save Draft dialog (already exists).Cmd/Ctrl + Shift + p
→ publish.
-
Testing & QA
- Unit test extension factory – returns expected list.
- Cypress e2e: create post, add blocks, autosave, refresh – draft persists.
- Visual regression with Storybook (optional).
-
Future Enhancements
- Collaborative cursors with Y.js.
- Comment & suggestion mode.
- AI slash‑command for content generation.
- Embed previews (Twitter, Loom, Figma, …).
Reference Snippet – NotionEditor.tsx
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import Link from "@tiptap/extension-link";
export function NotionEditor({ defaultContent, onUpdate }) {
const editor = useEditor({
extensions: [
StarterKit.configure({ codeBlock: false }),
Placeholder.configure({ placeholder: "Type / for blocks" }),
TaskList,
TaskItem,
Link.configure({ openOnClick: false })
],
content: defaultContent,
onUpdate: ({ editor }) => {
onUpdate?.({
json: editor.getJSON(),
markdown: editor.getHTML() // or custom serializer
});
}
});
return (
<div className="prose leading-relaxed max-w-none border rounded-md p-4 focus:outline-none">
<EditorContent editor={editor} />
</div>
);
}
Migration Timeline
Week | Task |
---|---|
1 | Scaffold editor component & basic extensions |
2 | Integrate into new‑post route, keep MDXEditor side‑by‑side for fallback |
3 | Update edit page, enable Markdown→JSON conversion |
4 | DB migration, backfill, remove old editor code |
Maintainer: @samishukri • Last updated: 7/17/2025