Xây Dựng Blog Cá Nhân — Phase 2: Render MDX Theo Yêu Cầu

Đây là Phase 2 trong series Xây Dựng Blog Cá Nhân. Ở Phase 1, bạn đã có skeleton Next.js 16 + ShadCN/UI chạy ngon lành rồi. Giờ mình sẽ gắn tầng nội dung vào — viết bài bằng file MDX, mỗi lần có người truy cập thì server compile và render thành trang React hoàn chỉnh, code block tô màu đẹp như VS Code.
Series: Xây Dựng Blog Cá Nhân — Lộ Trình Toàn Bộ
Trước đó: Phase 1 — Next.js 16 + ShadCN/UI Setup
Tiếp theo: Phase 3 — PostgreSQL + Drizzle ORM
Source code: github.com/chanhlt/my-blog — branch personal-blog-phase-2
MDX là gì?
MDX = Markdown + JSX. Nôm na là bạn viết Markdown như bình thường (heading, code block, danh sách, link), nhưng muốn chèn component React chỗ nào cũng được:
# Hello World
Đây là đoạn văn bình thường.
<CustomAlert type="warning">
Đây là component React nằm trong Markdown!
</CustomAlert>
```bash
npm install next-mdx-remote
```Tại sao chọn MDX cho blog?
- Cú pháp quen thuộc — viết bài bằng Markdown thôi, không cần CMS rườm rà
- Có cả sức mạnh React — muốn nhúng biểu đồ tương tác, callout box, tab gì cũng được
- Workflow Git — bài viết nằm trong repo, version control cùng code luôn
- Tương thích rộng — file
.mdxchạy được với bất kỳ tool nào hỗ trợ MDX
Render Theo Yêu Cầu vs Sinh Tĩnh
Next.js cho bạn hai cách chính để render nội dung:
| Tính năng | Sinh Tĩnh (SSG) | Theo Yêu Cầu (Server) |
|---|---|---|
| Khi nào compile | Lúc build | Lúc có request |
| Cập nhật nội dung | Phải rebuild | Ngay lập tức — sửa file, refresh |
| Thời gian build | Tăng theo số bài | Không đổi |
| Phù hợp với | Trang marketing, docs | Blog hay cập nhật |
| API Next.js | generateStaticParams() | export const dynamic = 'force-dynamic' |
Mình chọn render theo yêu cầu vì:
- Không cần rebuild — sửa file MDX, F5 trình duyệt, thấy ngay thay đổi
- Deploy đơn giản — đăng bài mới không cần build lại cả site
- Hợp với self-hosted — VPS của bạn thừa sức compile MDX mỗi request
- Dễ chuyển đổi — mai mốt cần SSG thì thêm
generateStaticParams()là xong
Cài Đặt Dependencies
Cần sáu package cho pipeline MDX:
npm install next-mdx-remote gray-matter remark-gfm rehype-pretty-code rehype-slug shiki| Package | Công dụng |
|---|---|
next-mdx-remote | Compile và render MDX trong React Server Components |
gray-matter | Parse frontmatter YAML (title, date, tags) từ file MDX |
remark-gfm | GitHub Flavored Markdown — bảng, task list, gạch ngang |
rehype-pretty-code | Tô màu syntax cho code block (dùng Shiki bên trong) |
rehype-slug | Tự động tạo id cho heading để làm anchor link |
shiki | Engine tô màu syntax đứng sau rehype-pretty-code |
Cấu Trúc Thư Mục Nội Dung
Tạo thư mục chứa bài viết MDX:
mkdir -p content/postsCấu trúc project giờ trông thế này:
my-blog/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── blog/
│ ├── page.tsx # Trang danh sách bài
│ └── [slug]/
│ └── page.tsx # Trang chi tiết bài
├── components/
│ ├── Header.tsx
│ └── Footer.tsx
├── content/
│ └── posts/ # File MDX nằm đây
│ ├── hello-world.mdx
│ └── my-second-post.mdx
├── lib/
│ ├── mdx.ts # MDX compiler config (dùng React.createElement, không có JSX)
│ └── posts.ts # Đọc/liệt kê bài từ đĩa
└── package.jsonGhi chú: Phase sau mình sẽ thêm song ngữ với
content/posts/en/vàcontent/posts/vi/. Giờ cứ giữ đơn giản một thư mục phẳng trước đã.
Viết Bài MDX Đầu Tiên
Tạo file content/posts/hello-world.mdx:
---
title: "Hello World — Bài Viết Đầu Tiên"
description: "Bài test ngắn để kiểm tra pipeline MDX hoạt động từ đầu đến cuối."
date: "2026-02-28"
tags: ["intro", "blog"]
---
Chào mừng đến blog của tôi! Bài viết này chứng minh pipeline MDX đã hoạt động.
## Tại Sao Tôi Xây Cái Này
Tôi muốn một blog mà:
- Nằm trong Git repo
- Dùng Markdown để viết
- Hỗ trợ tô màu code ngay từ đầu
## Ví Dụ Code
Đây là một hàm TypeScript đơn giản:
```typescript
function greet(name: string): string {
return `Hello, ${name}!`
}
console.log(greet('World'))
```
## Tiếp Theo
Ở phase tiếp theo mình sẽ thêm database với Drizzle ORM cho view count và subscription.Giải Thích Frontmatter
Khối YAML nằm giữa hai dòng --- gọi là frontmatter — chứa metadata của bài viết:
---
title: "Hello World" # Hiển thị làm <h1> của trang
description: "Bài test ngắn..." # Dùng trong thẻ <meta> và card bài viết
date: "2026-02-28" # Ngày xuất bản (YYYY-MM-DD)
tags: ["intro", "blog"] # Để lọc và hiển thị
---gray-matter sẽ parse khối này thành object JavaScript, tách riêng khỏi phần nội dung MDX bên dưới.
Định Nghĩa TypeScript Types cho Bài Viết
Tạo lib/posts.ts — file này lo việc đọc MDX từ ổ đĩa và trả về dữ liệu bài viết có type rõ ràng:
// lib/posts.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
// Interface TypeScript cho metadata bài viết
export interface PostMeta {
title: string
description: string
date: string
tags: string[]
image?: string
slug: string
}
// Bài viết đầy đủ = metadata + nội dung MDX thô
export interface Post extends PostMeta {
content: string
}Có type rồi thì autocomplete và type checking chạy khắp nơi — sai là IDE báo đỏ ngay.
Đọc Tất Cả Bài Viết
Thêm hàm getAllPosts() vào lib/posts.ts:
// lib/posts.ts (tiếp)
const POSTS_DIR = path.join(process.cwd(), 'content/posts')
function formatDate(date: Date | string): string {
const d = new Date(date)
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
export function getAllPosts(): PostMeta[] {
// 1. Đọc tất cả tên file .mdx từ thư mục posts
const filenames = fs.readdirSync(POSTS_DIR)
.filter((f) => f.endsWith('.mdx'))
// 2. Parse frontmatter từ mỗi file
const posts = filenames.map((filename) => {
const slug = filename.replace(/\.mdx$/, '')
const fullPath = path.join(POSTS_DIR, filename)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data } = matter(fileContents)
return {
slug,
title: data.title,
description: data.description,
date: formatDate(data.date),
tags: data.tags || [],
image: data.image,
}
})
// 3. Sắp xếp theo ngày (mới nhất trước)
return posts.sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
)
}Luồng xử lý:
- Đọc thư mục —
fs.readdirSync()liệt kê tất cả file.mdx - Parse từng file —
gray-matter()tách frontmatter khỏi nội dung - Xây metadata — trích
title,description,date,tagstừ frontmatter - Sắp theo ngày — bài mới nhất hiện trước
Đọc Một Bài Viết Theo Slug
Tiếp tục thêm getPostBySlug() vào cùng file:
// lib/posts.ts (tiếp)
export function getPostBySlug(slug: string): Post | null {
const fullPath = path.join(POSTS_DIR, `${slug}.mdx`)
// Trả về null nếu file không tồn tại
if (!fs.existsSync(fullPath)) {
return null
}
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
return {
slug,
title: data.title,
description: data.description,
date: formatDate(data.date),
tags: data.tags || [],
image: data.image,
content, // Chuỗi MDX thô — sẽ compile sau
}
}Điểm khác so với getAllPosts(): hàm này trả thêm field content — chuỗi MDX thô mà lát nữa mình sẽ compile thành React component.
Cấu Hình MDX Compiler
Tạo lib/mdx.ts — file này ở trong lib/ vì không chứa JSX. Custom component được đăng ký bằng React.createElement() thay vì JSX, giữ file là TypeScript thuần:
Không cần JSX trong
lib/mdx.ts: Thay vì viết JSX<Callout>, dùngReact.createElement(Callout, props). Kết quả runtime giống nhau, nhưng file giữ được extension.tsvà nằm đúng chỗ tronglib/— nơi dành cho utility code. Định nghĩa component thật (có JSX) nằm riêng trongcomponents/.
// lib/mdx.ts
import React from 'react'
import { compileMDX } from 'next-mdx-remote/rsc'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
// Tùy chọn rehype-pretty-code (tô màu bằng Shiki)
const prettyCodeOptions = {
theme: 'github-dark', // Theme tối cho code block
keepBackground: true, // Giữ màu nền của theme
defaultLang: 'plaintext', // Ngôn ngữ mặc định khi không chỉ định
}
export async function compileMDXContent(source: string) {
const { content } = await compileMDX({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypePrettyCode, prettyCodeOptions],
],
},
},
})
return content
}Pipeline Xử Lý Plugin
Khi compileMDX() chạy, nội dung MDX sẽ đi qua pipeline sau:
| Plugin | Giai đoạn | Làm gì |
|---|---|---|
remark-gfm | Remark (AST Markdown) | Bật GFM: bảng, task list, gạch ngang, autolink |
rehype-slug | Rehype (AST HTML) | Thêm id="my-heading" vào mỗi <h2>, <h3>,… cho anchor link |
rehype-pretty-code | Rehype (AST HTML) | Biến code block thành HTML tô màu bởi Shiki với màu theo theme |
Tại Sao next-mdx-remote/rsc?
Import /rsc rất quan trọng — nó có nghĩa MDX được compile trên server dưới dạng React Server Component. Lợi ích:
- Zero JavaScript phía client cho phần nội dung — nhẹ hẳn
- Tải trang nhanh hơn — MDX đã là HTML sẵn khi đến trình duyệt
- Truy cập file system trực tiếp — server component đọc file, query database thoải mái
Trang Liệt Kê Bài Viết
Tạo app/blog/page.tsx — trang liệt kê tất cả bài viết:
// app/blog/page.tsx
import Link from 'next/link'
import { getAllPosts } from '@/lib/posts'
export default function BlogPage() {
const posts = getAllPosts()
return (
<div className="max-w-3xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
{posts.length === 0 ? (
<p className="text-muted-foreground">Chưa có bài nào. Quay lại sau nhé!</p>
) : (
<div className="space-y-8">
{posts.map((post) => (
<article key={post.slug} className="group">
<Link href={`/blog/${post.slug}`}>
<div className="border rounded-lg p-6 transition-colors hover:bg-accent">
<time className="text-sm text-muted-foreground">
{post.date}
</time>
<h2 className="text-2xl font-semibold mt-1 mb-2 group-hover:text-primary">
{post.title}
</h2>
<p className="text-muted-foreground">{post.description}</p>
{post.tags.length > 0 && (
<div className="flex gap-2 mt-3">
{post.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 text-xs rounded-full bg-secondary text-secondary-foreground"
>
{tag}
</span>
))}
</div>
)}
</div>
</Link>
</article>
))}
</div>
)}
</div>
)
}Mở http://localhost:3000/blog — bài "Hello World" sẽ hiện ra trong danh sách.
Trang Chi Tiết Bài Viết
Tạo app/blog/[slug]/page.tsx — route động render từng bài:
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { getPostBySlug } from '@/lib/posts'
import { compileMDXContent } from '@/lib/mdx'
interface PageProps {
params: Promise<{ slug: string }>
}
// Bắt buộc render theo yêu cầu (không sinh tĩnh)
export const dynamic = 'force-dynamic'
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = getPostBySlug(slug)
if (!post) {
return { title: 'Không Tìm Thấy Bài Viết' }
}
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: 'article',
publishedTime: post.date,
},
}
}
export default async function PostPage({ params }: PageProps) {
const { slug } = await params
const post = getPostBySlug(slug)
if (!post) {
notFound()
}
// Compile MDX mỗi request (render theo yêu cầu)
const content = await compileMDXContent(post.content)
return (
<article className="max-w-3xl mx-auto px-4 py-12">
<header className="mb-8">
<Link
href="/blog"
className="inline-flex items-center text-muted-foreground hover:text-foreground mb-6"
>
← Quay lại blog
</Link>
<time className="block text-sm text-muted-foreground mb-2">
{post.date}
</time>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
{post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 text-sm rounded-full bg-secondary text-secondary-foreground"
>
{tag}
</span>
))}
</div>
)}
</header>
{/* Class prose cho typography đẹp */}
<div className="prose prose-slate dark:prose-invert max-w-none">
{content}
</div>
</article>
)
}Điểm cần chú ý
export const dynamic = 'force-dynamic'— bắt Next.js compile MDX mỗi request, không cache lúc buildparams: Promise<{ slug: string }>— Next.js 16 truyền params dạng Promise (khác với 14/15)compileMDXContent(post.content)— biến chuỗi MDX thô thành React element- Class
prose— Tailwind Typography tự style toàn bộ HTML cho đẹp, không cần viết CSS tay
Plugin Tailwind Typography
Class prose đến từ plugin @tailwindcss/typography. Cài vào:
npm install @tailwindcss/typographyRồi đăng ký vào app/globals.css:
@import "tailwindcss";
@plugin "@tailwindcss/typography";Lưu ý Tailwind v4: Tailwind v4 load plugin bằng
@plugin— không cầntailwind.config.ts. Package@tailwindcss/typographytự đăng ký classprosekhi được load.
Vậy là toàn bộ nội dung MDX render ra sẽ có typography đẹp sẵn — heading, paragraph, code block, danh sách, blockquote, bảng đều được style hết.
Dark Mode cho Prose
Class dark:prose-invert đảo màu typography khi ở dark mode. Thêm cạnh prose:
<div className="prose prose-slate dark:prose-invert max-w-none">
{content}
</div>Hiểu Luồng Dữ Liệu
Cùng trace xem chuyện gì xảy ra khi ai đó truy cập /blog/hello-world:
Chi tiết từng bước:
- Match route — Next.js map
/blog/hello-worldvàoapp/blog/[slug]/page.tsxvớislug = "hello-world" - Đọc file —
getPostBySlug('hello-world')mởcontent/posts/hello-world.mdxlên - Parse frontmatter —
gray-mattertách{ title, description, date, tags }ra, trả về phần nội dung MDX thô - Compile MDX —
compileMDXContent()chạy pipeline remark/rehype, cho ra React element - Render — React element render trên server (RSC), sinh ra HTML
- Response — HTML hoàn chỉnh bay về trình duyệt — zero JavaScript phía client
Tô Màu Syntax với Shiki
rehype-pretty-code dùng Shiki bên dưới — cùng engine tô màu của VS Code luôn. Nghĩa là:
- Tô màu chính xác — dùng grammar TextMate thật, không phải regex đoán mò
- Hỗ trợ theme — xài theme VS Code nào cũng được (
github-dark,one-dark-pro,dracula,…) - Nhiều ngôn ngữ — 200+ ngôn ngữ hỗ trợ sẵn, không cần cài thêm
Code Block Được Tô Màu Như Thế Nào
Khi bạn viết fenced code block trong MDX:
```typescript
const x: number = 42
```Pipeline sẽ xử lý thế này:
- Markdown parser thấy
```typescript→ tạo AST node code block vớilang: "typescript" - rehype-pretty-code chặn code block → gửi code đến Shiki
- Shiki tokenize
const x: number = 42bằng grammar TypeScript → áp màu themegithub-dark - Output — element
<pre><code>với inline color style cho mỗi token
Kết quả: keyword, string, number, type annotation — mỗi thứ một màu, y chang trong VS Code.
Tô Màu Theo Theme
Muốn code block tự đổi theo dark/light mode của trang? Cấu hình dual theme:
// lib/mdx.ts — tùy chọn: hỗ trợ hai theme
const prettyCodeOptions = {
theme: {
dark: 'github-dark',
light: 'github-light',
},
keepBackground: true,
defaultLang: 'plaintext',
}Cấu hình này tạo code block với CSS variable phản hồi theo class dark:. Ở đây mình giữ github-dark cho đơn giản — hầu hết blog dev đều dùng code block nền tối bất kể theme trang là gì.
Component MDX Tùy Chỉnh
Đây là "siêu năng lực" của MDX — nhúng React component thẳng vào nội dung. Chỉ cần truyền map components cho compileMDX():
// lib/mdx.ts — với component tùy chỉnh (dùng React.createElement, không có JSX)
import React from 'react'
import { compileMDX } from 'next-mdx-remote/rsc'
// Định nghĩa Callout component ở components/Callout.tsx (JSX nằm đó).
// Ở đây trong lib/mdx.ts, mình đăng ký nó bằng React.createElement — không cần JSX.
export async function compileMDXContent(source: string) {
const { content } = await compileMDX({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypePrettyCode, prettyCodeOptions],
],
},
},
components: {
// React.createElement giữ file không có JSX (.ts extension hợp lệ)
Callout: (props: any) => React.createElement(Callout, props),
},
})
return content
}Giờ trong file MDX bất kỳ, dùng luôn:
<Callout type="warning">
Đảm bảo cài đầy đủ dependency trước khi tiếp tục!
</Callout>Muốn thêm bao nhiêu component tùy chỉnh cũng được — tab, accordion, gallery ảnh, code sandbox nhúng, tuỳ bạn sáng tạo.
GitHub Flavored Markdown Thực Tế
Plugin remark-gfm mở khoá thêm nhiều tính năng Markdown. Xem qua từng cái:
Bảng
| Tính năng | Hỗ trợ |
|-----------|--------|
| Bảng | ✅ |
| Task list | ✅ |
| Gạch ngang | ✅ |Task List
- [x] Cài dependency
- [x] Tạo thư mục nội dung
- [ ] Viết bài đầu tiên
- [ ] Deploy lên productionGạch Ngang
~~Đoạn văn này bị gạch ngang~~Autolink
Truy cập https://nextjs.org để xem tài liệu.Tất cả đều render ngon lành vì remark-gfm biến chúng thành HTML chuẩn trước khi rehype xử lý tiếp.
Heading Anchor Link
Plugin rehype-slug tự động thêm id vào mỗi heading:
<!-- Input: ## My Heading -->
<!-- Output: <h2 id="my-heading">My Heading</h2> -->Nhờ vậy bạn có thể:
- Link thẳng vào section — chia sẻ
https://yourblog.com/blog/my-post#my-heading - Xây mục lục (ToC) — trích xuất heading ID để tạo table of contents
- Click heading lấy permalink — tiện cho người đọc muốn bookmark section cụ thể
Lỗi Thường Gặp và Cách Khắc Phục
1. "Module not found: gray-matter"
Chắc chắn đã cài đủ dependency chưa:
npm install next-mdx-remote gray-matter remark-gfm rehype-pretty-code rehype-slug shiki2. Code block không được tô màu
Kiểm tra code block đã có language tag chưa:
```typescript ← Phải chỉ định ngôn ngữ
const x = 42
```Thiếu language tag thì rehype-pretty-code sẽ fallback về defaultLang (mặc định là plaintext).
3. Frontmatter hiện ra dạng text thô
Nếu thấy --- và YAML hiện ra trong trang, kiểm tra:
parseFrontmatter: trueđã được đặt trong tùy chọncompileMDX()- Bạn đang truyền
post.content(không có frontmatter), không phải chuỗi file thô
gray-matter đã lo tách frontmatter rồi — đừng parse hai lần.
4. Style "prose" không có tác dụng
Cài plugin typography và import vào CSS:
npm install @tailwindcss/typography/* app/globals.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";5. Lỗi hydration mismatch
Nếu thấy lỗi "Text content does not match", kiểm tra:
- MDX không chứa code browser-only (kiểu
window,document) - Component dùng trong MDX phải là Server Component, hoặc wrap đúng với
'use client' - Đã đặt
export const dynamic = 'force-dynamic'trên trang chưa
Tham Chiếu File Đầy Đủ
Tổng hợp tất cả file tạo mới hoặc sửa trong phase này:
| File | Công dụng |
|---|---|
content/posts/hello-world.mdx | Bài test đầu tiên với frontmatter |
lib/posts.ts | getAllPosts() và getPostBySlug() — đọc MDX từ đĩa |
lib/mdx.ts | compileMDXContent() — MDX compiler + registry custom component (không JSX, dùng createElement) |
app/blog/page.tsx | Trang danh sách blog — hiện tất cả bài |
app/blog/[slug]/page.tsx | Trang chi tiết bài — render MDX theo yêu cầu |
app/globals.css | Thêm plugin @tailwindcss/typography |
Tóm Tắt
Xong phase này, bạn đã:
✅ Hiểu MDX là gì và tại sao nó hoàn hảo cho blog lập trình
✅ Chọn render theo yêu cầu thay vì sinh tĩnh để cập nhật nội dung tức thì
✅ Cài next-mdx-remote, gray-matter, remark-gfm, rehype-pretty-code, và rehype-slug
✅ Tạo thư mục content/posts/ và viết bài MDX đầu tiên
✅ Định nghĩa TypeScript types cho PostMeta và Post
✅ Xây getAllPosts() và getPostBySlug() để đọc bài từ đĩa
✅ Cấu hình MDX compiler với plugin remark/rehype và tô màu syntax Shiki
✅ Xây trang danh sách blog (/blog) và trang chi tiết bài (/blog/[slug])
✅ Thêm Tailwind Typography cho prose styling đẹp
Tiếp theo — Phase 3: PostgreSQL + Drizzle ORM Mình sẽ gắn database vào blog — view count, subscriber newsletter, comment — dùng Drizzle ORM kết nối PostgreSQL chạy trong Docker.
Mục Lục Series
| Bài | Tiêu đề | Trạng thái |
|---|---|---|
| BLOG-1 | Xây Dựng Blog Cá Nhân — Lộ Trình | ✅ Hoàn thành |
| BLOG-2 | Phase 1: Khởi Tạo Project — Next.js 16 + ShadCN/UI | ✅ Hoàn thành |
| BLOG-3 | Phase 2: Render MDX Theo Yêu Cầu | ✅ Bạn đang ở đây |
| BLOG-4 | Phase 3: PostgreSQL + Drizzle ORM | ✅ Hoàn thành |
| BLOG-5 | Phase 4: Tags, Search & Pagination | ✅ Hoàn thành |
| BLOG-6 | Phase 5: Docker Compose | ✅ Hoàn thành |
| BLOG-7 | Phase 6: Deploy lên Ubuntu VPS trên Hostinger | ✅ Hoàn thành |
| BLOG-8 | Phase 7: Thiết Lập Domain trên Hostinger | ✅ Hoàn thành |
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.