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.linkConfig customizes the labels of the link insertion modal, the link button itself is enabled by default and needs no configuration. Unlike video, links round-trip cleanly, serialize/deserialize (and therefore toHtml/toRichTextValue) both understand <a href="...">, so storing content as HTML and loading it back into the editor does not drop link formatting.tsx
<RichTextname="post"linkConfig={{modalTitle: "Insert Link",modalInputUrlLabel: "URL",invalidUrlMessage: "Invalid URL",}}/>
https:// URL, an http:// URL is rejected as invalid.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.