Tiptap
src/components/tiptap/ is the rich-text editor used for article bodies. Import from @/components/tiptap — everything else in the module is internal and may change without notice. See Tiptap Editor Core & Extensions for the extension architecture and lifecycle hook, and Toolbar, Upload Registry & Editor API for the toolbar and upload registry.
Source: src/components/tiptap/
Public Surface
Section titled “Public Surface”The barrel index.ts exports: Editor, DefaultToolbar, ToolbarButton, ToolbarSeparator, useDocumentEditor, createIDBUploadRegistry, buildExtensions, toolbarStateSelector, isMarkActive, isNodeActive, canRunChain, renderDocumentHTML, getDocumentText, and the types UseDocumentEditorArgs, IDBUploadRegistry, ToolbarState, ImageUploadConfig, ImageMetadata, DocumentPayload, DocumentFetchResult, SaveResult, SaveTrigger, EditorErrorHandler, ErrorScope.
Consumer
Section titled “Consumer”The only call site in the app is InputContent (src/components/ui/forms/hook-form/InputContent.tsx), the React Hook Form field used in the article authoring form’s ContentSection. InputContent builds an imageUploadConfig when objectId is set (5 MB per file, 50 MB total, 10 images, png/jpeg/webp, upload URL /api/articles/{id}/content/image) and exposes a save(id) method that calls flush(id) — forwarding the new article ID — to persist the body for a record created after the editor mounted.
flowchart LR
IC[InputContent] -->|args| H[useDocumentEditor]
H -->|buildExtensions| X[StarterKit + CustomHeadingNode + ImageNode + LoadingState + ...]
H -->|GET on mount| R["documentRoute /api/articles/{id}/content"]
H -->|debounced PATCH / flush| R
IC --> E[Editor]
E --> T[DefaultToolbar]
E --> V[ImageNodeView / ImageUploadNodeView]
V -->|upload / DELETE ?imageId| U["imageUpload.uploadUrl /api/articles/{id}/content/image"]
Key Design Decisions
Section titled “Key Design Decisions”immediatelyRender: false— Tiptap’suseEditormust not run on the server; this flag prevents SSR hydration mismatches.Fileobjects live in IndexedDB, not in node attrs — ProseMirror state must serialize to JSON, so upload nodes carry an opaquetempIdstring; the registry resolves it to the real file and its blob preview URL.flush(routeOverride)— the editor mounts before a draft record is created, soInputContent.save(id)callsflushwith the new article ID to save the body to the correct route.- Image node removal is server-gated — deleting an image node emits
imageDeleteRequested, which the node view intercepts and confirms with the server. The node is removed from the document only after the server DELETE succeeds.
Gotchas
Section titled “Gotchas”- Heading levels are 2–4, not 1–3. The doc comment in
BuildExtensions.tssays levels 1–3;CustomHeadingNodeconfigures[2, 3, 4]. parseHTMLdoes parse tagged images. TheImageNodedoc comment saysparseHTMLis empty; the code acceptsimg[data-image-id].- Drop fills the node, it does not start the upload. Dropping a file creates an upload node in the
pickingphase; the user still has to click Upload. The README says drop starts the upload. characterCountis alwaysundefined.toolbarStateSelectorreadseditor.storage.characterCount, butbuildExtensionsdoes not register a CharacterCount extension.minLengthis not read. The arg is declared onUseDocumentEditorArgsbut the hook never gates saves on it; minimum length is enforced by the article schema at publish time.
Related Links
Section titled “Related Links”- Tiptap Editor Core & Extensions
- Toolbar, Upload Registry & Editor API
- UI Forms —
InputContentlives inhook-form/ - Article Authoring & Publishing — the article form that uses
InputContent