Hôm nay mình cần kiểm tra xem thư mục blog Hugo (/blog/content/posts) có đủ nội dung so với bộ dữ liệu gốc export từ WordPress (/Documents/claude/convertWP/output/posts) không. Bài viết này ghi lại toàn bộ quá trình — từ bước đi sai đến bước đúng — để làm tài liệu tham khảo.
Bối cảnh
Có hai thư mục cần so sánh:
- Source:
/Documents/claude/convertWP/output/posts— dữ liệu gốc export từ WordPress, ~1.753 bài, frontmatter đơn giản - Dest:
/blog/content/posts— blog Hugo đang chạy, ~1.610 bài, frontmatter đầy đủ hơn (cóslug,author,sourceURL…)
Câu hỏi: bên Dest đang thiếu những bài nào, và bài nào có folder nhưng thiếu nội dung?
Bước 1 (sai): So sánh tên folder
Phản xạ đầu tiên là dùng ls rồi comm:
ls -1 /source/posts/ | sort > /tmp/source.txt
ls -1 /dest/posts/ | sort > /tmp/dest.txt
comm -23 /tmp/source.txt /tmp/dest.txt # có ở source, thiếu ở dest
Kết quả báo 643 bài thiếu và 617 bài thừa. Con số nghe vô lý.
Vấn đề: macOS và Linux normalize Unicode khác nhau. Folder tên ai-da-cưới-nam-nay ở source bị lưu dưới dạng NFD, còn dest lưu NFC (hoặc ngược lại), dẫn đến cùng một bài nhưng comm không nhận ra là giống nhau.
Bước 2 (đúng): So sánh qua Slug
Slug là trường trong frontmatter của mỗi file index.md, luôn là ASCII, không bị ảnh hưởng bởi encoding tên folder.
# Extract slug + body_size từ mỗi file
find /source/posts -name "index.md" | while read f; do
slug=$(grep -m1 '^slug:' "$f" | sed "s/slug: *//;s/['\"]//g")
# Nếu không có trường slug thì dùng tên folder
[ -z "$slug" ] && slug=$(dirname "$f" | xargs basename)
body=$(awk '/^---$/{c++; if(c==2){f=1;next}} f{t+=length($0)+1} END{print t+0}' "$f")
echo "$slug|$body|$f"
done | sort -t'|' -k1,1 > /tmp/source_slugs.txt
# Làm tương tự với dest
find /dest/posts -name "index.md" | while read f; do
# ... (tương tự)
done | sort -t'|' -k1,1 > /tmp/dest_slugs.txt
Sau đó dùng comm trên cột slug (đã ASCII):
cut -d'|' -f1 /tmp/source_slugs.txt > /tmp/src_keys.txt
cut -d'|' -f1 /tmp/dest_slugs.txt > /tmp/dst_keys.txt
comm -23 /tmp/src_keys.txt /tmp/dst_keys.txt > /tmp/only_in_source.txt # thiếu ở dest
comm -13 /tmp/src_keys.txt /tmp/dst_keys.txt > /tmp/only_in_dest.txt # thừa ở dest
comm -12 /tmp/src_keys.txt /tmp/dst_keys.txt > /tmp/common.txt # có ở cả hai
Bước 3: Phân loại kết quả
Với 146 slug “thiếu ở dest” theo cách so sánh trước, tiếp tục phân loại:
while read slug; do
src=$(grep "^$slug|" /tmp/source_slugs.txt | head -1 | cut -d'|' -f3)
body=$(grep "^$slug|" /tmp/source_slugs.txt | head -1 | cut -d'|' -f2)
date=$(grep -m1 '^date:' "$src" | awk '{print $2}' | cut -c1-10)
# Kiểm tra xem dest có bài cùng date không
found=$(grep -rl "date.*'${date}" /dest/posts/ 2>/dev/null | wc -l | tr -d ' ')
if [ "$body" -lt 100 ]; then echo "SMALL_BODY | $slug"
elif [ "$found" -gt 0 ]; then echo "ENCODING | $slug"
else echo "MISSING | $slug"
fi
done < /tmp/only_in_source.txt
Kết quả phân loại:
| Nhóm | Số bài | Ý nghĩa |
|---|---|---|
| MISSING | 109 | Thực sự thiếu — cần copy |
| ENCODING | 14 | Có ở cả hai nhưng slug encode khác nhau |
| SMALL_BODY | 23 | Source gần trống (<100 bytes) — bỏ qua |
Bước 4: Tìm bài có folder nhưng thiếu nội dung
Một vấn đề khác: một số bài có folder ở dest nhưng file index.md chỉ có frontmatter, phần body trống hoàn toàn.
# Tìm file dest nhỏ bất thường (< 300 bytes)
find /dest/posts -maxdepth 2 -name "index.md" | while read dest_file; do
dest_size=$(stat -f%z "$dest_file")
folder=$(dirname "$dest_file" | xargs basename)
if [ "$dest_size" -lt 300 ]; then
source_file="/source/posts/$folder/index.md"
if [ -f "$source_file" ]; then
source_size=$(stat -f%z "$source_file")
if [ "$source_size" -gt 500 ]; then
echo "$folder|$dest_size|$source_size"
fi
fi
fi
done
Tìm ra 13 bài bị mất nội dung.
Bước 5: Copy nội dung sang — giữ đúng định dạng
Dest có frontmatter phong phú hơn (author, slug, source URL, categories tiếng Việt…), không thể copy cả file từ source. Chỉ cần lấy phần body từ source, ghép vào sau frontmatter của dest.
while IFS= read -r folder; do
dest_file="/dest/posts/$folder/index.md"
source_file="/source/posts/$folder/index.md"
# Frontmatter của dest (từ đầu đến --- thứ hai)
dest_front=$(awk '/^---$/{if(++c==2) exit} {print}' "$dest_file")
# Body của source (sau --- thứ hai)
source_body=$(awk '/^---$/{c++; if(c==2){f=1; next}} f{print}' "$source_file")
# Ghi file mới
printf '%s\n---\n%s\n' "$dest_front" "$source_body" > "$dest_file"
done << 'FOLDERS'
no-title-2
long-long-away
thuytie
...
FOLDERS
Kết quả cuối cùng
Source (convertWP/output/posts): 1,753 bài
Dest (blog/content/posts): 1,610 bài
─────────────────────────────────────────────
[✓] Khớp slug (đồng bộ tốt): 1,607 bài
[✓] Body trống đã fix: 13 bài
[!] MISSING thực sự: 109 bài ← draft chưa public, bỏ qua
[~] Encoding khác nhau: 14 bài
[~] Source gần trống: 23 bài
[+] Thừa ở Dest (pages): 3 bài ← about-me, resume… giữ nguyên
Bài học rút ra
1. Đừng so sánh tên folder chứa Unicode
Tên folder tiếng Việt có dấu bị lưu theo NFD hoặc NFC tùy hệ điều hành. comm, diff sẽ báo sai lệch dù nội dung giống hệt nhau.
2. Dùng trường dữ liệu chuẩn hóa làm key so sánh
slug trong frontmatter là ASCII, nhất quán, unique — đây là key lý tưởng để so sánh cross-directory.
3. Phân loại trước khi hành động Không phải mọi “bài thiếu” đều thực sự thiếu. Phân loại thành MISSING / ENCODING / SMALL_BODY trước, rồi mới quyết định xử lý từng nhóm.
4. Giữ frontmatter của dest, lấy body từ source Hai phía có schema frontmatter khác nhau. Dest đã được enrich (thêm author, source URL, categories đúng tiếng Việt). Chỉ nên lấy phần nội dung bài viết từ source, không copy nguyên file.
5. awk để parse frontmatter — không dùng head hay tail
# Lấy body (sau --- thứ hai)
awk '/^---$/{c++; if(c==2){f=1; next}} f{print}' file.md
# Lấy frontmatter (đến --- thứ hai)
awk '/^---$/{if(++c==2) exit} {print}' file.md
Đây là cách parse frontmatter YAML/TOML trong markdown an toàn hơn dùng sed hay head -n.