@smartsuite-cms/emagazine-sdk
v1.0.2
Published
SDK to load and display e-magazine articles from Smart Suite CMS
Readme
📖 Hướng Dẫn Sử Dụng E-Magazine SDK
SDK giúp các project khác truy xuất dữ liệu bài đăng e-magazine từ hệ thống Smart Suite CMS.
📦 Cài Đặt
Cách 1: Cài từ npm (khuyến nghị)
npm install @smartsuite-cms/emagazine-sdkCách 2: Cài trực tiếp từ thư mục local
# Trong project khác, trỏ đến thư mục sdk
npm install ../path/to/CMS/sdkCách 3: Sao chép thư mục dist/ vào project
Copy thư mục sdk/dist/ và file sdk/package.json sang project đích, rồi import trực tiếp.
Build từ source (dành cho contributor)
cd sdk
npm install
npm run buildPublish lên npm registry
cd sdk
npm publish --access public
# hoặc private:
# npm publish --access restricted⚡ Khởi Tạo
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
const sdk = new EmagazineSDK({
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
cmsBaseUrl: 'YOUR_CMS_BASE_URL', // URL CMS để tạo link preview
})| Thuộc tính | Bắt buộc | Mô tả |
|---|---|---|
| supabaseUrl | ✅ | URL Supabase project của CMS |
| supabaseAnonKey | ✅ | Anon key của Supabase project |
| cmsBaseUrl | ❌ | URL gốc CMS (dùng tạo link preview). Mặc định tự tính từ supabaseUrl |
📋 API Reference
1. Lấy danh sách bài đăng
const result = await sdk.listEmagazines({
status: 'published', // 'draft' | 'pending' | 'published' | 'archived' | 'all'
page: 1,
limit: 10,
orderBy: 'published_at', // 'published_at' | 'created_at' | 'title' | 'views_count'
orderDirection: 'desc', // 'asc' | 'desc'
})
console.log(result.data) // Mảng bài viết
console.log(result.total) // Tổng số bài viết
console.log(result.totalPages) // Tổng số trangCác tùy chọn lọc
// Lọc theo danh mục
const result = await sdk.listEmagazines({
categoryId: 'uuid-of-category',
})
// Chỉ lấy bài nổi bật
const featured = await sdk.listEmagazines({
featured: true,
})
// Tìm kiếm theo tiêu đề
const searched = await sdk.listEmagazines({
search: 'từ khóa tìm kiếm',
})
// Kết hợp nhiều bộ lọc
const filtered = await sdk.listEmagazines({
status: 'published',
featured: true,
search: 'công nghệ',
page: 1,
limit: 5,
orderBy: 'views_count',
orderDirection: 'desc',
})Cấu trúc dữ liệu trả về (EmagazineItem)
interface EmagazineItem {
id: string
title: string
slug: string
summary: string | null
thumbnail_url: string | null
status: 'draft' | 'pending' | 'published' | 'archived' | 'trash'
editor_type: 'grapes' | 'custom'
seo_title: string | null
seo_description: string | null
views_count: number
is_featured: boolean
created_at: string
updated_at: string
published_at: string | null
author: { full_name: string; avatar_url: string | null } | null
category: { name: string } | null
}2. Lấy chi tiết bài viết theo ID
const article = await sdk.getEmagazineById('article-uuid-here')
if (article) {
console.log(article.title) // Tiêu đề
console.log(article.preview_url) // Link preview trên CMS
console.log(article.html_content) // Nội dung HTML
console.log(article.css_content) // CSS đi kèm
}3. Lấy chi tiết bài viết theo Slug
const article = await sdk.getEmagazineBySlug('bai-viet-dau-tien')Cấu trúc dữ liệu trả về (EmagazineDetail)
interface EmagazineDetail {
id: string
title: string
slug: string
summary: string | null
html_content: string | null // ⭐ Nội dung HTML
css_content: string | null // ⭐ CSS đi kèm
preview_url: string // ⭐ Link preview CMS
thumbnail_url: string | null
status: string
editor_type: 'grapes' | 'custom'
seo_title: string | null
seo_description: string | null
views_count: number
is_featured: boolean
created_at: string
updated_at: string
published_at: string | null
author: { full_name: string; avatar_url: string | null } | null
category: { name: string } | null
}4. Lấy danh sách danh mục
const categories = await sdk.listCategories()
// [{ id: '...', name: 'Technology', slug: 'technology', description: '...' }]5. Hiển thị bài viết lên DOM (Browser)
Sử dụng method renderToElement() để inject HTML + CSS vào một element:
const article = await sdk.getEmagazineById('article-uuid')
if (!article) return
const container = document.getElementById('article-container')!
const cleanup = sdk.renderToElement(article, container)
// Khi không cần nữa (ví dụ chuyển trang), gọi cleanup:
cleanup()Lưu ý: Method này sẽ tự động inject
<style>vào<head>và trả về hàm cleanup để xóa khi unmount.
Ví dụ trong React:
import { useEffect, useRef, useState } from 'react'
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
const sdk = new EmagazineSDK({
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
cmsBaseUrl: 'YOUR_CMS_BASE_URL',
})
function ArticleViewer({ articleId }: { articleId: string }) {
const containerRef = useRef<HTMLDivElement>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let cleanup: (() => void) | undefined
const load = async () => {
setLoading(true)
const article = await sdk.getEmagazineById(articleId)
if (article && containerRef.current) {
cleanup = sdk.renderToElement(article, containerRef.current)
}
setLoading(false)
}
load()
return () => cleanup?.()
}, [articleId])
if (loading) return <div>Đang tải...</div>
return <div ref={containerRef} />
}6. Tạo HTML Page hoàn chỉnh
Tạo chuỗi HTML đầy đủ (bao gồm <html>, <head>, <body>) để dùng cho iframe hoặc server-side rendering:
const article = await sdk.getEmagazineById('article-uuid')
if (article) {
const fullHTML = sdk.generateFullPageHTML(article)
// Hiển thị trong iframe
const iframe = document.getElementById('preview-frame') as HTMLIFrameElement
iframe.srcdoc = fullHTML
// Hoặc ghi ra file (server-side)
// fs.writeFileSync('output.html', fullHTML)
}🔧 Ví Dụ Hoàn Chỉnh
Vanilla JavaScript
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>E-Magazine Reader</title>
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
.article-card { border: 1px solid #eee; border-radius: 12px; padding: 16px; margin: 12px 0; cursor: pointer; transition: box-shadow 0.3s; }
.article-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
.article-card img { width: 100%; height: 200px; object-fit: cover; border-radius: 8px; }
.article-card h2 { margin: 12px 0 8px; }
.article-card p { color: #666; }
</style>
</head>
<body>
<h1>📰 E-Magazine</h1>
<div id="articles-list"></div>
<div id="article-detail" style="display:none"></div>
<script type="module">
import { EmagazineSDK } from './path-to-sdk/dist/index.mjs'
const sdk = new EmagazineSDK({
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseAnonKey: 'YOUR_SUPABASE_ANON_KEY',
cmsBaseUrl: 'YOUR_CMS_BASE_URL',
})
// Hiển thị danh sách bài viết
const { data: articles, total } = await sdk.listEmagazines({
status: 'published',
limit: 10,
})
const list = document.getElementById('articles-list')
articles.forEach(article => {
const card = document.createElement('div')
card.className = 'article-card'
card.innerHTML = `
${article.thumbnail_url ? `<img src="${article.thumbnail_url}" alt="${article.title}">` : ''}
<h2>${article.title}</h2>
<p>${article.summary || ''}</p>
<small>👤 ${article.author?.full_name || 'Ẩn danh'} · 📁 ${article.category?.name || ''}</small>
`
card.onclick = () => loadArticle(article.id)
list.appendChild(card)
})
// Xem chi tiết bài viết
async function loadArticle(id) {
const detail = await sdk.getEmagazineById(id)
if (!detail) return
const container = document.getElementById('article-detail')
container.style.display = 'block'
document.getElementById('articles-list').style.display = 'none'
sdk.renderToElement(detail, container)
}
</script>
</body>
</html>Next.js (Server Component)
// app/magazine/page.tsx
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
const sdk = new EmagazineSDK({
supabaseUrl: process.env.SUPABASE_URL!,
supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
cmsBaseUrl: process.env.CMS_BASE_URL,
})
export default async function MagazinePage() {
const { data: articles } = await sdk.listEmagazines({
status: 'published',
limit: 12,
})
return (
<div>
<h1>E-Magazine</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
{articles.map(article => (
<a key={article.id} href={`/magazine/${article.slug}`}>
{article.thumbnail_url && <img src={article.thumbnail_url} alt={article.title} />}
<h2>{article.title}</h2>
<p>{article.summary}</p>
</a>
))}
</div>
</div>
)
}// app/magazine/[slug]/page.tsx
import { EmagazineSDK } from '@smartsuite-cms/emagazine-sdk'
const sdk = new EmagazineSDK({
supabaseUrl: process.env.SUPABASE_URL!,
supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
cmsBaseUrl: process.env.CMS_BASE_URL,
})
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await sdk.getEmagazineBySlug(params.slug)
if (!article) return <div>Không tìm thấy bài viết</div>
return (
<div>
<style dangerouslySetInnerHTML={{ __html: article.css_content || '' }} />
<div dangerouslySetInnerHTML={{ __html: article.html_content || '' }} />
</div>
)
}⚠️ Lưu Ý Quan Trọng
RLS (Row Level Security): SDK sử dụng
anon keynên chỉ truy xuất được dữ liệu mà RLS policies cho phép. Nếu cần access tất cả, bạn cần tạo thêm RLS policy cho phép đọc public các bàipublished.CORS: Nếu gọi từ browser khác domain, đảm bảo Supabase project cho phép domain của bạn trong CORS settings.
Performance: SDK trả về dữ liệu phân trang (pagination). Hãy sử dụng
limithợp lý (khuyến nghị 10-20 item/trang).CSS Isolation: Khi render
html_content+css_content, CSS có thể ảnh hưởng đến layout hiện tại. Nên wrap trong một container có class riêng hoặc sử dụngiframevớigenerateFullPageHTML()để cách ly hoàn toàn.preview_url: Link preview trỏ đến trang CMS theo format
/corporate/e-magazine/preview/{id}. Cần đảm bảo CMS đang chạy và route này accessible.
📂 Cấu Trúc SDK
sdk/
├── package.json
├── tsconfig.json
└── src/
├── index.ts # Entry point - export SDK class và types
├── client.ts # EmagazineSDK class chính
└── types.ts # TypeScript interfaces🔄 Build & Phát Triển
# Cài đặt dependencies
cd sdk
npm install
# Build production
npm run build
# Watch mode (dev)
npm run devSau khi build, output nằm trong dist/:
dist/index.js— CommonJSdist/index.mjs— ES Moduledist/index.d.ts— TypeScript declarations
