Rich Text
A rich text field holds formatted text: headings, paragraphs, lists, links, quotes and optionally tables and images. Add a schema to let editors place your own blocks, such as a call to action or a video, between paragraphs. Render the value with the RichText component.
import {Config, Field} from 'alinea'
const CallToAction = Config.type('Call to action', {
fields: {
label: Field.text('Label'),
link: Field.link('Link')
}
})
export const Article = Config.document('Article', {
fields: {
body: Field.richText('Body', {
searchable: true,
enableTables: true,
schema: {CallToAction}
})
}
})Options
schema: block types editors can insert between text, keyed by name. Block names must start with an uppercase letter.enableTables: allow inserting and editing tables. Defaultfalse.enableImages: allow inserting images from the media library inline. Defaultfalse.searchable: add the text to the search index, together with searchable fields inside its blocks, see Search.placeholder: text shown while the editor is empty.link: limit which entries the link picker offers, with the samecondition,location,pickChildrenandlimitLocationsoptions as the Entry field.toolbar: choose the toolbar buttons, see below.extensions: a function that receives the default Tiptap extensions and returns the ones to use.The common options
help,width,inline,initialValue,required,validate,readOnly,hiddenandshared.
Value
The value is an array of nodes. Text nodes carry marks such as bold or links, element nodes have a lowercase _type and their own content, and blocks from the schema have their type name and _id plus the values of their fields.
[
{
"_type": "heading",
"level": 1,
"content": [
{
"_type": "text",
"text": "Hello world"
}
]
},
{
"_type": "paragraph",
"content": [
{
"_type": "text",
"text": "A paragraph follows"
}
]
}
]When you query the field, links to entries and files get an up-to-date href (so they keep working when the target moves), inline images get their src and alt text, and link and image fields inside blocks are resolved like everywhere else. Queries return the value as TextDoc, import that type from alinea to type your props.
Rendering
The RichText component from alinea/ui renders the value with plain HTML tags: h1–h6, p, ul, ol, li, a, b, i, blockquote, table and so on. Pass a tag name as a prop to change how that tag renders: a React element to add props such as a class name, or a component to take over completely.
import type {TextDoc} from 'alinea'
import {RichText} from 'alinea/ui'
import Link from 'next/link'
import type {ComponentProps} from 'react'
function TextLink({href = '', ...props}: ComponentProps<'a'>) {
return <Link href={href} {...props} />
}
function Heading2(props: ComponentProps<'h2'>) {
return <h2 className="prose-h2" {...props} />
}
export function Prose({doc}: {doc: TextDoc}) {
return (
<RichText
doc={doc}
// Add a class to every paragraph
p={<p className="prose-p" />}
// Render h2 headings with your own component
h2={Heading2}
// Route links through next/link
a={TextLink}
/>
)
}The overridable tags are h1–h6, p, b, i, ul, ol, li, blockquote, hr, img, br, small, sub, sup, a, table, tr, td and th. Pass text to wrap every piece of text in a component. Headings get an id (the anchor an editor set, or a slug of the heading text), so you can link to sections.
Blocks
Blocks are rendered only when you pass a component for their type name. The component receives the block's fields as props:
import {Config, Field, type Infer, type TextDoc} from 'alinea'
import {RichText} from 'alinea/ui'
const Quote = Config.type('Quote', {
fields: {
text: Field.text('Text', {multiline: true}),
author: Field.text('Author')
}
})
const blocks = {Quote}
export const Article = Config.document('Article', {
fields: {
body: Field.richText('Body', {schema: blocks})
}
})
function QuoteView({text, author}: Infer<typeof Quote>) {
return (
<figure>
<blockquote>{text}</blockquote>
<figcaption>{author}</figcaption>
</figure>
)
}
export function Body({doc}: {doc: TextDoc<typeof blocks>}) {
return <RichText doc={doc} Quote={QuoteView} />
}Type the document as TextDoc<typeof blocks> (a queried rich text field already has this type) and <RichText> checks the props of your block components. RichText<typeof blocks> is only needed when the document is typed as plain TextDoc.
HTML strings
To get HTML instead of React elements, for example for an RSS feed, render the component to a string. Import react-dom/server dynamically: Next.js refuses static imports of it in the app router.
import type {TextDoc} from 'alinea'
import {RichText} from 'alinea/ui'
export async function toHtml(doc: TextDoc) {
const {renderToString} = await import('react-dom/server')
return renderToString(<RichText doc={doc} />)
}Toolbar
By default the toolbar has heading styles, formatting, alignment, lists, links, anchors, quotes and a horizontal rule, plus tables and images when you enable them. Build your own from the presets in alinea/field/richtext: each is a group of buttons, and groups can be picked apart.
import {Field} from 'alinea'
import {formatting, links, lists} from 'alinea/field/richtext'
const {bold, italic, clear} = formatting.group
// A small editor for short texts: no headings, tables or images
export const summary = Field.richText('Summary', {
toolbar: {
formatting: {group: {bold, italic, clear}},
lists,
links
}
})The presets are headings, formatting, alignment, lists, links, anchors, images, tables, quotes and inserts. defaultToolbar({enableTables, enableImages}) returns the default layout, so you can extend it. Type your own buttons with ToolbarButton. The same module exports the built-in Tiptap extensions as extensions, to extend one in your extensions option, for example extensions.BulletList.extend({...}). The toolbar only controls buttons: pasted content can still contain other formatting, so trim the extensions too if an editor must not produce it.