@bettercms-ai/richtext

中文文档
0.19.0发布于 2026/11/02TypeScriptESM + CJS近 30 天下载 3724

The one Portable Text contract for BetterCMS: schema, converters, and rendering. Shared by the backend, the dashboard and every delivery SDK.

原始使用文档
文档目录8 节
  1. Install
  2. Reading a stored value
  3. Rendering
  4. Ingesting HTML
  5. Tables
  6. Subpath exports
  7. Ships as source
  8. Licence

@bettercms-ai/richtext

The one Portable Text contract for BetterCMS: schema, converters, and rendering. Shared by the backend, the dashboard, and every delivery SDK.

BetterCMS shipped four hand-rolled "Portable Text" trees before this package, and all four diverged from the spec in the same expensive way: no markDefs. Without markDefs a link has nowhere to live, so every one of them could render bold text and none of them could render a hyperlink. This package is the spec, so the official @portabletext/* packages can read and write our content without a shim.

Install

npm install @bettercms-ai/richtext

Reading a stored value

toPortableText reads every shape BetterCMS has ever stored and never throws — a throw here would be a blank field, and a blank field is one autosave away from being permanent. Unrecognised input returns [] and is reported through onUnreadable.

import { toPortableText, portableTextToPlainText } from "@bettercms-ai/richtext";

const value = toPortableText(entry.data.body);
const preview = portableTextToPlainText(value);

It handles a bare PortableText[], the {format, value, html} envelope, a Lexical state (object or JSON string), the bettercms-notion-doc document format, a pre-envelope {html} seed, bare HTML, and plain text — which stays literal, because running an author's copy through a Markdown parser is how a line beginning # silently became a heading.

Rendering

import { portableTextToHtml } from "@bettercms-ai/richtext/html";

const html = portableTextToHtml(value);
const inline = portableTextToHtml(value, { inline: true }); // unwraps a lone <p>

Storage is structured data and HTML is only ever an output, so author text can never be markup: @portabletext/to-html escapes text nodes by construction. The only attributes this writes are an href through safeHref and a class token matched against a pattern.

Ingesting HTML

import { htmlToPortableText } from "@bettercms-ai/richtext/ingest";
import { ptSchemaFor } from "@bettercms-ai/richtext";

htmlToPortableText(html, {
  parseHtml: (h) => new JSDOM(h).window.document, // required outside a browser
  schema: ptSchemaFor(["p", "h2", "link", "table"]),
});

The filter IS the schema. A tag with no place in the schema produces no node — <script> is not "sanitised away", it fails to parse into anything. One ptSchemaFor(accept) object drives the editor's toolbar, the server's write validation, and this filter at once, so they cannot drift.

Tables

A table is a block object of its own — {_type:"table", rows:[{_key, cells:[{_key, content}]}]} — and a cell holds blocks, not a string. So a link, a bolded word or a second paragraph inside a cell is ordinary rich text, and the ingest keeps it by ingesting each cell through the same converter as the document around it.

htmlToPortableText('<table><tbody><tr><td>Pro</td></tr></tbody></table>', { parseHtml, schema })
// → [{ _type: "table", _key: "…", rows: [{ _key: "…", cells: [{ _key: "…", content: [/* blocks */] }] }] }]

header sits on the cell (<th>), head on the row (<thead>), and span records a colspan without collapsing the grid. All three are recorded rather than inferred so that rendering a table reproduces the markup it came from — the property the corpus audit's --tables pass measures over every stored value, and the reason a table may be converted at all.

Tables stored before this — a generic bcmsBlock keyed builtin:table, holding a string matrix — are migrated on read by migrateTable. Nothing rewrites the database; the reader, the renderer and the plain-text pass all recognise the old shape and return the new one.

Subpath exports

importpulls inuse it for
@bettercms-ai/richtextnothing heavytypes, schema, reading, plain text
@bettercms-ai/richtext/htmlthe HTML serialiserrendering
@bettercms-ai/richtext/ingesta DOM parserHTML → Portable Text

The split is deliberate: ingest needs a DOM and html needs the serialiser, and neither should reach a dashboard bundle just because someone imported a type.

Ships as source

main points at ./src/index.ts, like @bettercms-ai/db and @bettercms-ai/content. The BetterCMS API runs Bun and consumes this directly; a dist-pointing export once crash-looped the container. Consumers that need compiled output should bundle it as they would any TypeScript source dependency.

Licence

Proprietary — part of BetterCMS.

版本与发布记录

@bettercms-ai/richtext 共发布 25 个版本,首次发布于 2026/08/27;过去 12 个月发布了 25 个版本。

版本发布日期距今
0.19.0latest2026/11/026 天前
0.18.02026/10/318 天前
0.17.22026/10/2613 天前
0.17.12026/10/2513 天前
0.17.02026/10/2415 天前
0.13.02026/10/2415 天前

关于 @bettercms-ai/richtext 的常见问题

@bettercms-ai/richtext 是什么?

The one Portable Text contract for BetterCMS: schema, converters, and rendering. Shared by the backend, the dashboard and every delivery SDK.

如何安装 @bettercms-ai/richtext?

在项目目录运行 npm install @bettercms-ai/richtext,或使用 pnpm add / yarn add / bun add。

@bettercms-ai/richtext 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bettercms-ai/richtext 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@bettercms-ai/richtext 可以商用吗?

@bettercms-ai/richtext 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@bettercms-ai/richtext 的下载量有多少?

根据 npm 官方下载统计,@bettercms-ai/richtext 在 2026/10/08 – 2026/11/06 这 30 天内被下载 3,724 次,日均约 124 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bettercms-ai/richtext 还在维护吗?

最新版本 0.19.0 发布于 2026/11/02(6 天前);至今共发布 25 个版本,其中过去 12 个月发布了 25 个;首次发布于 2026/08/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。