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

  1. Create editor component

    • Use useEditor from @tiptap/react.
    • Load StarterKit + extra extensions (defined in editor-extensions.ts).
    • Expose props: defaultContent, readOnly, onUpdate (returns both JSON & Markdown).
    • Apply Tailwind classes for the Notion look (prose, focus:outline-none, etc.).
  2. BubbleMenu & SlashMenu

    • BubbleMenu: bold, italic, underline, link.
    • SlashMenu: /h1, /h2, /table, /code, /todo, /image.
    • Use Headless UI / Radix Command for a11y list.
  3. Autosave Integration

    • In triggerAutosave() replace
      const currentContent = editor.getJSON();
    • Submit as JSON.stringify(currentContent).
  4. Route Changes (posts/new & posts/[id]) ✅

    • Swap <MDXEditorComponent><NotionEditor>.
    • Hidden <input name="content" /> now holds the serialized payload.
    • Keep existing Sheet, dialogs, autosave logic.
  5. Database Decision

OptionProsCons
text column, store MarkdownNo schema changeLose block richness (tables, embeds)
jsonb column, store TipTap JSONFull fidelity, future‑proofRequires 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.

  1. Migration Script

    • Drizzle migration adding a new content_json jsonb column (nullable).
    • Backfill: content_json = markdownToTiptapJSON(content).
    • Eventually drop old content column.
  2. SSR & Build Considerations

    • TipTap uses window; wrap hook in useClient guard or lazy‑import inside useEffect for SSR routes.
    • Continue to lazy‑load to keep bundle size small.
  3. Accessibility & Keyboard Shortcuts

    • Cmd/Ctrl + b/i/u for marks.
    • Cmd/Ctrl + s → opens Save Draft dialog (already exists).
    • Cmd/Ctrl + Shift + p → publish.
  4. Testing & QA

    • Unit test extension factory – returns expected list.
    • Cypress e2e: create post, add blocks, autosave, refresh – draft persists.
    • Visual regression with Storybook (optional).
  5. 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

WeekTask
1Scaffold editor component & basic extensions
2Integrate into new‑post route, keep MDXEditor side‑by‑side for fallback
3Update edit page, enable Markdown→JSON conversion
4DB migration, backfill, remove old editor code

Maintainer: @samishukri • Last updated: 7/17/2025