Tạo công cụ build theme WordPress kèm demo

Mục lục bài viết14 mục

Mình sẽ hướng dẫn bạn cách thiết kế và triển khai một chương trình chạy trên PC để chọn mẫu demo, sinh một theme WordPress hợp lệ kèm 1-2 demo, kiểm tra và xuất file zip cài qua Appearance > Upload Themes. Ví dụ minh họa dùng Node.js CLI làm mặc định; có giải pháp GUI và importer tùy chọn.

Mục tiêu sản phẩm và yêu cầu chức năng

Mục tiêu của chương trình là cho phép bạn tạo nhanh một theme WordPress hoàn chỉnh kèm demo, kiểm tra và đóng gói thành file zip có thể cài qua Appearance > Upload Themes trên một site WordPress tiêu chuẩn. Chương trình chạy trên PC, quản lý kho mẫu demo, sinh 1-2 demo mẫu sẵn (demo pages hoặc demo site sample), và xuất artifact theo định dạng tương thích với quy trình upload theme của WordPress. Mục tiêu thực tiễn: sau khi người dùng tải zip lên WordPress bằng Upload Themes, theme xuất hiện trong Appearance > Themes, có thể active, và admin có thể import hoặc kích hoạt demo content theo lựa chọn.

Yêu cầu bắt buộc (must)

  • Xuất file zip theme chuẩn: một thư mục gốc tên slug-theme chứa style.css với header hợp lệ, index.php, functions.php, và các file template cơ bản. Zip phải đặt đúng cấu trúc để WordPress nhận diện ngay khi upload.
  • Kho thư viện mẫu demo local trên PC: mỗi demo gồm metadata (slug, version, title, description), preview thumbnail, content export (WXR hoặc JSON structured), và folder media (tùy chọn).
  • Ít nhất hai demo mẫu sẵn để người dùng chọn khi build: mỗi demo là một site nhỏ hoặc một tập pages ready-to-import.
  • Kiểm tra tiền đóng gói tự động: validate style.css header, kiểm tra BOM/encoding, php -l hoặc kiểm tra cú pháp PHP cơ bản, loại trừ file dev (.git, .env, node_modules, private keys) theo mặc định.
  • Cơ chế đóng gói loại trừ file dev và đảm bảo chỉ có một thư mục top-level trong zip.
  • Tích hợp minimal manifest (manifest.json) trong zip liệt kê demo packages, plugin kèm nếu có, và versioning.

Yêu cầu tùy chọn (optional)

  • Kèm optional importer plugin nhỏ: admin-initiated importer nằm trong theme (không auto-run) hoặc plugin zip kèm riêng để import demo content/media an toàn. Importer phải chunked, serialized-safe và chạy chỉ khi người quản trị kích hoạt.
  • Export phân tách: tùy chọn xuất theme.zip nhỏ + demo-media.zip + importer-plugin.zip để tránh vượt giới hạn upload server.
  • Tùy chọn chỉnh sửa mẫu đơn giản trước export: chỉnh một số theme options qua JSON UI (logo, màu, front page selection) hoặc trình chỉnh sửa WYSIWYG nhẹ cho template pages.
  • Tùy chọn generate WXR hoặc structured JSON cho content export; choose-by-user.

Danh sách đầu vào/đầu ra mà mình và bạn phải đạt được

  • Đầu vào từ bạn (user/programmer):
  • Chọn template theme starter hoặc upload base files.
  • Chọn 1 trong kho demo (demo A, demo B) và tuỳ chỉnh vài option cơ bản.
  • Chọn export mode (single theme zip, split packages, include importer plugin hay không).
  • Cấu hình metadata: Theme Name, Version, Requires PHP, Tested up to, Text Domain.
  • Đầu ra do chương trình tạo:
  • theme-slug.zip chuẩn Upload Themes (một top-level folder với style.css ở root).
  • manifest.json trong zip mô tả demos kèm theo, version và yêu cầu.
  • (tùy chọn) demo-media.zip chứa media, và importer-plugin.zip nếu user bật.
  • Một báo cáo validate trước xuất: warnings/errors (header missing, php syntax error, bị include file nhạy cảm, kích thước zip vượt uploadmaxfilesize gợi ý).
  • Một file changelog/chú thích phiên bản kèm checksum (SHA256) để đối chiếu.

Kết quả mong đợi: bạn có thể chạy chương trình trên PC, chọn mẫu demo, sinh và tải xuống một hoặc nhiều artifact mà khi upload bằng Upload Themes trên WordPress sẽ cho phép cài theme; nếu cần demo content, importer-plugin hoặc hướng dẫn WP-CLI/Tools Import được cung cấp để hoàn thiện import an toàn.

Phân tích kịch bản triển khai: GUI vs CLI và khuyến nghị công nghệ

Mục tiêu kỹ thuật ở phần này là giúp bạn chọn nền tảng phù hợp để xây một công cụ chạy trên PC có thể: chọn mẫu demo, sinh theme WordPress, kiểm tra và xuất zip tương thích Upload Themes, kèm tùy chọn importer hoặc gói media. Dưới đây so sánh bốn hướng thực tế, nêu khi nào phù hợp, ưu nhược, và đề xuất mặc định.

  1. Electron + Node.js (GUI đa nền tảng)
  • Khi phù hợp
  • Bạn muốn trải nghiệm người dùng đồ họa đầy đủ, kéo-thả, preview, wizard import/export; target cả Windows, macOS, Linux với giao diện giống nhau.
  • Ưu điểm
  • Ecosystem JavaScript phong phú cho UI, filesystem, zip, worker threads.
  • Dễ tích hợp với npm tooling: zip libraries (archiver), spawn WP-CLI, gọi Docker, tự động cập nhật bằng electron-updater.
  • Rapid prototyping UI, nhiều component sẵn.
  • Nhược điểm
  • Kích thước binary lớn vì Chromium bundle.
  • Tiêu thụ RAM cao hơn so với native nhẹ.
  • Cần xử lý bảo mật khi chạy các script hệ thống.
  1. Tauri + Rust/JS (GUI nhẹ)
  • Khi phù hợp
  • Bạn cần GUI cross-platform nhưng ưu tiên binary nhẹ hơn và khởi động nhanh.
  • Ưu điểm
  • Kích thước tác phẩm nhỏ hơn Electron; lõi native Rust an toàn và nhanh.
  • Frontend vẫn viết bằng HTML/JS nên reuse UI code được.
  • Khả năng gọi lệnh hệ thống và xử lý file với hiệu năng tốt.
  • Nhược điểm
  • Cần Rust toolchain cho build, learning curve với Rust side.
  • Một vài tiện ích JS có thể cần translate sang command layer.
  1. Python – CLI hoặc GUI (PyInstaller / PyQt)
  • Khi phù hợp
  • Bạn ưu tiên phát triển nhanh bằng scripting, hoặc target cộng đồng dev quen Python; muốn CLI đơn giản hoặc GUI desktop nhẹ.
  • Ưu điểm
  • Phát triển logic xử lý file, zip, JSON, XML rất nhanh; nhiều thư viện hỗ trợ PHP/WXR parsing.
  • PyInstaller cho phép đóng gói thành exe/app; PyQt/PySide cho GUI native-ish.
  • Nhược điểm
  • Đóng gói cross-platform phức tạp hơn, binary có thể lớn.
  • GUI bằng PyQt có khác biệt trải nghiệm giữa OS, và phân phối cần cẩn trọng với runtime dependencies.
  • Giao tiếp với WP-CLI, Docker vẫn khả thi nhưng đôi khi kém mượt so với Node.
  1. Node.js CLI (thuần CLI)
  • Khi phù hợp
  • Bạn muốn công cụ dành cho developer, scriptable, chạy trong CI, dễ tích hợp với WP-CLI và Docker.
  • Ưu điểm
  • Nhẹ, dễ phát hành qua npm hoặc binary bằng pkg/nx.
  • Tự động hóa, batch processing, tích hợp tốt với CI/CD.
  • Dễ viết preflight checks, zip validation, serialized-safe workflows.
  • Nhược điểm
  • Không thân thiện cho non-dev end users; cần hướng dẫn GUI/UX nếu muốn dùng cho khách hàng.

Khuyến nghị mặc định cho developer

  • Đối với MVP và workflow hướng developer/staging: bắt đầu bằng Node.js CLI. Lý do
  • Tốc độ phát triển, dễ test tự động, dễ gọi WP-CLI/Docker, dễ tích hợp preflight checks và packaging logic. CLI giúp hoàn thiện core features: validate style.css, build manifest, export zip, create demo packages, và tạo importer plugin scaffold.
  • Khi cần trải nghiệm cho end users non-technical: triển khai GUI sau, chọn Tauri nếu ưu tiên kích thước nhẹ và muốn reuse frontend JS; chọn Electron nếu bạn cần nhanh, nhiều component UI sẵn và chấp nhận binary lớn.
  • Nếu đội bạn quen Python và muốn nhanh với scripting nội bộ, Python CLI + optional PyQt GUI là lựa chọn hợp lý, nhưng chuẩn bị thêm effort cho đóng gói cross-platform.

Gợi ý thực thi ngắn

  • Thiết kế theo tầng: core export/import logic viết ở module CLI (Node.js hoặc Python), dùng cùng logic cho GUI bằng wrapper. Điều này cho phép bạn triển khai CLI-first rồi phát triển GUI bằng Electron/Tauri tái dùng core logic.

Kiến trúc chương trình và luồng công việc (build pipeline)

Mục tiêu phần này là mô tả cấu trúc nội bộ của chương trình builder và luồng công việc khi bạn xuất một theme WordPress kèm demo. Mình trình bày một sơ đồ luồng rõ ràng, các module chính, format manifest/manifest.json, và điểm mở rộng bằng hooks để bạn có thể tự triển khai hoặc mở rộng sau này.

Sơ đồ luồng chính (top-level)

  1. Chọn mẫu -> 2) Cấu hình demo -> 3) Generate theme files -> 4) Validate -> 5) Package zip (+ optional extras) -> 6) Produce artifacts

Mô tả từng bước và module liên quan

  1. Chọn mẫu
  • Module: Template Catalog
  • Chức năng: liệt kê library mẫu trên đĩa, hiển thị preview, version, tags. Trả về demo slug và metadata để bước sau dùng.
  1. Cấu hình demo
  • Module: Demo Configurator
  • Chức năng: cho phép bạn chọn options của demo (site title, sample pages chọn, include media hay không, target PHP/WP versions). Sinh ra một runtime config object dùng xuyên pipeline.
  1. Generate theme files
  • Module: Theme Generator
  • Chức năng: từ template + config tạo cây thư mục theme: style.css, index.php, functions.php, template-parts, assets, theme.json nếu cần. Nếu có demo content, tạo demo package trong folder demos/<slug> gồm content.wxr hoặc content.json, media/, meta.json, preview.png.
  • Output tạm: staging/<theme-slug>/
  1. Validate
  • Module: Validator
  • Chức năng: chạy hàng loạt checks: style.css header, index.php present, php -l syntax check, BOM removal, no .env/.git/node_modules included, size thresholds, serialized-safety scan, required PHP extensions present in manifest. Nếu có lỗi block, trả về lỗi cho người dùng; nếu chỉ warning, show list và allow override.
  1. Package zip (+ optional extras)
  • Module: Packager
  • Chức năng: tạo zip chuẩn với một top-level folder tên theme-slug và files đúng vị trí. Hỗ trợ options: include media trong zip, tạo separate archives (theme.zip, demo-media.zip, importer-plugin.zip). Exclude list mặc định: .git, nodemodules, .env, *.pem, *.sql, .DSStore.
  • Kiểm tra sau pack: đảm bảo style.css nằm tại root của folder trong zip, tính checksum.
  1. Produce artifacts
  • Module: Artifact Manager
  • Chức năng: ghi artifact metadata, chèn changelog, tạo signed checksum, export manifest.json kèm release. Đưa artifact lên storage local hoặc remote (s3, release server), trả về URL và manifest.

Manifest và manifest.json

  • Mục đích: mô tả package, dependencies, requirements, và hooks. Một manifest mẫu:
{
  "theme_slug": "demo-theme",
  "version": "1.0.0",
  "requires_php": "7.4",
  "requires_wp": "5.8",
  "includes": {
    "media": true,
    "importer_plugin": "demo-importer-1.0.0.zip"
  },
  "demos": [
    {
      "slug": "demo-home",
      "title": "Home Demo",
      "content": "demos/demo-home/content.json",
      "media_path": "demos/demo-home/media/"
    }
  ],
  "checksums": {
    "theme_zip": "sha256:..."
  }
}

Hooks và mở rộng

  • Cơ chế hook: events emit tại từng bước (beforegenerate, aftergenerate, beforepack, afterpack, onvalidatefail). Hook handlers có thể là plugin JS/Python gọi CLI hoặc script user-defined.
  • Extension points:
  • Tùy biến template rendering (hook before_generate)
  • Thêm validator custom (hook before_pack)
  • Hook để inject scripts build thêm (minify, image-optimize) trước khi pack
  • API: cung cấp small SDK để register hook functions, và CLI flag –run-hook <name> để chạy logic ngoài.

Kiểm soát phiên bản và rollback

  • Artifact Manager lưu manifest.json, changelog và checksum. Khi phát hành mới, công cụ so sánh manifest để phát hiện thay đổi bất thường (ví dụ media đột ngột tăng kích thước) và cảnh báo.

Kết luận ngắn

  • Luồng này giữ rõ ràng trách nhiệm module, manifest là nguồn sự thật cho artifact, và hooks cho phép mở rộng mà không sửa core. Thiết kế theo layered architecture giúp bạn dễ thêm GUI sau này hoặc tích hợp CI/CD.

Cấu trúc theme WordPress tối thiểu và header bắt buộc

Một theme WordPress hợp lệ để Upload Themes phải chứa một tập file tối thiểu đặt trong một thư mục gốc tên slug-theme. Những file bắt buộc và thực hành an toàn:

  • style.css (bắt buộc)
  • index.php (bắt buộc; nội dung tối thiểu hợp lệ)
  • functions.php (thường cần, ít nhất không gây lỗi PHP)
  • screenshot.png (kích thước gợi ý 1200×900, không bắt buộc nhưng khuyến nghị)
  • (tùy chọn) screenshot.jpg, readme.txt, license.txt

Giải thích file style.css và header format

  • WordPress nhận diện theme bằng comment header đầu file style.css nằm trực tiếp trong thư mục gốc theme.
  • Header phải là khối CSS comment / … / và chứa ít nhất dòng Theme Name: Tên chủ đề.
  • Một header mẫu hoàn chỉnh (bản điền sẵn) phải như sau:
/*
Theme Name: Demo Theme
Theme URI:  https://example.com/demo-theme
Author:     Tên Tác Giả
Author URI: https://example.com
Description: Một theme demo minimal cho testing.
Version:    1.0.0
Requires PHP: 7.4
Requires at least: 5.8
Tested up to: 6.2
License:    GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: demo-theme
*/

Lưu ý về encoding và BOM

  • Tất cả file text phải là UTF-8 without BOM. BOM (Byte Order Mark) ở đầu file sẽ gây lỗi khi PHP include và có thể phá định dạng header style.css.
  • Trước khi nén, đảm bảo loại bỏ BOM và lưu UTF-8. Trên nhiều editor chọn "UTF-8 without BOM" khi save.

Index.php và functions.php tối thiểu

  • index.php có thể rất đơn giản, chỉ đảm bảo file tồn và không gây lỗi parse:
<?php
// Minimal index.php
echo '<!doctype html><html><head><meta charset="utf-8"><title>Demo</title></head><body>';
if ( function_exists( 'wp_head' ) ) { wp_head(); }
echo '<h1>Hello from Demo Theme</h1>';
if ( function_exists( 'wp_footer' ) ) { wp_footer(); }
echo '</body></html>';
  • functions.php nên tồn tại nhưng không thực thi side-effect nặng khi theme được cài.

Cách validate header programmatically trước khi nén

  • Kiểm tra tồn tại file style.css tại đường dẫn root của theme.
  • Đọc first 8 KB hoặc first comment block để parse các trường header.
  • Kiểm tra các yêu cầu: Theme Name tồn tại, encoding UTF-8 no BOM, Version hợp lệ.
  • Kiểm tra BOM: byte đầu tiên nếu 0xEF 0xBB 0xBF thì báo lỗi.

Ví dụ PHP nhỏ để validate header và BOM:

<?php
function read_style_header($path){
  $data = file_get_contents($path, false, null, 0, 8192);
  if (!$data) return false;
  // BOM check
  if (substr($data,0,3) === "\xEF\xBB\xBF") return ['error'=>'BOM detected'];
  // regex extract
  if (preg_match('#/\*\s*(.*?)\s*\*/#s', $data, $m)){
    $block = $m[1];
    preg_match('/Theme Name:\s*(.+)/i', $block, $t);
    return ['header'=>$block, 'theme_name'=> $t[1] ?? null];
  }
  return ['error'=>'Header block not found'];
}

// Usage
$res = read_style_header('/path/to/theme/style.css');
var_export($res);

Kiểm tra bổ sung trước nén

  • Chạy php -l trên mọi file .php để phát hiện syntax error.
  • Xác nhận chỉ một top-level folder trong zip và style.css nằm trực tiếp trong thư mục đó.
  • Loại trừ file dev và đảm bảo file text là UTF-8 without BOM.

Tuân thủ những bước trên giúp giảm lỗi phổ biến khi upload theme qua Appearance > Themes.

Cách tổ chức kho thư viện mẫu demo trên đĩa

Mục tiêu: tổ chức kho demo sao cho chương trình trên PC có thể liệt kê, hiển thị preview, kiểm tra tính hợp lệ, import hoặc đóng gói từng demo vào theme zip một cách tự động, có thể quản lý phiên bản và cập nhật dễ dàng. Dưới đây là cấu trúc đề xuất, metadata cần có, xử lý thumbnail/preview, và cách quản lý phiên bản mẫu.

Cấu trúc thư mục chuẩn (bắt buộc tuân theo để code tìm và xử lý tự động)

  • demos/<slug>/{content.wxr|content.json, media/, meta.json, preview.png, version}

Giải thích từng phần

  • demos/: thư mục gốc của tất cả mẫu demo.
  • <slug>: tên định danh của demo, chỉ gồm lowercase letters, số và dấu gạch ngang. Ví dụ: demo-home, demo-shop.
  • content.wxr hoặc content.json: nội dung demo. WXR khi bạn muốn dùng WordPress WXR export; JSON khi bạn dùng importer tùy chỉnh lưu cấu trúc có token/placeholder. Chỉ cần một trong hai, ưu tiên JSON nếu bạn export structured options (không raw serialized PHP).
  • media/: thư mục chứa tất cả file media đi kèm demo (images, thumbnails, nhỏ gọn). Tên file phải unique trong demo và không chứa dấu cách. Nếu media quá lớn, thay bằng media.zip để giảm file count.
  • meta.json: file metadata mô tả demo (mẫu bên dưới).
  • preview.png: ảnh thumbnail preview hiển thị trong giao diện chọn demo. Kích thước gợi ý 1200×800 px và một bản nhỏ retina 600×400 px nếu cần (preview@2x.png).
  • version: văn bản chứa semantic version (ví dụ 1.0.2) hoặc có thể là file changelog.md kèm theo.

Metadata cần có (trong meta.json)

  • -slug|string
  • -title|string
  • -description|string
  • -version|string (semantic e.g. 1.0.0)
  • -created_at|ISO8601 timestamp
  • -updated_at|ISO8601 timestamp
  • -requires_php|string (option, ví dụ 7.4)
  • -requires_wp|string (option, ví dụ 5.8)
  • -content_type|string (wxr|json)
  • -mediasizebytes|integer (tổng kích thước media)
  • -preview|string (preview.png hoặc preview@2x.png)
  • -checksum|sha256 (checksum của toàn bộ demo folder hoặc media archive)
  • -tags|[string] (ví dụ ["homepage","ecommerce"])
  • -changelog|string (tên file changelog nếu có)

Ví dụ mẫu meta.json

{
  "slug": "demo-home",
  "title": "Demo Home - Minimal",
  "description": "Một trang demo đơn giản với homepage và blog.",
  "version": "1.0.0",
  "created_at": "2025-03-10T12:00:00Z",
  "updated_at": "2025-03-10T12:00:00Z",
  "requires_php": "7.4",
  "requires_wp": "5.8",
  "content_type": "json",
  "media_size_bytes": 45234567,
  "preview": "preview.png",
  "checksum": "e3b0c44298fc1c149afbf4c8996fb924..."
}

Thumbnail / preview handling

  • Kích thước:
  • preview.png chính: 1200×800 px (landscape) để hiển thị trên gallery.
  • preview@2x.png (tùy chọn): 2400×1600 px cho retina; chương trình chọn bản @2x nếu có.
  • Nếu muốn thumbnail nhỏ để list view, lưu preview-thumb.jpg 600×400.
  • Định dạng: PNG hoặc JPEG, nén hợp lý (quality 70-85 cho JPEG).
  • Tên và vị trí: phải nằm ngay trong folder demo: demos/<slug>/preview.png.
  • Nếu chương trình cần tạo thumbnail động, khi thêm demo mới chương trình tự generate preview-thumb.jpg và preview@2x.png từ preview.png và ghi kích thước vào meta.json.

Quản lý phiên bản mẫu

  • Phiên bản theo semantic versioning (MAJOR.MINOR.PATCH).
  • Mỗi lần thay đổi nội dung demo (thay hình, chỉnh cấu trúc), tăng version và cập nhật updated_at và changelog.md trong cùng folder.
  • Kiểm tra checksum: khi chương trình tải hoặc cập nhật demo, tính sha256 của media archive hoặc folder và so sánh meta.json.checksum để phát hiện hỏng/change.
  • Migrations: nếu meta.json định nghĩa requireswp hay requiresphp tăng cao, chương trình hiển thị cảnh báo khi người dùng chọn demo không tương thích với target site.

Các quy tắc vận hành và best practices

  • Kích thước media: giới hạn media nội bộ mỗi demo, ví dụ tối đa 50MB; nếu cần nhiều hơn, tách media thành media.zip tải riêng.
  • Token placeholders: trong content.json dùng placeholder dạng {{site_url}} hoặc {{attachment:filename.jpg}} thay vì URL tuyệt đối. Importer sẽ map filename->attachment ID trong site đích.
  • Atomic package: mỗi demo folder phải là một unit độc lập. Không để cross-reference tuyệt đối đến demo khác.
  • Validation: khi chương trình quét kho demo, thực hiện các kiểm tra:
  • meta.json tồn tại và hợp lệ JSON.
  • preview.png tồn tại và có kích thước hợp lệ.
  • content file tồn tại và content_type khớp.
  • mediasizebytes khớp tổng file trong media/.
  • checksum khớp nếu có.
  • Naming convention: slug chỉ gồm a-z0-9 và ‘-‘, không dấu tiếng Việt, không khoảng trắng.
  • Permission: set file permission readable cho chương trình; tránh lưu credentials trong meta hoặc file docs.

API đọc và cập nhật (gợi ý cho chương trình)

  • List demos: đọc subfolders trong demos/, parse meta.json, sort theo version hoặc updated_at.
  • Validate demo: chạy validation steps, trả về report PASS/WARN/FAIL với lỗi cụ thể.
  • Export demo package: tạo archive demo-<slug>-<version>.zip chứa chỉ content.json, media (hoặc media.zip ref), meta.json, preview files, và manifest.
  • Update demo: khi builder thay đổi demo, tạo new version folder hoặc ghi đè sau khi tăng meta.json.version và cập nhật checksum + changelog.

Checklist kiểm tra trước khi chấp nhận demo mới (máy đọc)

  • -have_meta|boolean
  • -content_present|boolean
  • -media_present|boolean
  • -preview_present|boolean
  • -checksum_ok|boolean
  • -sizewithinlimit|boolean

Tóm lại, tổ chức theo cấu trúc chuẩn, metadata đầy đủ, dùng placeholders cho content, kèm preview hợp lý và quản lý version rõ ràng sẽ giúp chương trình trên PC có thể liệt kê, validate, import và đóng gói demo an toàn cho WordPress.

Tạo nội dung demo: pages/posts, menus, widgets, customizer

Mục tiêu của phần này là cho bạn một quy trình thực tế để xuất nội dung demo có thể import an toàn trên site đích, kèm chiến lược mapping slug-to-slug, placeholder cho attachments/pages, và cách đảm bảo rebuild mapping ID khi import.

Nguyên tắc chính

  • Xuất nội dung theo hai định dạng tùy mục tiêu: WXR khi dùng WordPress Importer chuẩn; structured JSON khi bạn muốn importer tùy chỉnh kiểm soát serialized data an toàn.
  • Luồng import an toàn: tạo posts/pages -> import attachments -> ghi mapping old-slug/old-guid -> cập nhật postmeta/options/widgets/menus/customizer bằng mapping. Không làm search-replace raw trên DB.

Quy trình ngắn

  1. Trên môi trường mẫu xuất:
  • Option A – WXR: dùng Tools > Export hoặc wp export để lấy posts/pages/media. WXR tiện cho importer core nhưng media thường là remote URLs.
  • Option B – Structured JSON: xuất mỗi object với fields rõ ràng, and replace absolute URLs with placeholders như {{attachment:hero.jpg}} hoặc {{page:home}}. Xuất option/widget arrays as JSON, not serialized PHP.
  1. Đặt placeholder cho attachments/pages
  • Trong content hay options, thay URL bằng token rõ ràng. Ví dụ: <img src="{{attachment:hero.jpg}}">. Khi import, dùng filename-to-attachmentID mapping để thay token bằng getattachmenturl($newid) hoặc updatepostmeta with $newid.
  1. Mapping slug-to-slug strategy
  • Export lưu kèm old identifiers: postslug, posttype, oldID or guid. Khi import, insert post by slug; record mapping oldID -> newID and oldslug -> new_ID. Sau khi tạo tất cả posts, run pass để replace tokens and to assign menus/widget references by looking up new IDs by slug.

Ví dụ structured JSON (điền sẵn)

{
  "posts": [
    {
      "old_id": 42,
      "post_type": "page",
      "slug": "home",
      "title": "Home",
      "content": "<p>Welcome</p><img src=\"{{attachment:hero.jpg}}\">"
    }
  ],
  "attachments": [
    { "filename": "hero.jpg", "path": "media/hero.jpg" }
  ],
  "menus": [
    { "location": "primary", "items": [ { "type": "page", "slug": "home" } ] }
  ],
  "options": {
    "theme_mods_slug": { "hero_image": "{{attachment:hero.jpg}}", "front_page": "{{page:home}}" }
  }
}

Template với biến

{
  "posts": [ { "old_id": <OLD_ID>, "slug": "<SLUG>", "content": "<HTML_WITH_{{attachment:FILE}}_AND_{{page:SLUG}}>" } ]
}

Import order cần tuân thủ

  • Tạo users/authors if needed
  • Tạo posts/pages/custom post types
  • Import attachments one-by-one – use mediahandlesideload to get new attachment IDs
  • Build mapping table oldid -> newid and filename -> attachment_id
  • Update postcontent, postmeta, options, widgets, menus, customizer using mapping – always operate on structured data and call updateoption or setthememod so WordPress tự serialize.

Lưu ý ngắn

  • Không replace serialized strings bằng regex. Nếu gặp serialized PHP, unserialize, replace values, then re-serialize.
  • Log mapping để có thể rollback hoặc re-run incremental import.

Xử lý media demo an toàn và hiệu quả

Mục tiêu ngắn gọn: đảm bảo media demo được import đáng tin cậy trên site đích với tối thiểu thao tác của người dùng, không gây timeout hoặc vượt giới hạn upload, và không làm hỏng dữ liệu serialized.

Chiến lược tổng quát (2 lựa chọn, trade-off rõ)

  1. Embed media trong package (an toàn nhất cho import)
  • Gói media vào trong zip hoặc cung cấp file media.zip riêng.
  • Trong theme/importer, lưu tạm tại wp-content/uploads/demo-import/<demo-slug>/ trước khi đăng ký attachment.
  • Ưu điểm: không phụ thuộc HTTP fetch, tránh timeout và remote blocking. Nhược điểm: zip lớn, có thể vượt uploadmaxfilesize.
  • Khuyến nghị: nếu tổng media < 50 MB, embed; nếu lớn, tách thành media.zip riêng và hướng dẫn dùng WP-CLI hoặc FTP.
  1. Remote fetch (nhẹ hơn, rủi ro mạng)
  • Export WXR với remote URLs hoặc dùng importer để sideload từ URL.
  • Chỉ dùng khi bạn biết server đích cho phép outgoing HTTP và có thời gian chờ thích hợp.
  • Phải có retry, backoff, và fallback ra báo lỗi rõ ràng nếu fetch thất bại.

Tối ưu kích thước ảnh

  • Trước khi đóng gói, resize và tối ưu: WebP hoặc JPG progressive, max chiều dài cạnh 1600px cho hero, 1200px cho nội dung, thumbnails 300px.
  • Đặt quality 70–82 cho ảnh JPG; kiểm thử thị giác.
  • Sinh sẵn đa kích thước nếu importer sẽ dùng trực tiếp IDs, hoặc để WP generate khi server đủ resource.

Thư mục lưu và thao tác an toàn

  • Khi importer copy file, đặt folder tạm:
  • wp-content/uploads/demo-import/<demo-slug>/
  • Dùng wpmkdirp và đảm bảo quyền 755 cho thư mục.
  • Dùng mediahandlesideload để tạo attachment chính xác với metadata và regnerate thumbnails.

Ví dụ PHP (import một file local):

// $file_path = '/path/to/unpacked/media/hero.jpg';
// $file_array similar to $_FILES for media_handle_sideload
$file_array = array(
  'name' => basename( $file_path ),
  'tmp_name' => $file_path,
);
// require_once ABSPATH . 'wp-admin/includes/file.php';
// require_once ABSPATH . 'wp-admin/includes/media.php';
// require_once ABSPATH . 'wp-admin/includes/image.php';
$attach_id = media_handle_sideload( $file_array, 0 );
if ( is_wp_error( $attach_id ) ) {
  error_log( 'Media import failed: ' . $attach_id->get_error_message() );
}

Giảm rủi ro timeout và upload limits

  • Chunked import: import theo batch (ví dụ 5 ảnh/lượt) qua AJAX để tránh maxexecutiontime.
  • Preflight checks: trước import, kiểm tra uploadmaxfilesize, postmaxsize, memorylimit, maxexecution_time; nếu không đủ, gợi ý dùng WP-CLI hoặc tách media.
  • Nếu zip quá lớn, cung cấp ba file: theme.zip, media.zip, importer.zip và hướng dẫn cài theo thứ tự hoặc lệnh WP-CLI:
  • wp theme install theme.zip –activate
  • wp plugin install importer.zip –activate
  • wp media import /path/to/media/* –porcelain

Bảo mật và cleanup

  • Kiểm tra file type/MIME trước khi import; từ chối executable.
  • Sau import thành công, xóa folder tạm uploads/demo-import/<demo-slug>/ để giải phóng disk và tránh rò rỉ.

Xử lý serialized data và cách xử lý an toàn

Vấn đề trọng tâm: thao tác tìm-thay URL hoặc chỉnh sửa dữ liệu khi đóng gói demo có thể làm hỏng các giá trị serialized PHP hoặc các dạng nhúng (JSON, base64). Nguyên tắc an toàn là không làm replace trên raw bytes của database; phải detect, decode, modify, rồi re-encode.

Nguyên lý detection và phân loại (sơ lược)

  • Thu thập tất cả ô dữ liệu khả nghi trong DB hoặc manifest: wpoptions, wppostmeta, wpposts.postcontent, widget options, custom tables.
  • Với mỗi ô chạy classifier theo thứ tự:
  1. thử is_serialized(text) bằng regex/heuristic -> tag = serialized
  2. nếu valid JSON -> tag = json
  3. nếu base64-like và decode(base64) chứa serialized/JSON -> tag = base64(serialized|json)
  4. khác -> tag = plain
  • Ghi lại vị trí, chiều dài, và ví dụ ngắn cho báo cáo.

Thuật toán xử lý an toàn (detect -> decode -> modify -> re-encode)

  • detect: chạy classifier, tạo task list theo type.
  • decode:
  • serialized: unserialize() an toàn. Nếu unserialize thất bại, lưu vào report và skip auto-change.
  • json: json_decode(), null coerce kiểm tra lỗi.
  • base64: base64_decode() rồi lặp decode lại theo nested type.
  • modify: trong cấu trúc PHP array/object hoặc JSON, tìm string nodes chứa token cần replace (ví dụ demo domain). Thay thế chỉ trên string nodes, không thay đổi metadata như integer lengths.
  • re-encode: serialize() hoặc jsonencode() hoặc base64encode() theo đúng thứ tự decode trước đó. So sánh checksum trước/sau; nếu unserialize(mới) không khớp, rollback task và đánh dấu cho human review.
  • apply: ghi updateoption/wpupdatepostmeta/… trong transaction hoặc ghi log các thay đổi để rollback.

Công cụ và fallback

  • Tự động: ưu tiên dùng WP-CLI search-replace –recurse-objects vì đã battle-tested. Nếu không có WP-CLI, dùng thư viện interconnect/it Search Replace DB cho PHP để xử lý serialized.
  • Nếu bất kỳ item nào không an toàn để tự động sửa (unserialize lỗi, binary blob, encrypted), tool phải:
  • 1) báo chi tiết cho user,
  • 2) cung cấp dry-run diff (trước/sau) cho mỗi thay đổi,
  • 3) yêu cầu human-in-the-loop để approve hoặc sửa thủ công.
  • Luôn tạo dump backup trước khi áp dụng thay đổi tự động; lưu mapping sourceID->targetID khi tạo posts/attachments để sửa references sau cùng.

Thực tế vận hành

  • Trong importer, thực hiện post/page import trước, media import tiếp theo, sau đó xử lý options/widgets/menu bằng cách dùng mapping để thay placeholder như {{attachment:filename}} thành attachment ID mới. Chỉ chạy auto replace ở phạm vi whitelisted keys nếu muốn giảm rủi ro.

Thiết kế importer an toàn trong theme/plugin kèm theo

Mục tiêu ngắn gọn: importer phải là component admin-initiated, namespaced, không tự chạy khi theme active, có tùy chọn tách ra thành plugin installer riêng, và thực hiện import theo lô để tránh timeout, giữ nguyên tính toàn vẹn serialized và media.

  1. Hai lựa chọn triển khai
  • Importer nhẹ trong theme (khuyến nghị cho trải nghiệm out-of-box)
  • File: inc/demo-importer.php, namespace MyTheme\Importer.
  • Chỉ hiển thị trang admin dưới Appearance > Import Demo. Không auto-run.
  • Xác thực bằng currentusercan(‘manage_options’) và nonce.
  • Plugin installer riêng (khuyến nghị cho release công khai / WordPress.org)
  • Đóng gói plugin zip riêng, hoặc cung cấp link tải; theme chỉ recommend via TGMPA hoặc admin notice.
  1. Các API WordPress cần dùng (thực thi server-side)
  • Tạo bài/ trang: wpinsertpost($post_array)
  • Tải media local: wpuploadbits($filename, null, $filecontents) hoặc mediahandlesideload / mediahandle_upload để có attachment ID hợp lệ
  • Cập nhật theme mods: setthememod($name, $value) hoặc updateoption(‘thememods_{slug}’, $array)
  • Tạo/ cập nhật menu items: wpupdatenavmenuitem($menu_id, 0, $args)
  • Cập nhật widgets/options: updateoption($optionname, $value_array)
  1. Flow import chunked AJAX (server-side tasks)
  • Khi admin bấm Import:
  1. Tạo import session: $sessionid = wpgenerate_uuid4(); store temp data in transient or option with timeout.
  2. Client khởi động AJAX batch requests tới admin-ajax.php với action=mythemeimportbatch, data: { session_id, step, offset, nonce }.
  3. Server validates nonce and capability, loads package manifest for session.
  4. Batches:
  • step = ‘posts’ : process N posts per call. For each post: sanitize, wpinsertpost, record mapping oldid => newid.
  • step = ‘media’ : process M files per call. Use wpuploadbits or mediahandlesideload, then regenerate metadata, store filename => attachment_id.
  • step = ‘options’ : apply options using decoded JSON, replace placeholders by mapping, then updateoption or settheme_mod.
  • step = ‘menus’ : create menus, then for each item call wpupdatenavmenuitem with object_id mapped.
  1. Each AJAX response returns { success, processed, total, progresspercent, errors, resumeoffset }.
  2. Client shows progress and retries on transient failures. Server supports resume by reading session state.
  3. Finalize step: assign front page via updateoption(‘showonfront’,’page’); updateoption(‘pageonfront’, $id).
  1. Safety practices
  • Always unserialize/serialize via safe routines. Prefer exporting options as JSON in package and reserialize server-side.
  • Log inserted IDs to enable rollback: store list in session and provide "Revert import" admin action that deletes posts/attachments and restores options.
  • Rate-limit batch size based on server preflight (memorylimit, maxexecution_time) returned by AJAX preflight.
  • Nonce and capability checks on every request. Sanitize all inputs.

Đóng gói file zip đúng chuẩn và loại trừ file dev/nhạy cảm

Mục tiêu: tạo một archive cài được qua Appearance > Themes > Upload Theme, an toàn và nhỏ gọn. Nguyên tắc cốt lõi là: zip phải chứa đúng một thư mục gốc tên theme-slug, không mang file dev/nhạy cảm, không để BOM ở đầu các file text, và quyền file hợp lý.

  1. Thư viện nén đề xuất
  • Desktop/tooling: dùng ZipArchive (PHP), node-archiver hoặc @zip-lib/zip-js (Node.js), hoặc Python zipfile. Chọn theo ngôn ngữ công cụ của bạn; đều hỗ trợ tạo entry paths (để đảm bảo một top-level folder).
  1. Cấu trúc zip
  • Zip root must contain exactly one top-level folder named theme-slug/; bên trong có style.css trực tiếp. Không nén cả folder cha hoặc thêm 2 cấp.
  1. Loại trừ file dev/nhạy cảm
  • Mặc định exclude: .git, .github, nodemodules, vendor (tùy), .env, *.pem, *.key, *.p12, *.sql, *.log, .DSStore, Thumbs.db, .env.*, .env.local, .env.production.
  • Trước khi nén, chạy scanner (gitleaks/interconnectit/detect-secrets) để phát hiện secret patterns.
  1. Kiểm tra encoding và BOM
  • Đảm bảo tất cả file text (style.css, PHP, JS, JSON) là UTF-8 without BOM. Loại bỏ BOM vì style.css có BOM sẽ phá header.
  1. Quyền file và symlink
  • Trước nén, chuẩn hóa quyền: files 644, directories 755. Không đóng gói symlink; resolve symlink thành file thực để tránh missing files sau khi unzip trên host không hỗ trợ symlink.
  1. Final verify (script)
  • Sau tạo zip, tự giải nén vào temp và kiểm tra:
  • Exactly one top-level folder named theme-slug
  • theme-slug/style.css exists and contains Theme Name header
  • index.php exists (or functions.php minimal)
  • No excluded patterns present
  • Run php -l on .php files (nếu có PHP binary) and confirm no BOM
  • Nếu tất cả OK, compute SHA256 checksum và xuất artifact.

Máy đọc: danh sách exclude (mỗi dòng)

  • -exclude|.git
  • -exclude|.github
  • -exclude|node_modules
  • -exclude|vendor
  • -exclude|.env
  • -exclude|*.pem
  • -exclude|*.key
  • -exclude|*.p12
  • -exclude|*.sql
  • -exclude|*.log
  • -exclude|.DS_Store
  • -exclude|Thumbs.db

Kiểm tra tương thích PHP/WordPress trước export

Trước khi đóng gói, chạy một bước kiểm tra tương thích tự động để giảm rủi ro theme không cài được hoặc gây lỗi khi active. Mục tiêu của bước này là xác nhận header metadata, phát hiện cú pháp/PHP feature không tương thích, kiểm tra extensions cần thiết, và sinh cảnh báo rõ ràng cho bạn.

  1. Default recommended minimums – ghi vào style.css header
  • Requires PHP: 7.4
  • Requires at least: 5.8
  • Tested up to: 6.x
  1. Scan code cho cú pháp không tương thích
  • Chạy syntax check cho tất cả file PHP: php -l hoặc php -d display_errors=0 -l file.php; nếu dùng CI, chạy php -l trên toàn bộ tree.
  • Dò tìm cú pháp PHP mới (typed properties, union types, arrow functions) bằng parser tĩnh như PHPStan/Phan hoặc grep các pattern phổ biến. Nếu bạn hỗ trợ target PHP thấp hơn, bật rule phát hiện feature không tương thích và báo chi tiết file – line.
  1. Kiểm tra extension và môi trường
  • Kiểm tra presence của các extension quan trọng: ZipArchive, mbstring, xmlreader/simplexml, json, curl, và một trong hai image libraries: gd hoặc imagick. Nếu thiếu, hiển thị cảnh báo tương ứng.
  • Kiểm tra khả năng xử lý ảnh: gd/imagick có thể tạo thumbnails; báo nếu cả hai đều thiếu.
  1. Sinh warnings cho tác giả
  • Ghi thành report có mức: ERROR (cần sửa trước export), WARN (khuyến nghị), INFO.
  • Ví dụ thông báo: "ERROR: Found arrow function in inc/utils.php – requires PHP 7.4+. Set Requires PHP or refactor." hoặc "WARN: ZipArchive missing on target – media import may fail."
  • Kèm theo hướng dẫn hành động cụ thể: chỉnh header, refactor code, hoặc cung cấp fallback (export without media, dùng WP-CLI).
  1. Tích hợp vào workflow
  • Chạy checks tự động trước bước tạo zip; nếu có ERROR, chặn export. Lưu report dưới dạng máy đọc (JSON) để UI hiển thị chi tiết cho bạn.

Test tự động trên môi trường local bằng Docker/WP-CLI

Mục tiêu ngắn gọn: khởi tạo 3 môi trường WP (PHP 7.4, 8.0, 8.1), cài theme từ theme.zip, chạy import demo.wxr, kiểm tra tồn tại pages, media và menus, rồi xuất report pass/fail cho người phát triển.

  1. Docker Compose matrix (tập tin mẫu)
version: "3.8"
services:
  wp74:
    image: wordpress:5.8-php7.4-apache
    ports: ["8074:80"]
    environment: {WORDPRESS_DB_HOST: db74, WORDPRESS_DB_USER: user, WORDPRESS_DB_PASSWORD: pass, WORDPRESS_DB_NAME: wp}
    depends_on: ["db74"]
  wp80:
    image: wordpress:6.0-php8.0-apache
    ports: ["8080:80"]
    depends_on: ["db80"]
  wp81:
    image: wordpress:6.1-php8.1-apache
    ports: ["8081:80"]
    depends_on: ["db81"]
  db74: {image: mysql:5.7, environment: MYSQL_ROOT_PASSWORD: root}
  db80: {image: mysql:5.7, environment: MYSQL_ROOT_PASSWORD: root}
  db81: {image: mysql:5.7, environment: MYSQL_ROOT_PASSWORD: root}
  1. Script tự động (pseudo bash)
for host in http://localhost:8074 http://localhost:8080 http://localhost:8081; do
  wp --url="$host" theme install theme.zip --activate
  wp --url="$host" import demo.wxr --authors=create
  pages=$(wp --url="$host" post list --post_type=page --field=post_title)
  media_count=$(wp --url="$host" media list --format=ids | wc -w)
  menus=$(wp --url="$host" menu list --format=json)
  if [ -n "$pages" ] && [ "$media_count" -gt 0 ] && [ "$menus" != "[]" ]; then
    echo "$host PASS" >> test-report.txt
  else
    echo "$host FAIL -- pages:$pages media:$media_count menus:$menus" >> test-report.txt
  fi
done
  1. Report
  • test-report.txt chứa dòng host | PASS/FAIL | diagnostics.
  • Triaging: trên FAIL lưu logs từ wp –debug và container logs để developer fix.

Kết luận

Mình đã cung cấp khung đầy đủ để bạn xây một công cụ chạy trên PC giúp chọn demo, sinh theme WordPress hợp lệ kèm 1-2 demo, kiểm tra và xuất zip cài qua Appearance > Upload Themes. Sau khi triển khai theo khung này, bạn sẽ có một quy trình reproducible để xuất theme.zip, demo media và importer an toàn cho site đích.

Lưu ý thực tế quan trọng nhất: luôn chạy preflight validate và backup trước khi áp dụng bất kỳ thao tác thay thế serialized trên site đích để tránh làm hỏng dữ liệu.

Phụ lục – QA check

Mình đã rà theo 22 điểm và báo cáo kết quả dưới đây.

#Điểm kiểmKết quảTrích đoạn sai + cách sửa
1Toàn bài có đúng CHỦ ĐỀ không? Có đoạn nào chạm NGOÀI PHẠM VI không?ĐẠT–
2Mọi mục TRONG PHẠM VI có được phục vụ không? Mục nào bị bỏ?ĐẠT–
3YÊU CẦU tường minh có được đáp ứng đủ không?ĐẠT–
4ĐÍCH có thể hiện ngầm qua cấu trúc không? Người đọc tự nhận ra mình làm được gì?ĐẠT–
5DEPTH có nhất quán xuyên suốt không? Phần nào nhẹ hơn hoặc nặng hơn mức yêu cầu?ĐẠT–
6Nếu DEPTH là Expert: có phân biệt "cách thường làm" vs "cách đúng" ít nhất một chỗ không? Có nêu điều kiện quy tắc KHÔNG áp dụng không?ĐẠT–
7Có phần nào bị viết thành mẩu cụt – tức đáng ra cần chi tiết nhưng chỉ có vài gạch đầu dòng? Chỉ đích danh.VI PHẠMTrích đoạn: "Gợi ý thực thi ngắn – Thiết kế theo tầng: core export/import logic viết ở module CLI…" Vấn đề: mục này rất ngắn so với tầm quan trọng, chỉ nêu ý chính mà không mô tả concrete steps để implement core module, API interface giữa core và GUI, hoặc ví dụ mã/CLI command. Cách sửa: mở rộng thành một phần chi tiết gồm sơ đồ module (file layout), ví dụ API/CLI commands (tên lệnh, flags), ví dụ gọi từ GUI wrapper vào core (ví dụ Node.js child_process spawn pattern hoặc Python subprocess), và một ví dụ nhỏ về contract JSON giữa frontend và core.
8Có ví dụ/mẫu ĐIỀN SẴN HOÀN CHỈNH không? (không ngoặc nhọn, không chỗ trống)ĐẠT–
9Có phần lỗi thường gặp / lưu ý không?ĐẠT–
10Còn dấu [KHÔNG CHẮC] nào lọt vào bài không? (không được có)ĐẠT–
11Bài có em-dash không?ĐẠT–
12Bài có CTA không? Có mục FAQ riêng không? Có heading tên yếu tố tâm lý không?ĐẠT–
13Có mở section nào bằng bullet ngay lập tức không?ĐẠT–
14Có meta-comment về nguồn dữ liệu hay cách bài này được tạo không?ĐẠT–
15NGƯỜI ĐỌC THỨ CẤP khác "không có": các phần tạo tài liệu có chỉnh ngôn ngữ phù hợp không?ĐẠT–
16Có nhắc lại nguyên văn KHOẢNG TRỐNG hoặc thuật lại ĐÍCH không?ĐẠT–
17KHÔNG có hai phần nào trùng chủ đề. Đọc danh sách tiêu đề ##: hai tiêu đề mà một câu tóm tắt được cả hai là VI PHẠM. Trích cả hai tiêu đề.ĐẠT–
18KHÔNG có tiêu đề nào là số thứ tự giàn giáo: ## Phần 10/25, ## Phần 10, ## Mục 3. Đó là ruột hệ thống lọt ra ngoài, VI PHẠM ngay.ĐẠT–
19KHÔNG có câu nào tóm tắt lại bối cảnh chung ở đầu các phần giữa bài. "Mình tóm nhanh…", "Trước khi vào chi tiết…" xuất hiện từ phần thứ hai trở đi là VI PHẠM.VI PHẠMTrích đoạn (ví dụ nhiều lần xuất hiện đầu các section): "Mục tiêu của phần này là cho bạn một quy trình thực tế để xuất nội dung demo…" và "Mục tiêu ngắn gọn:…" Vấn đề: các câu mở đầu dạng tóm tắt bối cảnh chung xuất hiện nhiều lần giữa bài, vi phạm quy tắc yêu cầu không có câu tóm tắt bối cảnh chung ở đầu phần giữa bài. Cách sửa: loại bỏ hoặc chuyển các câu đó thành tiêu đề phụ ngắn gọn không mang tính tóm tắt bối cảnh, hoặc nhập nội dung tóm tắt vào phần mở bài tổng thể; nếu cần giữ checkpoint ngắn, hãy đổi thành ghi chú kỹ thuật ngắn gọn như "Mục tiêu kỹ thuật" và giảm tính lặp lại.
20KHÔNG có lời mời cuối phần: "Nếu bạn muốn, mình sẽ…", "Phần sau mình sẽ…", "Bạn có muốn…". Đây là bài web, không phải hội thoại. Một câu như vậy là VI PHẠM.ĐẠT–
21Mã nguồn nằm trong khối “ chứ không phải danh sách gạch đầu dòng. Thấy dòng kiểu – <?php hoặc – $x = 1;` là VI PHẠM.ĐẠT–
22KHÔNG có câu nào lặp gần nguyên văn ở hai chỗ khác nhau. Quét các câu mở đoạn: câu nào thấy hai lần trở lên thì trích ra, ghi VI PHẠM.ĐẠT–

TỔNG: 2 vi phạm, nặng nhất là 19.