
RichText doesn't edit HTML — it edits a Slate JSON document. That distinction drives almost every decision about how to store and re-render its content, and it's easy to get tangled up between the editor's native value, the HTML you show on a public page, and the two conversion helpers that sit between them.RichText actually holdsRichText's defaultValue prop is a JSON string of Slate nodes (default "[]"), not HTML:tsx
<RichText name="content" defaultValue='[{"type":"paragraph","children":[{"text":"Hello"}]}]' />
<input name="content">, so whatever your form action receives for that field is already the editor's native value — no conversion needed to round-trip an edit-save-edit cycle.toRichTextValue at alldefaultValue — that's it. toRichTextValue only
matters when your source of truth is HTML instead.toHtml(richTextValue) — converts a parsed Slate value (an array, not the JSON string) into an HTML string, for rendering somewhere that isn't the editor: a public blog page, an email, a PDF.toRichTextValue(html) — the reverse: parses an HTML string into a Slate value array, for loading content that originated as HTML (a legacy CMS export, hand-authored HTML) into the editor for the first time.tsx
import { toHtml } from "@arkyn/components/toHtml";import { toRichTextValue } from "@arkyn/components/toRichTextValue";// Rendering stored content elsewhere as HTMLconst html = toHtml(JSON.parse(storedRichTextJson));// Loading HTML-sourced content into the editorconst value = toRichTextValue(legacyHtmlContent);
toRichTextValue returns an array, not a JSON stringRichText's defaultValue expects a JSON string, but
toRichTextValue returns a plain array of nodes. JSON.stringify() the
result before passing it to defaultValue:
<RichText defaultValue={JSON.stringify(toRichTextValue(html))} />.toHtml/toRichTextValue don't touch the DOM, so both can run just as well in a server-side loader as in the browser if you'd rather precompute the HTML once at write time than on every read.typescript
// Write path — store the editor's own value, untouchedasync function saveArticle(richTextJson: string) {await db.articles.update({ content: richTextJson });}// Read path (public page) — convert only when rendering as HTMLasync function renderArticle(id: string) {const article = await db.articles.findById(id);return toHtml(JSON.parse(article.content));}
imageConfig and videoConfig add insertion buttons to the toolbar. Image insertion follows the same two-step upload contract as FileUpload — imageConfig.action is an endpoint that receives the file and returns its public URL:tsx
<RichTextname="post"imageConfig={{action: "/api/upload/image",modalTitle: "Insert Image",modalInputUrlLabel: "Image URL",modalInputImageLabel: "Upload image",}}videoConfig={{modalTitle: "Insert Video",modalInputLabel: "Video URL",}}/>
videoConfig only accepts a YouTube URL (it extracts the ?v= parameter) — there's no upload endpoint for video, unlike images.videoConfig — pass hiddenButtons={["video"]} if you don't want it.
More importantly: toHtml serializes a video node into an <iframe>,
but toRichTextValue has no matching case to parse that <iframe>
back into a video node. If you store content as HTML and later load it
back into the editor for re-editing, any video embed will be silently
dropped. This is exactly why storing the native RichText JSON (above)
as your canonical value — rather than HTML — matters most for content
that includes video.toHtml's output on the display side almost always does so via dangerouslySetInnerHTML, since it's a plain HTML string. RichText is typically aimed at trusted content producers (admins, editors), not arbitrary end-user input — but if your editor is exposed to untrusted users, sanitize the HTML (e.g. with a library like DOMPurify) before rendering it, the same way you would for any other user-supplied HTML in your app. Arkyn does not sanitize this output for you.maxLimit (default 10000) counts characters against the serialized JSON string, not the visible text length — a heavily formatted document can hit the limit sooner than its word count suggests.<input> behind RichText does not forward an external ref — if you need imperative access to the underlying value, read it from onChange instead.slate, slate-react, slate-history, is-hotkey, and html-react-parser all need to be installed for the editor to work — see the RichText reference for the exact install command.RichText's built-in FieldWrapper/FieldLabel/FieldError composition reads fieldErrors the same way other form fields do.