การใช้เนื้อหา › Rich text
เริ่มต้น
เนื้อหา
Plugins
การดูแลระบบ
การ Deploy
สูตรสำเร็จ
Rich text
หน้านี้สอนอะไร
ผู้แก้ทำอะไรได้บ้างใน field แบบ rich text เนื้อหาถูกเก็บอย่างไร และจะแสดงบนหน้าเว็บอย่างปลอดภัยด้วย Nuxt, Next.js หรือ frontend ใดก็ได้อย่างไร
ควรอ่านก่อน: Fields
Field
{ name: 'body', type: 'richText', localized: true }ในหน้า admin field แบบ richText แก้ด้วย Tiptap: หัวข้อ (H2–H4) ตัวหนา ตัวเอียง
ขีดเส้นใต้ โค้ดในบรรทัด ลิงก์ รายการแบบจุดและตัวเลข คำพูดอ้างอิง รูปจากคลังสื่อ (หรือ URL) ตาราง ย้อนกลับและทำซ้ำ
ตารางเริ่มด้วยแถวหัวตาราง และเมื่อเคอร์เซอร์อยู่ในตารางจะมีปุ่มเพิ่มและลบแถวหรือคอลัมน์ ยังรวมเซลล์ไม่ได้
คีย์ลัดใช้ได้เหมือน editor อื่น (Ctrl/⌘ + B, I, U, Z)
ค่าถูกเก็บเป็น JSON ของ Tiptap ซึ่งเป็นโครงสร้างต้นไม้ของ node ไม่ใช่ HTML:
{
"type": "doc",
"content": [
{ "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "สวัสดี" }] },
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "อ่าน" },
{ "type": "text", "text": "คู่มือ", "marks": [{ "type": "link", "attrs": { "href": "/guide" } }] }
]
}
]
}JSON ทำให้เนื้อหาไม่ผูกกับวิธีแสดงผล เอกสารเดียวกันแปลงเป็น HTML เป็นข้อความธรรมดาสำหรับระบบค้นหา หรือเป็น component ของคุณเองก็ได้
แสดงบนหน้าเว็บ
ติดตั้งตัวแปลง:
npm install @easy-cms/richtextpnpm add @easy-cms/richtextyarn add @easy-cms/richtextbun add @easy-cms/richtext<script setup lang="ts">
import { renderRichText } from '@easy-cms/richtext'
const { data: post } = await useFetch(`/api/posts/${useRoute().params.slug}`)
const html = computed(() => renderRichText(post.value?.body))
</script>
<template>
<!-- ปลอดภัย: renderRichText escape ข้อความและตัด URL ที่ไม่ปลอดภัย -->
<div class="prose" v-html="html" />
</template>import { renderRichText } from '@easy-cms/richtext'
export function PostBody({ body }: { body: unknown }) {
// ปลอดภัย: renderRichText escape ข้อความและตัด URL ที่ไม่ปลอดภัย
return <div className="prose" dangerouslySetInnerHTML={{ __html: renderRichText(body) }} />
}import { renderRichText } from '@easy-cms/richtext'
const post = await fetch('/api/cms/posts/1').then((r) => r.json())
document.querySelector('#body')!.innerHTML = renderRichText(post.body)รองรับ node เหล่านี้: ย่อหน้า หัวข้อ รายการแบบจุดและตัวเลข คำพูดอ้างอิง code block การขึ้นบรรทัดใหม่
เส้นคั่น รูป และตาราง (แถวหัวตารางอยู่ใน <thead>) ส่วน mark ที่รองรับ: ตัวหนา ตัวเอียง ขีดเส้นใต้ ขีดฆ่า โค้ด และลิงก์
node ที่ไม่รู้จักจะเหลือแค่ข้อความโดยไม่มี markup เนื้อหาแบบใหม่จึงไม่ทำให้หน้าเว็บพัง
ความปลอดภัย
renderRichText escape ข้อความและ attribute ทั้งหมด และตัด URL ที่ไม่ปลอดภัย (javascript:, data:
และอะไรก็ตามที่ไม่ใช่ http(s), mailto, tel หรือลิงก์แบบ relative) ผลลัพธ์จึงใช้กับ v-html หรือ
dangerouslySetInnerHTML ได้อย่างปลอดภัย ลิงก์ภายนอกจะได้ rel="noopener noreferrer"
ข้อความธรรมดา
import { richTextToPlainText } from '@easy-cms/richtext'
const excerpt = richTextToPlainText(post.body).slice(0, 160) // meta description, ระบบค้นหาMarkdown
renderMarkdown(doc) แปลง rich text เป็น Markdown เช่น สำหรับผู้ช่วย AI (SEO สำหรับ AI),
อีเมล หรือการ export ข้อความที่ดูเหมือน Markdown จะถูก escape และ URL ที่ไม่ปลอดภัยจะถูกตัดทิ้งเหมือนตอนแปลงเป็น HTML
ตารางกลายเป็นตาราง GFM โดยย่อหน้าในเซลล์เดียวกันเชื่อมด้วย <br>
ส่ง nodes เพื่อกำหนดวิธีแปลง node ชนิดของคุณเอง
import { renderMarkdown } from '@easy-cms/richtext'
const markdown = renderMarkdown(post.body) // "## หัวข้อ\n\nย่อหน้าที่มีตัว **หนา**"ปรับแต่งผลลัพธ์
เปลี่ยนหรือเพิ่มตัวแสดงผลของ node ใดก็ได้ ฟังก์ชันจะได้ node และ children ที่แปลงแล้ว:
import { slugify } from '@easy-cms/core'
import { escapeHtml, renderRichText, safeUrl } from '@easy-cms/richtext'
renderRichText(post.body, {
nodes: {
// ห่อรูปด้วย figure พร้อมคำบรรยายจาก alt
image: (node) => {
const src = safeUrl(node.attrs?.src)
const alt = escapeHtml(String(node.attrs?.alt ?? ''))
return src ? `<figure><img src="${escapeHtml(src)}" alt="${alt}"><figcaption>${alt}</figcaption></figure>` : ''
},
// ใส่ id ให้หัวข้อสำหรับทำสารบัญ
heading: (node, children) => {
const level = Number(node.attrs?.level ?? 2)
return `<h${level} id="${escapeHtml(slugify(children))}">${children}</h${level}>`
},
},
// attribute ของทุกลิงก์ เช่น เปิดลิงก์ภายนอกในแท็บใหม่
linkAttributes: (href) => (href.startsWith('http') ? { target: '_blank', rel: 'noopener noreferrer' } : {}),
})ตัวแสดงผลที่เขียนเองจะได้ attribute ดิบ ให้ escape เองด้วย escapeHtml และ safeUrl ที่ export ไว้
(children ถูก escape มาแล้ว)
ขั้นต่อไป
- อัปโหลดและ media: รูปที่ผู้แก้แทรกลงในเนื้อหา
- หลายภาษา: rich text แยกตามภาษา