Skip to content

Rich Text

CruzJS Pro includes a rich text module for storing and managing HTML content associated with entities. It handles HTML sanitization, @mention resolution, attachment tracking, and full-text search integration.

Register the RichTextModule in your application:

import { RichTextModule } from '@cruzjs/pro/rich-text/rich-text.module';
export default createCruzApp({
modules: [RichTextModule],
});

Rich text content is associated with a specific entity through three identifiers: entityType, entityId, and fieldName. This allows multiple rich text fields per entity.

// Save rich text for an article's body field
trpc.richText.save.useMutation().mutate({
entityType: 'article',
entityId: 'art_abc123',
fieldName: 'body',
htmlContent: '<p>This is the article body with <strong>formatting</strong>.</p>',
});
type SaveRichTextInput = {
entityType: string; // e.g. 'article', 'comment', 'page'
entityId: string; // ID of the parent entity
fieldName: string; // e.g. 'body', 'description', 'notes'
htmlContent: string; // Raw HTML content
};

All HTML content is sanitized on save using an allowlist of safe tags. The sanitizer is compatible with Cloudflare Workers (no DOM APIs required).

Allowed tags include standard formatting elements: p, strong, em, a, ul, ol, li, h1-h6, blockquote, code, pre, img, br, hr, table, thead, tbody, tr, th, td.

Script tags, event handlers, and other potentially dangerous content are stripped automatically.

The rich text module supports @mentions that reference users or organizations. During save, mention tokens are resolved and replaced with linked anchor tags:

<!-- Input -->
<p>Hey @john, please review this.</p>
<!-- Output (after mention resolution) -->
<p>Hey <a href="/users/user_john" class="mention" data-mention-id="user_john">@John Smith</a>, please review this.</p>

Associate file attachments with rich text content:

// Add an attachment
trpc.richText.addAttachment.useMutation().mutate({
entityType: 'article',
entityId: 'art_abc123',
fieldName: 'body',
fileId: 'file_xyz789',
});
// Remove an attachment
trpc.richText.removeAttachment.useMutation().mutate({
entityType: 'article',
entityId: 'art_abc123',
fieldName: 'body',
fileId: 'file_xyz789',
});

Attachment records track which files are referenced in which rich text fields, enabling cleanup when content is deleted or files are removed.

If the SearchModule is also registered, rich text content is automatically indexed for full-text search:

const results = trpc.richText.search.useQuery({
query: 'typescript patterns',
entityType: 'article', // optional filter
});

The search indexes the plain text extracted from the HTML content, stripping all tags.

The module extracts plain text from HTML for use in notifications, previews, and search indexing. This is done server-side without DOM APIs.

// Stored HTML: <p>Hello <strong>world</strong></p>
// Extracted text: "Hello world"
ProcedureTypeDescription
richText.getqueryGet rich text content for an entity field
richText.savemutationSave (create or update) rich text content
richText.searchqueryFull-text search across rich text content
richText.addAttachmentmutationAssociate a file with rich text content
richText.removeAttachmentmutationRemove a file association
function ArticleEditor({ articleId }: { articleId: string }) {
const { data } = trpc.richText.get.useQuery({
entityType: 'article',
entityId: articleId,
fieldName: 'body',
});
const save = trpc.richText.save.useMutation();
return (
<RichTextEditor
initialContent={data?.htmlContent ?? ''}
onSave={(html) => {
save.mutate({
entityType: 'article',
entityId: articleId,
fieldName: 'body',
htmlContent: html,
});
}}
/>
);
}