npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

strapi-plugin-casso-article-editor

v1.2.0

Published

Strapi 5 custom field to write article bodies as HTML (CKEditor 5), Notion-like TipTap, or Markdown — with paste-to-upload images and Lark Docs import

Readme

strapi-plugin-casso-article-editor

A Strapi 5 custom field for long-form article bodies. One field, three writing modes — and the content keeps its original format instead of being flattened to HTML.

| Mode | Editor | Stored as | | --- | --- | --- | | HTML | CKEditor 5 document canvas | HTML string | | Notion | TipTap with / slash menu, callouts, columns, tables, task lists, code blocks | TipTap JSON | | Markdown | Split editor with live preview and > [!NOTE] callouts | Markdown |

Also included: paste-an-image straight into the Media Library (with alt text dialog), and one-click import from a Lark Docs / Wiki link.

Tài liệu tiếng Việt ở phần dưới.

Requirements

  • Strapi ^5.0.0
  • Node >=20
  • React 18, styled-components 6 (provided by the Strapi admin)

Install

npm install strapi-plugin-casso-article-editor

1. Enable the plugin

config/plugins.ts (or .js):

export default {
  'casso-article-editor': {
    enabled: true,
  },
};

Every option has a default and reads from env, so this is all the config you need to get started.

2. Wrap the admin Vite config

Create or edit src/admin/vite.config.ts:

import { withCassoArticleEditor } from 'strapi-plugin-casso-article-editor/vite';

export default (config) => withCassoArticleEditor(config);

This is required. It pre-bundles the CJS dependencies of @_sh/strapi-plugin-ckeditor (which Strapi 5.50+ auto-excludes from optimizeDeps, leaving them unbundled and blanking the admin panel) and injects plugin CSS through JS so the CKEditor toolbar survives strapi build. If you already have a Vite config, merge yours first and pass the result in.

3. Point a content type at the custom field

In src/api/<name>/content-types/<name>/schema.json:

{
  "attributes": {
    "content": {
      "type": "richtext",
      "customField": "plugin::casso-article-editor.body"
    },
    "contentHtml": {
      "type": "text",
      "configurable": false,
      "pluginOptions": { "content-manager": { "visible": false } }
    },
    "contentToc": {
      "type": "json",
      "configurable": false,
      "pluginOptions": { "content-manager": { "visible": false } }
    },
    "contentFormat": {
      "type": "string",
      "configurable": false,
      "pluginOptions": { "content-manager": { "visible": false } }
    }
  }
}

The three sibling fields are optional — declare the ones you want and the plugin fills them on every create/update, named <field><suffix>:

| Field | Suffix | Contents | | --- | --- | --- | | contentHtml | Html | Display-ready HTML, heading ids assigned, code highlighted | | contentToc | Toc | [{ id, text, level }] matching those ids | | contentFormat | Format | "html" | "markdown" | "tiptap" |

Do not mark them "writable": false: Strapi's document service strips non-writable attributes from the payload, so the plugin's writes are silently dropped and the fields stay empty forever. Hide them from the edit form with pluginOptions as above.

Restart Strapi, rebuild the admin, and the field shows up in the Content-Type Builder under custom fields.

Reading the content

Websites should read contentHtml and contentToc and render nothing themselves:

const article = await fetch(
  `${STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}&fields[0]=title&fields[1]=contentHtml&fields[2]=contentToc`,
).then((r) => r.json()).then((j) => j.data?.[0]);

// article.contentHtml -> "<h2 id=\"gioi-thieu\">Giới thiệu</h2>…"

That HTML is produced by the same renderer as the editor's own preview, so preview and website agree. It is written at save time, which makes it a cache: after adding the field or upgrading the plugin, re-render existing rows with POST /casso-article-editor/rerender.

The body itself is stored as a JSON envelope so the source format round-trips:

{ "__casso": 1, "format": "tiptap", "body": "{\"type\":\"doc\",…}" }

Legacy plain-HTML strings are still understood and reported as format: "html".

From a Strapi lifecycle, controller or script:

const { parseContent } = strapi.plugin('casso-article-editor').service('envelope');
const { format, body } = parseContent(entry.content);

// render on demand, same output as the stored HTML
const { html, toc } = strapi.plugin('casso-article-editor').service('render').render(entry.content);

Outside Strapi (a Next.js app, a migration script):

import { parseContent, serializeContent } from 'strapi-plugin-casso-article-editor/envelope';
import { renderArticle } from 'strapi-plugin-casso-article-editor/render';

The envelope subpath has no dependencies; render pulls TipTap, marked and lowlight, so keep it server-side. Both ship ESM and CJS.

Rendering it on a website

RENDERING.md is the full frontend spec: the fields to fetch and how, the complete list of classes and data-* attributes the stored HTML contains, the article stylesheet, the TOC contract, image URL handling, sanitisation, how to re-render stale HTML, and the mistakes that break rendering.

It ships inside the package, so an agent working in a consuming repo can read it from node_modules/strapi-plugin-casso-article-editor/RENDERING.md.

Lark Docs import

Paste a …/docx/<token> or …/wiki/<token> link and the plugin walks the document block tree through the Lark API, maps it to TipTap JSON (headings, lists, tables, grids to columns, callouts, code, task lists, quotes) and uploads every image into the Media Library with a Vietnamese-slug filename.

It uses its own Lark app, separate from any SSO app you may have, because it needs tenant-level read access to documents:

| Env | Default | Notes | | --- | --- | --- | | LARK_IMPORT_APP_ID | — | Lark app ID; import button stays hidden while empty | | LARK_IMPORT_APP_SECRET | — | Lark app secret | | LARK_IMPORT_API_BASE | https://open.larksuite.com | Use https://open.feishu.cn for Feishu | | LARK_IMPORT_ENABLED | true | Set false to disable the feature entirely |

Create a custom app in the Lark Developer Console, then:

  1. Add the permission scopes docx:document:readonly, wiki:wiki:readonly and drive:drive:readonly.
  2. Publish the app so the tenant token works.
  3. Share the documents (or the whole wiki space) with the app.

The admin calls GET /casso-article-editor/lark-import/status once per page load and only renders the "Import Lark" button when the server reports the app as configured — so leaving the env vars unset is a supported configuration, not an error.

Configuration reference

Set these in config/plugins.ts if you need to override the env defaults:

export default ({ env }) => ({
  'casso-article-editor': {
    enabled: true,
    config: {
      // Sibling fields the plugin keeps in sync, as `content` + suffix.
      // Set any of them to null to stop writing that field.
      formatFieldSuffix: 'Format',
      htmlFieldSuffix: 'Html',
      tocFieldSuffix: 'Toc',
      // Shift h1-h3 down one level when the body still has an <h1>
      // (Lark imports usually do), so the page title stays the only h1.
      demoteHeadings: true,
      // Re-render rows with no HTML yet on every boot. Off by default: on a
      // large site, call POST /casso-article-editor/rerender after a deploy instead.
      backfillOnBootstrap: false,
      larkImport: {
        enabled: true,
        appId: env('LARK_IMPORT_APP_ID', ''),
        appSecret: env('LARK_IMPORT_APP_SECRET', ''),
        apiBase: env('LARK_IMPORT_API_BASE', 'https://open.larksuite.com'),
      },
    },
  },
});

Customising the CKEditor preset

The HTML mode ships a preset named cassoArticle (a document-style canvas built on defaultHtmlPreset). To replace it, register your own preset with the same name from src/admin/app.ts — the plugin only adds its preset when that name is not already taken:

import { setPluginConfig, defaultHtmlPreset, getPluginPresets } from '@_sh/strapi-plugin-ckeditor';

export default {
  register() {
    setPluginConfig({
      presets: [
        ...Object.values(getPluginPresets()),
        { ...defaultHtmlPreset, name: 'cassoArticle', description: 'My canvas' },
      ],
    });
  },
};

Development

npm install
npm run build      # dist/{admin,server,envelope,render,vite}
npm run watch      # rebuild on change
npm run verify     # validate package.json exports against dist
npm test           # render regression suite (HTML contract + TOC)

To try changes against a real app, npm pack and install the tarball, or use npm run watch:link with strapi-plugin link.


Tiếng Việt

Custom field cho Strapi 5 dùng viết bài dài. Một field, ba chế độ viết, và nội dung giữ đúng format gốc thay vì bị ép hết về HTML.

| Chế độ | Editor | Lưu dưới dạng | | --- | --- | --- | | HTML | CKEditor 5, canvas kiểu trang giấy | chuỗi HTML | | Notion | TipTap với menu /, callout, cột, bảng, checklist, code block | TipTap JSON | | Markdown | Editor chia đôi có preview, hỗ trợ callout > [!NOTE] | Markdown |

Kèm theo: dán ảnh là upload thẳng vào Media Library (có hộp thoại đặt alt), và import một phát từ link Lark Docs / Wiki.

Cài đặt

npm install strapi-plugin-casso-article-editor

Bước 1 — bật plugin trong config/plugins.ts:

export default {
  'casso-article-editor': { enabled: true },
};

Mọi option đều có default đọc từ env nên chỉ cần bấy nhiêu là chạy.

Bước 2 — bọc vite config của admin tại src/admin/vite.config.ts. Bước này bắt buộc:

import { withCassoArticleEditor } from 'strapi-plugin-casso-article-editor/vite';

export default (config) => withCassoArticleEditor(config);

Lý do: từ Strapi 5.50, @_sh/strapi-plugin-ckeditor bị tự động đưa vào optimizeDeps.exclude, kéo theo các dependency CJS của nó không được pre-bundle và admin trắng trang. Helper cũng inject CSS qua JS để toolbar CKEditor không mất style sau strapi build.

Bước 3 — khai báo field trong schema content type:

{
  "attributes": {
    "content": {
      "type": "richtext",
      "customField": "plugin::casso-article-editor.body"
    },
    "contentHtml": {
      "type": "text",
      "configurable": false,
      "pluginOptions": { "content-manager": { "visible": false } }
    },
    "contentToc": {
      "type": "json",
      "configurable": false,
      "pluginOptions": { "content-manager": { "visible": false } }
    },
    "contentFormat": {
      "type": "string",
      "configurable": false,
      "pluginOptions": { "content-manager": { "visible": false } }
    }
  }
}

Ba field đi kèm đều không bắt buộc — khai báo field nào thì plugin ghi field đó mỗi lần create/update, tên theo quy ước <field><suffix>:

| Field | Suffix | Nội dung | | --- | --- | --- | | contentHtml | Html | HTML render sẵn, heading đã có id, code đã highlight | | contentToc | Toc | [{ id, text, level }] khớp đúng id đó | | contentFormat | Format | "html" | "markdown" | "tiptap" |

Đừng đặt "writable": false cho mấy field này: document service của Strapi loại attribute không writable khỏi payload, nên plugin ghi xong bị bỏ và field rỗng vĩnh viễn. Muốn ẩn khỏi form edit thì dùng pluginOptions như trên.

Đọc nội dung

Website nên đọc contentHtml + contentToc và không tự render gì:

const article = await fetch(
  `${STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}&fields[0]=title&fields[1]=contentHtml&fields[2]=contentToc`,
).then((r) => r.json()).then((j) => j.data?.[0]);

HTML đó do đúng renderer chạy trong preview của editor sinh ra, nên preview và website không lệch nhau. Nó được ghi lúc save, tức là một dạng cache: sau khi thêm field hoặc nâng version plugin thì render lại bản ghi cũ bằng POST /casso-article-editor/rerender.

Bản thân body vẫn lưu dưới dạng JSON envelope để giữ được format gốc:

{ "__casso": 1, "format": "tiptap", "body": "{\"type\":\"doc\",…}" }

Dữ liệu cũ là HTML thuần vẫn đọc được và trả về format: "html".

Trong Strapi:

const { parseContent } = strapi.plugin('casso-article-editor').service('envelope');
const { html, toc } = strapi.plugin('casso-article-editor').service('render').render(entry.content);

Ngoài Strapi (app Next.js, script migration):

import { parseContent, serializeContent } from 'strapi-plugin-casso-article-editor/envelope';
import { renderArticle } from 'strapi-plugin-casso-article-editor/render';

Subpath envelope không có dependency nào; render kéo theo TipTap, marked, lowlight nên chỉ dùng ở server. Cả hai ship ESM và CJS.

Render ở website

RENDERING.md là đặc tả đầy đủ cho frontend: fetch field nào và fetch thế nào, danh sách trọn vẹn class và data-* có trong HTML đã lưu, CSS cho bài viết, hợp đồng dữ liệu mục lục, xử lý URL ảnh, sanitize, cách render lại HTML đã cũ, và bảng các lỗi thường gặp.

File này nằm trong package nên khi vibe code ở dự án khác, agent đọc trực tiếp được ở node_modules/strapi-plugin-casso-article-editor/RENDERING.md.

Import từ Lark Docs

Dán link …/docx/<token> hoặc …/wiki/<token>, plugin đọc cây block qua Lark API rồi map sang TipTap JSON (heading, list, bảng, grid thành cột, callout, code, checklist, quote) và upload toàn bộ ảnh vào Media Library với tên file slug tiếng Việt.

Plugin dùng Lark app riêng, độc lập với app SSO nếu dự án có, vì cần quyền tenant đọc tài liệu:

| Env | Mặc định | Ghi chú | | --- | --- | --- | | LARK_IMPORT_APP_ID | — | Chưa set thì nút Import tự ẩn | | LARK_IMPORT_APP_SECRET | — | | | LARK_IMPORT_API_BASE | https://open.larksuite.com | Feishu dùng https://open.feishu.cn | | LARK_IMPORT_ENABLED | true | Đặt false để tắt hẳn |

Tạo custom app trên Lark Developer Console, bật scope docx:document:readonly, wiki:wiki:readonly, drive:drive:readonly, publish app, rồi share tài liệu (hoặc cả wiki space) cho app.

Admin gọi GET /casso-article-editor/lark-import/status một lần mỗi lần load trang và chỉ hiện nút "Import Lark" khi server báo đã cấu hình — nên để trống env là trạng thái hợp lệ, không phải lỗi.

License

MIT