Tips & Tricks (Atualizado: 02/06/2026)

Como criar um editor rich text com Claude Code

Implemente um editor rich text em React com Claude Code, Tiptap, toolbar, salvamento JSON/HTML e sanitização segura.

Como criar um editor rich text com Claude Code

Um editor rich text parece apenas um campo de formulário mais sofisticado. Em produção, ele vira uma decisão de arquitetura: qual formato salvar, como limpar HTML colado, como bloquear XSS, quais protocolos de link aceitar, como limitar imagens, como funcionar no teclado móvel e como renderizar o mesmo conteúdo em uma página pública.

Este guia usa Claude Code de forma controlada. Primeiro escolhemos Tiptap, depois definimos o escopo do prompt, implementamos um componente React/TypeScript com toolbar, salvamos JSON e HTML sanitizado e revisamos os principais riscos. A documentação oficial de Tiptap com React orienta instalar @tiptap/react, @tiptap/pm e @tiptap/starter-kit. O StarterKit já cobre parágrafos, títulos, negrito, itálico e listas.

Por que Tiptap

Usar contenteditable diretamente é tentador, mas não é uma boa base para um produto. A MDN documenta que contenteditable="true" mantém formatação colada, enquanto plaintext-only remove a formatação. Só essa diferença já mostra o quanto você teria de normalizar manualmente: seleção, histórico, colagem, comandos, acessibilidade e comportamento mobile.

Lexical também é uma alternativa forte. O site oficial Lexical o descreve como um framework leve e modular para editores de texto, com pacotes para listas, links e tabelas. Eu escolheria Lexical para nós muito customizados ou entradas de chat. Para CMS, base de conhecimento e descrição de produto, Tiptap costuma chegar mais rápido a uma primeira versão segura.

CritérioTiptapLexical
Primeira versãoRápida com StarterKitExige mais desenho de plugins
SalvamentoJSON e HTML diretosPrecisa de estratégia de EditorState
ExtensãoExtensionsÓtimo para custom nodes
Uso com Claude CodeFácil de restringirMelhor com requisitos muito detalhados

Instalação e prompt

Instale as dependências antes de pedir a mudança. Não comece com colaboração, comentários, menções ou menu slash. Esses recursos mudam schema, permissões e migrações. A primeira versão deve validar editar, salvar, carregar, visualizar e sanitizar.

npm install @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-link @tiptap/extension-image @tiptap/extension-character-count dompurify

A documentação de Claude Code explica que a ferramenta lê o codebase, edita arquivos, executa comandos e se integra a ferramentas de desenvolvimento. Por isso, dê um contrato claro:

Crie um editor rich text com Tiptap, React e TypeScript.
Edite apenas src/components/RichTextEditor.tsx.
Suporte bold, italic, H2/H3, listas, links, URL de imagem, saída JSON e HTML sanitizado.
Recuse links e imagens fora de http/https.
Use DOMPurify antes de preview ou salvamento.
Inclua uma checklist manual de testes.

Se o projeto ainda não tem fluxo com Claude Code, comece pelo guia inicial. Se o editor envia dados para formulário ou API, revise validação de formulários. Para Markdown, mantenha a conversão separada e consulte processamento Markdown.

Componente React/TypeScript pronto para copiar

O componente abaixo inclui toolbar, validação de URL, contador, saída JSON, HTML sanitizado e salvamento local. As classes Tailwind são opcionais; troque por CSS comum se necessário.

"use client";

import type { Editor, JSONContent } from "@tiptap/core";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import Image from "@tiptap/extension-image";
import CharacterCount from "@tiptap/extension-character-count";
import DOMPurify from "dompurify";
import type { ReactNode } from "react";
import { useState } from "react";

export type SavedEditorContent = {
  json: JSONContent;
  html: string;
  plainText: string;
};

type Props = {
  initialContent?: JSONContent | string;
  maxCharacters?: number;
  onChange?: (content: SavedEditorContent) => void;
};

const allowedTags = ["p", "br", "strong", "em", "s", "h2", "h3", "ul", "ol", "li", "blockquote", "code", "pre", "a", "img"];
const allowedAttrs = ["href", "src", "alt", "title", "target", "rel"];

function normalizeHttpUrl(value: string): string | null {
  const trimmed = value.trim();
  if (!trimmed) return null;

  for (const candidate of [trimmed, `https://${trimmed}`]) {
    try {
      const url = new URL(candidate);
      if (url.protocol === "http:" || url.protocol === "https:") return url.toString();
    } catch {
      // Try the next candidate.
    }
  }

  return null;
}

export function sanitizeEditorHtml(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: allowedTags,
    ALLOWED_ATTR: allowedAttrs,
    ALLOW_DATA_ATTR: false,
  });
}

function buildPayload(editor: Editor): SavedEditorContent {
  return {
    json: editor.getJSON(),
    html: sanitizeEditorHtml(editor.getHTML()),
    plainText: editor.getText(),
  };
}

export function RichTextEditor({
  initialContent = "<p>Start writing...</p>",
  maxCharacters = 8000,
  onChange,
}: Props) {
  const [lastSaved, setLastSaved] = useState<SavedEditorContent | null>(null);

  const editor = useEditor({
    extensions: [
      StarterKit.configure({ heading: { levels: [2, 3] } }),
      Link.configure({
        openOnClick: false,
        autolink: true,
        HTMLAttributes: { rel: "noopener noreferrer nofollow", target: "_blank" },
      }),
      Image.configure({ allowBase64: false }),
      CharacterCount.configure({ limit: maxCharacters }),
    ],
    content: initialContent,
    immediatelyRender: false,
    editorProps: {
      attributes: {
        class: "min-h-[260px] rounded-b-md border border-t-0 border-slate-300 bg-white p-4 leading-7 outline-none focus:ring-2 focus:ring-sky-500",
        "aria-label": "Rich text body",
      },
      transformPastedHTML(html) {
        return sanitizeEditorHtml(html);
      },
    },
    onUpdate({ editor }) {
      const payload = buildPayload(editor);
      setLastSaved(payload);
      onChange?.(payload);
    },
  });

  if (!editor) return null;

  const characters = editor.storage.characterCount.characters();
  const saveDraft = () => {
    const payload = buildPayload(editor);
    setLastSaved(payload);
    onChange?.(payload);
    window.localStorage.setItem("article-draft", JSON.stringify(payload));
  };

  return (
    <section className="rounded-md border border-slate-300 bg-slate-50">
      <div className="flex flex-wrap gap-1 border-b bg-white p-2" role="toolbar" aria-label="Formatting toolbar">
        <ToolButton active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()}>B</ToolButton>
        <ToolButton active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()}>I</ToolButton>
        <ToolButton active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>H2</ToolButton>
        <ToolButton active={editor.isActive("heading", { level: 3 })} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}>H3</ToolButton>
        <ToolButton active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()}>List</ToolButton>
        <ToolButton active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()}>1.</ToolButton>
        <ToolButton onClick={() => {
          const input = window.prompt("Link URL", "https://");
          const href = input ? normalizeHttpUrl(input) : null;
          if (!href) return window.alert("Use an http or https URL.");
          editor.chain().focus().extendMarkRange("link").setLink({ href }).run();
        }}>Link</ToolButton>
        <ToolButton onClick={() => {
          const input = window.prompt("Image URL", "https://");
          const src = input ? normalizeHttpUrl(input) : null;
          if (!src) return window.alert("Use an http or https image URL.");
          editor.chain().focus().setImage({ src, alt: "" }).run();
        }}>Image</ToolButton>
      </div>
      <EditorContent editor={editor} />
      <footer className="flex items-center justify-between gap-3 border-t p-3 text-sm">
        <span>{characters}/{maxCharacters} characters</span>
        <button type="button" onClick={saveDraft} className="rounded bg-slate-900 px-3 py-2 text-white">Save draft</button>
        {lastSaved && <span>HTML: {lastSaved.html.length} bytes</span>}
      </footer>
    </section>
  );
}

function ToolButton({ active, onClick, children }: { active?: boolean; onClick: () => void; children: ReactNode }) {
  return (
    <button type="button" aria-pressed={active} onClick={onClick} className={active ? "rounded border bg-sky-100 px-2 py-1" : "rounded border bg-white px-2 py-1"}>
      {children}
    </button>
  );
}

Salvar e carregar JSON ou HTML

O padrão mais seguro no início é salvar três valores: JSON para reedição, HTML sanitizado para renderização e texto puro para resumo, busca, RSS e limite de caracteres. O guia de saída JSON/HTML do Tiptap lembra que JSON ou HTML podem carregar conteúdo malicioso; validação continua obrigatória.

import { useEffect, useState } from "react";
import { RichTextEditor, type SavedEditorContent } from "./RichTextEditor";

export function ArticleEditorPage() {
  const [draft, setDraft] = useState<SavedEditorContent | null>(null);

  useEffect(() => {
    const raw = window.localStorage.getItem("article-draft");
    if (raw) setDraft(JSON.parse(raw) as SavedEditorContent);
  }, []);

  return (
    <main>
      <RichTextEditor initialContent={draft?.json ?? "<p>New article</p>"} onChange={setDraft} />
      <article dangerouslySetInnerHTML={{ __html: draft?.html ?? "" }} />
    </main>
  );
}

Segurança e revisão

O repositório oficial DOMPurify descreve a biblioteca como sanitizer XSS para HTML, MathML e SVG. Mesmo assim, repita validações no servidor: usuário, permissão, tamanho do HTML, tamanho do texto, tags permitidas, protocolos de URL e domínios de imagem. O cliente é apenas uma camada.

flowchart LR
  A["Editor UI"] --> B["Tiptap JSON"]
  A --> C["Sanitized HTML"]
  B --> D["Database draft"]
  C --> D
  D --> E["Public page"]
  E --> F["Search / RSS / CTA"]

Peça ao Claude Code uma segunda rodada só de review. Ela deve procurar link javascript:, imagem data:, colagem rica, payload grande, foco de teclado, seleção mobile, botão de salvar, recarga do draft e renderização pública.

Casos de uso

Primeiro: CMS de blog. JSON serve para reedição, HTML para publicação e texto puro para SEO, RSS e busca. Segundo: base de conhecimento interna. Títulos, listas, código e links resolvem muita documentação sem comentários ou aprovação. Terceiro: ecommerce. Rich text melhora a página de produto, mas HTML e imagens precisam de limites. Quarto: edição de rascunhos de IA. HTML gerado por IA também é input não confiável e deve passar pela mesma sanitização.

Armadilhas comuns

Salvar apenas HTML é cômodo, mas dificulta migração, Markdown, sumário e colaboração. Confiar só no sanitizing do cliente é arriscado, porque a API pode receber requisições diretas. Teste mobile não deve ficar para o fim: teclado virtual, seleção, URL longa e imagem costumam quebrar ali. Também evite adicionar tabelas, embeds e menções sem motivo claro de produto.

CTA e resultado testado

Um editor rich text acelera melhoria de conteúdo: artigos SEO, páginas de produto, material de treinamento, documentação e CTAs podem ser atualizados com menos atrito. Para transformar isso em processo, veja treinamento e consultoria Claude Code ou produtos e templates Claude Code.

No teste prático, o melhor resultado veio de definir primeiro o payload: JSON, HTML sanitizado, texto puro e validação de URL. Assim o Claude Code gerou uma mudança menor e mais fácil de revisar. Antes de publicar, teste colagem formatada, link javascript:, texto longo, largura mobile, salvar, recarregar e render público.

#Claude Code #editor rich text #React #Tiptap #gestão de conteúdo
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.