|

[WordPress → Astro] Phần 1 – Chuyển bài viết từ Database qua file Markdown

Bên cạnh blog Thuanbui này, mình còn có khá nhiều blog cá nhân khác được duy trì trong nhiều năm qua. Một số blog vẫn được cập nhật thường xuyên, nhưng cũng có vài blog gần như không còn viết thêm bài mới nữa.

Tuy nhiên, dù không cập nhật nội dung, mình vẫn phải duy trì hệ thống: cập nhật plugin, cập nhật core, quản lý server, database, backup… Những việc này thực ra không khó, nhưng lại tốn thời gian cho những website gần như không còn cập nhật nội dung.

Vì vậy mình đã bắt đầu nghĩ đến phương án mới: chuyển các blog ít cập nhật sang dạng trang tĩnh (static site) theo mô hình Jamstack. Với static site, mọi thứ đơn giản hơn rất nhiều: không cần database, không cần chạy PHP, không phải lo plugin bị lỗi hay vấn đề bảo mật. Chỉ cần build ra HTML và deploy lên CDN là xong.

Đây cũng là cơ hội để mình thử nghiệm và tìm hiểu thêm các công nghệ mới của web development. Ít nhiều chắc chắn sẽ giúp ích cho công việc Laravel Developer hiện tại.

Sau khi tìm hiểu một thời gian về các công cụ tạo static site, mình quyết định chọn Astro, vì các lý do:

Thật ra, mục tiêu cuối cùng của mình là chuyển 2 blog chính: Thuanbui.me và Yeuchaybo.com sang Astro. Tuy nhiên, trước khi làm vậy, mình muốn thử nghiệm trên một blog khác trước để rút kinh nghiệm. Blog mình chọn để làm chuột bạch đầu tiên là balodeplao.com – blog về du lịch, trải nghiệm của hai vợ chồng mình, đã rất lâu không có bài viết mới.

Trong bài viết này, mình sẽ chia sẻ lại toàn bộ quá trình migrate một blog từ WordPress sang Astro, từ việc export dữ liệu, chuyển đổi nội dung, xử lý hình ảnh, cho đến deploy static site.

Hy vọng kinh nghiệm này sẽ hữu ích cho các bạn đang có cùng ý định chuyển đổi từ WordPress sang Astro framework.

Stack sử dụng

Dưới đây là tech stack mình sẽ sử dụng cho blog khi chuyển qua Astro

LayerCông nghệChi phí
FrameworkAstroFree
ContentGitHub (Markdown files)Free
ImagesCloudflare R2Free (10GB)
HostingCloudflare PagesFree
CommentsGiscusFree
Online EditorSveltia CMSFree

Yêu cầu hệ thống

Mình sử dụng Macbook Air M2 để xử lý công việc. Khuyến khích mọi người sử dụng Linux hoặc macOS để tiện thao tác. Nếu đang dùng Windows thì có thể sử dụng WSL2 để chạy Linux.

  • Đã cài đặt Node.js v20+ ( dùng lệnh node --version để kiểm tra)
  • Đã có sẵn tài khoản Github (free) và Cloudflare (free)
  • Đã cài đặt Git (git --version) và Github CLI
  • GitHub CLI đã cài và đăng nhập (gh auth status)
  • Đã cài đặt rclone đã cài
  • Blog WordPress đang chạy

Công việc sẽ gồm 4 phần chính

  • Giai đoạn 1 – Chuyển đổi nội dung từ WordPress qua Markdown
  • Giai đoạn 2 – Cài đặt Astro
  • Giai đoạn 3 – Upload ảnh lên Cloudflare R2
  • Giai đoạn 4 – Đồng bộ lên Github và deploy lên Cloudflare Pages

Ngoài ra còn 2 giai đoạn phụ: Cài đặt Sveltia CMS để chỉnh sửa nội dung blog tiện lợi hơn và Giscus cho tính năng comment của blog, sẽ không được sử dụng cho blog balodeplao.com. Bao giờ mình chuyển đổi blog Thuanbui.me này qua Astro sẽ có bài hướng dẫn hai cái đó sau.

Bài viết [Phần 1] hôm nay sẽ chia sẻ về Giai đoạn 1 – Chuyển đổi nội dung từ WordPress qua Markdown. Đây là bước tốn nhiều thời gian nhất để bảo đảm nội dung và hình ảnh trên blog được giữ trọn vẹn sau khi chuyển qua Astro.

1. Export nội dung từ WordPress

WordPress lưu nội dung bài viết trong database. Trong khi đó các công cụ tạo static site như Astro, Hugo,… thường sử dụng Markdown file để quản lý nội dung, theo mô hình Jamstack (không cần database).

Vì vậy khi migrate từ WordPress sang Astro, bước đầu tiên là export nội dung từ WordPress, sau đó convert sang Markdown để dùng cho static site.

  1. Vào WordPress Admin → Tools → Export → All Content
  2. Click Download Export File
  3. Lưu file .xml về máy

2. Tạo thư mục làm việc

Tạo thư mục trên máy để xử lý file xml vừa mới tải về

mkdir ~/blog-migration
mv ~/Downloads/*.xml ~/blog-migration/
cd ~/blog-migrationCode language: Nginx (nginx)

3. Xử lý link ảnh

Chú ý: bước này không bắt buộc, bạn có thể bỏ qua nếu muốn giữ nguyên cấu trúc nội dung blog hiện tại.

Trên blog cũ, khá nhiều hình ảnh được chèn trong bài viết không phải ảnh gốc full size mà ảnh thumbnail (size Large / Medium). Các file này thường có thêm phần thông tin size được chèn sau tên, ví dụ filename-900x600.jpg

Mình muốn tải file gốc filename.jpg thay vì ảnh thumbnail nên cần xử lý file xml trước khi chuyển đổi qua markdown.

Tạo file mới có tên gọi wp-image-fixer.sh trong thư mục blog-migration và copy nội dung này vào

https://gist.github.com/10h30/f6720ebbad3d5acd40e20a9883690bcb

#!/usr/bin/env bash
# ==============================================================================
# wp-image-fixer.sh
# Replaces resized WordPress image URLs with originals in an XML export file.
# Usage:
#   ./wp-image-fixer.sh           # dry-run: scan and log results
#   ./wp-image-fixer.sh --apply   # apply replacements from cached log
# ==============================================================================

set -euo pipefail

LOG=".wp-image-check.log"
URLS=".wp-image-urls.tmp"
MAP=".wp-image-map.txt"
APPLY=false
PARALLEL=50
TIMEOUT=5

[[ "${1:-}" == "--apply" ]] && APPLY=true

info()    { echo "[info]  $*"; }
success() { echo "[ok]    $*"; }
warn()    { echo "[warn]  $*"; }
die()     { echo "[error] $*" >&2; exit 1; }

# ------------------------------------------------------------------------------
# Cleanup on exit or Ctrl+C
# ------------------------------------------------------------------------------
cleanup() {
    rm -f "$URLS"
    kill 0 2>/dev/null || true
}
trap cleanup INT TERM
trap 'rm -f "$URLS"' EXIT

# ------------------------------------------------------------------------------
# Detect existing log / prompt for input
# ------------------------------------------------------------------------------
if $APPLY; then
    [[ -f "$LOG" ]] || die "No scan found. Run the script without --apply first."
    INPUT=$(grep '^FILE|' "$LOG" | cut -d'|' -f2)
    info "Existing scan found for: $INPUT"
    echo
    read -rp "Apply fixes using cached results? (y/N): " confirm
    [[ "$confirm" != "y" ]] && exit 0
else
    if [[ -f "$LOG" ]]; then
        info "Removing previous scan results..."
        rm -f "$LOG"
    fi
    read -rp "Enter WordPress XML export file: " INPUT
    [[ -f "$INPUT" ]] || die "File not found: $INPUT"
    echo "FILE|$INPUT" > "$LOG"
fi

# ------------------------------------------------------------------------------
# DRY RUN — scan images and check originals
# ------------------------------------------------------------------------------
if ! $APPLY; then
    echo
    info "Extracting resized image URLs..."

    grep -oE 'https?://[^"[:space:]]+-[0-9]+x[0-9]+\.(jpg|jpeg|png|webp)' "$INPUT" \
        | sort -u > "$URLS"

    TOTAL=$(wc -l < "$URLS" | tr -d ' ')
    info "Found $TOTAL resized image URLs"
    echo

    if [[ "$TOTAL" -eq 0 ]]; then
        warn "No resized images found. Nothing to do."
        rm -f "$LOG"
        exit 0
    fi

    EST=$(( (TOTAL / PARALLEL) * TIMEOUT ))
    info "Checking originals (~${EST}s estimated)..."
    echo

    check_url() {
        local resized="$1"
        local log="$2"
        local timeout="$3"
        local original
        original=$(echo "$resized" | sed -E 's/-[0-9]+x[0-9]+(\.(jpg|jpeg|png|webp))/\1/')

        local code
        code=$(curl -s -o /dev/null -w "%{http_code}" \
            --max-time "$timeout" \
            --head \
            --retry 1 --retry-delay 1 \
            "$original" 2>/dev/null || echo "000")

        # Fall back to GET range if server blocks HEAD
        if [[ "$code" == "405" || "$code" == "403" || "$code" == "000" ]]; then
            code=$(curl -s -o /dev/null -w "%{http_code}" \
                --max-time "$timeout" \
                -H "Range: bytes=0-0" \
                "$original" 2>/dev/null || echo "000")
        fi

        if [[ "$code" == "200" || "$code" == "206" || "$code" == "301" || "$code" == "302" ]]; then
            echo "OK|$resized|$original" >> "$log"
        else
            echo "MISS|$resized|$original ($code)" >> "$log"
        fi
    }
    export -f check_url

    # Feed via cat to avoid xargs -a flag (not supported on all systems)
    cat "$URLS" | xargs -P "$PARALLEL" -I{} bash -c 'check_url "$@"' _ {} "$LOG" "$TIMEOUT" &
    WORKER_PID=$!

    while kill -0 "$WORKER_PID" 2>/dev/null; do
        DONE=$(grep -c '^\(OK\|MISS\)' "$LOG" 2>/dev/null || true)
        printf "\r    Progress: %d / %d" "$DONE" "$TOTAL" >&2
        sleep 0.3
    done
    wait "$WORKER_PID" 2>/dev/null || true

    DONE=$(grep -c '^\(OK\|MISS\)' "$LOG" 2>/dev/null || true)
    printf "\r    Progress: %d / %d\n" "$DONE" "$TOTAL" >&2

    OK_COUNT=$(grep -c '^OK'   "$LOG" || true)
    MISS_COUNT=$(grep -c '^MISS' "$LOG" || true)

    echo
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    success "$OK_COUNT images ready to fix"
    [[ "$MISS_COUNT" -gt 0 ]] && warn "$MISS_COUNT originals not found — will be skipped"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

    if [[ "$MISS_COUNT" -gt 0 ]]; then
        echo
        info "Skipped URLs:"
        grep '^MISS' "$LOG" | cut -d'|' -f2
    fi

    echo
    info "To apply fixes run:  ./wp-image-fixer.sh --apply"
    echo
    exit 0
fi

# ------------------------------------------------------------------------------
# APPLY — fast single-pass Perl replacement
# ------------------------------------------------------------------------------
INPUT=$(grep '^FILE|' "$LOG" | cut -d'|' -f2)
OUTPUT="fixed-$(basename "$INPUT")"

echo
info "Applying fixes to $INPUT..."

grep '^OK' "$LOG" | awk -F'|' '{print $2 "\t" $3}' > "$MAP"
COUNT=$(wc -l < "$MAP" | tr -d ' ')

if [[ "$COUNT" -eq 0 ]]; then
    warn "No fixes to apply."
    rm -f "$MAP" "$LOG"
    exit 0
fi

REPLACED=$(perl - "$INPUT" "$MAP" "$OUTPUT" <<'PERL'
use strict;
use warnings;

my ($infile, $mapfile, $outfile) = @ARGV;

open(my $mfh, '<', $mapfile) or die "Cannot open map: $!";
my %map;
while (<$mfh>) {
    chomp;
    my ($from, $to) = split(/\t/, $_, 2);
    $map{$from} = $to if defined $from && defined $to;
}
close($mfh);

my $pattern = join('|', map { quotemeta($_) } sort { length($b) <=> length($a) } keys %map);
my $regex    = qr/$pattern/;

open(my $in,  '<', $infile)  or die "Cannot open input: $!";
open(my $out, '>', $outfile) or die "Cannot open output: $!";

my %seen;
while (my $line = <$in>) {
    $line =~ s/($regex)/do { $seen{$1} = 1; $map{$1} }/ge;
    print $out $line;
}
close($in);
close($out);

print scalar keys %seen, "\n";
PERL
)

rm -f "$MAP" "$LOG"

echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
success "$REPLACED image URLs replaced — saved as $OUTPUT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echoCode language: Bash (bash)

Cấp quyền thực thi cho file vừa tạo

chmod +x wp-image-fixer.shCode language: CSS (css)

Chạy lệnh sau để kiểm tra danh sách có bao nhiêu file cần chỉnh sửa

./wp-image-fixer.sh

Nhập vào tên file xml để xử lý và ngồi đợi khoảng vài phút để hệ thống xử lý, tùy thuộc vào số lượng hình ảnh đang có trên blog.

Mục đích của file này là để kiểm tra xem file gốc filename.jpg có tồn tại không. Nhiều trường hợp file gốc không còn trên server, việc đổi tên sẽ khiến link đến file không tồn tại

Kết quả như sau: có 43 link không cần cập nhật vì file gốc không tồn tại

Enter WordPress XML export file: balampdplo.WordPress.2026-03-10.xml

[info]  Extracting resized image URLs...
[info]  Found 1782 resized image URLs

[info]  Checking originals (~175s estimated)...

    Progress: 1782 / 1782

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[ok]    1739 images ready to fix
[warn]  43 originals not found  will be skipped
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


[info]  Skipped URLs:

[info]  To apply fixes run:  ./wp-image-fixer.sh --applyCode language: YAML (yaml)

Cần chạy lệnh trên thêm một lần nữa với tham số --apply để áp dụng những thay đổi này lên file xml. Bước đầu tiên chỉ để kiểm tra và tạo log, bước apply này sẽ tạo ra một file mới với cái link hình ảnh đã được cập nhập.

./wp-image-fixer.sh --apply

Xác nhận y và đợi vài giây là xong

[info]  Existing scan found for: balampdplo.WordPress.2026-03-08.xml

Apply fixes using cached results? (y/N): y

[info]  Applying fixes to balampdplo.WordPress.2026-03-08.xml...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[ok]    1739 image URLs replaced  saved as fixed-balampdplo.WordPress.2026-03-08.xml
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Code language: YAML (yaml)

File xml sau khi chỉnh sửa sẽ có tên fixed-xxxx.xml sẵn sàng được chuyển đổi qua Markdown ở bước kế tiếp

4. Chuyển đổi XML sang Markdown

Để chuyển đổi file XML sang Markdown (*.md), mình sử dụng công cụ wordpress-export-to-markdown.

npx wordpress-export-to-markdown \
  --prefix-date=true \
  --post-folders=false \
  --frontmatter-fields=title,author,date:pubDate,categories,tags,coverImage,draft,slug \
  --save-images=all \
  --date-folders=noneCode language: Nginx (nginx)

Lưu ý: Với 100+ bài, quá trình download ảnh mất 10–20 phút. Không đóng Terminal trong khi xử lý.

Toàn bộ nội dung sẽ được lưu trong thư mục output/:

  • Tất cả nội dung của log được lưu dưới định dạng .md trong thư mục con posts
  • Các trang (pages) được lưu trong thư mục pages
  • Các nội dung khác sẽ nằm trong custom
  • Thư mục images/ chứa toàn bộ ảnh
output
├── custom
│   ├── advanced_ads
│   │   └── images
│   ├── cp_popups
│   ├── foogallery
│   ├── google_maps
│   ├── surl
│   ├── tablepress_table
│   └── wpcf7_contact_form
├── pages
│   ├── _drafts
│   └── images
└── posts
    └── imagesCode language: Nginx (nginx)

5. Dọn dẹp ảnh, Gutenberg block, shortcode,…

Đây là bước tốn nhiều thời gian nhất. Tùy thuộc vào số lượng bài viết, cấu trúc nội dung bài viết mà cần phải tùy biến cho phù hợp.

Đầu tiên, mình kiểm tra một số file md thì thấy còn khá nhiều link hình ảnh vẫn còn liên kết đến ảnh gốc dạng https://…., ví dụ

[![](images/sansai-ryori-bldl01.jpg)](https://balodeplao.com/wp-content/uploads/2017/04/sansai-ryori-bldl01.jpg)Code language: Markdown (markdown)

Mình chạy thử lệnh này để kiểm tra xem bao nhiêu file gặp tình trạng này

grep -rn 'wp-content' output/posts/ --include='*.md'Code language: PHP (php)

Kết quả trả về quá nhiều nên không thể chỉnh sửa thủ công được. Dùng lệnh này để xử lý toàn bộ

find output/posts/ -name '*.md' -exec perl -i -pe '
s/\[!\[.*?\]\(([^)]+)\)\]\((https?:)?\/\/[^)]*wp-content\/uploads[^)]*\)/![]($1)/g;
s/\[\]\((https?:)?\/\/[^)]*wp-content\/uploads[^)]*\)//g;
' {} +Code language: Markdown (markdown)

Lệnh này sẽ cập nhật tất cả các link dạng [![](images/local.jpg)](https://site.com/image.jpg) thành ![](images/local.jpg)

Tiếp theo dùng lệnh này để kiểm tra xem các link ảnh trong file md có tồn tại trong thư mục images. Vì nếu ảnh không tồn tại sẽ bị lỗi khi Astro build sau này.

# Find all images referenced in .md files, then check if the file actually exists
find output/posts/ -name "*.md" | while read f; do
  dir=$(dirname "$f")
  grep -oE '!\[[^]]*\]\(images/[^)]+\)' "$f" | grep -oE 'images/[^)]+' | while read img; do
    if [ ! -f "$dir/$img" ]; then
      echo "MISSING: $dir/$img (in $f)"
    fi
  done
doneCode language: Bash (bash)

Kết quả

MISSING: /home/mcj/test_folder/output/posts/_drafts/images/q (in /home/mcj/test_folder/output/posts/_drafts/id-4313.md)
MISSING: /home/mcj/test_folder/output/posts/_drafts/images/ir (in /home/mcj/test_folder/output/posts/_drafts/id-4313.md)
MISSING: /home/mcj/test_folder/output/posts/images/credit-card-statement-620x387.jpg (in /home/mcj/test_folder/output/posts/2015-07-10-lam-gi-khi-the-tin-dung-cua-ban-bi-hack.md)
MISSING: /home/mcj/test_folder/output/posts/images/ubersuv-voi-gia-uberBLACK.jpg (in /home/mcj/test_folder/output/posts/2015-12-07-ubersuv-nhieu-cho-hon-gia-khong-doi.md)Code language: JavaScript (javascript)

Để xử lý, mình sẽ tải thủ công các file này về hoặc xóa link trong file markdown tương ứng để tránh gặp lỗi khi Astro compile.

Tiếp theo sẽ cần phải dọn dẹp các hình ảnh hồi xa xưa còn dùng shortcode caption, và xóa các dòng comment liên quan đến Guterberg.

Chạy thử kiểm tra

bash << 'EOF'
cd output/posts
fixed=0
for file in *.md; do
  original=$(cat "$file")
  content=$(perl -0777 -pe '
    s/\\\[caption\b[^\[]*?\\\]\s*(!\[[^\]]*\]\([^)]*\))\s*(.*?)\s*\\\[\/caption\\\]/
      my $img = $1; my $cap = $2;
      $cap =~ s|^\s+||; $cap =~ s|\s+$||;
      $img =~ m|!\[([^\]]*)\]\(([^)]*)\)|;
      my $alt = $1; my $src = $2;
      $cap ? "![$cap]($src)" : "![$alt]($src)"
    /gxse;
    s/<!--\s*wp:[^>]+-->//g;
    s/<!--\s*\/wp:[^>]+-->//g;
  ' "$file")
  content=$(printf '%s' "$content" | perl -0777 -pe 's/\n{3,}/\n\n/g')
  if [ "$content" != "$original" ]; then
    echo "Would fix: $file"
    ((fixed++))
  fi
done
echo ""
echo "Total: $fixed files would be fixed"
EOFCode language: Nginx (nginx)

Chạy thiệt để chỉnh sửa

bash << 'EOF'
cd output/posts
fixed=0
for file in *.md; do
  original=$(cat "$file")
  content=$(perl -0777 -pe '
    s/\\\[caption\b[^\[]*?\\\]\s*(!\[[^\]]*\]\([^)]*\))\s*(.*?)\s*\\\[\/caption\\\]/
      my $img = $1; my $cap = $2;
      $cap =~ s|^\s+||; $cap =~ s|\s+$||;
      $img =~ m|!\[([^\]]*)\]\(([^)]*)\)|;
      my $alt = $1; my $src = $2;
      $cap ? "![$cap]($src)" : "![$alt]($src)"
    /gxse;
    s/<!--\s*wp:[^>]+-->//g;
    s/<!--\s*\/wp:[^>]+-->//g;
  ' "$file")
  content=$(printf '%s' "$content" | perl -0777 -pe 's/\n{3,}/\n\n/g')
  if [ "$content" != "$original" ]; then
    printf '%s\n' "$content" > "$file"
    echo "✅ Fixed: $file"
    ((fixed++))
  fi
done
echo ""
echo "✨ Done! Fixed $fixed files."
EOFCode language: Nginx (nginx)

6. Xóa ảnh không sử dụng

Mình cũng sẽ kiểm tra xem có file ảnh nào nằm trong thư mục images nhưng không sử dụng (không xuất hiện trong bất kỳ file md nào).
Kiểm tra thử xem có bao nhiêu file ảnh không sử dụng

bash << 'EOF'
cd output/posts
{
  grep -ohE 'images/[^)]+' *.md
  grep -ohE '^coverImage:\s*"[^"]*"' *.md | grep -ohE '[^/"]+\.(jpg|jpeg|png|webp|gif)' | sed 's/^/images\//'
} | sort -u > .used_images.tmp
unused=0
while read img; do
  rel="images/$(basename "$img")"
  if ! grep -qF "$rel" .used_images.tmp; then
    echo "UNUSED: $img"
    ((unused++))
  fi
done < <(find images/ -type f)
rm .used_images.tmp
echo "---"
echo "Total unused: $unused"
EOFCode language: Bash (bash)

Kết quả

UNUSED: images/Nusa-Dua-BLDL-17.jpg
UNUSED: images/Flower-Dome-07.jpg
UNUSED: images/Cloud-Forest-09.jpg
UNUSED: images/Cloud-Forest-08.jpg
UNUSED: images/Flower-Dome-06.jpg
UNUSED: images/Wings-of-Time06.jpg
UNUSED: images/Nusa-Dua-BLDL-16.jpg
---
Total unused: 60Code language: YAML (yaml)

Xóa các file không sử dụng bằng lệnh này

bash << 'EOF'
cd output/posts
{
  grep -ohE 'images/[^)]+' *.md
  grep -ohE '^coverImage:\s*"[^"]*"' *.md | grep -ohE '[^/"]+\.(jpg|jpeg|png|webp|gif)' | sed 's/^/images\//'
} | sort -u > .used_images.tmp
deleted=0
while read img; do
  rel="images/$(basename "$img")"
  if ! grep -qF "$rel" .used_images.tmp; then
    echo "DELETED: $img"
    rm "$img"
    ((deleted++))
  fi
done < <(find images/ -type f)
rm .used_images.tmp
echo "---"
echo "Total deleted: $deleted"
EOFCode language: Bash (bash)

Vậy là xong Giai đoạn 1. Tất cả bài viết trên WordPress đã được chuyển đổi thành công qua file markdown, sẵn sàng để dọn nhà qua Astro.

Hẹn gặp lại [Phần 2] Cài đặt và cấu hình Astro

Theo dõi
Thông báo của
guest
0 Comments
Cũ nhất
Mới nhất Được bỏ phiếu nhiều nhất
Phản hồi nội tuyến
Xem tất cả bình luận