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, source URL…)

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ếu617 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ómSố bàiÝ nghĩa
MISSING109Thực sự thiếu — cần copy
ENCODING14Có ở cả hai nhưng slug encode khác nhau
SMALL_BODY23Source 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.