Hiển thị page và bài viết blog
Theme Z-CMS không gọi Content API để lấy page hoặc bài viết blog. Với mỗi URL công khai, site-runtime xác định website và truyền kết quả cho theme đang hoạt động. Theme chỉ hiển thị dữ liệu được nhận.
Luồng xử lý một request:
- Người dùng mở một URL công khai.
site-runtimegọi Render API nội bộ một lần.- API xác định hostname, ngôn ngữ, route, nội dung đã xuất bản, menu, cấu hình theme và capability của các plugin đang hoạt động.
site-runtimechọn template phù hợp trong theme.- Template hiển thị
content,archivevàctx.
Bước 1: Import type cho template
Phần tiêu đề “Bước 1: Import type cho template”Dùng type từ @zcmsorg/theme-sdk để input của template luôn khớp với contract của runtime.
import { defineTheme, type ArchiveTemplateProps, type PageTemplateProps, type ThemeManifest,} from "@zcmsorg/theme-sdk";import manifestJson from "../theme.json";
const manifest = manifestJson as unknown as ThemeManifest;
interface Settings { accent: string;}Không import type từ cms-api, site-runtime, Prisma hoặc module nội bộ khác của Z-CMS.
Bước 2: Hiển thị một page
Phần tiêu đề “Bước 2: Hiển thị một page”Runtime truyền page đã xuất bản và khớp với URL vào content.
function Page({ ctx, content }: PageTemplateProps<Settings>) { return ( <article> <h1>{content.title}</h1> {content.excerpt ? <p>{content.excerpt}</p> : null} <div>{ctx.renderBlocks(content.blocks)}</div> </article> );}Các field thường dùng trong content:
| Field | Mục đích |
|---|---|
title |
Tiêu đề page hoặc bài viết |
excerpt |
Phần tóm tắt, có thể không có |
path |
Đường dẫn nội dung trước khi xử lý tiền tố ngôn ngữ |
contentType.key |
Loại nội dung, ví dụ page hoặc post |
data |
Các field tùy chỉnh của content type |
blocks |
Danh sách block được tạo trong editor |
publishedAt |
Thời gian xuất bản hoặc null |
author |
Thông tin tác giả hoặc null |
seo |
Dữ liệu SEO đã xử lý cho nội dung |
Chỉ nội dung đã xuất bản mới được đưa vào public render payload. Theme không cần tự lọc nội dung nháp.
Bước 3: Đọc custom field an toàn
Phần tiêu đề “Bước 3: Đọc custom field an toàn”Các giá trị trong content.data là JSON nên có type unknown. Hãy kiểm tra từng giá trị trước khi hiển thị.
function text(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback;}
function Page({ ctx, content }: PageTemplateProps<Settings>) { const subtitle = text(content.data.subtitle);
return ( <article> <h1>{content.title}</h1> {subtitle ? <p>{subtitle}</p> : null} {ctx.renderBlocks(content.blocks)} </article> );}Không ép toàn bộ content.data sang một interface đáng tin cậy nếu theme không kiểm tra dữ liệu lúc chạy. Chủ website có thể thay đổi field của content type mà không phát hành phiên bản theme mới.
Bước 4: Hiển thị một bài viết blog
Phần tiêu đề “Bước 4: Hiển thị một bài viết blog”Khi content.contentType.key là post, site-runtime chọn templates.post. Nếu theme không khai báo template này, runtime dùng templates.page.
function Post({ ctx, content }: PageTemplateProps<Settings>) { const date = content.publishedAt ? new Date(content.publishedAt).toLocaleDateString(ctx.locale) : "";
return ( <article> <a href={ctx.url("/blog")}>Quay lại blog</a> <h1>{content.title}</h1> <p>{[date, content.author?.name].filter(Boolean).join(" · ")}</p> {content.excerpt ? <p>{content.excerpt}</p> : null} {ctx.renderBlocks(content.blocks)} </article> );}Bước 5: Hiển thị danh sách bài viết blog
Phần tiêu đề “Bước 5: Hiển thị danh sách bài viết blog”Route danh sách như /blog nhận archive thay vì content. Runtime truyền danh sách bài viết đã xuất bản và thông tin phân trang.
function Archive({ ctx, archive }: ArchiveTemplateProps<Settings>) { return ( <section> <h1>{archive.title}</h1>
{archive.items.length === 0 ? ( <p>Chưa có bài viết.</p> ) : ( <ul> {archive.items.map((item) => ( <li key={item.id}> <a href={ctx.url(item.path)}>{item.title}</a> {item.excerpt ? <p>{item.excerpt}</p> : null} </li> ))} </ul> )}
{archive.page > 1 ? ( <a href={ctx.url(`${archive.basePath}?page=${archive.page - 1}`)}>Trang trước</a> ) : null} {archive.page < archive.totalPages ? ( <a href={ctx.url(`${archive.basePath}?page=${archive.page + 1}`)}>Trang sau</a> ) : null} </section> );}Luôn truyền đường dẫn nội bộ của content và archive qua ctx.url(). Hàm này thêm đúng tiền tố ngôn ngữ và giữ nguyên query string. Các path trong ctx.alternates đã hoàn chỉnh nên không truyền qua ctx.url().
Bước 6: Đăng ký template
Phần tiêu đề “Bước 6: Đăng ký template”Template page là bắt buộc. Các template nội dung khác có thể dùng page làm fallback; riêng archive route cần có archive.
export default defineTheme<Settings>({ manifest, Layout, templates: { home: Home, page: Page, post: Post, archive: Archive, notFound: NotFound, error: ErrorPage, }, blocks,});| Request | Template do runtime chọn |
|---|---|
/ có nội dung trang chủ |
home, fallback sang page |
Nội dung có key là post |
post, fallback sang page |
| Nội dung khác | page |
Archive của content type, ví dụ /blog |
archive |
| Đường dẫn không tồn tại | notFound |
Bước 7: Hiển thị block
Phần tiêu đề “Bước 7: Hiển thị block”Gọi ctx.renderBlocks(content.blocks) thay vì tự lặp qua danh sách block trong từng template. Đăng ký component cho mỗi loại block mà theme hỗ trợ.
import type { BlockProps } from "@zcmsorg/theme-sdk";
function value(value: unknown): string { return typeof value === "string" ? value : "";}
function Hero({ props }: BlockProps<Record<string, unknown>, Settings>) { return ( <section> <h2>{value(props.heading)}</h2> {props.subheading ? <p>{value(props.subheading)}</p> : null} </section> );}
const blocks = { "core/hero": Hero, "core/richtext": RichText, "core/features": Features, "core/image": ImageBlock, "core/cta": CallToAction,};Tên loại block phải có namespace. Trong môi trường development, block chưa được theme hỗ trợ sẽ hiện cảnh báo; trong production, block đó bị bỏ qua. Vì vậy cần kiểm tra tất cả loại block mà website mục tiêu đang dùng.
Bước 8: Hiển thị menu, asset và link đa ngôn ngữ
Phần tiêu đề “Bước 8: Hiển thị menu, asset và link đa ngôn ngữ”- Đọc vị trí menu từ
ctx.menus.primary,ctx.menus.footerhoặc key khác đã khai báo trongtheme.json. - Dùng
ctx.url(path)cho link nội bộ. - Dùng
ctx.asset("assets/logo.png")cho file nằm trong package theme. - Dùng
ctx.site.brandcho logo và màu chính của website. - Dùng
ctx.settingscho thiết lập do theme khai báo. - Dùng
ctx.alternatesđể tạo bộ chuyển ngôn ngữ.
Bước 9: Kiểm tra toàn bộ luồng nội dung
Phần tiêu đề “Bước 9: Kiểm tra toàn bộ luồng nội dung”Trước khi đóng gói, hãy kiểm tra:
- Page có và không có excerpt.
- Page chứa tất cả core block mà theme hỗ trợ.
- Bài viết có và không có tác giả hoặc ngày xuất bản.
- Archive blog rỗng và archive có nhiều trang.
- Custom field bị thiếu hoặc có giá trị sai kiểu.
- Ngôn ngữ mặc định và ngôn ngữ phụ, đặc biệt là link phân trang archive.
- Block chưa được hỗ trợ để xác nhận page vẫn hiển thị.
Tiếp theo, xem cách tích hợp theme với plugin.