Bạn thấy thế nào?
Đăng nhập để phản hồi.
Bạn thấy thế nào?
Đăng nhập để phản hồi.
Trong vài năm trở lại đây, Markdown và MDX đã trở thành lựa chọn phổ biến cho việc xây dựng hệ thống blog, documentation và knowledge base. Không chỉ vì sự đơn giản, mà còn vì khả năng mở rộng linh hoạt, thân thiện với developer và phù hợp với tư duy sản phẩm hiện đại.
Bài viết này nhằm mục tiêu:
Markdown là một ngôn ngữ đánh dấu nhẹ (lightweight markup language), cho phép bạn viết nội dung với cú pháp đơn giản nhưng có thể render thành HTML đầy đủ.
Ví dụ Markdown cơ bản:
# Tiêu đề
**In đậm** *In nghiêng*
- Item 1
- Item 2Markdown phù hợp cho:
MDX = Markdown + JSX
Điểm khác biệt cốt lõi:
Ví dụ:
<MyCustomComponent filePath="Hello MDX" />Điều này mở ra khả năng:
MDX không chỉ cho phép nhúng React components, mà còn có thể nhúng các HTML demo tương tác. Hệ thống blog này hỗ trợ 3 chế độ hiển thị HTML demo:
Chế độ đơn giản nhất: một nút để mở HTML trong tab mới. Zero bundle overhead, phù hợp cho demo đơn giản.
Click to open in new tab
Ưu điểm:
Sử dụng khi: Demo đơn giản, không cần preview trực tiếp.
Chế độ mạnh mẽ với preview trực tiếp ngay trong bài viết. Sử dụng @codesandbox/sandpack-react (~150KB).
Ưu điểm:
Sử dụng khi: Muốn showcase demo ngay trong bài viết, tăng engagement.
Nhúng iframe từ CodeSandbox với đầy đủ tính năng: share, fork, edit online.
Lưu ý: Chế độ này cần sandbox ID từ CodeSandbox. Demo bên dưới sử dụng local HTML.
Ưu điểm:
Sử dụng khi: Demo phức tạp, nhiều files, cần edit online.
| Tiêu chí | Markdown | MDX |
|---|---|---|
| Dễ học | ✅ Rất dễ | ⚠️ Cần biết React |
| Tính động | ❌ Không | ✅ Có |
| Khả năng mở rộng | Thấp | Rất cao |
| Phù hợp blog cá nhân | ✅ | ✅ |
| Phù hợp product docs | ⚠️ | ✅ |
Kết luận ngắn: Nếu blog của bạn chỉ để viết → Markdown đủ dùng. Nếu blog là một phần của sản phẩm → MDX là lựa chọn chiến lược.
Một sai lầm phổ biến là xem blog chỉ như:
“Một chỗ để đăng bài viết.”
Trong thực tế, blog là:
Mỗi H2 nên:
Ví dụ H6 thường dùng cho:
Blog tốt không phải là blog viết hay, mà là blog giải quyết được vấn đề thật.
Khi làm việc với MDX, bạn thường sẽ gặp:
remarkrehypecontentlayernext-mdx-remoteTwoslash là công cụ mạnh mẽ giúp hiển thị thông tin kiểu TypeScript trực tiếp trong code blocks. Để sử dụng Twoslash, thêm twoslash vào meta string của code fence.
Sử dụng ^? để hiển thị type của một biến hoặc expression:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
Ngoài Twoslash, chúng ta có thể sử dụng các Shiki transformers để highlight code theo nhiều cách khác nhau.
function processMarkdown(content: string) {
const lines = content.split('\n'
titledescriptionslugheading structure (H1-H6)Tags giúp:
Ví dụ tags:
Blog hiện đại không chỉ là nơi viết bài, mà là:
Markdown và MDX chỉ là công cụ. Giá trị thật sự nằm ở:
Tư duy sản phẩm + sự thấu hiểu người đọc
Phần này dùng chính các component đã đăng ký trong mdxComponents của blog để minh hoạ thay vì chỉ mô tả lý thuyết. Cách tiếp cận: show, don’t tell.
<FluidImage> phá vỡ giới hạn container để tạo khoảnh khắc visual lớn — phù hợp ảnh hero giữa bài, biểu đồ, ảnh chụp chi tiết. Dùng plain <img> (không phải next/image) vì kích thước intrinsic không biết trước.
Cú pháp:
// Component BlogLayout với TypeScript
export const BlogLayout = ({ children }: { children: React.ReactNode })
interface User {
id: number;
name: string;
email: string;
}
function getUserById(id:
async function fetchBlogPosts() {
try {
const response = await fetch('/api/posts');
const posts = await response.json
import { MDXRemote } from 'next-mdx-remote/rsc';
import { mdxComponents } from '@/shared/ui/mdx-components';
interface BlogPostProps {
content: string;
// Đây là một đoạn code rất dài để test tính năng word wrap toggle
// Bạn có thể click vào icon word wrap ở góc trên bên phải để bật/tắt word wrap
export function processMarkdownContent
Để highlight các dòng cụ thể, sử dụng meta string với syntax {số-dòng} hoặc {dòng-bắt-đầu-dòng-kết-thúc}:
// Dòng 2 và dòng 4-6 sẽ được highlight
function createMDXOptions() {
const options = {
remarkPlugins: [remarkGfm, remarkSmartypants],
rehypePlugins: [
Giải thích syntax:
{2} - Highlight dòng 2{2,4} - Highlight dòng 2 và 4{2-4} - Highlight dòng 2 đến 4{2,4-6} - Highlight dòng 2 và dòng 4-6// Generic function với type constraints
function processContent<T extends { id: string }>(
items: T[]
// Dòng 1 được highlight
import { createMDXOptions } from './mdx-config';
// Dòng 3-5 được highlight (function definition)
export function renderMDX(content: string) {
const
Word highlighting có thể được thực hiện bằng cách sử dụng Twoslash syntax // ^^^ hoặc highlight classes từ rehype-pretty-code. Hiện tại, tính năng này đang được phát triển và sẽ được cập nhật trong tương lai.
Ví dụ với Twoslash (khi enable):
function createMDXOptions() {
return { /* config */ };
}
// ^^^
// Highlight từ "createMDXOptions"Ví dụ với rehype-pretty-code (cần cấu hình thêm):
Word highlighting trong rehype-pretty-code thường được xử lý thông qua CSS classes hoặc custom transformers. Tính năng này sẽ được cải thiện trong các phiên bản tương lai.
// Generic function với type constraints
function wrapInArray<T>(value: T): T[] {
return [value];
}
const numberArray = wrapInArray(42);
// ^?
const stringArray = wrapInArray('hello');
// ^?
const objectArray = wrapInArray({ id: 1, name: 'Product' });
// ^?interface Post {
id: string;
title: string;
author: {
name: string;
email: string;
};
}
async function fetchPosts(): Promise<Post[]> {
const response = await fetch('/api/posts');
return response.json();
}
// TypeScript tự động infer return type
const posts = await fetchPosts();
// ^?
// Truy cập nested properties
const firstAuthor = posts[0]?.author;
// ^?type Success = { success: true; data: string };
type Error = { success: false; error: string };
type Result = Success | Error;
function processResult(result: Result) {
if (result.success) {
// TypeScript biết đây là Success type
const data = result.data;
// ^?
return data.toUpperCase();
} else {
// TypeScript biết đây là Error type
const error = result.error;
// ^?
console.error(error);
}
}interface BlogPost {
id: string;
title: string;
content: string;
author: string;
publishedAt: Date;
updatedAt: Date;
}
// Partial - tất cả properties trở thành optional
type PartialPost = Partial<BlogPost>;
// ^?
// Pick - chọn một số properties
type PostPreview = Pick<BlogPost, 'id' | 'title' | 'author'>;
// ^?
// Omit - loại bỏ một số properties
// Conditional type để extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'Alice' };
}
type UserType = ReturnType<typeof getUser>;
// ^?
// Awaited type để unwrap Promise
type AwaitedUser = Awaited<Promise<UserType>>;
// ^?interface User {
id: number;
name: string;
email: string;
}
// Tạo readonly version
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
const user: ReadonlyUser = {
// ^?
id: 1,
name: 'Alice',
email: 'alice@example.com'
};
// Không thể modify
// user.name = 'Bob'; // Error!function createUser(name: string, email: string) {
return {
id: Math.random(),
id
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
function createConfig(): Config
// Old implementation
function fetchData(url: string) {
return fetch(url).then
interface ApiResponse<T> {
data: T;
error?: string;
}
async function fetchUser(id: number): Promise<ApiResponse<User>> { // [!code highlight]
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// ^?
return data; // [!code highlight]
}
interface User {
id: number;
name: string;
}<FluidImage
src="./hero-image.png"
alt="Mô tả ảnh"
caption="Caption tuỳ chọn"
priority={false}
/>Khi nào dùng:
Khi nào KHÔNG dùng:
 thườngKhi muốn so sánh implementation giữa các ngôn ngữ/framework, <CodeGroup> cho phép tab switching mà không cần rời context.
type Post = {
slug: string;
title: string;
featured?: boolean;
};
const findHero
<Premium> ẩn nội dung khỏi free reader, hiển thị paywall CTA. Phù hợp gate kiến thức nâng cao mà không phải khoá toàn bộ post.
<Link> của blog tự động prepend locale, vì vậy không cần manual /vi/... prefix. Ví dụ: hoặc .
from typing import Optional, List
from dataclasses import dataclass
@dataclass
class Post:
slug: str
title: str
featured: bool = False
def find_hero(posts: List[Post]) -> Optional[Post]:
return next((p for p in posts if p.featured), posts[0] if posts else None)struct Post {
slug: String,
title: String,
featured: bool,
}
fn find_hero(posts: &[Post]) -> Option<&Post> {
posts.iter().find(|p| p.featured).or(posts.first())
}Một bài viết chuyên sâu về cách tiếp cận Markdown/MDX trong xây dựng blog, hướng đến developer và product thinker.