Lê Duy Khương (Daniel)

Chuỗi: docs-as-code

Năng suất & công cụ dev

Viết tài liệu bằng Markdown — từ file đẹp mắt đến tài liệu máy đọc được

Markdown giúp tài liệu trở thành text thuần: dễ đọc, dễ diff, dễ review, và đủ cấu trúc để máy kiểm tra. Bài học này đi từ heading, list, code block tới frontmatter và checklist viết tài liệu bền.

2026-06-2910 phút đọcVI

Bài này biến docs-as-code từ ý tưởng thành thao tác: Markdown, frontmatter, heading, checklist và cách viết để Git/CI đọc được.

Tóm tắt Lesson 2: viết tài liệu bằng Markdown, frontmatter và cấu trúc máy đọc được


Mục tiêu học tập

Sau bài học này, bạn sẽ:

  • Giải thích được vì sao plain text thắng các định dạng nhị phân (binary, ví dụ Word .docx) trong môi trường docs-as-code.
  • Viết thành thạo các thành phần Markdown nền tảng: heading (tiêu đề), list (danh sách), table (bảng), code block (khối mã), link (liên kết), emphasis (nhấn mạnh).
  • Thêm frontmatter (siêu dữ liệu YAML ở đầu file) đúng chuẩn để tài liệu sẵn sàng cho tự động hoá.
  • Viết câu rõ ràng, ngắn (≤ 35 từ), và điều chỉnh ngôn ngữ theo audience (đối tượng đọc) — dev hay business.
  • Vẽ diagram-as-code (sơ đồ bằng mã) với Mermaid, ví dụ luồng shell (vỏ ứng dụng chứa các mini-app)mini-app (ứng dụng con chạy bên trong shell) trong NovaApp.
  • Chọn đúng loại tài liệu (README, runbook, ADR, API doc, how-to) cho từng nhu cầu.
  • Tổ chức tài liệu song ngữ tiếng Việt chính + tiếng Anh phụ.

1. Vì sao plain text thắng

Tài liệu của bạn có thể viết bằng Word, Google Docs, Confluence, hoặc plain text. Trong docs-as-code, chúng ta chọn plain text — cụ thể là Markdown. Đây không phải sở thích cá nhân, mà là một quyết định kỹ thuật có bốn lý do rõ ràng.

1. So sánh được (Diffable). Khi bạn sửa một file Markdown, công cụ kiểm soát phiên bản như Git cho bạn thấy chính xác dòng nào thêm, dòng nào xoá. So sánh hai phiên bản tài liệu NovaApp Payments trở nên trực quan: bạn thấy ngay "đã đổi giới hạn giao dịch từ 5 triệu lên 10 triệu". Với file Word nhị phân, Git chỉ báo "file đã thay đổi" — bạn không biết đổi gì.

2. Tìm được bằng grep (Greppable). Plain text cho phép tìm kiếm toàn văn bằng các công cụ dòng lệnh đơn giản. Muốn biết "mini-app nào nhắc tới Wallet"? Một lệnh tìm kiếm quét toàn bộ repository (kho mã) trong tích tắc. Định dạng nhị phân giấu nội dung trong cấu trúc riêng, công cụ phổ thông không đọc được.

3. Bền (durable). File Markdown chỉ là các ký tự. Mở được bằng bất kỳ trình soạn thảo nào, trên bất kỳ hệ điều hành nào, sau 20 năm vẫn đọc được. Định dạng nhị phân phụ thuộc phần mềm cụ thể — phần mềm ngừng phát triển thì tài liệu có nguy cơ "chết theo".

4. Độc lập công cụ (tool-agnostic). Bạn không bị khoá vào một nhà cung cấp. Cùng một file Markdown render được bằng nhiều công cụ: trang web tĩnh, trình xem trên GitHub, hay terminal. Đội ngũ NovaApp có thể đổi công cụ xuất bản mà không phải viết lại một dòng tài liệu nào.

Bảng so sánh nhanh:

Tiêu chíPlain text / MarkdownWord / định dạng nhị phân
So sánh khác biệt (diff)Theo từng dòng, rõ ràng"File đã thay đổi", mờ mịt
Tìm kiếm (grep)Toàn văn, tức thìPhụ thuộc phần mềm riêng
Kiểm soát phiên bảnTự nhiên với GitCồng kềnh, dễ xung đột
Tuổi thọHàng chục nămGắn với vòng đời phần mềm
Khoá nhà cung cấpKhông

Tóm lại: plain text biến tài liệu thành một tài sản kỹ thuật được quản lý giống như mã nguồn — chứ không phải một tệp đính kèm trôi nổi trong email.


2. Markdown nền tảng

Markdown là một cú pháp gọn để viết văn bản có cấu trúc bằng plain text. Bạn gõ vài ký tự đặc biệt, công cụ render (kết xuất) sẽ biến chúng thành tiêu đề, danh sách, bảng... Dưới đây là bộ thành phần cốt lõi, kèm ví dụ thật.

Heading (tiêu đề)

Dùng dấu #. Một dấu là cấp 1, hai dấu là cấp 2, và cứ thế tới cấp 6. Heading tạo nên dàn ý của tài liệu.

# NovaApp — Tài liệu mini-app Payments
## Tổng quan
### Luồng thanh toán

Nguyên tắc: mỗi tài liệu chỉ nên có MỘT heading cấp 1 (#) làm tiêu đề chính. Các phần lớn dùng ##, phần con dùng ###. Đừng nhảy cóc từ # thẳng xuống ####.

List (danh sách)

Danh sách không thứ tự dùng -. Danh sách có thứ tự dùng 1., 2....

Các mini-app trong NovaApp:
 
- Payments — thanh toán và chuyển tiền
- Booking — đặt chỗ dịch vụ
- Wallet — ví điện tử
- Rewards — tích điểm thưởng
 
Quy trình mở mini-app:
 
1. Người dùng mở shell NovaApp
2. Shell tải danh sách mini-app
3. Người dùng chọn Payments
4. Shell khởi chạy mini-app Payments

Table (bảng)

Bảng dùng dấu | để ngăn cột và một hàng --- để ngăn header với dữ liệu.

| Mini-app | Chủ sở hữu | Trạng thái |
|----------|------------|------------|
| Payments | Team A     | Production |
| Booking  | Team B     | Beta       |
| Wallet   | Team A     | Production |

Code block (khối mã)

Code block dùng ba dấu backtick. Thêm tên ngôn ngữ ngay sau backtick mở để được tô màu cú pháp (syntax highlighting).

```json
{
  "miniApp": "Payments",
  "version": "1.4.0",
  "maxTransaction": 10000000
}
```

Với mã ngắn nằm trong câu, dùng một dấu backtick: gọi hàm launchMiniApp("Payments") để khởi chạy.

Cú pháp [chữ hiển thị](đường-dẫn). Liên kết được tới file khác trong repo (đường dẫn tương đối) hoặc tới URL bên ngoài.

Xem thêm [kiến trúc tài liệu](../architecture/lesson-3.md) và
[trang chủ Mermaid](https://mermaid.js.org).

Mẹo: ưu tiên đường dẫn tương đối khi liên kết nội bộ. Khi đổi công cụ xuất bản, liên kết tương đối vẫn hoạt động; URL tuyệt đối thì dễ gãy.

Emphasis (nhấn mạnh)

*nghiêng* hoặc _nghiêng_
**đậm** hoặc __đậm__
`mã inline`
> trích dẫn / callout (ghi chú nổi bật)

Dùng đậm cho thuật ngữ quan trọng lần đầu xuất hiện, nghiêng cho nhấn nhẹ, callout (>) cho cảnh báo hay lưu ý mà người đọc không được bỏ qua.


3. Cấu trúc & frontmatter

Một file Markdown tốt không chỉ có nội dung. Nó còn mang theo metadata (siêu dữ liệu) — thông tin mô tả về chính tài liệu đó. Phần metadata này nằm ở đầu file, bao trong hai dòng ---, viết bằng YAML (một định dạng cặp khoá-giá trị dễ đọc). Khối này gọi là frontmatter.

Hãy nhìn lại đầu file bài học này:

---
title: "Lesson 2 — Viết tài liệu bằng Markdown"
track: fundamentals
lesson: 2
status: draft
date: "2026-06-02"
language: vi
---

Với một tài liệu mini-app NovaApp, frontmatter điển hình sẽ là:

---
title: "Runbook — Khởi động lại dịch vụ Payments"
type: runbook
status: published
date: "2026-06-02"
owner: "Team A"
mini_app: "Payments"
---

Vì sao frontmatter quan trọng cho tự động hoá

Frontmatter là cầu nối giữa "tài liệu cho người đọc" và "tài liệu cho máy xử lý". Các trường metadata mở ra nhiều khả năng tự động:

  • status (draft / published / deprecated) — công cụ có thể tự ẩn các bản nháp, gắn nhãn cảnh báo lên tài liệu đã lỗi thời, hay chặn xuất bản tài liệu chưa duyệt.
  • owner — biết ai chịu trách nhiệm để định tuyến câu hỏi hay yêu cầu duyệt đúng người.
  • date — phát hiện tài liệu quá cũ, nhắc rà soát định kỳ.
  • title / mini_app — sinh tự động mục lục, trang chỉ mục, hay bộ lọc theo mini-app.

Không có frontmatter, máy chỉ thấy một khối văn bản. Có frontmatter, mỗi tài liệu trở thành một bản ghi có cấu trúc mà các script và pipeline (chuỗi xử lý tự động) khai thác được. Đây chính là điều phân biệt một thư mục chứa file .md rời rạc với một hệ tài liệu được quản lý.

Lưu ý: giữ tên trường nhất quán toàn dự án. Nếu file này dùng owner mà file kia dùng author, tự động hoá sẽ rối. Hãy chốt một bộ trường chuẩn và tuân thủ.


4. Viết cho người đọc

Markdown lo phần hình thức; bạn vẫn phải lo phần nội dung. Tài liệu kỹ thuật thất bại không phải vì sai cú pháp, mà vì người đọc không hiểu. Dưới đây là các nguyên tắc humanization (nhân bản hoá — viết cho người, không phải cho máy).

Câu rõ ràng, ngắn (≤ 35 từ)

Câu dài chôn vùi ý chính. Hãy cắt câu dài thành nhiều câu ngắn. So sánh:

  • Khó đọc: "Khi người dùng mở mini-app Payments trong NovaApp thì shell sẽ kiểm tra phiên đăng nhập, sau đó tải cấu hình, rồi mới khởi chạy giao diện và nếu phiên hết hạn thì shell sẽ chuyển hướng người dùng về màn hình đăng nhập trước khi cho phép tiếp tục."
  • Dễ đọc: "Khi người dùng mở Payments, shell kiểm tra phiên đăng nhập. Nếu phiên còn hiệu lực, shell tải cấu hình rồi khởi chạy giao diện. Nếu phiên hết hạn, shell chuyển hướng về màn hình đăng nhập."

Câu thứ hai chia một chuỗi rối thành các bước có thể theo dõi.

Calibrate theo audience (điều chỉnh theo đối tượng đọc)

Cùng một thông tin, hai đối tượng cần hai cách trình bày khác nhau.

Yếu tốViết cho devViết cho business
Trọng tâmCách hoạt động, tham số, mãGiá trị, kết quả, rủi ro
Thuật ngữDùng tự nhiên (API, SDK, payload)Giải thích hoặc tránh
Ví dụĐoạn mã, lệnh gọiTình huống nghiệp vụ
Độ chi tiếtCao, đầy đủ edge caseVừa đủ để ra quyết định

Ví dụ, mô tả tính năng nâng hạn mức giao dịch Payments:

  • Cho dev: "Tham số maxTransaction trong cấu hình Payments tăng từ 5000000 lên 10000000. Cập nhật schema validation tương ứng."
  • Cho business: "Hạn mức giao dịch của Payments tăng từ 5 triệu lên 10 triệu mỗi lần, giúp khách hàng lớn giao dịch thuận tiện hơn."

Nguyên tắc humanization cốt lõi

  1. Đồng cảm người đọc — hình dung người đọc đang cố làm gì, viết để giúp họ làm xong việc đó.
  2. Ví dụ và phép loại suy cho khái niệm khó — một ví dụ NovaApp cụ thể hơn nhiều một định nghĩa trừu tượng.
  3. Visual aids (hỗ trợ trực quan) — bảng, danh sách, sơ đồ chia nhỏ thông tin dày đặc.
  4. Đánh dấu rõ điểm quyết định — nếu người đọc phải chọn, hãy nói rõ "chọn A nếu..., chọn B nếu...".
  5. Tone (giọng văn) phù hợp ngữ cảnh — runbook khi sự cố thì ngắn gọn, dứt khoát; tài liệu giới thiệu thì thân thiện, dẫn dắt.

5. Diagram-as-code (Mermaid)

Sơ đồ giúp người đọc nắm luồng phức tạp nhanh hơn nhiều so với đoạn văn. Nhưng sơ đồ vẽ bằng công cụ kéo-thả lại là file nhị phân — mất hết ưu thế plain text ở mục 1: không diff được, không grep được, dễ lỗi thời.

Diagram-as-code giải quyết vấn đề: bạn mô tả sơ đồ bằng text, công cụ render thành hình. Mermaid là cú pháp phổ biến nhất, render được ngay trên nhiều nền tảng. Sơ đồ trở thành plain text — version-control được, sửa bằng trình soạn thảo, so sánh khác biệt theo dòng.

Dưới đây là một sequence diagram (sơ đồ tuần tự) mô tả luồng shell NovaApp khởi chạy mini-app Payments:

Loading diagram...

Đọc đoạn mã trên, bạn thấy ngay từng bước trao đổi giữa các thành phần — kể cả khi chưa render thành hình. Đó là sức mạnh của diagram-as-code: bản thân mã đã tự diễn giải.

Mermaid còn hỗ trợ nhiều loại sơ đồ khác. Một flowchart (lưu đồ) đơn giản cho quy trình duyệt nâng hạn mức:

Loading diagram...

Mẹo: giữ sơ đồ vừa phải. Một sơ đồ 30 node rối hơn ba sơ đồ nhỏ rõ ràng. Mỗi sơ đồ nên trả lời đúng một câu hỏi.


6. Các loại doc + template

Không phải tài liệu nào cũng giống nhau. Mỗi loại phục vụ một mục đích, và chọn đúng loại giúp người đọc tìm đúng thứ họ cần. Dưới đây là năm loại phổ biến.

Loại docTrả lời câu hỏiKhi nào dùng
READMEĐây là gì? Bắt đầu thế nào?Cổng vào của mỗi thành phần/repo
RunbookKhi sự cố/tác vụ vận hành, làm gì?Quy trình vận hành lặp lại, xử lý sự cố
ADR (Architecture Decision Record)Vì sao chọn cách này?Ghi lại quyết định kiến trúc quan trọng
API docGọi dịch vụ này thế nào?Mô tả endpoint, tham số, phản hồi
How-to (hướng dẫn)Làm việc X cụ thể thế nào?Hướng dẫn từng bước cho một tác vụ

Phân biệt nhanh để chọn đúng:

  • README là tổng quan và điểm khởi đầu — ví dụ README của mini-app Payments giải thích nó làm gì và cách chạy local.
  • Runbook là kịch bản hành động khi vận hành — ví dụ "Khởi động lại dịch vụ Payments khi treo". Viết ngắn, đánh số bước, dứt khoát.
  • ADR ghi lại MỘT quyết định cùng bối cảnh và hệ quả — ví dụ "Vì sao Payments và Wallet dùng chung Auth Service". Đọc Lesson 3 và templates/product-docs/03-design/adr/ để hiểu sâu hơn.
  • API doc mô tả hợp đồng giao tiếp — ví dụ endpoint POST /payments/transfer với tham số và mã lỗi.
  • How-to dẫn người đọc qua một tác vụ cụ thể từ đầu đến cuối.

Dùng template có sẵn

Kit này đã có sẵn bộ template tài liệu sản phẩm ở templates/product-docs/. Cấu trúc đánh số 00-discovery → 04-delivery theo đúng vòng đời phát triển, kèm một thư mục traceability/ nối mọi tầng với nhau:

templates/product-docs/
├── 00-discovery/      # Vì sao xây? (BRD + research)
├── 01-product/        # Sản phẩm gì? (brief, PRD, roadmap)
├── 02-requirements/   # Cần làm chính xác gì? (functional, non-functional)
├── 03-design/         # Xây thế nào? (architecture, ADR, UX)
├── 04-delivery/       # Chia việc & kiểm thử thế nào? (epics, stories, test-plans)
└── traceability/      # Bản đồ nối mọi tầng đầu-cuối

Mỗi thư mục có README riêng mô tả vai trò, quy ước đặt tên, và một file _TEMPLATE-*.md để bạn sao chép rồi điền vào. Đừng viết tài liệu từ con số không — bắt đầu từ template, bạn vừa nhanh hơn vừa nhất quán với cả đội.


7. Khi nào cần song ngữ (tuỳ chọn)

💡 Đọc nhanh — chỉ cần khi dự án của bạn ra cộng đồng quốc tế. Nếu đội bạn chỉ làm tài liệu tiếng Việt, bỏ qua mục này cũng được.

Kit này tiếng Việt là chính, nhưng có thêm bản tiếng Anh để tiếp cận cộng đồng open-source. Hai cách tổ chức thường gặp:

  • File song song: mỗi tài liệu có hai bản, phân biệt bằng đuôi (README.md tiếng Anh + README.vi.md tiếng Việt). Hợp khi cả hai bản đều cần đầy đủ.
  • Khối phụ trong cùng file: giữ một file tiếng Việt, chèn dòng > EN: ... tóm tắt dưới phần quan trọng. Hợp cho tài liệu nội bộ chỉ cần "chạm" tiếng Anh.

Rủi ro lớn nhất là drift (lệch nội dung) — bản này cập nhật, bản kia quên. Cách giảm: khai báo language: trong frontmatter, và khi sửa một bản thì mở luôn bản kia trong cùng commit.


🛠 Bài tập thực hành

Mục tiêu: chuyển một runbook (hoặc một tài liệu Word có sẵn) sang Markdown chuẩn docs-as-code, cho một mini-app NovaApp giả định.

Bối cảnh: Bạn được giao một runbook viết kiểu văn xuôi: "Khi dịch vụ Booking của NovaApp bị treo, kỹ thuật viên cần kiểm tra log, khởi động lại tiến trình, xác nhận dịch vụ hồi phục, rồi báo cho đội trực."

Yêu cầu:

  1. Tạo file booking-restart-runbook.md.
  2. Thêm frontmatter đầy đủ với tối thiểu các trường: title, type: runbook, status, date, owner, mini_app.
  3. Viết nội dung runbook với:
    • Một heading cấp 1 làm tiêu đề.
    • Một danh sách có thứ tự cho các bước xử lý.
    • Một bảng liệt kê các lệnh/kiểm tra và kết quả mong đợi.
    • Ít nhất một callout (>) cảnh báo điểm dễ sai.
  4. Thêm một sơ đồ Mermaid (flowchart hoặc sequence) mô tả luồng xử lý sự cố Booking.
  5. Áp dụng nguyên tắc câu ≤ 35 từ và viết tone dứt khoát (đây là runbook xử lý sự cố).
  6. Tự kiểm: mỗi câu có dưới 35 từ không? Frontmatter có đủ trường không? Sơ đồ Mermaid có render được không (cú pháp đúng)?

Gợi ý: mở templates/product-docs/04-delivery/ để tham khảo cách trình bày một tài liệu giao việc, và xem lại mục 5 cho cú pháp Mermaid.

Tiêu chí hoàn thành: file của bạn diffable (mỗi dòng một ý), greppable (tìm "Booking" ra ngay), và một người chưa biết gì về sự cố này vẫn theo được các bước.


Tóm tắt

  • Plain text thắng vì diffable, greppable, bền, và độc lập công cụ — biến tài liệu thành tài sản kỹ thuật được quản lý như mã nguồn.
  • Markdown nền tảng gồm heading, list, table, code block, link, emphasis — đủ để viết hầu hết tài liệu kỹ thuật.
  • Frontmatter (metadata YAML đầu file) là cầu nối tới tự động hoá: status, owner, date mở ra lọc, cảnh báo, định tuyến tự động.
  • Viết cho người đọc: câu ngắn ≤ 35 từ, calibrate theo audience (dev vs business), áp dụng nguyên tắc humanization.
  • Diagram-as-code (Mermaid) giữ sơ đồ ở dạng text — version-control được, ví dụ luồng shell → mini-app Payments.
  • Chọn đúng loại doc: README, runbook, ADR, API doc, how-to — mỗi loại trả lời một câu hỏi riêng. Dùng templates/product-docs/.
  • Docs song ngữ: tiếng Việt chính + tiếng Anh phụ, hai cách tổ chức (file song song hoặc khối phụ trong file), cảnh giác drift.

Đọc tiếp

  • Lesson 3 — Kiến trúc tài liệu: cách tổ chức nhiều tài liệu thành một hệ thống mạch lạc, có traceability (truy vết) từ BRD tới test.
  • templates/product-docs/: bộ template tài liệu sản phẩm đánh số 00-discovery → 04-delivery + traceability/ — sao chép, xoá phần không cần, điền vào.
  • Skill docs-as-code-intro: điểm khởi đầu tương tác để dựng nhanh một bộ tài liệu docs-as-code cho dự án của bạn.
LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.