skills/huynhsang2005/blog-tanstack/reactjs-tiptap-editor

reactjs-tiptap-editor

SKILL.md

ReactJS Tiptap Editor Skill

When to use

  • Building admin content editors (posts, projects, pages).
  • Implementing WYSIWYG editing with Markdown output.
  • Adding rich text capabilities to forms.

Guardrails

  • Admin-only: This editor is for authenticated admin users only.
  • Markdown output: Always export as Markdown to store in DB (content_md column).
  • Sanitize on render: When displaying content, parse Markdown → safe HTML (see content-format doc).
  • Size: This is a heavy component (~200KB); lazy-load on admin routes.

Workflow checklist

  1. Install if not present: bun add reactjs-tiptap-editor
  2. Import editor component in admin route/component.
  3. Configure extensions (bold, italic, headings, lists, code blocks, links, images).
  4. Set output="markdown" to get Markdown strings.
  5. Store Markdown in DB; render with unified/remark/rehype on public pages.

Setup pattern

// domains/blog/ui/PostEditor.tsx (admin only)
import { RichTextEditor } from 'reactjs-tiptap-editor'
import { useState } from 'react'

export function PostEditor({ initialContent }: { initialContent?: string }) {
  const [content, setContent] = useState(initialContent ?? '')
  
  return (
    <RichTextEditor
      content={content}
      onChange={(newContent) => setContent(newContent)}
      output="markdown" // ← Critical: outputs Markdown
      dark={true} // Match theme
      extensions={[
        'bold', 'italic', 'strike', 'code',
        'heading', 'bulletList', 'orderedList',
        'codeBlock', 'blockquote',
        'link', 'image'
      ]}
    />
  )
}

Integration with domain patterns

// domains/blog/server/create-post.server.ts
import { createServerFn } from '@tanstack/start'
import { CreatePostInput } from '../schema'

export const createPost = createServerFn({ method: 'POST' })
  .validator(CreatePostInput)
  .handler(async ({ data }) => {
    // data.content_md comes from Tiptap editor as Markdown
    const supabase = getSupabaseClient()
    const { data: post, error } = await supabase
      .from('posts')
      .insert({ ...data, content_md: data.content_md }) // ← Store Markdown
      .select()
      .single()
    
    if (error) throw error
    return post
  })

Best practices

  • ✅ Lazy-load: use .lazy.tsx for admin routes to avoid bloating public bundle
  • ✅ Markdown output: always use output="markdown" to avoid storing HTML
  • ✅ Dark mode: match dark={true} to repo's futuristic theme
  • ✅ Limited extensions: only enable what users need
  • ❌ Don't store raw HTML from Tiptap (security risk)
  • ❌ Don't load Tiptap on public routes (bundle size)

Rendering on public pages

// src/routes/blog/$slug.tsx (public)
import { parseMarkdown } from '~/lib/markdown/processor'

export const Route = createFileRoute('/blog/$slug')({
  loader: async ({ params }) => {
    const post = await getPost(params.slug)
    const contentHtml = await parseMarkdown(post.content_md) // ← Safe HTML
    return { post, contentHtml }
  }
})

function BlogPost() {
  const { contentHtml } = Route.useLoaderData()
  return (
    <article>
      <div dangerouslySetInnerHTML={{ __html: contentHtml }} />
    </article>
  )
}

References

  • Library patterns: docs/dev-1/docs/16-library-patterns.md#reactjs-tiptap-editor-rich-text
  • Content format: docs/dev-1/docs/10-content-format.md (Markdown pipeline)
  • Admin spec: docs/dev-1/docs/08-admin-dashboard-spec.md

Tooling

Weekly Installs
3
First Seen
Jan 24, 2026
Installed on
trae2
claude-code1