Lọc và đánh giá repo GitHub theo truy vấn phức hợp

Mình sẽ hướng dẫn bạn triển khai pipeline nhiều pha để tìm, lọc và gán nhãn các repository GitHub theo truy vấn phức hợp, rồi xuất kết quả máy đọc được (CSV/JSON) kèm cờ chấp nhận/loại và lý do. Ví dụ minh họa sẽ dùng truy vấn in:readme với nhóm từ khóa và hai truy vấn code-level filename:requirements.txt và filename:Dockerfile.

Phân tách yêu cầu và mục tiêu đầu bài

Mục tiêu của phần này là làm rõ phạm vi và đầu ra mong đợi cho một quy trình tìm, lọc và đánh giá các repo GitHub theo truy vấn phức hợp, nhằm xuất danh sách cuối cùng ở dạng máy đọc được (CSV hoặc JSON) kèm cờ chấp nhận/loại và lý do. Mình tóm tắt các ràng buộc thực thi, format đầu ra, phân biệt tiêu chí bắt buộc và tiêu chí chấm điểm, kênh ưu tiên để kiểm tra và tự động hóa, và các rủi ro chính cần giảm thiểu.

  1. Phạm vi đầu vào và đầu ra
  • Đầu vào: một hoặc nhiều truy vấn repository-search (readme/metadata) và một hoặc nhiều truy vấn code-search (filename hoặc content).
  • Đầu ra bắt buộc: file CSV hoặc JSON chứa các repo được xét kèm metadata và cờ quyết định. CSV là kênh chính để trao tay đồng đội; JSON dùng cho tích hợp tự động.
  • Trường bắt buộc trong CSV/JSON: fullname, htmlurl, stars, size, pushedat, languageprimary, isfork, isarchived, hasrequirements (bool), hasdockerfile (bool), passoverall (bool), score (numeric), failreasons (semicolons separated).
  1. Tiêu chí bắt buộc vs tiêu chí chấm điểm
  • Tiêu chí bắt buộc (must): archived == false (mặc định, có tuỳ chọn khác), repo tồn tại trên public, repo thỏa repository-search qualifiers cơ bản, và file-presence checks theo yêu cầu AND/OR (ví dụ phải có cả requirements.txt và Dockerfile nếu yêu cầu). Nếu một must fail thì repo bị mark reject với lý do tương ứng.
  • Tiêu chí chấm điểm (score): stars, recency (pushed_at), size (heuristic), language match percentage, README length, số contributors, CI presence. Tính điểm theo rubric, đặt ngưỡng pass; nếu không đạt nhưng close thì chuyển sang manual review.
  1. Kênh ưu tiên thực hiện
  • Web kiểm tra nhanh: dùng để prototype truy vấn, tinh chỉnh từ khóa, xem counts và sample kết quả trước khi scale. Dùng khi muốn feedback tức thì.
  • REST API tự động hóa: kênh chính cho harvest production. Dùng search/repositories và search/code, pagination, total_count check, phân mảnh query khi cần, và calls tới contents, languages, releases, commits để hậu kiểm.
  • gh CLI: tuỳ chọn cho thao tác ad-hoc, debug và small-batch automation; không dùng để scale lớn thay cho REST API.
  1. Rủi ro chính cần giảm thiểu (và biện pháp ngắn)
  • Bỏ sót do giới hạn trả về search (totalcount cap): phát hiện totalcount, phân mảnh truy vấn theo stars/pushed/owner buckets.
  • Index lag giữa repo metadata và code index: nếu pushed_at mới nhưng code-search không trả file, queue re-check sau vài phút hoặc dùng contents API để xác thực.
  • Sai kết hợp query code-level vs repo-level: tách repository-search và code-search, chạy code-search per-filename rồi intersect theo full_name.
  • Phân trang, rate limit và retry: luôn pagination đầy đủ, đọc Link headers, dùng backoff và checkpointing.
  • Forks/mirrors/duplicates: hậu xử lý loại fork mặc định bằng repo.fork, nhưng cung cấp option giữ fork nếu có commits ahead/releases; dedupe theo full_name normalized.

Kết quả đầu ra phải cho phép audit: lưu toàn bộ truy vấn, buckets dùng để phân mảnh, total_count gốc, và timestamp khi thu thập để truy vết nguyên nhân nếu cần.

Tổng quan về hai loại search của GitHub: repository search vs code search

Repository search và code search là hai công cụ khác nhau về scope, cú pháp qualifier và cách lập chỉ mục. Hiểu rõ khác biệt này là tiền đề để bạn thiết kế pipeline nhiều pha an toàn.

  • Scope
  • Repository search: tìm trên metadata của repository và các trường như name, description, README (khi dùng in:readme), topics. Nó thuận tiện để lọc theo stars, language, pushed, size, archived, fork. Kết quả trả về là các repo.
  • Code search: tìm trên nội dung file đã được lập chỉ mục và từng file là đơn vị tài liệu. Nó phù hợp với filename:, path:, content:, và trả về file hits kèm thông tin repo chứa file đó.
  • Qualifier hỗ trợ (tóm tắt)
  • Repository search hỗ trợ qualifiers repo-level: stars:, language:, pushed:, size:, archived:, fork:, in:readme/in:description/topic:…
  • Code search hỗ trợ filename:, path:, extension:, content token, repo: (để giới hạn scope). Một số qualifier repo-level không hoạt động trong code search.
  • Cách index và timing lag
  • Repository metadata cập nhật nhanh hơn. Trường như stars, pushed_at thường cập nhật gần như tức thì.
  • Code search dựa trên chỉ mục nội dung mã nguồn phân tán, có độ trễ không cố định. File mới thêm hoặc vừa bị xóa có thể chưa hiện trong code search trong vài giây tới vài phút, đôi khi lâu hơn.
  • Hậu quả khi ghép qualifier sai
  • Nếu bạn ghép qualifier file-level vào repository search hoặc ngược lại, truy vấn sẽ không trả kết quả bạn mong. Ví dụ đặt filename:requirements.txt và mong repository search trả repo có file đó là sai lầm.
  • Viết filename:requirements.txt filename:Dockerfile trong một truy vấn code search cũng sai mục đích khi bạn muốn repo có cả hai file. Code search áp qualifiers trên cùng một file, nên nhiều filename trong cùng query thường có nghĩa AND trên cùng file, chứ không phải "repo chứa cả hai". Kết quả là bạn dễ bỏ sót repo đúng yêu cầu.
  • Tại sao cần multi-phase pipeline
  • Vì mỗi loại search trả thông tin khác nhau và có giới hạn riêng, pipeline nên: (1) chạy repository search để lọc metadata và readme; (2) chạy code search riêng cho từng filename hoặc nội dung cần; (3) chuẩn hóa và hợp nhất theo full_name của repo; (4) kiểm tra bổ sung (contents API, git trees) để bù đắp index lag và tránh false positive/negative. Cách này đảm bảo bạn không dựa vào một nguồn chỉ mục duy nhất và giảm rủi ro bỏ sót hoặc dính rác.

Thiết kế pipeline nhiều pha: luồng cao và lý do từng pha cần có

Mục tiêu của pipeline này là thu một danh sách các repo GitHub thỏa một tập điều kiện phức hợp và xuất ra file có cờ chấp nhận/loại bỏ kèm lý do. Luồng cao gồm các pha rõ ràng để giảm rủi ro bỏ sót và hạn chế kết quả rác. Dưới đây mô tả step-by-step, lý do cho từng pha, và các checkpoints audit để tránh mất mát khi hợp nhất repository search và code search.

  1. Phase A — Repository search (metadata + in:readme)
  • Hành động: chạy truy vấn repository search (in:readme OR description OR topics) với qualifiers repo-level như stars, language, pushed, archived, size, fork:false nếu muốn loại fork sớm.
  • Lý do: nhanh, ít tốn API, thu candidate dựa trên README và metadata. Giúp giảm scope trước khi làm code search tốn kém.
  • Checkpoint audit: lưu query text, totalcount trả về, và nếu totalcount > threshold (ví dụ 900) đánh dấu cần phân mảnh. Ghi log bucket hóa nếu phải chia query.
  1. Phase B — Phân mảnh repository queries khi cần
  • Hành động: nếu total_count vượt giới hạn an toàn, tách truy vấn theo buckets an toàn (stars ranges, pushed date ranges, owner alphabet ranges). Chạy từng bucket rồi gộp kết quả.
  • Lý do: tránh bị giới hạn trả về (API cap 1000 per query) và đảm bảo coverage. Phân mảnh theo trường monotonic (stars, pushed) an toàn hơn theo text.
  • Checkpoint audit: lưu metadata per-bucket, totalcount mỗi bucket, pages fetched, và note buckets có totalcount vượt giới hạn lại.
  1. Phase C — Code search per-filename
  • Hành động: với mỗi filename cần kiểm (ví dụ filename:requirements.txt, filename:Dockerfile) chạy một truy vấn code search riêng, phân trang đầy đủ, thu repository.full_name từ mỗi hit.
  • Lý do: code search hoạt động ở cấp file. Để kiểm yêu cầu "có cả A và B", phải chạy từng filename riêng rồi giao nhau theo repo. Không ghép nhiều filename trong một query file-level.
  • Checkpoint audit: lưu query per-filename, totalcount, pages, và set(repos) thu được. Nếu code-search trả totalcount lớn, áp chiến lược phân mảnh chuyên biệt (thêm language, path, date) và log lý do phân mảnh.
  1. Phase D — Hợp nhất theo full_name
  • Hành động: chuẩn hóa key bằng fullname lowercase. Tạo tập candidates = union/ intersection tùy yêu cầu: thường là (reposfromreposearch) ∩ (reposfromfilename1) ∩ (reposfromfilename2).
  • Lý do: đây là bước quan trọng nhất. Mapping file hits sang repo và merge với repo-level candidates là nơi dễ xảy ra false negative/positive nếu không checkpoint.
  • Checkpoint audit: record counts trước và sau merge, xuất sample diff (ví dụ top-50 repo chỉ có trong code search nhưng không trong repo search) để rà soát index lag hoặc phân trang thiếu.
  1. Phase E — Hậu xử lý và loại trùng
  • Hành động: dedupe, gọi repository endpoint để fetch fork, archived, language, stargazerscount, size, pushedat. Nếu muốn loại fork, filter fork:true ở bước này và ghi lý do. Loại mirror bằng mirror_url nếu có, hoặc heuristics.
  • Lý do: xác nhận metadata chính xác, loại bỏ mirror/fork theo policy, tránh dựa vào dữ liệu file-level thiếu fork flag.
  • Checkpoint audit: export list bị loại với lý do tường minh để review.
  1. Phase F — Kiểm tồn tại file thực và kiểm chứng nội dung (optional but recommended)
  • Hành động: với danh sách candidate cuối, kiểm tra existence chặt bằng Contents API hoặc git/trees recursive trên default branch; nếu cần, check top N branches. Lấy nội dung file để xác minh tokens (ví dụ requirements chứa == hoặc Dockerfile có FROM).
  • Lý do: bù cho index lag và biến thể tên file; tránh false positive do code search index lỗi.
  • Checkpoint audit: log re-checks, file paths xác thực, trường hợp phải re-try do index lag.
  1. Phase G — Áp rubric, gán nhãn pass/reject và xuất CSV/JSON
  • Hành động: đánh giá từng repo theo rubric gồm mandatory checks (archived false, files present) và scoring items (stars, pushed recency, size, language match). Gán pass/trượt và list fail_reasons chuẩn.
  • Lý do: minh bạch quyết định, dễ audit, cho đồng đội lý do rõ ràng.
  • Checkpoint audit: lưu score breakdown per repo, lưu mẫu records để kiểm tra chất lượng.
  1. Phase H — Kiểm soát chất lượng và feedback loop
  • Hành động: sample ngẫu nhiên N repo từ các lớp (pass, reject, borderline) để kiểm tra thủ công; monitor metrics: recall proxy (fraction reposfromrepo_search also had file hits), queries hitting >1000, rate limit events.
  • Lý do: phát hiện sớm lỗi phân mảnh, index lag, hay mapping sai.
  • Checkpoint audit: generate run report với queries, buckets, pages fetched, API quota used, samples for manual review, và anomalies list.

Ghi chú vận hành ngắn

  • Luôn checkpoint sau mỗi trang và mỗi bucket để resume nếu rate-limited.
  • Ghi rõ full trace: query text → bucket id → page → repo ids → file hits → decisions. File CSV/JSON cuối phải chứa cột fail_reasons chi tiết để audit.

Phân rã truy vấn ví dụ thành nhóm tiêu chí: nội dung, siêu dữ liệu repo, hiện diện file

Đoạn này tách truy vấn mẫu thành ba nhóm tiêu chí rõ ràng để thiết kế pipeline nhiều pha. Mục tiêu: từ truy vấn tổng hợp "in:readme (OR keywords) + language:Python + stars/size/pushed/archived" tạo ra một truy vấn repository-level cho lọc metadata và readme, và các truy vấn code-level riêng cho filename:requirements.txt và filename:Dockerfile. Sau đó mình trình bày cách ghép giao nhau theo full_name repo.

  1. Nhóm tiêu chí nội dung (content-level)
  • Mục tiêu: tìm repo whose README hoặc description chứa các từ khoá quan tâm. Dùng repository search với in:readme, in:description, topic:…
  • Ví dụ từ khoá OR an toàn nhóm (phải có ngoặc): ( "machine learning" OR "deep learning" OR "model serving" )
  • Ghi chú: đặt ngoặc để đảm bảo OR được áp đúng.
  1. Nhóm siêu dữ liệu repo (repo-level metadata)
  • Các qualifier áp dụng ở cấp repo: language, stars, size, pushed, archived, fork
  • Những qualifier này chỉ hợp lệ trong search/repositories, không phải trong search/code.
  1. Nhóm hiện diện file (file-level)
  • Những yêu cầu kiểu filename thuộc về code search vì file là đơn vị tài liệu được index.
  • Không ghép nhiều filename trong cùng một truy vấn file-level nếu mục tiêu là "repo có cả file A và file B". Thay vào đó chạy từng truy vấn filename riêng rồi giao nhau theo full_name.

Truy vấn repository-level (sẵn sàng dán vào web UI hoặc REST API, chú ý URL-encode khi gọi API)

  • Mẫu an toàn:

in:readme ( "machine learning" OR "deep learning" OR "model serving" ) language:Python stars:>50 size:<100000 pushed:>2022-01-01 archived:false fork:false

  • Giải thích ngắn:
  • in:readme (…) nhóm OR các cụm từ tìm trong README.
  • language:Python lọc repo có Python là ngôn ngữ chính.
  • stars, size, pushed, archived, fork là bộ lọc metadata; điều chỉnh giá trị theo mục tiêu của bạn.
  • Khi gọi REST API: URL-encode toàn bộ query và dùng perpage=100; kiểm tra totalcount và header Link để quyết định phân mảnh nếu cần.

Truy vấn code-level cho file presence

  • Mẫu cho requirements.txt:

q = ‘filename:requirements.txt’ Endpoint REST: GET https://api.github.com/search/code?q=filename:requirements.txt&per_page=100

  • Mẫu cho Dockerfile:

q = ‘filename:Dockerfile’ Endpoint REST: GET https://api.github.com/search/code?q=filename:Dockerfile&per_page=100

  • Lưu ý quan trọng:
  • Chạy mỗi filename thành một truy vấn riêng. Lấy tất cả trang kết quả và extract repository.full_name từ mỗi item.
  • Để yêu cầu repo có cả hai file, thực hiện intersection sets theo fullname: reposfinal = reposfromreposearch ∩ reposwithrequirements ∩ reposwith_Dockerfile.

Kiểm nhanh trên web để hiệu chỉnh từ khóa

  • Dán truy vấn repository-level vào thanh tìm kiếm GitHub; xem số kết quả và vài sample kết quả trên trang đầu.
  • Nếu count quá lớn (thấy nhiều trang), thêm qualifier tách nhỏ như stars:>500 hoặc pushed:>2023-01-01; nếu quá nhỏ, giảm ngưỡng stars hoặc bỏ qualifier size.
  • Thử từng truy vấn code-level đơn lẻ trên web để kiểm tra biến thể tên file phổ biến (requirements-dev.txt, Dockerfile.dev). Nếu thấy nhiều biến thể, thêm truy vấn cho các biến thể đó.

Ghi chú ghép kết quả và kiểm tra index lag

  • Khi hợp nhất, luôn chuẩn hóa full_name bằng lowercase và dedupe.
  • Nếu một repo xuất hiện trong repo-level set nhưng không thấy file-level match, khả năng có index lag. Với repo đã được push gần đây, queue re-check sau vài phút thay vì loại ngay.
  • Nếu code search trả quá nhiều hits cho một filename (tổng_count lớn), phân mảnh query theo language, path hoặc created/pushed windows để tránh cap 1000 result.

Tóm tắt bước ngắn gọn để triển khai:

  1. Chạy repository search bằng truy vấn mẫu trên, phân mảnh nếu totalcount quá lớn. Lưu set repocandidates.
  2. Chạy code search cho filename:requirements.txt và filename:Dockerfile, thu sets repoperfile.
  3. Intersect theo fullname: finalcandidates = repocandidates ∩ setreq ∩ set_docker.
  4. Hậu xử lý: gọi GET /repos/{owner}/{repo} để xác nhận fork/archived/language/stars/size/pushed; loại fork nếu policy yêu cầu; kiểm tra README tồn tại; nếu cần xác minh file thật, dùng /repos/{owner}/{repo}/contents/{path} hoặc git/trees trên default branch.
  5. Ghi kết quả với cột flags hasrequirements, hasdockerfile, fail_reasons và xuất CSV.

Đây là phân rã truy vấn mẫu thành các truy vấn repo-level và file-level riêng để bạn triển khai pipeline nhiều pha, tránh ghép qualifiers sai và tối ưu hóa kiểm tra tính tồn tại file.

Viết lại truy vấn repository search cho in:readme với nhóm từ khóa OR và qualifiers

Dưới đây là các truy vấn mẫu an toàn để bạn dùng khi muốn tìm repo có keyword trong README, kết hợp nhóm từ khóa bằng OR, và kèm các qualifier metadata như stars, size, pushed, archived, language. Mình cho cả hai dạng ready-to-run: để dán vào thanh tìm kiếm web GitHub và để gọi REST API (URL-encoded). Mỗi qualifier được giải thích, và mình nêu cách tinh chỉnh khi kết quả quá rộng hoặc quá hẹp.

Mẫu truy vấn an toàn cho web UI (copy-paste vào thanh tìm kiếm GitHub)

in:readme ( "machine learning" OR "deep learning" OR "model serving" ) language:Python stars:>50 pushed:>2022-01-01 size:<100000 archived:false fork:false

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

  • in:readme: tìm các từ trong nội dung README. Dùng khi bạn muốn dự án mô tả chức năng ở README.
  • ( "machine learning" OR "deep learning" OR "model serving" ): nhóm OR phải nằm trong ngoặc. Các cụm có dấu cách cần dấu nháy kép để gọi là phrase. Nhóm này tìm README chứa ít nhất một cụm.
  • language:Python: lọc repo có ngôn ngữ chính là Python. Đây là filter cấp repo.
  • stars:>50: chỉ lấy repo có >50 sao. Thay giá trị tùy mục tiêu tín nhiệm.
  • pushed:>2022-01-01: repo có cập nhật gần hơn ngày này. Dùng để ưu tiên dự án còn hoạt động.
  • size:<100000: kích thước repo dưới ngưỡng. Xem lưu ý về đơn vị size phía dưới.
  • archived:false: loại repo archived.
  • fork:false: bỏ fork ngay từ lúc search nếu bạn muốn tránh forks.

Mẫu gọi REST API (URL-encoded)

curl -H "Authorization: token YOUR_TOKEN" \
  "https://api.github.com/search/repositories?q=in%3Areadme+%28%22machine+learning%22+OR+%22deep+learning%22+OR+%22model+serving%22%29+language%3APython+stars%3A%3E50+pushed%3A%3E2022-01-01+size%3A%3C100000+archived%3Afalse+fork%3Afalse&per_page=100"

Lưu ý quan trọng và biến thể khi quá rộng hoặc quá hẹp

  • Nếu kết quả quá rộng (ví dụ total_count lớn hoặc bạn thấy nhiều trang), phân mảnh query trước khi thu toàn bộ bằng API. Thường xử lý bằng cách tách theo ranges stars hoặc theo pushed windows. Ví dụ chia stars thành buckets như stars:>1000, 500..1000, 100..499, <100.
  • Nếu kết quả quá hẹp, giảm ngưỡng stars hoặc bỏ qualifier size, hoặc mở rộng phrases trong nhóm OR. Ví dụ thay stars:>50 thành stars:>10, hoặc bỏ size qualifier.
  • Nếu bạn thấy nhiều kết quả không liên quan do README chỉ nhắc qua, thêm qualifier như topic: hoặc in:description để thắt chặt ý định, hoặc yêu cầu presence của file thực tế bằng code search sau này (xem pipeline tổng thể).
  • Về size: trường size ở metadata có ý nghĩa tương đối; trước khi dùng làm ngưỡng quyết định, kiểm tra vài repo mẫu để calibrate đơn vị và phân phối trong corpus của bạn. Nếu không chắc, đừng dùng size làm kill-switch; dùng nó như cảnh báo và thực hiện kiểm tra tiếp theo bằng API contents hoặc tree.

Chiến thuật an toàn khi kết hợp OR và qualifiers

  • Luôn nhóm các OR bằng ngoặc, và quote các cụm có dấu cách. Thiếu ngoặc là nguồn lỗi phổ biến dẫn tới truy vấn sai ý.
  • Dùng repository search để lọc readme/metadata và dùng code search riêng để kiểm tra presence của filename (ví dụ requirements.txt, Dockerfile). Nếu bạn cần repo chứa cả hai file, chạy hai truy vấn code riêng rồi giao nhau theo full_name. Không cố gắng đặt nhiều filename trong một truy vấn code duy nhất để yêu cầu "có cả hai" vì code search hoạt động cấp file.
  • Khi dùng API, URL-encode toàn bộ query, set perpage=100 và đọc totalcount. Nếu total_count vượt ngưỡng an toàn của bạn, kích hoạt logic phân mảnh.

Ví dụ biến thể cho trường hợp bạn muốn mở rộng scope readme (bớt chặt)

in:readme ( "ml" OR "machine learning" OR "deep-learning" OR "model serving" ) language:Python stars:>10 pushed:>2021-01-01 archived:false

Ví dụ nếu bạn ưu tiên strict chất lượng (chặt hơn)

in:readme ( "machine learning" OR "model serving" ) language:Python stars:>100 pushed:>2023-01-01 size:<50000 archived:false fork:false

Tóm tắt kỹ thuật ngắn

  • Dùng repository search (in:readme + metadata qualifiers) để tạo candidate set. Dùng code search riêng cho filenames cần có. Luôn URL-encode khi gọi API. Nếu query trả quá nhiều kết quả, phân mảnh theo stars hoặc pushed ranges. Kiểm tra vài mẫu thủ công để hiệu chỉnh thresholds như stars và size.

Viết truy vấn code search riêng cho filename:requirements.txt và filename:Dockerfile

Mô tả ngắn: bạn chạy hai truy vấn code search riêng, một cho mỗi filename, rồi lấy trường repository.fullname từ hits và giao nhau theo fullname để tìm repo chứa cả hai file. Không ghép filename vào cùng một truy vấn vì filename: qualifiers áp dụng trên file-level, không ngầm yêu cầu repo có nhiều file khác nhau.

Truy vấn mẫu (web UI)

  • filename:requirements.txt
  • filename:Dockerfile

Truy vấn mẫu (REST API curl, URL-encoded, per_page=100)

curl -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/search/code?q=filename%3Arequirements.txt&per_page=100&page=1"
curl -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/search/code?q=filename%3ADockerfile&per_page=100&page=1"

Cách chạy và lưu full_name

  1. Chạy mỗi truy vấn riêng, phân trang với per_page=100 và page=1..N.
  2. Từ mỗi response JSON lấy items[].repository.fullname và lưu thành setrequirements và setdockerfile. (Ví dụ JSON path .items[].repository.fullname)
  3. finalrepos = intersection(setrequirements, set_dockerfile).

Lưu ý wildcard và giới hạn filename qualifier

  • Wildcard/regex hỗ trợ hạn chế; nếu repo dùng tên biến thể (requirements-dev.txt, Dockerfile.prod) bạn phải chạy thêm queries cho từng biến thể.
  • Search API trả totalcount; nếu totalcount > 1000, truy vấn quá rộng và bạn phải phân mảnh query (ví dụ thêm language: hoặc repo: buckets) để tránh bỏ sót.
  • Code search index có độ trễ; nếu repo vừa push file mới, hãy re-check bằng Contents API (/repos/{owner}/{repo}/contents/{path}) trước khi loại bỏ.

Cách hiệu chỉnh nhanh trên giao diện web GitHub

  1. Mở trang tìm kiếm GitHub, dán truy vấn thử vào ô tìm kiếm. Dùng ngoặc để nhóm OR và nháy kép cho cụm từ có dấu cách. Ví dụ:
  • in:readme ( "machine learning" OR "model serving" ) language:Python stars:>50 archived:false
  1. Đọc ngay số kết quả phía trên. Nếu thấy lớn (hàng nghìn) tức là cần phân mảnh; nếu con số nhỏ, truy vấn có vẻ chặt.
  1. Dùng thanh Filters bên trái để chỉnh nhanh – language, stars, pushed, license, archived, fork. Thử bật/tắt fork để xem ảnh hưởng.
  1. Mở vài repo mẫu từ trang kết quả để kiểm tra thực tế: xem README, search trong repo (kí hiệu tìm trong repo), và kiểm tra file mong muốn như requirements.txt hoặc Dockerfile bằng nút Contents.
  1. Khi truy vấn ổn trên web, chuyển sang gọi API để tự động hóa. Kiểm tra total_count trả về từ /search/repositories; nếu >1000, phải phân mảnh query. Ví dụ gọi đầu tiên bằng curl:
curl -H "Authorization: token $GITHUB_TOKEN" \
  "https://api.github.com/search/repositories?q=in:readme+(%22machine%20learning%22%20OR%20%22model%20serving%22)+language:Python+stars:>50&per_page=1"
  1. Ghi lại truy vấn final và ví dụ sample repo để dùng làm kiểm thử khi triển khai API.

Chiến lược phân mảnh truy vấn repository search khi total_count lớn

Mục tiêu của phần này là giúp bạn phát hiện khi một truy vấn repository search quá rộng và chia nó an toàn thành các truy vấn con sao cho không bỏ sót repo hữu dụng. Mình trình bày các bước phát hiện, ba chiến thuật chính để phân mảnh (stars, pushed date, owner alphabet), cách xây buckets không overlapping, cơ chế traceability và checkpoint để audit, rồi kèm ví dụ truy vấn và mẫu mã hóa thực thi bằng Python.

  1. Phát hiện truy vấn quá rộng
  • Khi bạn gọi Search API, đọc trường totalcount trong response đầu tiên. Nếu totalcount vượt threshold bạn đặt (ví dụ 900), kích hoạt chế độ phân mảnh. Threshold nên nhỏ hơn giới hạn trả về của API; đặt mặc định 900 để phòng trường hợp API giới hạn 1000.
  • Ngoài total_count, đọc header Link để biết có rel="last". Nếu API trả rel="last" với số trang lớn, đó là dấu cần xem xét.
  • Ghi log ngay: query string, timestamp, totalcount, firstpagesize, reason="totalcount>threshold". Lưu query gốc để traceability.
  1. Nguyên tắc chung khi phân mảnh
  • Chọn một trường metadata stable và monotonic nếu có thể. Trường phải hỗ trợ range qualifier trong repository search. Ví dụ tốt: stars, created, pushed. Tránh phân mảnh theo free-text từ readme vì dễ làm thay đổi semantics.
  • Mỗi bucket phải là disjoint hoặc có overlap được kiểm soát kèm bước dedupe sau khi thu. Mình khuyến nghị dùng disjoint ranges để đơn giản: ví dụ stars:>=10000, stars:5000..9999, stars:1000..4999, stars:0..999.
  • Ghi metadata về mỗi bucket: bucketid, bucketquery, expectedtotalcount (API trả), pages_fetched. Điều này cho traceability và audit.
  1. Chiến thuật phân mảnh an toàn

A. Stars buckets – ưu tiên khi stars phù hợp với mục tiêu

  • Kịch bản: truy vấn gốc có total_count lớn. Chia theo stars ranges giảm dần để lấy repo có nhiều traction trước.
  • Cách tạo buckets:
  1. Xác định max_star dựa trên sample nhanh: chạy query stars:>X thử với X = 10000, 5000, 1000… để tìm điểm giảm natural.
  2. Sinh buckets disjoint, ví dụ:
  • stars:>=10000
  • stars:5000..9999
  • stars:1000..4999
  • stars:100..999
  • stars:0..99
  • Mỗi bucket chạy độc lập; nếu một bucket vẫn trả total_count>threshold, tiếp tục phân mảnh bucket đó theo pushed date hoặc thu hẹp range stars trong bucket.

B. Pushed date buckets – khi stars không phân bố hữu ích

  • Dùng khi bạn quan tâm tới activity recent hoặc stars không có ý nghĩa.
  • Chia theo khoảng thời gian non-overlapping, ví dụ tuần hoặc tháng:
  • pushed:>2024-01-01
  • pushed:2023-07-01..2023-12-31
  • pushed:2023-01-01..2023-06-30
  • pushed:<2023-01-01
  • Khi dataset quá lớn, giảm window tới tuần hoặc ngày cho phần có density cao.

C. Owner alphabet buckets – khi không thể dùng numeric ranges

  • Dùng khi bạn không muốn thay đổi semantics search (ví dụ muốn giữ toàn bộ readme keyword nhưng phải chia do cap).
  • Tách theo ký tự đầu owner: repo: a..m và n..z không trực tiếp supported trong qualifier nhưng bạn có thực hiện bằng thêm qualifier user: hoặc run queries repo:owner_prefix nếu bạn có danh sách owners. Thực tế áp dụng:
  • Lấy danh sách owner đầu tiên bằng search theo tên owner prefix nếu bạn có nguồn owners.
  • Hoặc dùng created/updated date kết hợp.
  1. Xây buckets không overlapping và traceability
  • Luôn tạo buckets disjoint. Nếu dùng stars: a..b và stars: b+1..c thì không overlap.
  • Giữ mapping bucket -> query -> response meta trong storage (database hoặc file). Mẫu bản ghi:
  • bucketid | query | totalcount | pagesfetched | firstpagetime | completeflag | items_count
  • Sau khi thu từng bucket, merge kết quả và dedupe theo fullname normalized (lowercase owner/name). Dedupe cho traceability giữ lại originbucket cho mỗi repo để biết repo nằm trong bucket nào.
  1. Khi một bucket vẫn quá lớn
  • Nếu bucket total_count vẫn > threshold, áp chiến lược phân mảnh lồng: ví dụ trong một stars bucket còn quá lớn, phân mảnh theo pushed date nhỏ hơn. Tránh phân mảnh bằng từ khoá readme vì semantics sẽ thay đổi. Luồng lồng:
  • stars bucket -> nếu >threshold -> chia thành pushed date sub-buckets.
  • hoặc chia stars bucket tiếp thành two subranges equally sized bằng thử nghiệm binary search: tăng lower bound cho đến khi mỗi subrange <= threshold.
  1. Hậu kiểm an toàn để tránh bỏ sót
  • Sau khi hoàn tất thu buckets, nếu unioncount < totalcount (API gốc) cần investigate: có thể do total_count cap của API hoặc index lag. Ghi log cảnh báo.
  • Lấy sample dự phòng: chọn ngẫu nhiên N repo (ví dụ N=50) từ bucket boundaries chưa thu hoặc từ kết quả web UI và kiểm tra presence via /repos/:owner/:repo endpoints hoặc contents API để đảm bảo không bỏ sót loại repo bạn thực sự cần.
  • Nếu phát hiện trường hợp index lag (repo mới có file nhưng code search chưa thấy), lập queue re-check sau delay configurable (ví dụ 5-15 phút).
  1. Tối ưu rate limit và checkpoint
  • Mỗi page fetch lưu checkpoint: (bucketid, pagenumber). Nếu pipeline dừng, resume từ checkpoint.
  • Tính toán estimate calls: pagesperbucket = ceil(totalcountbucket / per_page). Nếu số calls quá lớn, phân bố chạy theo thời gian hoặc dùng nhiều tokens.
  • Sử dụng exponential backoff khi gặp 403/429 và đọc X-RateLimit-Reset header để resume.
  1. Ví dụ truy vấn và mô tả
  • Truy vấn gốc repository search (ví dụ):
in:readme ( "machine learning" OR "model serving" ) language:Python archived:false
  • Phát hiện total_count = 25400 -> phân mảnh theo stars. Một bucket ví dụ:
in:readme ( "machine learning" OR "model serving" ) language:Python archived:false stars:>=5000
  • Nếu bucket stars:>=5000 trả total_count 1200 -> vẫn >900 -> phân mảnh bucket này theo pushed date:
in:readme ( "machine learning" OR "model serving" ) language:Python archived:false stars:>=5000 pushed:>2023-01-01
  1. Mẫu pseudo-code Python cho bucket loop
# Ghi chú: đoạn mã là mẫu minh họa, dùng requests hoặc thư viện HTTP bạn chọn.
THRESHOLD = 900
per_page = 100

def search_repos(q, page=1):
    # gọi https://api.github.com/search/repositories?q={q}&per_page={per_page}&page={page}
    # trả về dict with total_count and items
    pass

def collect_bucket(q):
    first = search_repos(q, page=1)
    total = first['total_count']
    if total <= THRESHOLD:
        fetch_all_pages(q, total)
        return
    # nếu vẫn > threshold, chọn field để tách tiếp (stars/pushed)
    # ví dụ: phân mảnh theo stars ranges externally generated
    buckets = generate_stars_buckets()
    for bq in buckets:
        collect_bucket(original_query + ' ' + bq)

def fetch_all_pages(q, total):
    pages = math.ceil(min(total, 1000) / per_page)
    for p in range(1, pages+1):
        resp = search_repos(q, page=p)
        save_items(resp['items'])
        checkpoint_save(q, p)
  1. Ghi log, audit và lý do phân mảnh
  • Mỗi hành động phân mảnh phải ghi: parentquery, childquery, totalcountparent, totalcountchild. Khi xuất CSV cuối cùng, thêm cột source_bucket để trace repo đến query gốc.
  • Lưu tất cả queries dùng để tạo kết quả, kèm timestamp và token id nếu dùng multi-token, để audit khi cần chứng minh vì sao một repo được/không được thu thập.

Kết luận tóm tắt kỹ thuật

  • Phát hiện bằng totalcount và Link header. Phân mảnh bằng fields monotonic như stars hoặc pushed date. Tạo buckets disjoint, ghi metadata trace cho mỗi bucket. Nếu bucket vẫn quá lớn, lồng phân mảnh. Dedupe theo fullname và luôn checkpoint page-level. Thực hiện sample re-check và dùng Contents API cho các trường hợp nhạy cảm để giảm false negative do index lag.

Chiến lược phân mảnh cho code search và lưu ý không đấu filename vào cùng query

Code search khác repository search vì nó hoạt động ở cấp file: mỗi hit là một file trong repo, không phải một bản tóm tắt repo. Do đó các qualifier filename: áp dụng cho file đang match, không ngầm nói "repo có file A và file B". Nếu bạn viết filename:requirements.txt filename:Dockerfile trong một truy vấn, engine sẽ cố match cả qualifier trên cùng một file, nên gần như vô nghĩa cho mục tiêu "repo chứa cả hai file". Quy tắc đầu tiên: luôn chạy mỗi filename một truy vấn riêng và sau đó intersect kết quả theo repo.full_name.

Chiến lược thực thi an toàn và tiết kiệm rate:

  • Thứ tự truy vấn thông minh: chạy trước các filename ít phổ biến (ví dụ Dockerfile) để giảm candidate set nhanh. Lưu tập repo.full_name từ mỗi truy vấn filename, rồi intersect theo yêu cầu AND; nếu chỉ cần OR thì union.
  • Khi code search trả quá nhiều hits (total_count lớn hoặc gặp cap), đừng cố lấy mọi page. Thay vào đó:
  • Phân đoạn theo repository buckets đã lấy từ repository search (chạy code search chỉ trong tập đó bằng repo:owner/name qualifiers) để giới hạn scope.
  • Hoặc sample stratified theo stars/pushed: chọn mẫu tỷ lệ theo buckets popularity để kiểm tra presence file trên các lớp quan tâm.
  • Fallback contents-check khi code search không thực tế: dùng Contents API hoặc Git Trees cho từng repo candidate để kiểm tra existence của path cụ thể (GET /repos/{owner}/{repo}/contents/{path} hoặc GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1). Lợi ích – loại bỏ paging trên code search và cho kết quả xác thực trên branch mặc định; chi phí – một request per check, nên apply chỉ cho candidate shortlist hoặc cho sample.
  • Khi hits quá lớn và bạn cần coverage: phân mảnh code search theo language, path hoặc pushed date; hoặc phân đoạn repo candidates bằng stars/created windows rồi chạy filename queries trong từng bucket. Luôn log bucket, pages fetched và dùng checkpoint để resume.

Lấy kết quả REST API: cách gọi search repositories và search code, pagination và xử lý giới hạn

Mục tiêu: minh họa cách gọi hai endpoint search của GitHub REST API, xử lý phân trang an toàn, đọc total_count và header Link, kiểm soát rate limit bằng header, áp retry/backoff và checkpoint để resume khi bị gián đoạn.

  1. Endpoint mẫu và tham số cơ bản
  • Repository search:
GET https://api.github.com/search/repositories?q=in:readme+("machine+learning"+OR+"deep+learning")+language:Python+stars:>50+archived:false&per_page=100&page=1
  • Code search (file-level):
GET https://api.github.com/search/code?q=filename:requirements.txt+language:Python&per_page=100&page=1
  • Chú ý: per_page tối đa thường là 100. Luôn URL-encode query khi gọi qua HTTP client.
  1. Đọc total_count và phát hiện cap
  • Response JSON có trường total_count. Kiểm tra ngay page 1 để biết tổng kết quả khả dụng.
  • Nếu total_count vượt ngưỡng an toàn (ví dụ bạn đặt threshold nội bộ 900) thì cần phân đoạn truy vấn để tránh bị bỏ sót do giới hạn trả về cho một truy vấn.
  • Lưu ý: total_count phản ánh số kết quả tìm được theo index; dùng nó để quyết định phân mảnh query.
  1. Phân trang thực tế và Link header
  • Khi response có nhiều page, server trả header Link chứa rel="next" và rel="last". Phân tích header này để biết page tiếp theo và tổng page.
  • Luôn lấy perpage=100 khi muốn giảm số gọi. Lặp: request page=1..lastpage hoặc follow rel="next" cho đến khi không còn.
  • Ví dụ trích xuất Link:
Link: <https://api.github.com/search/code?q=...&page=2&per_page=100>; rel="next", <https://api.github.com/search/code?q=...&page=10&per_page=100>; rel="last"
  • Parse để lấy số last page và lặp hoặc follow rel="next".
  1. Xử lý giới hạn trả về lớn (cap) và phân mảnh an toàn
  • Nếu totalcount > thresholdcap (ví dụ 1000) bạn phải tách query thành sub-queries. Hai chiến lược an toàn:
  • Theo metadata range: chia bằng stars, created, hoặc pushed ranges. Ví dụ stars:>=500, stars:100..499, stars:<100.
  • Theo thời gian: pushed:>2023-01-01, pushed:2022-01-01..2022-12-31, v.v.
  • Mỗi sub-query phải đảm bảo total_count nhỏ hơn threshold để thu toàn bộ pages. Ghi log sub-queries để traceability.
  1. Rate limit headers, retry và backoff
  • Mỗi response chứa header liên quan rate limit. Kiểm tra:
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset (epoch seconds khi quota reset)
  • Khi nhận status 403 hoặc 429, đọc X-RateLimit-Reset để biết khi resume. Áp dụng exponential backoff với jitter khi gặp transient errors:
  • initial delay = 1s, multiply by 2 mỗi lần, cap ở 60s, cộng random jitter 0-1000ms.
  • Khi Remaining thấp, tự throttle: sleep until safe window or rotate token nếu có nhiều token.
  1. Checkpointing để resume an toàn
  • Sau mỗi page thành công lưu checkpoint nhỏ:
  • query identifier, bucket id (nếu phân mảnh), current page, timestamp, etag/last-modified nếu có.
  • Lưu checkpoint vào persistent store (file, DB). Khi pipeline khởi động lại, đọc checkpoint và tiếp tục từ page+1 của bucket.
  • Ghi log counts per page để phát hiện mất page.
  1. Xử lý kết quả code search và hợp nhất theo repo
  • Code search trả items file-level. Khi thu repo list cho filename, trích item.repository.full_name và chuẩn hóa lowercase.
  • Lưu sets per filename rồi thực hiện union/intersection theo yêu cầu.
  • Dedupe bằng full_name và, khi cần loại fork, gọi GET /repos/{owner}/{repo} để kiểm tra fork boolean trước khi final.
  1. Mẫu gọi đơn giản bằng Python (requests) với pagination, backoff và checkpoint
# pseudocode, fill token and persistence
import requests, time, random, json
TOKEN = "ghp_xxx"
HEADERS = {"Authorization": f"token {TOKEN}", "Accept": "application/vnd.github.v3+json"}
def fetch_search(q, page):
    url = "https://api.github.com/search/repositories"
    params = {"q": q, "per_page": 100, "page": page}
    r = requests.get(url, headers=HEADERS, params=params)
    return r

# loop with backoff and checkpointing (simplified)
page = load_checkpoint() or 1
while True:
    r = fetch_search(query, page)
    if r.status_code == 200:
        data = r.json()
        process_items(data["items"])
        save_checkpoint(page)
        link = r.headers.get("Link","")
        if 'rel="next"' in link:
            page += 1
            continue
        break
    elif r.status_code in (403,429):
        reset = int(r.headers.get("X-RateLimit-Reset", time.time()+60))
        sleep = max(reset - time.time(), 1)
        time.sleep(sleep + random.random())
    else:
        # transient retry with backoff
        for t in [1,2,4,8]:
            time.sleep(t + random.random())
            r = fetch_search(query, page)
            if r.status_code == 200:
                break
        else:
            raise RuntimeError("failed after retries")
  1. Ghi chú vận hành và kiểm tra
  • Log mỗi query và total_count, pages fetched, và mọi sub-queries khi phân mảnh. Giữ trace để audit thiếu hụt.
  • Kiểm thử nhỏ: chạy query prototype trên web để ước lượng total_count và điều chỉnh phân mảnh trước khi scale.
  • Nếu pipeline tốn nhiều calls, cân nhắc: chạy code search chỉ cho candidate repo từ repository search bằng repo: qualifier để giảm phạm vi, hoặc dùng Contents API cho kiểm tra file existence từng repo.

Kết luận: thực hiện pagination đúng, kiểm tra total_count để phát hiện khi cần phân mảnh, dùng Link header để phân trang, đọc X-RateLimit-* để quyết định retry hoặc sleep, và lưu checkpoint sau mỗi page để resume an toàn. Thiết kế sub-query theo metadata range khi một truy vấn quá rộng để tránh bỏ sót kết quả.

Chuẩn hóa và hợp nhất dữ liệu theo full_name

Mở đầu ngắn: tại bước hợp nhất, key duy nhất để join là repo full_name dưới dạng owner/name. Mình khuyến nghị một quy ước rõ ràng và các bước xử lý để tránh mismatch do chữ hoa, encoding hay khác biệt trả về giữa repository search và code search.

  1. Quy ước chuẩn hóa
  • Luôn lưu full_name chuẩn hóa dạng lowercase, không khoảng trắng thừa, và chuẩn Unicode NFKC. Ví dụ: "Owner/Repo-Name" -> "owner/repo-name".
  • Loại bỏ BOM và trim hai đầu chuỗi. Thay dấu backslash hoặc multiple slashes bằng single slash nếu vô tình có.
  1. Cách extract từ API responses
  • Repository search items: dùng field repository.fullname hoặc item.fullname từ /search/repositories.
  • Code search items: mỗi file hit trả item.repository.full_name trong v3 API. Khi dùng GraphQL, map repository.nameWithOwner.
  • Nếu một response không cung cấp full_name trực tiếp, compose bằng owner.login + "/" + repo.name.
  1. Dedupe và merge
  • Sau thu thập, map mỗi record vào dictionary keyed by normalizedfullname.
  • Khi gặp nhiều nguồn cho cùng fullname, merge theo rule: keep latest metadata fields by pushedat, union boolean flags (hasrequirements, hasdockerfile), aggregate lists (reasons, query_sources).
  • Ghi provenance – lưu which query and which API page tạo entry đầu tiên.
  1. Xử lý encoding và ký tự đặc biệt
  • Trước khi normalize, apply Unicode NFKC normalization để hợp nhất chữ hoa/nhỏ và ký tự kết hợp.
  • Nếu owner hoặc repo name chứa percent-encoding trong web UI, decode percent-encoding trước khi lowercasing.
  • Đối với ký tự không chuẩn (ví dụ unicode lookalikes), chỉ normalise; không tự động replace ký tự bằng ASCII vì owner name thực có thể chứa unicode.
  1. Kiểm nghiệm nhanh
  • Sau hợp nhất, kiểm tra counts: number of unique normalizedfullname phải bằng số dòng output. Nếu giảm đột ngột so với expectation, log sample 50 raw names để debug mismatch.

Kết quả bạn phải giao: một bảng/dataset keyed bằng full_name (lowercase, NFKC) kèm cột cờ cho mỗi filename tìm được, nguồn truy vấn, thời gian lấy và danh sách lý do/nhãn cho accept/reject.

Hậu xử lý: loại fork, mirror, archived và cách tùy chọn giữ lại fork

Mình trình bày một quy trình hậu xử lý rõ ràng để xử lý fork, mirror và archived, kèm chính sách auto-remove, auto-keep và review-queue cùng lý do cho mỗi trường hợp.

  1. Thu thập metadata cơ bản (rẻ)
  • Gọi GET /repos/{owner}/{repo}. Lấy fields: fork (boolean), mirrorurl, archived, stargazerscount, pushed_at, size, parent/source nếu có.
  1. Quy tắc loại mirror
  • Nếu mirror_url != null -> auto-remove. Lý do: mirror thường chỉ sao chép từ nguồn khác; giữ sẽ sinh trùng lặp.
  1. Quy tắc xử lý archived
  • Nếu archived == true -> default là auto-remove. Nếu mục tiêu là snapshot hoặc nghiên cứu lịch sử, chuyển vào review-queue và gắn tag reason="archived".
  1. Quy tắc xử lý fork
  • Bước nhanh sàng lọc:
  • Nếu fork == true và stargazerscount <= 3 và pushedat < 365 ngày và forkscount <= 2 -> candidateauto_remove.
  • Nếu fork == true và (stargazerscount >= 10 OR forkscount > 5 OR releasescount > 0) -> candidateauto_keep.
  • Các trường hợp còn lại -> candidate_review.
  • Lý do: stars/forks/releases là chỉ báo cộng đồng; pushed_at phản ánh hoạt động.
  1. Kiểm tra phát triển độc lập (chi tiết, tốn API)
  • Với candidatereview hoặc candidateauto_remove có nghi ngờ, so sánh commit head:
  • Lấy default branch sha của fork và của parent/source.
  • Nếu sha equal -> identical, an toàn để remove.
  • Nếu not equal then call GET /repos/{owner}/{repo}/compare/{parentdefault}…{forkdefault}. Nếu aheadby > 0 hoặc status != identical -> fork phát triển độc lập -> promote to autokeep hoặc review manual depending on stars.
  • Lý do: so sánh commit xác định xem fork có thay đổi độc lập hay chỉ là bản sao.
  1. Whitelist owners và policy audit
  • Dự trữ danh sách owners/org whitelist; repos của họ không auto-remove even if fork==true.
  • Luôn lưu lý do quyết định và provenance: which checks passed, timestamps, API responses.
  1. Hành động cuối và ghi nhãn
  • Auto-remove: remove from final list; ghi reason như "mirror", "fork-identical", "archived".
  • Auto-keep: giữ trong list; ghi reason như "fork-active", "releases", "stars>=10".
  • Review-queue: xuất CSV riêng gồm fullname, reasonflags, links to compare URL, và hướng dẫn human check (e.g., inspect commits, check releases).

Thực thi theo thứ tự: metadata filter -> cheap heuristics -> compare commit cho nghi ngờ -> quyết định. Luôn cache parent sha và throttle compare calls để tránh vượt rate limit.

Kiểm tồn tại README và ánh xạ ngôn ngữ repo

Mình trình bày bước hậu xử lý đơn lẻ bạn áp lên mỗi repo candidate để (1) xác nhận có README hợp lệ và (2) đánh giá mức độ "Python" của repo bằng languages API. Kết quả dùng để gán cờ và lý do trong CSV (ví dụ hasreadme, readmelen, languageprimary, pythonbytespct, pythonfile_evidence).

  1. Kiểm README bằng REST API
  • Gọi: GET https://api.github.com/repos/:owner/:repo/readme
  • Trường trả về quan trọng: content (base64), encoding, path, sha.
  • Hành động:
  • Nếu response 200: decode base64 content, strip whitespace, lấy độ dài ký tự. Ví dụ ngưỡng tối thiểu: 80 ký tự; nếu <80, mark fail reason "readmetooshort".
  • Nếu 404: mark "no_readme".
  • Ví dụ curl (thay OWNER/REPO và TOKEN):
curl -s -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/repos/OWNER/REPO/readme \
  | jq -r '.content' | base64 --decode > temp_readme.md
wc -c temp_readme.md
  • Ghi lại readmelen (số ký tự) và readmepath.
  1. Ánh xạ ngôn ngữ bằng Languages API
  • Gọi: GET https://api.github.com/repos/:owner/:repo/languages
  • Response là object mapping language -> bytes. Tính tổng bytes and share của Python:
  • pythonbytespct = languages["Python"] / sum(all bytes) * 100
  • language_primary = language có bytes lớn nhất (first key by value)
  • Quy tắc đề xuất để automatic accept là:
  • Nếu languageprimary == "Python" OR pythonbytespct >= 30 => mark "pythonprimary" (high confidence).
  • Nếu 5% <= pythonbytespct < 30 => mark "python_partial" (needs check).
  • Nếu pythonbytespct < 5% và languageprimary != "Python" => mark "pythonabsent".
  1. Nếu primary language khác nhưng có file Python
  • Kiểm chứng file-level evidence (ít tốn hoặc tốn thêm calls):
  • Tùy chọn A (tối ưu rate): gọi code search per-repo cho *.py không phải luôn cần, thay bằng check tồn tại file manifest: GET /repos/:owner/:repo/contents/{path} cho paths phổ biến như requirements.txt, setup.py, pyproject.toml.
  • Tùy chọn B (chắc chắn hơn): GET https://api.github.com/search/code?q=repo:OWNER/REPO+extension:py và lấy count; nếu count>0 => mark "pythonfilespresent".
  • Quy tắc kết hợp:
  • Nếu languageprimary != "Python" nhưng (requirements.txt or setup.py or pyproject.toml exists) OR pythonfilespresent => mark "pythonevidence" với confidence cao hơn.
  • Nếu không có evidence, treat as non-Python.
  1. Ghi nhãn output (CSV/JSON)
  • Các trường gợi ý: fullname, hasreadme (true/false), readmelen, readmepath, languageprimary, pythonbytespct (float), pythonfileevidence (true/false), pythonstatus (pythonprimary | pythonpartial | pythonevidence | pythonabsent), notes.
  • Ví dụ lý do loại: "noreadme", "readmetooshort", "languageabsent", "pythonpartialneedsmanualreview".
  1. Vận hành và lưu ý thực tế
  • Cache languages và readme results một thời gian (ví dụ 24h) để tránh gọi lại nhiều lần.
  • Nếu repo mới pushed gần đây và code search không thấy file bạn cần, cân nhắc re-check sau 5-10 phút do index lag.
  • Nếu bạn cần tuyệt đối chắc chắn về presence của file trên branch non-default, gọi GET /repos/:owner/:repo/branches để lấy branch list rồi kiểm tra contents trên branch đó; đây tốn thêm API calls, chỉ làm cho candidate lọt vào shortlist.

Kết thúc phần: sau bước này, mỗi repo có cờ readme và đánh giá Python rõ ràng để dùng trong rubric tổng thể và cột lý do của file CSV xuất ra.

Áp bộ lọc siêu dữ liệu sau hợp nhất và gán nhãn pass/trượt cho từng tiêu chí

Mình trình bày một checklist thực thi để áp bộ lọc metadata lên tập repo đã hợp nhất (kết quả repository search giao với code search), rồi gán nhãn pass hoặc fail cho từng tiêu chí. Mỗi repo phải được gán một hoặc nhiều reason token để xuất vào cột fail_reasons trong CSV. Các bước dưới đây là thao tác tự động trong pipeline hậu xử lý.

  1. Chuẩn hóa key
  • Key join: full_name lowercased (owner/name). Dùng làm khóa duy nhất trước khi đánh kiểm tra metadata.
  1. Trường metadata cần kiểm
  • stars (stargazers_count)
  • size (repo.size) — trước khi dùng, xác minh đơn vị qua một sample; treat thresholds như heuristic
  • pushed_at (thời điểm push mới nhất)
  • archived (boolean)
  • languageprimary (repository.language) và pythonbytes_pct từ /languages nếu cần
  1. Quy tắc pass/fail cho từng criterion (mẫu ngưỡng đề xuất, bạn chỉnh theo nhu cầu)
  • archived
  • pass: archived == false
  • fail token: archived
  • stars
  • pass: stargazers_count >= 50
  • marginal (manual review): 10 <= stargazerscount < 50 -> token starslow
  • fail: stargazerscount < 10 -> token starstoo_low
  • size
  • action: trước tiên sample vài repo để xác minh unit; dùng threshold tương đối
  • pass: size <= 200000 (heuristic) -> token size_ok
  • warn/review: 200000 < size <= 1000000 -> token sizelargereview
  • fail: size > 1000000 -> token sizetoolarge
  • pushed_at (recency)
  • pass: pushedat within last 12 months -> token recentpush
  • review: 12–36 months -> token stale_medium
  • fail: >36 months -> token stale_old
  • language
  • pass: languageprimary == "Python" OR pythonbytespct >= 30 -> token languagematch
  • review: 5% <= pythonbytespct < 30 -> token language_partial
  • fail: pythonbytespct < 5% -> token language_mismatch
  1. Logic tổng hợp quyết định
  • Tiêu chí bắt buộc (hard-fail): archived -> immediate fail; nếu bạn muốn loại fork ở đây thì thêm fork:true -> token fork_excluded.
  • Nếu có bất kỳ hard-fail token nào, pass_overall = false.
  • Nếu không hard-fail, tính score từ các criterion (ví dụ stars: 0-3 pts, pushed: 0-2, size: 0-1, language: 0-2). Nếu tổng điểm >= thresholdaccept (ví dụ 4) -> passoverall = true, else pass_overall = false and tag reason tokens thu được.
  • Nếu repo thiếu evidence file-level (ví dụ code search đã trả hit nhưng tree check không xác nhận) thì thêm token indexlagor_missing, đặt into manual review bucket rather than final reject.
  1. Cú pháp token và cấu trúc CSV
  • Cột bắt buộc: fullname | htmlurl | stars | size | pushedat | languageprimary | pythonbytespct | hasrequirements | hasdockerfile | passoverall | failreasons
  • fail_reasons là chuỗi phân tách bằng dấu chấm phẩy, ví dụ:
  • "archived;starstoolow;language_mismatch"
  • hoặc "missingDockerfile;sizelarge_review"
  • Gợi ý: thêm cột debugqueries để lưu truy vấn reposearch/code_search đã sinh cho audit.
  1. Kiểm soát an toàn trước khi reject
  • Trước khi reject vì size hoặc stars thấp, nếu repo có hasrequirements == true OR hasdockerfile == true OR readmelen >= 200 thì đẩy vào manualreview thay vì auto-reject.
  • Nếu pushedat gần đây nhưng code search không thấy file đã mong đợi, thêm token indexlag_suspected và queue recheck sau 5–15 phút.
  1. Ghi log và traceability
  • Với mỗi repo, log: applied thresholds, tokens assigned, timestamp, and source queries. Điều này cần trong CSV audit hoặc file log đi kèm.

Áp checklist này vào bước hậu xử lý của pipeline; điều chỉnh ngưỡng theo dữ liệu mẫu của bạn và luôn giữ manual_review bucket cho các trường hợp biên.

Thiết kế rubric đánh giá và loại bỏ: tiêu chí bắt buộc, tiêu chí chấm điểm, ngưỡng quyết định

Mục tiêu: đưa ra một rubric tự động để gán pass/reject/manual-review cho từng repo sau khi đã hợp nhất repository-search và code-search. Mình trình bày: (1) các kiểm tra bắt buộc; (2) các mục chấm điểm với điểm và trọng số; (3) quy tắc quyết định; (4) mẫu lý do tự động để ghi vào cột fail_reasons.

  1. Kiểm tra bắt buộc (hard-fail nếu không thỏa)
  • archived == false => nếu true thì fail token "archived"
  • repository.private == false => nếu true thì fail token "private_repo"
  • hasrequirements OR hasdockerfile tùy policy: nếu bộ tiêu chí yêu cầu cả hai nhưng thiếu file nào thì fail token "missingrequirements" hoặc "missingDockerfile"
  • fork handling: nếu policy đòi không có fork thì fork == false, else annotate is_fork true và tiếp tục
  1. Mục chấm điểm (scoring items)
  • stars: >=500 -> 3 điểm; 100–499 -> 2; 10–99 -> 1; <10 -> 0
  • pushed recency: <6 tháng -> 2; 6–12 tháng -> 1; >12 tháng -> 0
  • language match: primary == Python OR pythonbytespct >=30% -> 2; 10–30% ->1; else 0
  • README quality: exists and len >=200 chars -> 1; else 0
  • size: <=50MB ->1; 50–200MB ->0; >200MB -> -1 (penalty)
  • file evidence: each required file present (requirements.txt, Dockerfile) -> +2 mỗi file
  1. Quy tắc tổng hợp và ngưỡng
  • Tính total_score = sum(points) + penalties
  • Mandatory checks must pass; nếu bất kỳ hard-fail thì final = REJECT
  • Nếu mandatory pass:
  • total_score >= 5 => ACCEPT (auto)
  • totalscore 3–4 => MANUALREVIEW (uy tín trung bình)
  • total_score <=2 => REJECT (auto)
  • Ngoài ra: nếu repo vừa pushedat very recent (<1 giờ) nhưng codesearch thiếu file, gắn token "indexlagsuspected" và đặt vào MANUAL_REVIEW thay vì REJECT.
  1. Cách ghi lý do tự động (fail_reasons)
  • Tạo list token khi kiểm tra từng mục; ví dụ:
  • archived -> "archived"
  • missing Dockerfile -> "missing_Dockerfile"
  • stars low -> "stars<10"
  • language mismatch -> "python_bytes<10%"
  • size large -> "size>200MB"
  • index lag -> "indexlagsuspected"
  • Combine tokens thành chuỗi phân tách bằng dấu chấm phẩy, ví dụ "missing_Dockerfile;stars<10;size>200MB"
  1. Ví dụ cụ thể
  • Repo A: archived=false, stars=120, pushed=3 tháng, pythonbytes=40%, hasrequirements=true, has_dockerfile=true, size=10MB
  • points: stars 2 + pushed 2 + language 2 + readme 1 + files 4 = 11 -> ACCEPT
  • fail_reasons = ""
  • Repo B: archived=false, stars=2, pushed=26 tháng, pythonbytes=2%, hasrequirements=false, has_dockerfile=true, size=300MB
  • mandatory pass; points: stars0+pushed0+language0+readme0+file(1)=1; penalty size -1 => total 0 -> REJECT
  • failreasons = "missingrequirements;stars<10;python_bytes<10%;size>200MB"

Ghi chú vận hành: lưu tokens chi tiết cho audit; giữ manualreview list để rà tay cho các case trung tính hoặc indexlag.

Quy ước đầu ra CSV/JSON: trường bắt buộc và ví dụ lý do loại bỏ có thể sinh tự động

Định nghĩa cột (ngắn gọn)

  • full_name: owner/repo (chuẩn hóa lowercase).
  • html_url: liên kết GitHub tới repo.
  • stars: số stargazers hiện tại (integer).
  • size: kích thước repo theo GitHub metadata (sử dụng như chỉ báo; kiểm tra thêm khi cần).
  • pushed_at: ISO8601 timestamp của lần push gần nhất.
  • language: primary language (repository.language).
  • has_requirements: true nếu tồn tại file requirements.txt (sau kiểm contents/tree).
  • has_dockerfile: true nếu tồn tại Dockerfile (sau kiểm contents/tree).
  • fork: boolean (repo.fork).
  • archived: boolean (repo.archived).
  • score: numeric tổng theo rubric (int hoặc float).
  • pass_bool: true nếu tổng thể đạt ngưỡng accept.
  • fail_reasons: danh sách token lý do loại hoặc flags, phân tách bằng dấu chấm phẩy.

Mẫu dòng CSV (header + một dòng mẫu) fullname,htmlurl,stars,size,pushedat,language,hasrequirements,hasdockerfile,fork,archived,score,passbool,fail_reasons acme/example-repo,https://github.com/acme/example-repo,128,10240,2025-06-01T12:34:56Z,Python,true,true,false,false,9,true,

Mẫu JSON tương ứng (một object) { "fullname": "acme/example-repo", "htmlurl": "https://github.com/acme/example-repo", "stars": 128, "size": 10240, "pushedat": "2025-06-01T12:34:56Z", "language": "Python", "hasrequirements": true, "hasdockerfile": true, "fork": false, "archived": false, "score": 9, "passbool": true, "fail_reasons": "" }

Ví dụ lý do loại tự động (các token phổ biến)

  • archived ; fork ; missingrequirements ; missingdockerfile ; stars<10 ; pythonbytes<10% ; size>200MB ; indexlag_suspected

Ghi chú vận hành ngắn: generate failreasons bằng rules engine sau mỗi kiểm tra; giữ fullname làm khóa để trace và audit.

Kiểm soát chất lượng và tránh bẫy thường gặp

Những bẫy phổ biến và cách né

  • OR và dấu ngoặc: luôn nhóm các biểu thức OR bằng ngoặc tròn. Nếu không, query có thể được hiểu sai và trả kết quả quá rộng hoặc quá hẹp. Ví dụ: in:readme ( "machine learning" OR "deep learning" ) language:Python.
  • Cụm từ có dấu cách: đặt trong dấu nháy kép. Ví dụ "model serving". Thiếu nháy sẽ thành hai token AND, làm lệch kết quả.
  • Filename qualifiers ở cấp file: filename: qualifier là file-level. Nhiều filename trong một query file-level không nghĩa là "repo có cả hai file". Để yêu cầu repo chứa cả requirements.txt và Dockerfile, chạy hai truy vấn code riêng rồi giao nhau theo full_name.
  • Index lag (độ trễ lập chỉ mục): code search và repository metadata cập nhật không đồng bộ. Nếu repo vừa push file, chờ vài phút rồi re-check bằng contents API hoặc git trees trước khi reject.
  • 1000-result cap: Search API có giới hạn trả về cho một truy vấn. Luôn kiểm tra total_count; nếu vượt ngưỡng, phân đoạn query an toàn theo range (stars, pushed date, owner alphabet) và hợp nhất kết quả. Lưu logs của các buckets để audit.
  • Khác biệt web vs API: web UI có thể hiển thị mặt trận tổng hợp, API trả file-level items. Khi tự động hoá, tin vào API và test tương thích cú pháp. Prototype trên web nhưng xác thực bằng API.
  • Rate limits và retries: đọc header X-RateLimit-*, dùng exponential backoff với jitter, checkpoint per-page để resume, và đa token hoặc phân phối chạy nếu lượng lớn.

Checklist kiểm tra chất lượng trước khi khóa danh sách

  • [ ] Query syntax kiểm (ngoặc, nháy, URL-encode khi gọi API).
  • [ ] Kiểm tra total_count trên response đầu; nếu > threshold, bật phân đoạn.
  • [ ] Lấy hết trang theo per_page=100 và lưu checkpoint sau mỗi page.
  • [ ] Chuẩn hóa full_name (lowercase) khi hợp nhất repo và file hits.
  • [ ] Loại trùng và dedupe theo full_name; loại fork chỉ sau khi xác minh repo.fork flag.
  • [ ] Với từng repo sample, verify existence bằng /contents hoặc /git/trees thay vì tin tưởng code search duy nhất.
  • [ ] Ghi lại lý do accept/reject cho mỗi repo trong trường fail_reasons.

Test-sampling tactics (kiểm thử ngẫu nhiên để phát hiện lỗi)

  • Lấy sample ngẫu nhiên 50 repo từ candidate set sau hợp nhất; cho mỗi repo chạy nội kiểm: GET /repos/:owner/:repo/contents/requirements.txt và /Dockerfile. Đo tỷ lệ false negatives và false positives.
  • Nếu false-negative cao, kiểm tra index lag: re-run code search cho sample sau 5-10 phút; nếu tăng thì thêm delay hoặc queue re-check cho repo vừa push.
  • Nếu total_count buckets có chênh lệch lớn, audit buckets: pick 10 buckets lớn nhất, compare counts between web UI và API, document buckets đã phân đoạn.
  • Log mọi query, total_count, pages fetched và thời điểm chạy để có traceability khi audit nguyên nhân bỏ sót hoặc rác.

Ví dụ minh họa chạy đầy đủ với truy vấn đã nêu và diễn giải kết quả

Mục tiêu kịch bản: lấy các repo public có README chứa một trong các cụm "machine learning", "deep learning", hoặc "model serving", ngôn ngữ chính Python, stars >= 50, không archived, và phải có cả requirements.txt và Dockerfile. Kết quả xuất ra CSV với cờ pass/reject và lý do.

  1. Truy vấn repository search (prototype, REST API)
  • Truy vấn con người đọc được:

in:readme ( "machine learning" OR "deep learning" OR "model serving" ) language:Python stars:>50 archived:false pushed:>2021-01-01

  • URL-encoded cho REST call:

q=in%3Areadme+%28%22machine%20learning%22+OR+%22deep%20learning%22+OR+%22model%20serving%22%29+language%3APython+stars%3A%3E50+archived%3Afalse+pushed%3A%3E2021-01-01

  • Endpoint mẫu:

GET https://api.github.com/search/repositories?q=<URL-ENCODED-Q>&per_page=100&page=1

  1. Phát hiện total_count và phân mảnh nếu cần
  • Kiểm tra field totalcount ở response đầu. Nếu totalcount > 900 (ngưỡng an toàn gần limit), phân mảnh theo buckets stars để tránh cap 1000:
  • stars:>1000
  • stars:500..1000
  • stars:100..499
  • stars:50..99
  • Chạy từng bucket tuần tự, ghi log bucket và tổng count bucket để traceability.
  1. Lấy candidate_repos
  • Gộp tất cả pages từ mỗi bucket, chuẩn hóa full_name thành lowercase và dedupe.
  • Lưu candidate_repos set.
  1. Code search per-filename
  • Không cố nhồi nhiều filename vào một query. Thay vào đó chạy hai queries code search riêng:
  • filename:requirements.txt
  • filename:Dockerfile
  • REST endpoints:

GET https://api.github.com/search/code?q=filename%3Arequirements.txt&perpage=100&page=N GET https://api.github.com/search/code?q=filename%3ADockerfile&perpage=100&page=N

  • Từ mỗi hit, extract item.repository.full_name và chuẩn hóa.
  1. Hợp nhất theo repo
  • Tập hasrequirements = set of repo.fullname from first code search
  • Tập has_dockerfile = set from second
  • finalcandidates = candidaterepos ∩ hasrequirements ∩ hasdockerfile
  1. Git tree checks cho top N
  • Sắp xếp finalcandidates theo stargazerscount desc, chọn top N (ví dụ N = 200) để kiểm tra chi tiết bằng git tree.
  • Đối với mỗi repo trong top N:
  • Lấy default branch via repository.default_branch
  • GET /repos/{owner}/{repo}/git/trees/{default_branch}?recursive=1
  • Kiểm tra tồn tại paths chính xác: requirements.txt, Dockerfile, và các biến thể phổ biến (requirements-dev.txt, docker/Dockerfile). Nếu không thấy trên default branch, thử branches phổ biến: main, master, develop (giới hạn tối đa 3 thêm).
  • Nếu file tồn tại, GET /repos/{owner}/{repo}/contents/{path} để lấy content và xác thực token (ví dụ requirements có dấu "==" hoặc Dockerfile có FROM).
  1. Compute rubric cho mỗi repo
  • Mandatory checks (must):
  • archived == false
  • has_requirements confirmed by tree/content
  • has_dockerfile confirmed by tree/content
  • Scoring items:
  • stars: >=500 -> 3, 100..499 -> 2, 50..99 -> 1
  • pushed_at: <6 months -> 2, 6-12 months ->1, else 0
  • language: Python bytes >=20% OR presence of setup.py/pyproject.toml -> +1
  • Decision:
  • pass_overall = true if all mandatory pass and score >= 3
  • else if mandatory pass but score 1-2 -> mark manual_review
  • else reject with reasons aggregated
  1. Export CSV
  • Các cột bắt buộc: fullname | htmlurl | stars | size | pushedat | language | hasrequirements | hasdockerfile | fork | archived | score | passoverall | fail_reasons
  • Mẫu dòng CSV (ví dụ):
full_name,html_url,stars,size,pushed_at,language,has_requirements,has_dockerfile,fork,archived,score,pass_overall,fail_reasons
orgA/model-serving-pkg,https://github.com/orgA/model-serving-pkg,124,1500,2024-03-12,Python,true,true,false,false,5,true,
userB/old-ml-tool,https://github.com/userB/old-ml-tool,12,800,2019-10-01,Python,true,false,false,false,1,false,"missing_dockerfile;old_push"
  1. Ví dụ pseudo-code Python (khung, cần bổ sung token handling, pagination, backoff)
# pseudo-code, not full implementation
def search_repos(q):
  r = requests.get("https://api.github.com/search/repositories", params={"q":q,"per_page":100})
  return r.json()

def search_code(filename):
  q = f"filename:{filename}"
  # paginate and collect repository.full_name from items
  return set_of_full_names

repos = set()
for bucket_q in repo_bucket_queries:
  resp = search_repos(bucket_q)
  if resp['total_count'] == 0: continue
  repos.update(extract_full_names(resp))
# run code searches
req_repos = search_code("requirements.txt")
docker_repos = search_code("Dockerfile")
final = repos & req_repos & docker_repos
# further checks: git trees for top N, compute rubric, output CSV
  1. Diễn giải kết quả và hướng tinh chỉnh
  • Nếu final set quá rộng:
  • Tăng stars threshold, thêm pushed:> date gần hơn, hoặc require language:Python trong repo search thay vì hậu kiểm.
  • Phân mảnh lại buckets nhỏ hơn để tránh bị cap 1000 và hiểu rõ phân bố.
  • Nếu final set quá hẹp:
  • Mở rộng biến thể filename (requirements-dev.txt, Pipfile, pyproject.toml, Dockerfile.dev), hoặc mở rộng in:readme sang in:description OR topics.
  • Giảm stars threshold hoặc bỏ pushed filter tạm thời để kiểm tra recall.
  • Để debug false negatives:
  • Lấy sample repo từ repository search hits nhưng không vào final; chạy tree check thủ công để xem file nằm trên branch non-default hoặc tên biến thể. Nếu nhiều trường hợp như vậy, bổ sung biến thể tên hoặc thêm branch checks.
  • Để kiểm soát rate limit:
  • Cache repo metadata, chỉ gọi git tree cho top N hoặc những repo pass mandatory checks. Sử dụng exponential backoff và checkpoint per-page.

Kết quả cuối cùng là CSV có đủ cột để đồng đội đọc và hiểu lý do accept/reject cho từng repo, cùng logs truy vấn và bucket trace để audit bất kỳ trường hợp bỏ sót nào.

Tự động hóa thực tế: mẫu pseudo-code / cấu trúc script REST API (phases, modules, checkpoints)

Mục tiêu kỹ thuật: cung cấp kiến trúc script để tự động hóa workflow search repositories và search code qua GitHub REST API, có cấu trúc module rõ ràng (searchrepo, searchcode, pagination, merge, postprocess, csv_export), checkpointing per-page/bucket để resume an toàn, quản lý token, xử lý song song an toàn và rate-limit handling.

Kiến trúc cao cấp

  1. Phases (luồng logic)
  • Phase A: repo discovery — chạy một hoặc nhiều truy vấn repository search, với phân mảnh (buckets) nếu total_count vượt threshold.
  • Phase B: code discovery — cho mỗi filename criterion chạy code search riêng, lấy toàn bộ pages, thu set(repofullname).
  • Phase C: merge — hợp nhất candidate repo theo full_name; áp logic giao/hoặc tùy yêu cầu.
  • Phase D: postprocess — fetch metadata repo, loại fork/mirror theo policy, kiểm README/languages, apply rubric scoring.
  • Phase E: export — ghi CSV/JSON và ghi checkpoint final.
  1. Modules (đóng gói theo trách nhiệm)
  • searchrepo: build repo queries, call /search/repositories, detect totalcount, decide buckets.
  • pagination: generic paginator cho REST calls, checkpoint per page and per bucket.
  • searchcode: run code queries per filename, extract repository.fullname, checkpoint per page.
  • merge: normalize full_name, dedupe, intersect/union per rules.
  • postprocess: repo object fetch, languages API, readme/content verification, fork/mirror checks, rubric scoring.
  • csvexport: write CSV/JSON rows, include failreasons list and provenance (which query, which bucket).
  • token_manager: manage token pool, rotate tokens, read rate-limit headers, compute sleep-windows.
  • utils/logging: structured logs with query->pages->counts mapping for audit.

Checkpointing and resume

  • Checkpoint granularity: per-bucket per-page for repo searches; per-filename per-page for code searches; per-repo postprocess state (not_processed, processing, done, failed).
  • Persist checkpoint as small JSON file with timestamps and last success page indices. On restart, resume from first incomplete checkpoint.
  • Example keys:

{ "repobucket:stars1000+": {"page":3,"done":false}, "code_query:requirements.txt":{"page":10,"done":true}, "post:owner/repo":{"state":"done"} }

Token management, parallelism safe và rate-limit handling

  • Token pool: accept list of personal access tokens. Each worker requests a token from token_manager which tracks X-RateLimit-Remaining and X-RateLimit-Reset for that token.
  • Token selection policy: prefer token with largest remaining, or round-robin but check remaining > min_threshold before use.
  • Concurrency control: use bounded worker pool (threads or async tasks) with semaphore limiting simultaneous search requests to search-specific max (search endpoints often stricter).
  • Rate-limit handling: after each request, read response headers:
  • X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After (if present).
  • If remaining <= safety_margin: mark token as sleeping until reset, remove from rotation.
  • On 403/429: exponential backoff with jitter, and requeue the page for later attempt.
  • Backoff strategy: base_sleep = 1s, backoff *= 2 on retries, add random jitter up to 20%.

Pseudo-code kiểu Python (khung, để triển khai)

# pseudo-code (Python-style)
from queue import Queue
import time, json, requests, threading

# modules: token_manager, pagination, search_repo, search_code, merge, postprocess, csv_export

class TokenManager:
    def __init__(self, tokens):
        self.tokens = [{"tok":t,"remaining":99999,"reset":0} for t in tokens]
        self.lock = threading.Lock()
    def acquire(self):
        with self.lock:
            # choose token with remaining > threshold
            for entry in sorted(self.tokens, key=lambda e:-e['remaining']):
                if entry['remaining'] > 10 and time.time() < entry['reset']:
                    return entry['tok']
            # fallback: wait shortest reset
            wait = min(e['reset'] - time.time() for e in self.tokens if e['reset']>time.time())
        time.sleep(max(wait,1))
        return self.acquire()
    def update_from_headers(self, token, headers):
        with self.lock:
            for e in self.tokens:
                if e['tok']==token:
                    e['remaining'] = int(headers.get('X-RateLimit-Remaining', e['remaining']))
                    e['reset'] = int(headers.get('X-RateLimit-Reset', e['reset']))
                    break

def paginated_get(url, params, token_manager, checkpoint_key, checkpoint_store):
    page = checkpoint_store.get(checkpoint_key, {}).get("page", 1)
    while True:
        token = token_manager.acquire()
        params.update({"per_page":100, "page":page})
        resp = requests.get(url, params=params, headers={"Authorization":f"token {token}"})
        token_manager.update_from_headers(token, resp.headers)
        if resp.status_code == 200:
            data = resp.json()
            yield data
            checkpoint_store[checkpoint_key] = {"page": page, "done": False}
            # persist checkpoint_store to disk here
            if not has_next_page(resp): break
            page += 1
        elif resp.status_code in (403,429):
            # rate limited; backoff and retry
            retry_after = int(resp.headers.get("Retry-After", 0))
            time.sleep(max(retry_after, 5))
        else:
            raise RuntimeError(f"HTTP {resp.status_code} for {url}")

# example search_repo usage
def run_repo_bucket(query, bucket_name, token_manager, checkpoint_store, out_set):
    url = "https://api.github.com/search/repositories"
    for page_data in paginated_get(url, {"q":query}, token_manager, f"repo:{bucket_name}", checkpoint_store):
        for item in page_data.get("items",[]):
            out_set.add(item["full_name"].lower())

Thao tác song song an toàn

  • Thiết kế: producer-consumer
  • Producer: spawn tasks để runrepobucket for each bucket sequentially or low-concurrency to avoid hitting search endpoint cap.
  • Consumer: code_search workers that run filename queries in parallel but bounded.
  • Không parallelize phân trang một truy vấn đơn lẻ; thay vào đó parallelize nhiều queries/buckets.

Resume và auditability

  • Sau mỗi page thành công, persist checkpoint file.
  • Ghi logs: query, bucket, page, items_count, timestamp.
  • Khi pipeline hoàn thành, export final CSV cùng checkpoint snapshot used so bạn biết queries và pages đã chạy.

CSV output schema (bắt buộc fields)

  • fullname | htmlurl | stars | size | pushedat | language | hasrequirements | hasdockerfile | fork | archived | score | pass | failreasons | provenance_queries
  • provenance_queries chứa list các truy vấn + bucket names that led to inclusion (giúp audit).

Gợi ý vận hành

  • Run small smoke test on web UI queries to ensure query syntax right, then run REST pipeline on a small bucket first.
  • Start with conservative concurrency and monitor rate-limit headers; tune token pool size and concurrency.
  • Keep checkpoint file in durable storage and rotate tokens periodically.

Phần này cung cấp khung module và kỹ thuật vận hành để bạn xây script thực thi được, có checkpointing per-page/bucket, quản lý token an toàn, và xử lý parallelism với backoff.

Thước đo hiệu năng, logging và traceability để audit lỗi hợp nhất

Mục tiêu: đảm bảo mọi bước trong pipeline có thể truy lại, tái hiện và phân tích root cause khi hợp nhất giữa repository search và code search bị sai. Mình đưa ra tập metrics cần thu, cấu trúc log tối thiểu và quy ước lưu query+khoảng thời gian để audit.

Metrics cơ bản cần thu (per run, per query, per bucket)

  • queriesrun: id truy vấn duy nhất, query string (URL-encoded khi gọi API), querytype (repo|code), bucket_name nếu phân mảnh.
  • totalcountperquery: giá trị totalcount trả về từ API ở page 1.
  • pages_fetched: số pages đã lấy, per query.
  • items_returned: tổng số items thu được (sau dedupe trên query).
  • reposcandidatecount: số repo duy nhất sau gộp từ repository search.
  • reposwithfile_hits: số repo có ít nhất một file hit từ code search per filename.
  • reposafterintersection: số repo ở bước giao giữa repo candidates và các file-sets.
  • falsenegativesamplingmetric: samplesize, sampleids, manualcheckoutcomes (foundmissingfile:boolean), fnrate = foundmissing/ samplesize.
  • ratelimitheaders: X-RateLimit-Remaining, X-RateLimit-Reset, per request where applicable.
  • duration_ms: thời gian xử lý từng page / từng query.
  • apierrors: list (statuscode, endpoint, page, retry_count).
  • dedupe_actions: count duplicates removed and dedupe keys used.

Logging format và nội dung tối thiểu

  • Mỗi bản ghi log phải có timestamp ISO8601, run_id, component, level, message, and structured payload.
  • Ví dụ một log line JSON (ghi vào file dòng-delimited):
{"ts":"2026-07-20T10:12:03Z","run_id":"run-20260720-003","component":"search_repo","query_id":"q1-stars-500+","query":"in:readme (\"machine learning\" OR \"deep learning\") language:Python stars:>500","total_count":1240,"pages_fetched":10,"items_returned":1000,"note":"total_count>1000 triggered segmentation"}
  • Lưu thêm per-request headers snapshot:
{"ts":"2026-07-20T10:12:03Z","run_id":"run-...","component":"http","endpoint":"/search/repositories","page":1,"status":200,"headers":{"X-RateLimit-Remaining":"4800","X-RateLimit-Reset":"162..."}}

Traceability để audit lỗi hợp nhất

  • Giữ mapping queryid -> bucketname -> pages -> itemids -> repofull_name. Mỗi repo record kèm provenance list: which queries contributed it and timestamp.
  • Khi hợp nhất theo fullname, lưu join record: repofullname, sourcesets = ["reposearch:q1","codesearch:requirements.txt:qA"], mergedtime, mergedstatus (passed|rejected|review), fail_reasons.
  • Lưu snapshots of sets at major checkpoints: candidatesbeforecodesearch, candidatesaftereachfilename, finalcandidates. Snapshots dạng newline-delimited fullname lists with header metadata.

False-negative sampling workflow (audit thực tiễn)

  • Sau một run, randomly sample N repos from repocandidates that did not appear in code hits. For each, perform authoritative check using Contents API or git/trees on default branch and up to K common branches. Log result into falsenegativemetric payload. Nếu fnrate > threshold, flag run for recheck and increase wait time for index-lag re-query.

Storage, retention và truy vấn logs

  • Store structured logs và snapshots trong object store (S3) partitioned by run_id and date. Keep index file per run listing queries and buckets.
  • Retention: keep full audit for at least 90 days; keep aggregated metrics for longer.
  • Expose a small audit CSV per run: runid, queryid, totalcount, pagesfetched, reposcandidatecount, reposwithfilehits, fnrate, note.

Practical notes ngắn

  • Luôn lưu nguyên văn query string cùng timestamp và token_id (ẩn token value) để biết token gây rate limit.
  • Khi phát hiện total_count > threshold, log truncation reason and buckets created.
  • Mỗi quyết định loại bỏ repo phải kèm fail_reasons array và provenance để truy vết.

Kịch bản vận hành theo ngân sách API và chính sách retry

Mô tả ngắn: đặt ba chế độ chạy tùy mục tiêu và ngân sách API, ước tính chi phí gọi API theo từng bước, chính sách backoff và resume để an toàn khi bị rate limit hoặc gián đoạn.

  • Các chế độ chạy
  • Fast sampling (low calls)
  • Mục tiêu: test nhanh, shortlist nhỏ.
  • Hành động: 1 repository-search phân đoạn nhẹ; code-search chỉ cho một filename phổ biến; hậu kiểm bằng GET /repos/:owner/:repo/contents cho top-N repo.
  • Tính cost ước tính (per candidate batch): 1 repo-search call per query page + ~Ncontentcalls (N small). Dùng khi cần kết quả trong vài phút.
  • Full harvest (high calls, multi-token)
  • Mục tiêu: lấy toàn bộ tập dữ liệu cho phân tích toàn diện.
  • Hành động: phân mảnh queries để tránh cap 1000 result; chạy code-search cho mọi filename biến thể; phân trang tới hết; cho mỗi candidate gọi repository metadata, languages, readme, git trees as needed.
  • Tính cost ước tính: số queriesrepo × pages + sum(filequeries × pages) + per-repo metadata calls. Chuẩn bị multi-token hoặc tăng quota, chạy theo batch.
  • Incremental nightly run
  • Mục tiêu: cập nhật danh sách, bù index-lag.
  • Hành động: chạy các truy vấn đã lưu, chỉ lấy các repo changed since lastrun (pushed:>lastrun), re-check code-search cho recent pushes, revalidate flagged repos.
  • Tính cost ước tính: thấp hơn full harvest nhiều vì chỉ delta.
  • Cách ước tính chi phí gọi (thực tế)
  • Break down per logical action: searchrepopage, searchcodepage, repometadata, contentscheck, gittree. Tính tổng = Σ(actionscount × costperaction). Tránh hardcode global numbers; thay bằng công thức dựa trên số queries, pages, và candidate repo.
  • Ví dụ mẫu: nếu bạn có 10 repo-search queries × 5 pages = 50 searchrepopage calls; 2 filename queries each with 20 pages = 40 searchcodepage calls; kết hợp 500 candidate repos × 3 metadata/contents calls = 1500 calls. Tổng = 1590 calls.
  • Backoff strategy và retry policy
  • Mỗi request: on 403/429 read X-RateLimit-Remaining and X-RateLimit-Reset. Nếu remaining == 0 sleep until reset. Nếu 5xx or transient network error, apply exponential backoff with full jitter: basedelay = 1s, multiply by 2 each retry, jitter uniform(0, delay). Giới hạn retrycount (ví dụ 5) để tránh vòng lặp vô tận.
  • Throttling chủ động: giới hạn concurrency so với token quota. Nếu dùng nhiều token, shard queries theo token id theo round-robin, track per-token remaining.
  • Idempotency và checkpoint: sau mỗi page hoặc per-repo verification persist checkpoint (queryid, page, lastrepo_processed). Khi retry resume từ checkpoint; tránh duplicate processing bằng idempotent writes.
  • Resume policy và robustness
  • Lưu state durable: run_id, bucket list, pages completed, candidate lists, provenance. Nếu bị kill, resume từ last saved page/bucket.
  • Re-check window for index lag: nếu repo pushed_at within last T minutes and code-search returned no file, schedule re-check after delay window (e.g., 10 minutes) instead of immediate reject.
  • Alerting: nếu many queries hit total_count>threshold or error rate spikes, escalate and pause full harvest.

Những nguyên tắc vận hành ngắn gọn: thiết kế theo buckets nhỏ, track provenance, dùng checkpoint để resume, ưu tiên delta runs cho nightly, và áp retry with exponential backoff plus jitter kết hợp multi-token sharding khi cần mở rộng.

Hướng dẫn thử nghiệm mẫu và đo recall/precision trên tập mẫu

Mục tiêu: chọn mẫu đại diện, tạo ground truth bằng cách đọc git trees, chạy pipeline, so sánh kết quả để tính recall và precision, tune thresholds, và thiết lập báo cáo định kỳ.

  1. Chọn sample
  • Chọn stratified sample theo buckets: stars (high/med/low), size (small/medium/large), pushed recency (recent/old), archived (yes/no).
  • Kích thước mẫu khởi điểm: 300–500 repo chia đều theo buckets. Với ngân sách API thấp, chọn 100–200 repo nhưng vẫn giữ stratification.
  1. Tạo ground truth
  • Với mỗi repo trong mẫu, lấy git tree của default branch bằng API: GET /repos/{owner}/{repo}/git/trees/{branch}?recursive=1.
  • Xác định presence của files cần kiểm thử, ví dụ requirements.txt và Dockerfile, bằng cách tìm path chính xác hoặc các biến thể. Lưu kết quả boolean và note đường dẫn nếu có. Đây là ground truth. Ghi timestamp kiểm tra.
  1. Chạy pipeline under test
  • Chạy chính xác pipeline tự động của bạn, với cùng ngày chạy. Export output list repo được pipeline gán là pass cho tiêu chí file. Lưu method và phiên bản pipeline config.
  1. So sánh và tính metric
  • Precision = TP / (TP + FP) where TP = repo pipeline đánh là có file và ground truth cũng có; FP = pipeline đánh có nhưng ground truth không.
  • Recall = TP / (TP + FN) where FN = pipeline không phát hiện nhưng ground truth có.
  • Tính F1 = 2 precision recall / (precision + recall). Báo cả counts TP, FP, FN, TN.
  1. Phân tích lỗi và tune thresholds
  • Phân loại FN theo nguyên nhân: filename variant, branch non-default, index lag, pagination miss, fork confusion.
  • Với mỗi nguyên nhân, áp biện pháp: thêm biến thể filename queries, thêm re-check branches phổ biến, tăng phân mảnh query để tránh cap 1000, hoặc thêm Contents API kiểm tra per-repo.
  • Sau chỉnh, lặp lại test trên cùng mẫu. Ghi delta metric.
  1. Báo cáo định kỳ và triage
  • Lập báo cáo weekly gồm: queries chạy, total candidates, TP/FP/FN/TN, precision/recall/F1, top 5 lỗi phổ biến, hành động tiếp theo.
  • Nếu pipeline chạy trên toàn bộ corpus, chạy incremental validation mỗi tuần trên mẫu khác 10% so với mẫu trước để tránh overfitting.
  1. Kiểm soát thực thi
  • Ghi provenance: query strings, pages fetched, timestamps, token id. Giữ log để audit khi thấy bất thường.
  • Nếu phát hiện total_count>threshold trong bất kỳ truy vấn nào, đánh dấu bucket đó và kiểm tra phân mảnh lại trước khi chốt kết quả.

Kết quả cần giao: file CSV mẫu chứa fullname | groundtruthhasfiles | pipelinehasfiles | TP/FP/FN flag, cùng summary metrics và notes cho từng lỗi chính.

Danh sách lý do loại bỏ chuẩn để dùng trong cột fail_reasons và cách tự sinh lý do

Mục tiêu: cung cấp tập mã lý do chuẩn để gán tự động khi repo bị loại, cùng quy tắc engine để map các kiểm tra thành lý do rõ ràng, dễ đọc và parse được.

Danh sách mã lý do chuẩn (tên ngắn, dùng trong cột fail_reasons)

  • archived
  • fork
  • mirror
  • missing_requirements
  • missing_dockerfile
  • language_mismatch
  • starsbelowthreshold
  • sizetoolarge
  • inactive
  • no_readme
  • indexlagsuspected
  • rate_limited
  • pagination_truncated
  • missing_license
  • private_repo
  • failscontentvalidation

Quy ước sinh lý do

  • Mỗi repo có thể có nhiều lý do; lưu dưới dạng chuỗi phân tách dấu chấm phẩy. Ví dụ: "missingrequirements;languagemismatch".
  • Luôn kèm metadata ngắn gọn nếu cần debug, theo mẫu key=value trong ngoặc vuông. Ví dụ: missingrequirements[checked=2024-07-01], starsbelow_threshold[value=12,threshold=50].
  • Lý do bắt buộc trước tiên: những lý do kill-switch (archived, private_repo, mirror) được kiểm trước; nếu khớp và policy yêu cầu reject ngay thì dừng kiểm tra tiếp.

Rules engine – xử lý thứ tự và cách map

  • Bước 1: thu metadata repo (repo API): archived, private, fork, mirrorurl, stargazerscount, size, pushed_at, language.
  • Nếu archived true -> add "archived" and stop if policy auto-reject archived.
  • Nếu private true -> add "private_repo" and stop.
  • Nếu mirror_url present -> add "mirror" and stop or flag per policy.
  • Bước 2: siêu dữ liệu đánh giá: stars, size, pushed_at, language
  • Nếu stars < threshold -> add "starsbelowthreshold[…]" (but continue; may still accept via other rules).
  • Nếu size > threshold -> add "sizetoolarge[…]".
  • If primary language absent or low share -> add "language_mismatch[…]".
  • If pushed_at older than inactivity window -> add "inactive[…]".
  • Bước 3: file-presence checks (code search or contents API)
  • Nếu requirements variants none found -> add "missing_requirements".
  • Nếu Dockerfile variants none found -> add "missing_dockerfile".
  • If code search returned results but fewer than expected and pushedat very recent -> add "indexlag_suspected".
  • If contents fetched but file content fails lightweight validation (tokens) -> add "failscontentvalidation".
  • Bước 4: operational checks
  • Nếu a search query hit totalcount cap or API returned truncated results -> add "paginationtruncated".
  • If rate limit encountered during repo processing -> add "rate_limited".

Mẫu logic mapping (ví dụ Python pseudo-code)

def evaluate_repo(repo_meta, file_checks, config):
    reasons = []
    if repo_meta['archived']:
        reasons.append('archived')
        return reasons
    if repo_meta['private']:
        reasons.append('private_repo'); return reasons
    if repo_meta.get('mirror_url'):
        reasons.append('mirror'); return reasons

    if repo_meta['stargazers_count'] < config['min_stars']:
        reasons.append(f"stars_below_threshold[value={repo_meta['stargazers_count']},thr={config['min_stars']}]")
    if repo_meta['size'] > config['max_size_kb']:
        reasons.append(f"size_too_large[value={repo_meta['size']}]")
    if not file_checks['requirements']:
        reasons.append('missing_requirements')
    if not file_checks['dockerfile']:
        reasons.append('missing_dockerfile')
    if file_checks.get('index_lag'):
        reasons.append('index_lag_suspected')
    return reasons

Ghi chú vận hành ngắn

  • Gán lý do ngay khi phát hiện để audit traceability. Lưu cả query và timestamp kiểm tra.
  • Giữ thứ tự kiểm tra nhất quán để dễ đọc logs: kill-switch metadata -> heuristic metadata -> file-presence -> operational.

Checklist deployment và vận hành (runbook)

Mục tiêu: đảm bảo lần chạy production an toàn, có thể tái lập và dễ audit.

Trước khi chạy production

  • Xác thực token và secrets
  • Quay vòng token truy cập API; áp policy rotation định kỳ (ví dụ 30-90 ngày).
  • Kiểm tra quyền token tối thiểu cần thiết; lưu token trong secret manager.
  • CVE và secret scanning
  • Chạy quét secret trên script và config (truffleHog, detect-secrets hoặc dịch vụ tương đương).
  • Áp kiểm tra CVE cho bất kỳ dependency nào dùng để chạy pipeline.
  • Block deploy nếu phát hiện secret rò rỉ hoặc CVE nghiêm trọng.
  • Chuẩn bị dữ liệu đầu ra
  • Định nghĩa schema CSV/JSON đầu ra; tạo thư mục backup versioned.
  • Tự động xuất backup CSV trước khi overwrite; giữ ít nhất 3 phiên bản gần nhất.
  • Giới hạn và cảnh báo
  • Thiết lập alert nếu bất kỳ truy vấn search trả total_count >1000; log query, timestamp, đề xuất phân mảnh.
  • Alert khi thấy rate limit, 403/429 hoặc timeout trong phân trang.
  • Kế hoạch cho index lag và re-run
  • Nếu repo vừa pushed trong window gần (configurable, ví dụ 10 phút) và code search không trả kết quả, đánh dấu để re-run sau delay (ví dụ 5–15 phút) tối đa 3 lần; log lý do.
  • Đánh dấu soft-includes cho trường hợp nghi ngờ index lag, đẩy vào review queue.

Vận hành và hồi phục

  • Checkpointing
  • Ghi checkpoint sau mỗi page/bucket đã fetch để resume khi lỗi.
  • Logging và audit
  • Lưu query, total_count, pages fetched, buckets phân mảnh, và token id dùng.
  • Khi có sự cố
  • Tạm dừng pipeline, snapshot current CSV, thông báo team, điều tra logs, thực hiện re-run theo checkpoint.
  • Quyền truy cập khẩn cấp
  • Danh sách người có thể rotate token và access logs; thực hiện hành động theo playbook đã kí.

Các bước tiếp theo tùy chọn: dùng gh CLI, parallel tokens, hoặc chuyển sang GitHub Enterprise

Mình trình bày khi nào lựa chọn mỗi công cụ hoặc mô hình là hợp lý, những trade-off chính, và gợi ý cách tích hợp vào pipeline đã có để schedule harvest qua CI/Terraform. Đầu tiên xác định mục tiêu vận hành: bạn cần thu thập một lần lớn, chạy định kỳ với độ phủ cao, hay làm harvest incremental với latency thấp. Từ đó chọn đường đi.

  1. Khi gh CLI hữu ích
  • Khi bạn muốn prototype nhanh, chạy kiểm tra trên máy dev hoặc máy CI nhỏ, và dùng flow tương tác. gh support cả search và api wrappers, dễ tích hợp script shell.
  • Ưu điểm: không cần code nhiều, dễ debug, output JSON có thể pipe vào jq để xử lý.
  • Hạn chế: gh CLI vẫn dùng token đơn lẻ, không phù hợp khi cần throughput cao; khó kiểm soát pagination và parallelism ở quy mô lớn.
  1. Khi cần multi-token (parallel tokens)
  • Khi lượng repo/code search lớn và bạn bị rate limit. Phân phối workload qua nhiều tokens giảm thời gian wall-clock. Dùng token pool với round-robin request hoặc per-bucket token assignment (ví dụ mỗi stars-bucket dùng token khác).
  • Ưu điểm: tăng throughput, rút ngắn thời gian harvest.
  • Rủi ro: cần quản lý token rotation an toàn, coord retries, và tránh vi phạm ToS. Cần central rate-limiter để tránh spike trên một token. Log token id (ẩn) để audit.
  1. Khi chuyển sang GitHub Enterprise / GHES
  • Khi bạn có volume cực lớn, cần SLA, hoặc muốn truy cập API enterprise với higher rate limits và private data. GHES/Enterprise Cloud có mức quota khác và thường cho phép enterprise token management.
  • Ưu điểm: rate limits cao hơn, infrastructure controllable, network proximity cho faster cloning.
  • Hạn chế: chi phí, cần admin setup, có thể không phù hợp nếu mục tiêu là public GitHub rộng lớn.
  1. Trade-offs tóm tắt
  • Simplicity (gh CLI) vs Throughput (multi-token) vs Scale+SLA (Enterprise).
  • Chi phí vận hành lên theo thứ tự trên. Độ phức tạp orchestration, secrets management và audit tăng khi dùng multi-token hoặc Enterprise.
  1. Tích hợp CI/Terraform để schedule harvest
  • CI: đặt pipeline job theo cron (GitHub Actions, GitLab CI, Jenkins). Job stages: (a) run repo-search buckets, (b) run per-filename code-search or per-repo contents checks, (c) postprocess + export CSV. Sử dụng job matrix để parallelize buckets; assign each matrix runner a token from secret store.
  • Terraform: quản lý infra cho runners, secrets (vault/Secrets Manager), scheduler infra (Cloud Scheduler) và role bindings cho GitHub App/token. Terraform cũng provision state for checkpoints storage (S3/GCS) để resume.
  • Patterns: rate-limiter sidecar hoặc centralized request queue; token vault rotation via CI secrets; idempotent jobs that write incremental checkpoints after each page/bucket.
  1. Operational cautions
  • Log every query and total_count to detect >1000 cap and force fragmentation.
  • Limit parallelism per token; implement exponential backoff on 403/429.
  • Keep small pilot runs after config changes to validate behavior before full harvest.

Cuối cùng, chọn giải pháp phù hợp với ngân sách và mục tiêu recall/latency. Nếu bạn cần, mình có thể đưa cấu trúc job matrix mẫu cho GitHub Actions + pseudo-workflow phân phối token và checkpointing.

Kết luận

Sau khi triển khai pipeline theo hướng dẫn, bạn sẽ có một luồng nhiều pha đáng tin cậy để tìm, lọc và gán nhãn các repository phù hợp rồi xuất kết quả máy đọc được (CSV/JSON) kèm cờ và lý do. Mình lưu ý thực tế nhất: hãy thiết kế sampling và checkpointing để phát hiện độ trễ chỉ mục và giới hạn trả về của API, nhằm cho phép resume an toàn và tránh mất mát hoặc bỏ sót khi chạy ở quy mô.

Danh mục công cụ SEO chạy cục bộ không cần API

Mình cung cấp khung sàng lọc, bộ lệnh grep mẫu, quy trình kiểm chứng 30 phút và mẫu bảng evidence để bạn thu thập, phân loại và xác minh các repository công cụ SEO chạy cục bộ mà không cần khóa API; kèm chỉ dẫn rõ ràng để ghi nhãn, ưu tiên và bàn giao.

Phạm vi vận hành và định nghĩa thuật ngữ

Mục này định nghĩa ranh giới vận hành cho toàn bộ khảo sát: bạn cần biết chính xác khi nào một repository được xem là "không có kết nối API" để tránh đưa nhầm công cụ phụ thuộc dịch vụ trả phí. Mình trình bày ngắn, thực dụng và có quy tắc áp dụng khi rà soát.

Repo

  • Định nghĩa vận hành: một repository là toàn bộ cây mã nguồn công khai trên Git hosting (ví dụ GitHub, GitLab), bao gồm mã, tài liệu (README, docs/), ví dụ (examples/, fixtures/) và file cấu hình (Dockerfile, CI). Khi mình nói "repo" nghĩa là clone được để kiểm tra build/run cục bộ.
  • Chú ý: monorepo có nhiều subproject tính từng subfolder như một repo logic nếu mỗi subfolder có entrypoint riêng.

Tool SEO

  • Định nghĩa vận hành: phần mềm hoặc tập hợp script/neural pipeline mà mục tiêu chính là hỗ trợ công việc SEO. Công dụng phổ biến: crawl/thu thập dữ liệu, audit on-page, phân tích log, phân tích từ khóa, thu thập SERP, phân tích liên kết nội bộ, sinh sitemap/schema, tối ưu ảnh, đo hiệu năng. Từ "tool" ở đây nhấn mạnh tới sản phẩm có thể chạy và trả output hữu dụng (report, CSV, graph) cho workflow SEO.
  • Không tính: thư viện thuần để dev tích hợp mà không có entrypoint hoặc ví dụ chạy; bài tập học tập nếu không có hướng dẫn chạy.

"Không có kết nối API" – định nghĩa vận hành (cốt lõi)

  • Nguyên tắc: một repo được gắn nhãn "không có kết nối API" nếu chức năng chính của tool có thể chạy và tạo ra output hữu dụng mà không yêu cầu khóa API, client SDK của dịch vụ bên thứ ba, hoặc credential dịch vụ đám mây để hoạt động.
  • Cụ thể:
  • Không yêu cầu người dùng cung cấp biến môi trường kiểu APIKEY, CLIENTID/SECRET, SERVICE_ACCOUNT JSON, hoặc đăng ký tài khoản bên ngoài để chạy chức năng core.
  • Không khởi tạo hay gọi SDK thương hiệu (vd. googleapis, aws-sdk, serpapi client) trong luồng thực thi mặc định để tạo output chính.
  • Việc tool có thêm chế độ "enhancement" dùng API khi người dùng chủ động cung cấp key là chấp nhận được, nhưng phải rõ ràng: core functionality vẫn hoạt động khi không có key.
  • Ghi chú: việc tool truy vấn trang HTML công khai bằng HTTP/HEAD hoặc dùng headless browser để render JavaScript không bị coi là "kết nối API" theo định nghĩa này, miễn là không cần khóa/credential của dịch vụ bên thứ ba.

Ngoại lệ điển hình và cách xử lý

  • Ngoại lệ A – "Mixed-mode" (core offline, enhancement online): repo có core chạy cục bộ nhưng có flag kích hoạt enrichment qua API (ví dụ: enrich keyword data via paid API). Xử lý: chấp nhận repo nếu core thực tế đã được chứng minh chạy offline; khi mô tả, ghi rõ "core không cần API – enrichment có thể dùng API".
  • Ngoại lệ B – "Optional SDK only for CI/tests": repo import SDK chỉ trong test/devDependencies hoặc CI để chạy demo. Xử lý: kiểm tra entrypoint và ví dụ. Nếu production entrypoint không dùng SDK, vẫn coi là không API, kèm note "SDK chỉ dùng cho tests".
  • Ngoại lệ C – "Internal API endpoints": repo gọi domain api.something nhưng đó là endpoint của cùng project hoặc self-hosted service mà người dùng có thể tự deploy. Xử lý: nếu có kịch bản deploy độc lập để chạy local end-to-end, chấp nhận; nếu runtime cần service cloud trả phí, loại khỏi "không API".
  • Ngoại lệ D – "Requires login to target site": tool dùng headless browser và cần tài khoản mục tiêu để thu dữ liệu. Đây là không phải API-key của bên thứ ba nhưng vẫn yêu cầu credentials người dùng. Xử lý: gắn nhãn "yêu cầu login" và không gán nhãn "không API" nếu login bắt buộc cho core.

Lưu ý quan trọng về scraping vs API

  • Phân biệt căn bản:
  • Scraping HTML: truy vấn trang công khai, parse HTML/DOM, thường không cần API key. Đây vẫn có rủi ro ToS, robots.txt và anti-bot.
  • API call: truy vấn endpoint được thiết kế cung cấp dữ liệu programmatic, thường yêu cầu key/quota. Gọi endpoint dạng /api/v1/… hoặc các domain api.* thường là API nhưng không tuyệt đối – cần kiểm chứng runtime.
  • Quy trình kiểm chứng runtime (tóm lược): grep tìm biến env tên KEY/TOKEN, quét dependency SDK, grep URL có api/ hoặc domain bên thứ ba, rồi thực hiện curl thử endpoint không kèm header/auth để xem có trả dữ liệu công khai. Nếu endpoint trả 401/403 hoặc code chờ key/secret hoặc mã nguồn set Authorization header từ env, coi là API-dependent.

Kết luận vận hành ngắn

  • Trong bài tổng hợp, mình chỉ đưa vào những repo mà mình đã kiểm chứng theo định nghĩa trên: core functionality chạy cục bộ không cần khóa/SDK bên thứ ba. Mọi repo mixed-mode hoặc có nghi ngờ đều được ghi chú rõ ràng với evidence và tag tương ứng để bạn và nhóm kỹ thuật dễ rà soát.

Tiêu chí sàng lọc và loại trừ repo

Mục tiêu phần này: cung cấp danh sách kiểm tra bắt buộc và các tiêu chí loại trừ rõ ràng để bạn lọc các repository SEO "không API" hiệu quả, đồng thời cho bộ grep/rg mẫu để tự động hoá bước rà soát ban đầu. Mọi repo vào danh sách cuối phải qua các bước kiểm tra tĩnh và runtime cơ bản theo checklist bên dưới.

Danh sách kiểm tra bắt buộc (must-pass)

  • License hợp lệ
  • Yêu cầu: license mã nguồn mở (MIT, Apache-2.0, BSD, GPL v3 tùy chính sách). Nếu không có license rõ ràng, loại.
  • Kiểm tra nhanh: tìm file LICENSE hoặc trường license trong package.json/pyproject.toml.
  • Cập nhật gần đây
  • Yêu cầu: pushed within 24 months (mốc tính từ ngày rà). Nếu repo không được cập nhật trong >24 tháng, đánh dấu review thêm về tính tương thích/ổn định.
  • Kiểm tra nhanh: xem last commit date trên GitHub hoặc dùng gh api.
  • Mức duy trì tối thiểu
  • Yêu cầu: ít nhất 1 commit trong 24 tháng và/hoặc activity trong issues/PRs cho thấy maintainer responsive. Nếu repo abandoned và không có branch bảo trì, loại hoặc gắn cảnh báo.
  • Manifest dependencies
  • Yêu cầu: có file manifest rõ ràng (requirements.txt, package.json, pyproject.toml, go.mod, Gemfile, composer.json). Nếu không có manifest, phải có Dockerfile hoặc hướng dẫn build reproducible.
  • Presence of CI / Docker secrets indicators
  • Yêu cầu: không để secrets/credentials hardcoded trong repo; nhưng nếu CI workflow tham chiếu tới secrets thì phải kiểm tra xem secrets đó dùng cho feature optional hay cho core. Nếu CI yêu cầu secret để build/run core, loại.
  • Khả năng build/run cơ bản
  • Yêu cầu: repo phải có hướng dẫn Quickstart rõ ràng, hoặc Dockerfile/docker-compose để chạy demo. Nếu không build được trong môi trường thử nghiệm (container) trong 30 phút, loại hoặc gắn trạng thái cần kiểm chứng thêm.

Quy trình rà soát tĩnh nhanh (5-15 phút)

  1. Clone repo hoặc dùng GitHub web tree.
  2. Chạy bộ lệnh tìm nhanh các file và patterns quan trọng. Mẫu dùng ripgrep (rg) hoặc grep -E.

Mẫu lệnh rà soát nhanh:

# tìm license, manifest, docker, ci workflows, env example
rg -n --hidden "(LICENSE|license|requirements.txt|pyproject.toml|package.json|go.mod|Gemfile|composer.json|Dockerfile|docker-compose.yml|.env.example|.github/workflows)" || true

# tìm biến môi trường và từ khóa API
rg -n --hidden "(API_KEY|API-KEY|APIKEY|ACCESS_TOKEN|CLIENT_SECRET|CLIENT_ID|GOOGLE_APPLICATION_CREDENTIALS|OPENAI_API_KEY|SERPAPI_KEY)" || true

# tìm SDK/clients phổ biến
rg -n --hidden "(googleapis|@google-cloud|aws-sdk|boto3|@azure|openai|serpapi|serpstack|semrush|ahrefs|mozapi)" || true

# tìm gọi HTTP trực tiếp có URL literal
rg -n --hidden "(https?://[A-Za-z0-9._:-]+)" || true

# tìm tham chiếu secrets trong GitHub Actions
rg -n --hidden "\${{\s*secrets\.[A-Z0-9_]+}}" .github || true

Tiêu chí loại trừ (hard exclusion) kèm ví dụ grep patterns

  • Yêu cầu API key để chạy chức năng chính
  • Loại ngay nếu README, .env.example, hoặc code explicit yêu cầu API_KEY để produce core output.
  • Patterns:
(?i)\b(API_KEY|OPENAI_API_KEY|SERPAPI_KEY|GOOGLE_API_KEY|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|CLIENT_SECRET)\b
  • Sử dụng SDK/clients thương hiệu cho chức năng chính
  • Nếu dependency list chứa libraries tên dịch vụ thương mại (serpapi, semrush, ahrefs, googleapis, @google-cloud, aws-sdk, openai) và import được dùng trong luồng main -> loại.
  • Mẫu grep:
rg -n --hidden "(serpapi|semrush|ahrefs|googleapis|@google-cloud|aws-sdk|openai)" || true
  • CI/Docker build bắt buộc cần secret ngoài
  • Nếu .github/workflows hoặc Dockerfile bắt buộc đăng nhập vào dịch vụ bên ngoài để build/run core, loại.
  • Pattern:
rg -n --hidden "\${{\s*secrets\.[A-Z0-9_]+}}" .github || true
rg -n --hidden "(aws ecr login|gcloud auth|az login|docker login)" .github Dockerfile || true
  • Repo chỉ là wrapper cho SaaS
  • Nếu code chỉ orchestration gọi API thương mại và không thực thi logic phân tích cục bộ, loại.
  • Tìm các cuộc gọi HTTP trực tiếp đến domains của SaaS:
rg -n --hidden "https?://(api|www)\.(serpapi|semrush|ahrefs|moz|majestic|googleapis|openai|bing)\." || true
  • License không cho phép tái sử dụng theo mục tiêu của bạn
  • Nếu license restrictive (ví dụ không cho phép commercial use) và bạn cần công cụ dùng nội bộ/thuê ngoài, loại.

Các tiêu chí cảnh báo (soft flags) cần review thủ công

  • Presence of headless browser deps (puppeteer, playwright, selenium)
  • Không loại ngay, nhưng cần kiểm tra binary dependency và deploy complexity.
  • High concurrency defaults, no rate-limit config
  • Nếu code mặc định concurrency cao và không có throttle config, gắn cảnh báo operational.
  • Pre-seeded credentials in example files
  • Nếu .env.example chứa token-like strings (không phải ‘CHANGE_ME’), cần manual inspection.

Output checklist mẫu để gắn vào bảng repo (bắt buộc điền)

  • license_status: OK / MISSING / INCOMPATIBLE
  • last_pushed: YYYY-MM-DD
  • active_maintainer: yes/no (commit in 24 months)
  • manifest_present: yes/no (manifest path)
  • apikeyindicators: count + matches (list)
  • sdk_imports: list
  • cisecretrefs: list
  • canbuildquickstart: yes/no (notes)
  • classification: NOAPI / REQUIRESAPI / MIXED / NEEDS_MANUAL

Lưu ý vận hành và tips

  • Không dựa vào README một mình. Luôn quét manifest và CI.
  • Nếu repo là "MIXED" (core offline, enhancement requires API) chấp nhận khi core thực sự chạy offline; ghi rõ tính năng nào cần API.
  • Khi automation báo dấu hiệu API (>=1 indicator), chuyển vào hàng review thủ công; không loại tự động nếu chỉ có 1 dấu hiệu nhẹ như import devDependency.

Kết luận ngắn Dùng checklist bắt buộc trên để sàng lọc nhanh 70–80% trường hợp. Bộ lệnh rg/grep mẫu đưa ở trên là công cụ thực tế để chạy trên từng repo. Mọi repo có dấu hiệu API phải qua bước kiểm chứng runtime trước khi được gạt vào danh sách "không API".

Hệ phân loại (taxonomy) theo chức năng SEO

Mục tiêu: cung cấp khung phân loại rõ ràng để gán nhãn repository công cụ SEO không phụ thuộc API, đồng thời định nghĩa cách quyết định primary/secondary và quy tắc khi một repo đa năng.

11 loại chính (taxonomy)

  1. Crawler / site scraper – thu thập HTML, sitemap discovery, link queueing.
  2. Audit on-page – kiểm tra thẻ meta, heading, canonical, accessibility rules.
  3. Log analysis – parser access log, session stitching, traffic attribution.
  4. Keyword tooling – extractor, n-gram, TF-IDF, corpus analysis từ nội dung cục bộ.
  5. SERP scraper / SERP parser – thu SERP bằng HTML scraping hoặc headless rendering.
  6. Internal link analysis – build site graph, anchor analysis, pagerank nội bộ.
  7. Schema / JSON-LD – validator, generator, snippet templates cho schema.org.
  8. Sitemap generator / parser – tạo sitemap.xml, sitemap index, chunking.
  9. Image SEO tools – alt audit, responsive srcset generator, local compression.
  10. Performance tooling – Lighthouse-like local audits, resource timing aggregator.
  11. Utilities kỹ thuật – canonicalization, redirect checker, robots.txt validator, bulk fetchers.

Tiêu chí để quyết định Primary vs Secondary

  • Primary = chức năng lõi của repo, thể hiện rõ ở một hoặc nhiều tiêu chí sau:
  • Entrypoint rõ ràng: có CLI command, bin script, hoặc module entrypoint chuyên cho chức năng đó. (trọng số cao)
  • Output mẫu: có sample output hoặc example report dành riêng cho chức năng. (trọng số cao)
  • Code chuyên dụng: folder/module riêng chứa logic chính cho chức năng. (trọng số trung bình)
  • Tests / CI: test suite hoặc CI job dành cho chức năng đó. (trọng số trung bình)
  • Docs dedicated: section docs/README mô tả usage và options cho chức năng. (trọng số thấp)
  • Secondary = chức năng hỗ trợ hoặc phụ; thường thỏa ít nhất hai điều:
  • Có module/flag thực hiện tính năng nhưng không phải entrypoint chính.
  • Có ví dụ nhỏ hoặc helper scripts nhưng thiếu test hoặc docs riêng.
  • Code liên quan chiếm diện tích nhỏ so với repo.

Quy tắc gán nhãn vận hành (reproducible)

  • Bằng chứng cần thiết: để gán primary cho type X, repo phải có 1) entrypoint hoặc CLI command để chạy X và 2) sample output hoặc test chứng minh X hoạt động. Nếu thiếu một trong hai, gán là secondary hoặc mixed.
  • Nếu repo chỉ mô tả chức năng bằng văn bản mà không có entrypoint hay sample output, không gán nhãn functional.
  • Luật ưu tiên: nếu nhiều loại thỏa điều kiện primary, chọn primary theo điểm tổng:
  • Entrypoint riêng: +30
  • Sample output: +20
  • Code module >= 20% LOC: +15
  • Tests/CI presence: +10
  • Docs section: +5

Loại có điểm cao nhất là primary; các loại khác đạt ngưỡng >=45% điểm max được gán secondary.

Rules cho repo đa năng (multi-tool)

  • Định nghĩa: đa năng khi có >=2 chức năng độc lập, mỗi chức năng thỏa cả 3 yêu cầu cơ bản: entrypoint riêng, code/module chuyên dụng, và output/example.
  • Trường hợp common pattern: monorepo có subprojects – xử lý per-subfolder. Gán nhãn theo subproject nếu subfolder có entrypoint và docs.
  • Nếu chức năng phụ chỉ là helper/formatter/output exporter thì không coi là chức năng độc lập.
  • Khi repo có nhiều chức năng nhưng chỉ được bật qua flag của một CLI chung, vẫn cho multi-label nếu code được tách module, tests riêng, và docs hướng dẫn chạy từng tính năng độc lập.
  • Metadata yêu cầu: mỗi nhãn secondary phải kèm evidence_links (path tới entrypoint hoặc example) trong bảng tổng hợp.

Ghi chú vận hành ngắn

  • Mọi quyết định nhãn phải lưu evidence: đường dẫn file, dòng README, lệnh CLI dùng thử. Nếu evidence mơ hồ, gán tag NEEDSMANUALREVIEW và không đưa vào primary.

Các ngưỡng và quy tắc phân loại đa năng

Mục tiêu phần này là cung cấp một bộ quy tắc số hóa, có thể lặp lại, để quyết định khi nào một repo SEO được gán multi-label và cách chọn primary_type. Quy tắc dựa trên bằng chứng trong repo: entrypoint, code density, sample output, tests, docs. Mỗi tiêu chí có điểm, tổng điểm quyết định nhãn. Bạn dùng quy trình này để audit nhanh và để bảng tổng hợp có thể tự động xếp hạng.

  1. Nguyên tắc vận hành ngắn
  • Phân loại dựa trên hành vi thực thi và bằng chứng, không dựa trên tiêu đề README hay tag marketing.
  • Mỗi loại chức năng phải có bằng chứng tối thiểu để được gán: entrypoint hoặc module thực thi, và ít nhất một dạng output hoặc test minh họa.
  • Multi-label chỉ dành cho repo có ít nhất hai chức năng độc lập theo định nghĩa dưới đây.
  1. Định nghĩa "chức năng độc lập" (operational)

Một chức năng X được coi là độc lập nếu cùng lúc thỏa cả ba:

  • Entrypoint riêng: có CLI command/module/binary/subfolder chạy X mà không cần khởi chạy toàn bộ hệ thống.
  • Code chuyên dụng: tồn tại module hoặc folder chứa logic rõ rệt cho X (không chỉ vài hàm helper).
  • Output minh chứng: có sample output, example file, hoặc test fixture chỉ rõ kết quả X.
  1. Các tiêu chí đánh giá và điểm số (scoring)

Mỗi repo cho mỗi chức năng ứng viên sẽ được chấm điểm theo các tiêu chí sau. Điểm tổng từ 0 đến 100 cho mỗi chức năng.

  • Entrypoint (0 – 30 điểm)
  • 30 pts: có bin/cli script hoặc package entrypoint riêng cho chức năng; docs có ví dụ run đầy đủ.
  • 20 pts: có CLI flag –mode hoặc subcommand rõ ràng để chạy chức năng, kèm snippet.
  • 10 pts: chỉ có function exportable (module) nhưng không có CLI hướng dẫn.
  • 0 pts: không có entrypoint nào.
  • Code density / separation (0 – 20 điểm)
  • 20 pts: module/folder riêng chiếm >= 30% LOC của repo (sau loại trừ templates/vendor).
  • 15 pts: module riêng chiếm 20-29% LOC.
  • 10 pts: module riêng chiếm 10-19% LOC hoặc >=3 file chuyên dụng.
  • 0 pts: code rải rác, không có module rõ rệt.
  • Sample output / fixtures (0 – 20 điểm)
  • 20 pts: có sample output file (CSV/JSON/HTML/GraphML) khớp với chức năng và có schema.
  • 12 pts: có example snippet trong README hiển thị output.
  • 6 pts: có test fixture nhưng không có output sample.
  • 0 pts: không có.
  • Tests & CI (0 – 15 điểm)
  • 15 pts: có unit/integration tests targeting chức năng + CI job chạy tests.
  • 8 pts: có tests nhưng không CI.
  • 0 pts: không có tests.
  • Docs / Usage section (0 – 15 điểm)
  • 15 pts: docs riêng hoặc dedicated section trong README giải thích toàn bộ workflow chức năng + options.
  • 8 pts: có vài dòng usage hoặc flags.
  • 0 pts: không có docs.
  1. Quy tắc quyết primary_type
  • Tính điểm cho mỗi loại chức năng khả dĩ theo bảng trên. Loại có điểm cao nhất là primary_type.
  • Tỷ lệ an toàn: để chọn primary cần >= 50 điểm. Nếu cao nhất < 50, gắn primary theo highest-score nhưng kèm tag NEEDS_VERIFICATION.
  • Nếu top two types có điểm chênh < 10 và đều >= 40, đánh dấu primary là type thường gặp trong README bằng thứ tự xuất hiện, đồng thời gán multi-label.
  1. Quy tắc gán multi-label
  • Gán multi-label (Secondary types) khi mỗi chức năng phụ đạt tối thiểu 45 điểm theo cùng thang điểm.
  • Nếu một chức năng phụ đạt 30-44 điểm, có thể gán secondary có tag WEAK_EVIDENCE; yêu cầu reviewer chạy kiểm thử nhanh 30 phút để xác nhận trước khi xuất bản.
  • Không gán secondary nếu điểm < 30.
  1. Xử lý repo monorepo và subproject
  • Nếu repo là monorepo, áp quy tắc trên theo subfolder. Mỗi subfolder được coi như một "sub-repo" để tính điểm. Điểm tổng của toàn repo có thể là max(subproject scores) cho primary và các subprojects đủ điều kiện cho secondary.
  1. Cách đo code density thực thi (hướng dẫn nhanh)
  • Loại trừ thư mục: node_modules, vendor, dist, build, .venv.
  • Tính LOC bằng ripgrep + wc hoặc cloc; đo % LOC của folder chức năng / total LOC.
  • Nếu repo nhỏ (< 1000 LOC), ưu tiên count files và function definitions: >=3 file chuyên dụng hoặc >=4 function/class tên liên quan đủ điều kiện thay cho threshold %.
  1. Kiểm chứng tối thiểu trước khi gán multi-label (30 phút rule)

Trước khi public multi-label, thực hiện:

  • Chạy entrypoint chức năng phụ theo docs để sinh sample output (nếu có), hoặc chạy test tương ứng.
  • Nếu không thể chạy do environment hoặc API key, ghi rõ trong notes và không gán secondary; hoặc gán nhưng kèm tag REQUIRESAPICHECK / REQUIRES_BUILD.
  • Ghi artifact: command run + output snippet + đường dẫn file trong bảng.
  1. Template evidence lưu vào bảng tổng hợp

Mỗi nhãn phải kèm cột evidence (copy-paste format):

  • entrypoint: path or command
  • code_path: path/to/module
  • sample_output: path/to/sample.json
  • tests: path/to/tests or CI link
  • docs_section: README#section-link
  • score: numeric

Ví dụ evidence string ngắn:

  • "entrypoint: bin/audit | code: src/audit/ | sample: examples/audit.json | tests: tests/test_audit.py | docs: README#audit | score: 72"
  1. Quy tắc ưu tiên khi có xung đột
  • Nếu primary_type dựa trên điểm nhưng reviewer có thông tin vận hành (ví dụ chức năng phụ thực sự là core trong thực tế usage) thì reviewer có quyền override kèm ghi lý do ngắn trong notes và initial.
  • Override cần 1 reviewer kỹ thuật + 1 biên tập xác nhận.
  1. Ghi nhãn trạng thái chứng thực
  • Tags chuẩn kèm nhãn: VERIFIED, WEAKEVIDENCE, NEEDSVERIFICATION, REQUIRES_API, MULTI.
  • Chỉ repo với primary score >=50 và tag VERIFIED mới được dùng làm "recommendation" trong phần top tools.
  1. Ví dụ minh họa ngắn
  • Repo A: entrypoint crawl (bin/crawl) + sample out crawl.json + code src/crawl (35% LOC) + tests -> crawl score 85 -> primary: Crawl.
  • Repo B: một CLI với flags –crawl –audit nhưng code audit chỉ 2 file, sample output chỉ cho crawl -> crawl score 70, audit score 38 -> primary Crawl; audit not gán secondary (score <45) -> tag audit WEAK_EVIDENCE.
  1. Checklist nhanh để áp quy tắc (copy-paste)
  • [ ] Có entrypoint cho chức năng X? (yes/no)
  • [ ] Có module/folder chuyên dụng? (yes/no)
  • [ ] Có sample output? (yes/no)
  • [ ] Có tests/CI? (yes/no)
  • [ ] Có docs usage? (yes/no)
  • [ ] Tính điểm theo bảng và ghi score
  • [ ] Nếu secondary gán, chạy kiểm thử 30 phút nếu score 30-44
  • [ ] Ghi evidence vào bảng

Kết luận ngắn Bộ quy tắc điểm này giúp bạn đưa việc gán nhãn thành quy trình có thể lặp, audit và tự động hóa một phần. Mọi quyết định vượt ngưỡng hoặc override đều phải ghi bằng chứng vào trường notes để giữ minh bạch khi biên tập bài.

Truy vấn tìm kiếm hiệu quả trên GitHub và GitLab

Mục tiêu phần này là cung cấp tập query mẫu và checklist tìm nhanh repo tool SEO "không cần API" trên GitHub và GitLab, kèm hướng lọc giảm nhiễu. Bạn dùng các query này để thu thập danh sách ban đầu, rồi áp quy trình kiểm mã/runtime để loại bỏ phụ thuộc API.

  1. Nguyên tắc khi tạo query
  • Tìm trong README để ưu tiên repo là tool chứ không phải tutorial: dùng in:readme.
  • Giới hạn thời gian cập nhật: dùng pushed:>=YYYY-MM-DD (24 tháng gần nhất).
  • Loại bỏ fork/archived: archived:false.
  • Ưu tiên repo có entrypoint: tìm package.json, setup.py, bin/, cmd/ hay Dockerfile.
  • Tránh từ khóa gây nhiễu: tutorial, example, course, demo, slides.
  1. Các trường lọc thường dùng (GitHub Advanced Search)
  • in:readme – tìm chuỗi trong README
  • language:Python | language:JavaScript | language:Go – lọc ngôn ngữ
  • pushed:>=2024-07-19 – cập nhật 24 tháng (thay ngày tương ứng)
  • size:>50 – loại repo rất nhỏ (kB)
  • stars:>3 – ưu tiên repo có cộng đồng tối thiểu
  • topic:seo | topic:crawler | topic:cli – khi có topic
  • filename:package.json | path:/bin | filename:Dockerfile – tìm dấu hiệu runnable
  • archived:false – loại kho lưu trữ đã archive
  1. Query mẫu GitHub (copy-paste và sửa pushed: theo ngày bạn chạy)

Crawl / site crawler (Python)

in:readme ("site crawler" OR "site-crawler" OR "crawler") language:Python pushed:>=2024-07-19 archived:false size:>50 stars:>3 filename:requirements.txt

On-page audit CLI (Node)

in:readme ("on-page audit" OR "on page audit" OR "SEO audit") language:JavaScript pushed:>=2024-07-19 archived:false filename:package.json in:readme usage

SERP scraper (HTML scraping, no paid API)

in:readme (serp OR "SERP scraper" OR "search results scraper") (language:Python OR language:JavaScript) pushed:>=2024-07-19 archived:false -in:readme "Google API" -in:readme "API key"

Sitemap generator

in:readme ("sitemap generator" OR "generate sitemap") pushed:>=2024-07-19 archived:false language:Python OR language:Ruby size:>10

Keyword extractor (local)

in:readme ("keyword extractor" OR "keyword tool" OR "ngram") language:Python pushed:>=2024-07-19 archived:false size:>20

Ghi chú: thay stars:>3 nếu bạn muốn mở rộng phạm vi. Khi mở rộng, tăng kiểm chứng sau lọc.

  1. Tránh từ khóa gây nhiễu
  • Tránh các truy vấn chỉ có "seo" hay "keyword" đơn lẻ. Thay bằng chuỗi cụ thể kết hợp CLI/usage.
  • Loại trừ bằng -in:readme tutorial OR demo OR "course" để giảm tutorial repositories.
  • Ví dụ: add -in:readme tutorial -in:readme demo vào query.
  1. Dùng GitLab Explore / API
  • GitLab Explore web: search với từ khóa + sắp xếp theo Last activity; lọc Public projects; dùng ngôn ngữ để thu hẹp.
  • Dùng GitLab API để thu bulk: ví dụ endpoint public gitlab.com:
  • GET https://gitlab.com/api/v4/projects?search=TERM&withprogramminglanguage=python&lastactivityafter=2024-07-19
  • Thực tế: GitLab search kém linh hoạt hơn GitHub; do đó thu thập chủ yếu từ GitHub, bổ sung GitLab cho loại hiếm.
  1. Cách lọc trùng và sắp hàng
  • Lấy fullname (owner/repo) + repourl để dedupe.
  • Sắp theo last push (mới nhất) hoặc theo apidependencyscore (sau khi scan), hoặc theo priority: application usefulness, verified non-API, last commit.
  • Nếu số lượng lớn, ưu tiên review repo có stars>10 trước.
  1. Checklist truy vấn cho thu thập ban đầu (dùng cho automation/nhân sự)
  • Chạy 12 biến thể query cho mỗi loại (kết hợp keywords, language, pushes).
  • Lưu kết quả raw (CSV: fullname, url, description, stars, pushedat, language).
  • Dedupe theo full_name.
  • Lọc nhanh manifest: giữ repo có package.json / requirements.txt / setup.py / Dockerfile / bin/ / cmd/.
  • Chấm điểm sơ bộ phụ thuộc API bằng grep: tìm "API_KEY|APIKEY|service account|google-credentials|serpapi" (thang 0-3).
  • Lấy shortlist 3x số repo cần cho bước runtime check (ví dụ cần 30 repo – shortlist 90).
  1. Những query bổ sung giúp tìm công cụ cục bộ
  • Tìm explicitly "no external API" trong README:
in:readme "no external API" OR in:readme "no API key" pushed:>=2024-07-19
  • Tìm repo có Dockerfile + CLI:
filename:Dockerfile in:readme ("Usage" OR "Quickstart") pushed:>=2024-07-19
  1. Ghi chú vận hành
  • Các query chỉ là bước thu thập ban đầu. Không bao giờ coi một query trả kết quả là xác nhận "không API". Luôn chạy quy trình kiểm tra phụ thuộc API tĩnh và runtime trước khi gán vào danh sách "không API".
  • Giữ log truy vấn và xuất CSV để audit. Với mỗi repo lưu evidence paths: README line, manifest file, sample command.
  1. Mẫu workflow nhanh (áp dụng cho thu 150 candidates)
  • Stage 1 – Query & collect raw (automate 12 queries per type) – export CSV.
  • Stage 2 – Dedupe + manifest filter (package files, Dockerfile) – keep ~3x target.
  • Stage 3 – Static scan grep for API indicators – mark probable API.
  • Stage 4 – Manual/runtime quick test 30 phút cho shortlisted repos.

Dùng những query mẫu ở trên, bạn sẽ thu được candidate chất lượng cao để tiếp quy trình kiểm chứng chi tiết theo checklist đã mô tả. Hãy luôn lưu kết quả truy vấn và bằng chứng để biên tập có thể trích dẫn chính xác khi soạn phần tổng hợp.

Quy trình thu thập bulk và lọc trùng

Mục tiêu: tự động thu gom danh sách repo từ GitHub/GitLab, sàng lọc theo stars/size/pushed date, loại trùng bằng fullname và repourl, và lưu log truy vấn có thể audit được.

Tổng quan bước chính (tự động)

  1. Thu gom candidates bằng API hoặc gh CLI
  • Ví dụ dùng GitHub REST qua gh CLI:
  • gh api -X GET "search/repositories?q=site:github.com+topic:seo+language:Python+stars:>3+pushed:>=2024-07-19&perpage=100" > hitspage1.json
  • Hoặc dùng curl:
  • curl -s -H "Authorization: token $GHTOKEN" "https://api.github.com/search/repositories?q=seo+stars:>3+pushed:>=2024-07-19&perpage=100" > hits_page1.json
  • Lặp qua pages để thu hết; cho GitLab dùng GitLab API tương tự.
  1. Chuẩn hoá metadata và lưu theo bản ghi
  • Từ mỗi API response trích các trường bắt buộc: fullname (owner/repo), repourl (htmlurl), cloneurl, stars, size(kB), pushed_at, language, license, topics.
  • Lưu mỗi repo như một dòng JSON (JSONL) để dễ xử lý: one JSON object per line.
  1. Dedupe ban đầu theo fullname và repourl
  • Sau thu gom nhiều page/queries, gộp tất cả JSONL rồi dedupe:
  • jq -c ‘.items[] | {fullname: .fullname, repourl: .htmlurl, cloneurl: .cloneurl, stars: .stargazerscount, size: .size, pushedat: .pushedat, language: .language}’ hits*.json > all.jsonl
  • jq -s ‘uniqueby(.repourl)’ all.jsonl > dedupbyurl.jsonl
  • jq -s ‘uniqueby(.fullname)’ dedupbyurl.jsonl > dedup_final.jsonl
  • Luôn ưu tiên dedupe theo repourl trước, sau đó theo fullname để tránh forks hoặc chuyển tên repo.
  1. Lọc theo stars/size/pushed date
  • Dùng jq hoặc csvkit để apply filter:
  • jq ‘select(.stars >= 3 and .size >= 20 and (.pushedat >= "2024-07-19"))’ dedupfinal.jsonl > filtered.jsonl
  • Ghi chú: ngưỡng stars/size có thể điều chỉnh theo yêu cầu dự án; giữ cấu hình ở file params.yml để reproducible.
  1. Báo cáo và export cho bước scan tiếp theo
  • Xuất csv cho workflow review:
  • jq -r ‘[.fullname, .repourl, .cloneurl, .stars, .size, .pushedat, .language] | @csv’ filtered.jsonl > shortlist.csv

Quy trình clone bulk (tự động có kiểm soát)

  • Tạo script clone list từ shortlist.csv:
  • xargs -P4 -I{} bash -c ‘git clone –depth 1 {}’ < <(cut -d, -f3 shortlist.csv | tr -d ‘"’)
  • Với nhiều repo, dùng –depth 1 để giảm thời gian và dung lượng. Ghi nhãn commit hash HEAD vào metadata sau clone.

Cách lọ trùng nâng cao

  • Loại forks: kiểm tra .fork = true trong API result; nếu bạn muốn chỉ chính repo chính, loại fork trừ khi fork có activity hơn upstream.
  • Rename history: nếu same fullname nhưng khác repourl (unlikely), giữ bản mới nhất (pushed_at latest).
  • Mirror hoặc duplicate content: dùng checksum của file README.md hoặc đo cosine similarity của README để phát hiện duplicate logic. Nếu README giống > 95% thì coi là duplicate.

Lưu log truy vấn cho audit (bắt buộc)

  • Mỗi lần chạy ghi 1 dòng JSONL log với cấu trúc:
  • timestamp, querystring, apiendpoint, page, totalcount, resultfile
  • Ví dụ dòng log human+machine:
  • 2026-07-19T10:02:00Z | search/repositories?q=seo+stars:>3… | api.github.com | page=1 | total=234 | hits_page1.json
  • Lưu cả raw response file và parsed JSONL; đặt tên file có tiền tố timestamp để dễ truy vết.

Trường metadata bắt buộc vào bảng giao nhận (mẫu)

  • fullname | repourl | cloneurl | stars | sizekb | pushedat | language | license | apidependencyscore | evidencepaths | retrieved_at

Ghi chú vận hành ngắn

  • Chạy thu gom bằng cron hoặc CI job, version script trong repo nội bộ và commit logs. Nếu cần mở rộng, thêm GitLab search và Awesome lists crawl.
  • Luôn dedupe theo repourl trước rồi fullname. Giữ logs chi tiết để audit; mọi quyết định loại trừ phải có evidence path.

Quy trình kiểm tra phụ thuộc API tĩnh (scan code)

Mục tiêu: trong bước sàng lọc nhanh, bạn cần phát hiện các dấu hiệu trong mã nguồn và cấu hình báo rằng repo có phụ thuộc API bên thứ ba hoặc yêu cầu khoá. Quy trình dưới đây là checklist thực thi bằng công cụ dòng lệnh, các regex thực dụng, và tiêu chí chấm điểm flags để đánh giá mức rủi ro. Mình viết theo hướng để bạn có thể chạy tự động trên hàng chục repo rồi trả về bảng kết quả để quyết tiếp.

  1. Tệp và vị trí phải quét
  • File môi trường mẫu và config vận hành: .env, .env.example, .env.sample, docker-compose.yml, .env.production, .env.local, config/.yml|.yaml|*.json, settings.py, config.py.
  • Docker / container / build: Dockerfile, docker-compose.yml, Dockerfile.*.
  • CI / workflows: .github/workflows/*.yml, .gitlab-ci.yml, .circleci/config.yml, Jenkinsfile, .travis.yml.
  • Scripts khởi động / entrypoints: bin/, scripts/, start.sh, entrypoint.sh, Procfile.
  • Manifest phụ thuộc: package.json, requirements.txt, pyproject.toml, Pipfile, go.mod, Gemfile, composer.json.
  • Thư mục examples, demos, notebooks: examples/, demo/, notebooks/*.ipynb.
  • Tài liệu hướng dẫn: README.md, docs/*.
  • Tệp credential mẫu: client_secret.json, credentials.json, service-account.json, .pem, .p12.
  1. Regex mẫu hữu dụng để phát hiện dấu hiệu

Mình đưa các regex để dùng với ripgrep (rg) hoặc grep -E. Chạy trong thư mục repo.

  • Biến môi trường tên khóa:
  • (?i)\b(?:API[-]?KEY|APIKEY|ACCESS[-]?TOKEN|ACCESS[-]?KEY|SECRET[-]?KEY|CLIENT[-]?SECRET|CLIENT[-]?ID|OPENAIAPIKEY|SERPAPI|SERPAPIKEY|GOOGLEAPIKEY|BINGAPI_KEY)\b
  • Header/authorization:
  • (?i)\bAuthorization\b|\bBearer\s+[A-Za-z0-9\-_\.]+|\bx-api-key\b|\bX-Api-Key\b
  • Endpoint domain có vẻ là API:
  • (?i)https?://(?:api|data|services|graph|search|graphql|maps)\.[A-Za-z0-9\.-]+
  • (?i)https?://(?:[\w\.-]+\.)?(?:googleapis|openai|serpapi|serpstack|bing|azure|amazonaws|aws|algolia|ahrefs|semrush|moz|facebook|twitter|linkedin)\.[A-Za-z0-9\.-]+
  • Path chỉ API/graphQL:
  • (?i)/api/|/v[0-9]+/|/graphql\b|/oauth|/token\b|/auth\b
  • SDK imports (manifest + code):
  • Node package.json scan: (?i)"(openai|serpapi|googleapis|aws-sdk|@aws-sdk|@azure|algoliasearch|serpstack|semrush|ahrefs|moz)":
  • Python requirements.txt / imports: (?i)\b(openai|serpapi|google-api-python-client|boto3|azure|algoliasearch|semrush|ahrefs|moz)\b
  • Code import statements: (?m)^\s(?:import|from).(openai|serpapi|google|boto3|azure|algoliasearch|semrush|ahrefs)\b
  • CI secrets usage:
  • (?i)\$\{\{ secrets\.[A-Z0-9_]+ \}\}|\bsecrets\.[A-Z0-9]+\b|\$[A-Z0-9]+
  1. Lệnh cơ bản bằng ripgrep / grep để chạy nhanh
  • Quét biến môi trường tên khóa:
rg -n --hidden "(?i)\b(?:API[_-]?KEY|ACCESS[_-]?TOKEN|CLIENT[_-]?SECRET|OPENAI_API_KEY|SERPAPI|GOOGLE_API_KEY)\b"
  • Quét endpoint REST/graphQL:
rg -n --hidden "(?i)https?://(?:api|data|services|graph|search|graphql|maps)\.[A-Za-z0-9\.-]+|/api/|/graphql\b"
  • Quét SDK trong manifests:
rg -n --hidden "(?i)openai|serpapi|googleapis|aws-sdk|@azure|algoliasearch|semrush|ahrefs|moz" package.json requirements.txt pyproject.toml go.mod Gemfile composer.json || true
  • Quét CI secrets:
rg -n --hidden "\${{ *secrets\.[A-Z0-9_]+ *}}|secrets\.[A-Z0-9_]+|GITHUB_TOKEN|AWS_SECRET_ACCESS_KEY" .github .gitlab-ci.yml || true
  1. Cách tự động hoá chạy trên nhiều repo (outline script)
  • Ý tưởng: với danh sách repo clone sẵn, script chạy các rg command trên và xuất JSON line per repo chứa các flag. Mình mô tả pseudocode bash:
#!/usr/bin/env bash
for repo in $(cat repos.txt); do
  cd "$repo" || continue
  res_env=$(rg -n --hidden "(?i)\b(?:API[_-]?KEY|ACCESS[_-]?TOKEN|CLIENT[_-]?SECRET|OPENAI_API_KEY|SERPAPI)" || true)
  res_sdk=$(rg -n --hidden "(?i)openai|serpapi|googleapis|aws-sdk|semrush|ahrefs" package.json requirements.txt pyproject.toml go.mod || true)
  res_endpoint=$(rg -n --hidden "(?i)https?://(?:api|data|services|graph|search)\." || true)
  res_ci=$(rg -n --hidden "\${{ *secrets\.[A-Z0-9_]+ *}}|GITHUB_TOKEN|AWS_SECRET_ACCESS_KEY" .github .gitlab-ci.yml || true)
  jq -n --arg repo "$repo" --arg env "$res_env" --arg sdk "$res_sdk" --arg endpoint "$res_endpoint" --arg ci "$res_ci" '{repo:$repo, env:($env|length>0), sdk:($sdk|length>0), endpoint:($endpoint|length>0), ci:($ci|length>0)}' >> scan_results.jsonl
done
  1. Tiêu chí chấm điểm flags (scoring) và quyết định
  • Mỗi indicator gán điểm:
  • Credential env var tên rõ ràng (APIKEY, *SECRET, *_TOKEN) : +3
  • SDK import (manifest hoặc code import) : +4
  • Literal external API endpoint với api/ hoặc domain thương hiệu : +3
  • CI secret reference / Docker ENV referencing API key : +3
  • Authorization header usage in code: +2
  • Credential files present (service-account.json, client_secret.json): +5
  • Tổng điểm interpret:
  • 0 – 1 : likely no external API dependency for core feature
  • 2 – 4 : possible hybrid – manual runtime check recommended
  • >=5 : likely requires external API or credentials – exclude for "no-API" list or mark as requires-key
  • Ghi kết luận per repo: apistatus = "NOAPI", "POSSIBLEAPI", "REQUIRESAPI" kèm score và evidence paths.
  1. Gợi ý kiểm tra thêm khi score ở ngưỡng trung gian
  • Nếu score 2-4, mở file location match để xem context: biến env có trong .env.example nhưng code không dùng? SDK chỉ xuất hiện trong devDependencies? Nếu ambiguous, chạy quick runtime test sau bước tĩnh để xác minh.
  1. Lưu kết quả và metadata bắt buộc
  • Mỗi repo output JSONL cần ít nhất:
  • repourl, scandate, apiscore, apistatus, evidencepaths (mảng), topmatches (lấy 5 dòng có match)
  • Ví dụ trường:
  • { "repo":"owner/repo", "date":"2026-07-19", "score":6, "status":"REQUIRES_API", "evidence":["README.md:12","package.json:dependencies.serpapi"], "notes":"uses serpapi SDK" }
  1. Lưu ý vận hành và các cảnh báo
  • Regex có thể đưa false positive. Luôn mở evidence file xem ngữ cảnh, đặc biệt với endpoint domain thuộc chính trang target (ví dụ api.example.com là API public của target site not a third-party).
  • Kiểm tra cách code dùng env var: nếu var chỉ để kích hoạt optional feature, classify là "optional", không loại thẳng.
  • Không vội loại repo chỉ vì một placeholder API_KEY trong .env.example; kết hợp score sẽ giảm false positive.

Kết luận ngắn

  • Dùng bộ tệp cần quét + regex trên để thực hiện scan tĩnh nhanh. Áp scoring như đề xuất để phân loại tự động. Với các repo score cao, loại khỏi danh sách "không API"; với score trung bình, chuyển sang bước runtime 30 phút để xác minh cơ chế gọi endpoint và tính bắt buộc của API key. Nếu bạn muốn, mình có thể cung cấp script hoàn chỉnh (bash + jq) để chạy bulk và xuất bảng scan_results.jsonl theo schema trên.

Quy trình kiểm tra phụ thuộc API runtime trong 30 phút

Mục tiêu: trong 30 phút xác định nhanh liệu một repo SEO có phụ thuộc API bên thứ ba khi chạy chức năng chính hay không. Kết quả phải là một trong ba nhãn rõ ràng: REQUIRESAPI, OFFLINEOK, NEEDS_LOGIN. Dưới đây là quy trình timeboxed, công cụ lệnh có thể sao chép, và quy tắc quyết định dùng evidence thu được.

Thời gian tổng: 30 phút. Phân bổ gợi ý: Tĩnh scan 7 phút, curl endpoint tests 6 phút, chạy sample với mạng vô hiệu 10 phút, quan sát network bằng strace/mitmproxy 6 phút, kết luận 1 phút.

  1. Chuẩn bị nhanh 1 phút
  • Clone repo; mở README để lấy lệnh Quickstart hoặc ví dụ chạy. Ghi lại command ví dụ để chạy sau. Chuẩn bị terminal với rg/ripgrep, curl, docker, strace (Linux), hoặc mitmproxy nếu cần.
  1. Tĩnh scan 7 phút
  • Mục đích: tìm dấu hiệu mạnh báo cần API trước khi chạy runtime.
  • Chạy các lệnh:
rg -n --hidden "(?i)\b(API[_-]?KEY|ACCESS[_-]?TOKEN|CLIENT[_-]?SECRET|OPENAI_API_KEY|SERPAPI|GOOGLE_API_KEY|AWS_ACCESS_KEY_ID)\b" || true
rg -n --hidden "(?i)https?://(?:api|auth|graph|search|v[0-9])\.[A-Za-z0-9\.-]+" || true
rg -n --hidden "(?i)openai|serpapi|googleapis|aws-sdk|boto3|azure|semrush|ahrefs|moz" package.json requirements.txt pyproject.toml go.mod || true
rg -n --hidden "ENV|ARG|secrets\." .github .gitlab-ci.yml Dockerfile docker-compose.yml || true
  • Ghi chép nhanh: nếu thấy biến env kiểu API_KEY, SDK có tên thương hiệu, endpoint api.* hoặc .github/workflows tham chiếu secrets, đánh dấu high suspicion và note file/line.
  1. Curl endpoint tests 6 phút
  • Mục đích: kiểm tra endpoint nghi ngờ có trả dữ liệu công khai hay yêu cầu auth.
  • Từ kết quả tĩnh scan lấy danh sách endpoint nghi ngờ. Với mỗi endpoint:
  • Thử curl đơn giản không header:
curl -i -sS 'https://example.com/api/search?q=test' | head -n 20
  • Nếu trả 200 JSON không yêu cầu auth, note "public JSON".
  • Nếu trả 401/403/302->login hoặc body có thông báo "API key required", note "protected".
  • Nếu trả 429 hoặc trả HTML challenge, note "rate-limited / protected".
  • Kiểm tra cookie/Referer requirement:
curl -i -sS -H "Referer: https://example.com" 'https://example.com/api/search?q=test'
  • Ghi evidence: HTTP status, response snippet, headers Authorization/WWW-Authenticate/Retry-After.
  1. Chạy sample với mạng vô hiệu 10 phút
  • Mục đích: xác minh command Quickstart có thể chạy core feature offline.
  • Nếu repo có Dockerfile/Docker Compose, dùng chế độ network none:
# build nếu cần
docker build -t repo-test . || true
# chạy command ví dụ trong container với network disabled
docker run --rm --network none repo-test <example-command> > run.out 2>&1 || true
  • Nếu không dùng Docker, tắt mạng tạm thời hoặc chặn egress DNS/HTTP (local dev): ví dụ disconnect network adapter hoặc dùng iptables để chặn.
  • Chạy lệnh Quickstart (thay command bằng lệnh từ README). Quan sát:
  • Kết quả A: tool hoàn thành và tạo output tương tự README mà không có lỗi network => OFFLINE_OK.
  • Kết quả B: tool thất bại với error rõ "missing APIKEY" hoặc exception từ SDK init => REQUIRESAPI.
  • Kết quả C: tool vẫn chạy nhưng output giảm chức năng (ví dụ thiếu enrichment) hoặc log chỉ ra "skipping external enrichment" => mark NEEDSLOGIN nếu yêu cầu session, hoặc OFFLINEOK với chú thích "reduced features".
  • Lưu run.out và capture stderr để làm evidence.
  1. Quan sát network calls nếu vẫn mơ hồ 6 phút
  • Nếu tĩnh scan không rõ hoặc chạy sample với network bật mà muốn biết thực tế outbound hosts, dùng một trong hai cách:

A. strace (Linux) – nhanh, không cần proxy

# chạy binary/command dưới strace để log syscalls mạng
strace -f -e trace=network -o /tmp/strace.log <example-command> 2>&1 || true
rg "connect|sendto" /tmp/strace.log || true
  • Kiểm tra các connect tới hostname/IP. Nếu có connect tới api.thirdparty.com hoặc openai.com, đây là evidence mạnh REQUIRES_API.

B. mitmproxy – chi tiết nội dung HTTPS (khi có thể thiết lập)

  • Chạy mitmdump để log requests:
mitmdump -w /tmp/mitmflows -q
# trong terminal khác, export http(s)_proxy
export HTTP_PROXY=http://127.0.0.1:8080
export HTTPS_PROXY=http://127.0.0.1:8080
# chạy example-command
<example-command>
# sau đó:
mitmdump -n -r /tmp/mitmflows --no-upstream-cert=false
  • Lưu ý: mitmproxy cần cài cert trên môi trường test. Nếu quá phức tạp hoặc không được phép, ưu tiên dùng strace.
  1. Quy tắc quyết định nhanh – rule of thumb
  • REQUIRES_API nếu:
  • README/.env.example yêu cầu API_KEY AND code/runtime throws missing-key error; OR
  • strace/mitmproxy shows outbound calls to known third-party API domains; OR
  • repo imports third-party SDK and that SDK is used in code path that produces core output.
  • OFFLINE_OK nếu:
  • Running Quickstart with network disabled produces core output matching README; AND
  • Static scan không show mandatory credential files or CI secrets; AND
  • No outbound connect observed in strace when run with network enabled (or endpoint tests showed public JSON without auth).
  • NEEDS_LOGIN if:
  • Endpoint requires session cookies or 302->login redirect and repo uses headless browser/login flows; or
  • Quickstart runs only when provided site credentials; classify as needs-login, not REQUIRES_API.
  1. Ghi chép evidence và báo cáo 1 phút
  • Ghi 3 dòng kết luận: repourl, verdict (REQUIRESAPI / OFFLINEOK / NEEDSLOGIN), evidence pointers (file:line, curl output snippet, strace lines, run.out).
  • Nếu verdict là POSSIBLE/ambiguous, đánh dấu cho deeper review và không đưa vào danh sách "no-API" công khai.

Lưu ý an toàn và đạo đức

  • Chạy curl tests và probing với giới hạn thấp để tránh gây traffic lớn. Nếu target là site ngoài, dùng sample local HTML thay vì thử hàng loạt trên production. Tuân thủ robots.txt và chính sách nội bộ khi thực hiện probing.

Kết thúc: theo quy trình này bạn sẽ có evidence tái lập được trong 30 phút để gắn nhãn repo cho danh sách tổng hợp một cách an toàn và có thể giải trình.

Mẫu script tự động hoá quét dấu hiệu API (outline, bash + ripgrep + jq + curl)

Mục tiêu: quét nhanh một repo local, thu các dấu hiệu API (env var, SDK, endpoint, CI secrets), thử curl endpoint công khai an toàn, và xuất kết quả JSON để load vào bảng tổng hợp.

Lưu ý vận hành ngắn

  • Chạy trên clone local của repo. Không tự động truy vấn nhiều host ngoài. Curl chỉ test nhẹ, rate-limited và không gửi credentials.
  • Nếu bạn muốn chạy bulk, lặp gọi script cho mỗi repo clone.
  1. Các pattern dùng (ripgrep)
  • Biến môi trường/placeholder: APIKEY, ACCESSTOKEN, CLIENTSECRET, GOOGLEAPPLICATIONCREDENTIALS, AWSSECRETACCESSKEY
  • SDK names: googleapis, boto3, aws-sdk, openai, serpapi, serpstack, semrush, ahrefs, azure
  • Endpoint API-like: domain với api., path chứa /api/, /v1/, /graphql, /token, /oauth
  1. Script outline (bash)
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="${1:-.}"
OUT_JSON="${2:-scan-result.json}"
TS=$(date --iso-8601=seconds)

cd "$REPO_DIR"

# 1. static scans with ripgrep
rg --hidden -n --no-ignore -S -e '(?i)\b(API[_-]?KEY|ACCESS[_-]?TOKEN|CLIENT[_-]?SECRET|GOOGLE_APPLICATION_CREDENTIALS|AWS_SECRET_ACCESS_KEY)\b' || true > /tmp/rg_env.txt
rg --hidden -n --no-ignore -S -e '(?i)(googleapis|boto3|aws-sdk|openai|serpapi|semrush|ahrefs|azure)' package.json requirements.txt pyproject.toml go.mod Gemfile || true > /tmp/rg_sdk.txt
rg --hidden -n --no-ignore -o -e 'https?://[A-Za-z0-9\.\-_/:%?&=]+' || true > /tmp/rg_urls.txt
rg --hidden -n --no-ignore -S -e '(?i)secrets\.|env:\s|ACTIONS_SECRET' .github .gitlab-ci.yml docker-compose.yml Dockerfile || true > /tmp/rg_ci.txt

# 2. extract candidate endpoints (unique, filter local)
grep -Eo 'https?://[A-Za-z0-9\.\-_/:%?&=]+' /tmp/rg_urls.txt | sort -u > /tmp/urls_all.txt
# filter out localhost and file urls
rg -v 'localhost|127\.0\.0\.1|file:|/static/|/assets/' /tmp/urls_all.txt > /tmp/urls_external.txt || true

# 3. light curl tests (non-destructive) - only GET, 5s timeout, stop after 10 urls
MAX=10
CNT=0
> /tmp/endpoint_tests.json
while read -r url && [ "$CNT" -lt "$MAX" ]; do
  CNT=$((CNT+1))
  # safe GET without cookies, no auth headers
  out=$(mktemp)
  http_status=$(curl -sS -I -L --max-time 5 "$url" 2>/dev/null | head -n 1 | awk '{print $2}' || echo "")
  echo "{\"url\":\"$url\",\"http_status\":\"$http_status\"}" >> /tmp/endpoint_tests.json
done < /tmp/urls_external.txt

# 4. build result JSON fields
jq -n \
  --arg repo_dir "$REPO_DIR" \
  --arg timestamp "$TS" \
  --slurpfile env /tmp/rg_env.txt \
  --slurpfile sdk /tmp/rg_sdk.txt \
  --slurpfile urls /tmp/urls_external.txt \
  --slurpfile ci /tmp/rg_ci.txt \
  --slurpfile tests /tmp/endpoint_tests.json \
  '{
    repo_path: $repo_dir,
    timestamp: $timestamp,
    env_matches: ($env | map(.)),
    sdk_matches: ($sdk | map(.)),
    external_urls: ($urls | map(.)),
    ci_matches: ($ci | map(.)),
    endpoint_tests_raw: ($tests | map(fromjson? // .)),
    verdict: (if ($sdk|length) > 0 or ($env|length) > 0 or ($ci|length) > 0 then "POSSIBLE_API" elif ($urls|length) > 0 then "POSSIBLE_EXTERNAL_ENDPOINTS" else "NO_OBVIOUS_API" end)
  }' > "$OUT_JSON"

echo "Wrote $OUT_JSON"
  1. Giải thích các bước chính
  • Step 1: ripgrep scan tĩnh cho env vars, SDK names, URL literal, CI references. Lưu kết quả để tra cứu.
  • Step 2: gom URL ngoài, loại bỏ localhost/file.
  • Step 3: curl nhẹ tối đa N urls để xem status code – không gửi auth. Nếu 401/403/401+WWW-Authenticate xuất hiện thì đó là dấu hiệu mạnh cần API/cred.
  • Step 4: tổng hợp JSON giúp nhập vào bảng.
  1. Các trường JSON để điền vào bảng tổng hợp (bắt buộc)
  • repopath – đường dẫn repo local hoặc repourl khi thu từ Git remote
  • timestamp – thời điểm quét
  • env_matches – mảng dòng tìm thấy chứa tên biến môi trường (file:line)
  • sdk_matches – mảng dòng tìm thấy tên SDK/dependency (file:line)
  • external_urls – mảng URL literal phát hiện trong code/docs
  • ci_matches – mảng dòng từ CI/CD/Dockerfile có tham chiếu secrets/env
  • endpointtestsraw – mảng objects { url, http_status } từ curl thử nghiệm
  • verdict – one of NOOBVIOUSAPI | POSSIBLEEXTERNALENDPOINTS | POSSIBLE_API
  • notes – reviewer note ngắn (nên fill thủ công)
  • reviewer – initials của người quét
  1. Hướng dùng nhanh
  • Chạy script: ./scan-api-signs.sh /path/to/repo result.json
  • Mở result.json bằng jq để review: jq ‘.’ result.json
  • Quy tắc quyết định khi import: nếu verdict==POSSIBLEAPI hoặc endpointtests shows 401/403 -> mark NEEDSMANUALREVIEW và run runtime check 30 phút theo checklist.
  1. Ghi chú an toàn
  • Curl chỉ GET, timeouts ngắn. Không cố gắng brute-force hay đăng nhập. Nếu script phát hiện domain thương mại (serpapi, openai, googleapis) treat as high-risk ngay và không thử thêm.

Bảng dữ liệu tổng hợp repo và trường bắt buộc

Mục tiêu của bảng là chuẩn hóa thông tin đầu vào để đội viết và reviewer có thể so sánh, lọc và kiểm chứng nhanh. Mình trình bày mẫu schema (CSV / Google Sheet) và giải thích từng trường, định dạng giá trị, quy tắc điền, và ví dụ mẫu để bạn dùng trực tiếp.

Mẫu header CSV / Google Sheet (1 dòng)

  • repourl | primarytype | secondarytypes | evidenceentrypoints | apidependencyscore | lastcommit | license | runstatus | reviewer | notes

Giải thích và quy tắc điền từng trường

  • repo_url
  • Nội dung: URL đầy đủ tới repo (https://github.com/owner/name hoặc gitlab.com/…).
  • Quy tắc: bắt buộc, duy nhất, không dùng redirect. Nếu repo là monorepo và bạn phân loại subproject, ghi thêm subpath trong notes.
  • Ví dụ: https://github.com/example/seo-crawler
  • primary_type
  • Nội dung: loại chức năng chính theo taxonomy của bạn (ví dụ crawl, auditonpage, loganalysis, keywordtool, serpscraper, internallink, schema, sitemap, image_seo, performance, utilities).
  • Quy tắc: một giá trị duy nhất, dùng từ khóa chuẩn trong taxonomy. Nếu không rõ, để "utilities" và note giải thích.
  • Ví dụ: crawl
  • secondary_types
  • Nội dung: các loại phụ, nếu có, phân cách bằng dấu phẩy (no space) hoặc pipe nếu bạn thích (ví dụ auditonpage,internal_link).
  • Quy tắc: dùng cùng vocabulary như primary_type. Nếu không có, để trống.
  • Ví dụ: auditonpage,internal_link
  • evidence_entrypoints
  • Nội dung: chứng cứ cho entrypoint chạy được độc lập. Ghi dạng list; mỗi mục là "type|path|command" để máy đọc và con người hiểu.
  • Format khuyến nghị: entrytype|fileorcommand|short_note
  • entrytype ∈ {cli, module, docker, example}
  • fileorcommand: đường dẫn file (bin/cli) hoặc command ví dụ
  • short_note: 1-6 từ mô tả
  • Ví dụ:
  • cli|bin/seo-crawl|"usage: crawl –start"
  • docker|Dockerfile| "docker-compose demo"
  • module|src/audit/main.py|"importable audit.run()"
  • Quy tắc: ít nhất một entry nếu repo cho là runnable. Nếu chỉ là library, nêu module importable.
  • apidependencyscore
  • Nội dung: chỉ số đánh giá phụ thuộc API theo quy 0-10.
  • Định nghĩa:
  • 0 = Không dấu hiệu API; core chạy offline.
  • 1-3 = Có vài dấu hiệu mơ hồ (endpoint public, optional enrichment).
  • 4-6 = Có SDK hoặc env var mẫu; có khả năng cần key cho một số tính năng.
  • 7-9 = Rõ ràng phụ thuộc API cho chức năng chính.
  • 10 = Bắt buộc API key/SDK; không thể chạy core without key.
  • Quy tắc: reviewer ghi số nguyên. Luôn kèm notes giải thích ngắn (ví dụ "SDK openai in deps" hoặc "requests to api.serpapi.com observed").
  • Ví dụ: 2
  • last_commit
  • Nội dung: timestamp ISO (YYYY-MM-DD) của commit gần nhất trên default branch.
  • Quy tắc: bắt buộc; dùng git API hoặc gh cli để lấy. Nếu repo archived, thêm chú thích trong notes.
  • Ví dụ: 2026-04-12
  • license
  • Nội dung: ký hiệu license chuẩn (SPDX) hoặc "unknown".
  • Quy tắc: ưu tiên SPDX id (MIT, Apache-2.0, GPL-3.0-only). Nếu multiple, liệt kê cả và note.
  • Ví dụ: MIT
  • run_status
  • Nội dung: trạng thái chạy kiểm thử nhanh nhất (one of predefined).
  • Giá trị phép: runnable | requiressetup | needsapikeys | failsbuild | manualreviewneeded | unknown
  • Giải thích ngắn:
  • runnable: chạy local với sample input và không cần key.
  • requires_setup: cần DB, binary, hoặc steps but no external API.
  • needsapikeys: core yêu cầu key; không thể tiếp tục.
  • fails_build: không build/run trong 30 phút.
  • manualreviewneeded: ambiguous, cần reviewer deeper check.
  • Ví dụ: requires_setup
  • reviewer
  • Nội dung: initials hoặc username người kiểm tra (ví dụ TD, minh).
  • Quy tắc: bắt buộc cho mỗi dòng sau khi được triage; dùng định dạng consistent.
  • Ví dụ: annt
  • notes
  • Nội dung: ghi chú ngắn, evidence summary hoặc cảnh báo pháp lý. Không dài quá 200 chữ.
  • Quy tắc: phải nêu bằng chứng cụ thể ngắn gọn, ví dụ file path hoặc log snippet, và tag [KHÔNG CHẮC] nếu còn mơ hồ.
  • Ví dụ: "uses puppeteer; Dockerfile lacks chrome binary; optional SERP enrich via serpapi (env SERPAPI_KEY) — [KHÔNG CHẮC]"

Một số hướng dẫn vận hành thực tế khi điền bảng

  • Nếu evidence_entrypoints liệt kê nhiều mục, sắp theo độ ưu tiên: docker, cli, module, example.
  • Luôn đặt apidependencyscore kèm một short note giải thích (cột notes). Không để score trống.
  • lastcommit là chỉ số duy trì; nếu >24 tháng, mark runstatus manualreviewneeded.
  • Khi runstatus == needsapikeys, trong notes nêu rõ biến môi trường hoặc domain API (vd. OPENAIAPI_KEY, api.serpapi.com).
  • Đặt reviewer khác với người thu thập ban đầu để có kiểm chứng độc lập.

Ví dụ một hàng CSV hoàn chỉnh

  • https://github.com/example/seo-crawler | crawl | auditonpage | cli|bin/seo-crawl|"usage: crawl –start";docker|Dockerfile|"compose demo" | 2 | 2026-04-12 | MIT | requiressetup | annt | "crawler core runs offline; optional SERP enrich needs SERPAPIKEY in .env.example"

Kết thúc: mẫu này vừa cho máy đọc dễ parse, vừa đủ thông tin cho biên tập và reviewer ra quyết định nhanh. Nếu bạn muốn, mình sẽ xuất kèm mẫu Google Sheet với validation rules (drop-down cho primarytype và runstatus, numeric validation cho apidependencyscore) để paste trực tiếp vào team.

Quy trình kiểm thử nhanh 30 phút cho mỗi repo

Mục tiêu: trong tối đa 30 phút xác định nhanh liệu một repo công cụ SEO có phù hợp với tiêu chí "không phụ thuộc API" và thu đủ bằng chứng để ghi vào bảng tổng hợp. Quy trình này timeboxed, lặp được, và cho ra kết luận tạm thời: KHÔNG-API | CẦN-API | YÊU CẦU-LOGIN | MANUAL_REVIEW. Mỗi bước có lệnh mẫu để bạn chạy và thu bằng chứng.

0: Chuẩn bị nhanh (trước khi bắt đầu)

  • Mở terminal, đảm bảo có rg (ripgrep), curl, docker, jq, git. Nếu có mitmproxy/strace thì tốt để bắt kết nối.
  • Tạo thư mục evidence/<repo-name>/ để lưu stdout, file log và snapshot.
  1. 0-5 phút – đọc README + manifest

Mục tiêu: tìm bằng chứng trực tiếp về cách chạy, nhu cầu API và ví dụ dùng. Các việc thực hiện:

  • Mở README.md, tìm sections: Usage, Quickstart, Requirements, Environment.
  • Mở manifest files: package.json, requirements.txt, pyproject.toml, go.mod, Dockerfile, docker-compose.yml, .env.example, .github/workflows/*.yml.
  • Ghi nhanh vào notes: có lệnh CLI sample không? có .env.example chứa *_KEY? có Dockerfile kéo binary từ external?

Lệnh mẫu:

# clone once
git clone <repo_url> repo && cd repo

# save README head lines
sed -n '1,200p' README.md > ../evidence/README_snippet.txt

# list common manifests
ls -1 package.json requirements.txt pyproject.toml go.mod Dockerfile docker-compose.yml .env.example .github/workflows 2>/dev/null > ../evidence/manifests.txt

Kết quả mong đợi: nếu README có example command và không nhắc API key, gắn flag "READMEOK". Nếu .env.example có APIKEY hoặc CI workflow refs secrets, gắn flag "POTENTIAL_API".

  1. 5-10 phút – scan tĩnh mã nguồn bằng regex

Mục tiêu: tìm dấu hiệu SDK, biến môi trường API, endpoint cứng, hoặc thư viện cloud. Việc thực hiện:

  • Chạy ripgrep với bộ pattern quan trọng; lưu kết quả vào file evidence.

Lệnh mẫu:

# patterns cơ bản
rg -n --hidden --no-ignore -S "(API[_-]?KEY|APIKEY|ACCESS_TOKEN|CLIENT_SECRET|OPENAI|SERPAPI|GOOGLE_API|AWS_ACCESS_KEY_ID|AZURE|serpapi|googleapis|aws-sdk|boto3|requests_oauthlib|openai|serper|semrush|ahrefs)" > ../evidence/rg_keys.txt || true

# tìm URL endpoint literal
rg -n --hidden --no-ignore -o "https?://[^\s'\"`]+" > ../evidence/rg_urls.txt || true

# tìm HTTP client calls
rg -n --hidden --no-ignore "(requests\.get|axios\(|fetch\(|http\.Get\(|curl )" > ../evidence/rg_http_calls.txt || true

Cách đọc nhanh:

  • Nếu thấy SDK tên thương hiệu hoặc biến env *APIKEY -> tăng đánh dấu "CẦN-API".
  • Nếu chỉ thấy requests to google.com/search hoặc other public pages -> khả năng là scraping HTML, không API key.
  1. 10-20 phút – chạy sample nhanh (network on, rồi off)

Mục tiêu: reproduce example, quan sát runtime behavior và xác định dependency network. Phần A – chạy sample với network on:

  • Nếu README có command sample, chạy với verbose/–dry-run nếu có. Lưu stdout/stderr.

Lệnh mẫu:

# ví dụ chạy CLI nếu có
# thay bằng command thực tế từ README
./bin/seo-tool --example-flag > ../evidence/run_on_stdout.txt 2> ../evidence/run_on_stderr.txt || true
  • Quan sát logs cho các host được gọi. Nếu không có logs, bật tracing network đơn giản:
# chạy trong background để log connections (Linux)
sudo ss -tnp | rg seo-tool > ../evidence/ss_before.txt
# hoặc dùng strace để log connect syscalls nếu binary/entrypoint đơn giản
strace -f -e trace=network -o ../evidence/strace.log ./bin/seo-tool --example-flag 2>&1 || true

Phần B – chạy sample với network disabled:

  • Mục đích: xem tool có hoạt động core khi không có internet.
  • Cách nhanh: dùng docker run –network none nếu có Dockerfile, hoặc tắt network tạm thời (cẩn thận).
# nếu có Dockerfile with ENTRYPOINT
docker build -t temp-repo . 2>/dev/null || true
docker run --rm --network none -v $(pwd):/work -w /work temp-repo ./bin/seo-tool --example-flag > ../evidence/run_off_stdout.txt 2> ../evidence/run_off_stderr.txt || true

Kết luận tạm thời:

  • Nếu sample chạy và tạo output tương tự cả khi network off -> classify KHÔNG-API (core runs offline).
  • Nếu chạy thất bại với error về missing API key or unable to reach api.domain -> classify CẦN-API.
  • Nếu chạy chỉ cần login session cookie -> classify YÊU CẦU-LOGIN.
  • Nếu ambiguous (runs but misses features) -> classify MANUAL_REVIEW with note what missing.
  1. 20-30 phút – capture evidence và phân loại

Mục tiêu: đóng gói bằng chứng, ghi metadata, quyết định final label tạm thời. Việc thực hiện:

  • Thu thập files evidence/*, tóm tắt bằng 1 file verdict.txt gồm:
  • repourl, primarytype, runstatus (runnable | requiressetup | needsapikeys | failsbuild | manualreviewneeded), apidependency_score (0-10), evidence pointers (paths), reviewer initials, timestamp.
  • Ghi short note 1-2 câu cho biên tập: ví dụ "Core crawler runs offline on sample; optional SERP enrich via SERPAPI documented in .env.example – api_score 2 – notes: .env.example line 4".

Mẫu lệnh đóng gói evidence:

# create verdict
cat > ../evidence/verdict.txt <<EOF
repo: <repo_url>
run_status: $(grep -q "missing API" ../evidence/run_on_stderr.txt && echo "needs_api_keys" || echo "runnable")
api_dependency_score: 2
evidence_files: $(ls -1 ../evidence | tr '\n' ',' )
reviewer: <your_initials>
timestamp: $(date -Iseconds)
notes: <short note>
EOF

Quy tắc quyết định nhanh (rule-of-thumb)

  • any SDK import or explicit API_KEY variable in README or .env.example -> classify CẦN-API unless README documents optional mode and you validated offline core works.
  • literal calls to api.thirdparty.com or presence of secrets in CI -> classify CẦN-API.
  • only requests to public HTML pages and no SDK/env var -> likely KHÔNG-API but add ToS risk note.
  • if ambiguous, mark MANUALREVIEW and set apidependency_score >=3.

Ghi chú vận hành

  • Luôn lưu stdout/stderr và strace/ss logs; chúng là bằng chứng bạn sẽ gắn vào bảng.
  • Nếu bạn gặp repo mixed-mode (core offline, enchancement via API) hãy ghi rõ field runstatus and notes: "coreok – enrichmentrequiresKEY".
  • Nếu thấy dấu hiệu bypass CAPTCHA hoặc external captcha-solver libs, flag đỏ và ghi cảnh báo pháp lý trong notes.

Kết thúc 30 phút: bạn có verdict, evidence files, và một dòng note để copy vào bảng tổng hợp. Làm theo quy trình này cho mỗi repo để đảm bảo nhất quán giữa các reviewer.

Phương pháp rút ra điểm mạnh thực dụng từ README, issues và thử nhanh

Mục tiêu: trong lần đọc nhanh README, issues và một thử nghiệm ngắn, bạn cần rút ra các điểm mạnh thực dụng có thể trích ra trực tiếp vào phần per-tool của bài. Phương pháp dưới đây cho bạn một khung có thể thực hiện lặp lại, gồm thang bằng chứng A/B/C, mẫu mapping evidence -> claim và mẫu cách ghi nguồn cùng mức độ tin cậy.

  1. Nguyên tắc vận hành ngắn
  • Luôn ưu tiên bằng chứng thực thi hơn tuyên bố văn chương. Một câu trong README không bằng một lệnh có thể chạy được và output mẫu.
  • Ghi mọi bằng chứng theo 3 yếu tố: nguồn file/line, snapshot output, và tag mức độ tin cậy (A/B/C).
  • Mỗi claim về điểm mạnh phải kèm 1 mẫu evidence tối thiểu: A hoặc B. Nếu chỉ có C, ghi rõ là "không xác minh".
  1. Thang bằng chứng (tiêu chí nhanh)
  • Cấp A (chắc chắn): có command/example trong README hoặc scripts kèm input mẫu, bạn chạy được trong 30 phút và thu được output giống ví dụ; hoặc có test/CI chạy thành công với fixture minh họa. Mức tin cậy cao.
  • Cấp B (hỗ trợ): README có ví dụ lệnh hoặc output mẫu nhưng không chạy ngay (thiếu dependency) hoặc có issue/PR diễn tả use-case thực tế nhưng chưa có sample output tái lập. Mức tin cậy trung bình, cần note "cần xác minh".
  • Cấp C (yếu): chỉ có mô tả tính năng bằng ngôn từ, roadmap hoặc feature request; không có ví dụ hay logs. Không đủ để rút thành điểm mạnh, chỉ dùng như gợi ý.
  1. Quy trình 15 phút để rút claim thực dụng
  • Bước 0 (0-2 phút) đọc nhanh mục Usage/Quickstart và .env.example. Tìm lệnh mẫu và bất kỳ biến env như API_KEY.
  • Bước 1 (2-6 phút) scan tĩnh: rg/grep tìm keywords liên quan chức năng (ví dụ internal link, sitemap, audit, serp). Lưu ra list file/line để trích dẫn.
  • Bước 2 (6-12 phút) thử chạy example nhỏ: dùng sample input hoặc examples/fixtures. Chụp stdout và, nếu có output file, lưu snapshot.
  • Bước 3 (12-15 phút) soạn evidence entry: nguồn (file:line hoặc issue#), bản copy output (hoặc path file), và gán mức A/B/C. Nếu không thể chạy, gán B hoặc C và nêu nguyên nhân.
  1. Mapping evidence -> claim, ví dụ cụ thể
  • Claim: "Tối ưu internal links"
  • Evidence cấp A: README có lệnh ./cli link-graph --site example.com --out graph.json + bạn chạy lệnh, file graph.json chứa nodes/edges. Trích dẫn: README.md line X; evidence file: evidence/graph.json. Mức tin cậy: A.
  • Evidence cấp B: README mô tả tính năng và có screenshot graph nhưng không có sample output; hoặc issue user kèm hình ảnh export graph. Mức tin cậy: B.
  • Evidence cấp C: chỉ có câu "provides internal link analysis" trong features list. Mức tin cậy: C.
  • Mẫu sample output graph (JSON nhỏ):
{
  "nodes": [
    {"id": "https://example.com/", "title": "Home"},
    {"id": "https://example.com/blog/post-1", "title": "Post 1"}
  ],
  "edges": [
    {"source": "https://example.com/", "target": "https://example.com/blog/post-1", "anchor": "Read more", "nofollow": false}
  ]
}
  • Ghi claim vào bài: "Rút internal link graph với xuất JSON nodes/edges (evidence: README.md#Usage, run output evidence/graph.json) — mức tin cậy A."
  • Claim: "Audit on-page cho meta-description và H1"
  • Evidence A: sample CSV chứa rows: url, status, titleok, metadesclen, h1count. Bạn chạy example và CSV khớp format.
  • Evidence B: tests/fixtures có asserts cho title/meta; no sample CSV but tests imply structure.
  • Evidence C: chỉ có bullet "checks meta tags".
  1. Cách ghi nguồn và biểu diễn mức độ tin cậy trong bài
  • Format trích dẫn trên một dòng ngắn cho mỗi claim:
  • [ToolName] — claim short. Evidence: README.md#Usage (line X), example run: evidence/run_stdout.txt. Confidence: A.
  • Nếu evidence là issue/PR, ghi thêm link issue và trích đoạn comment. Ví dụ:
  • Evidence: issue#42 comment by @user "ran cli –audit on 200 URLs, output report.csv" (link), Confidence: B.
  • Không dùng language tưởng tượng. Nếu claim dựa trên B hoặc C, thêm câu nhắc rõ: "chưa xác minh cục bộ" hoặc "dựa trên docs".
  1. Template ngắn để ghi vào bảng tổng hợp (một dòng)
  • fields: repourl | shortclaim | evidence_refs | confidence
  • Ví dụ:
  • https://github.com/org/repo | "Export internal link graph (nodes/edges JSON)" | README.md#usage (L23), evidence/graph.json | A
  1. Quy ước viết câu mô tả điểm mạnh trong bài
  • Cấu trúc 3 câu ngắn:
  1. Chức năng chính: "Tool X xuất internal link graph dạng JSON (nodes/edges)."
  2. Lợi ích thực dụng: "Dễ tích hợp vào pipeline graph analysis; dùng để phát hiện orphan pages và anchor distribution."
  3. Bằng chứng & cảnh báo: "Bằng chứng: README.md#Usage + thử chạy local (evidence/graph.json). Confidence: A. Lưu ý: không thay thế dữ liệu clickstream."
  • Nếu confidence B hoặc C, bắt đầu câu thứ 3 bằng "Cần kiểm chứng: …"
  1. Khi evidence là screenshot hoặc sample image
  • Kèm metadata về kích thước, đường dẫn file và mô tả ngắn. Ví dụ:
  • Screenshot: docs/screens/linkgraph.png — shows force-directed graph with node sizes representing in-degree. Confidence: B (screenshot không cho JSON schema).
  • Nếu claim dựa vào hình ảnh, cố gắng chạy thử để lấy native output; nếu không được, hạ confidence.
  1. Một số lưu ý phòng ngừa
  • Nếu README có lệnh nhưng kèm biến env tên *_KEY, đừng gán A ngay. Chỉ gán A nếu bạn chạy được mà không cần key, hoặc xác định rõ key là optional.
  • Nếu evidence từ issues: kiểm tra ngày và activity; một issue cũ chưa trả lời có confidence thấp.
  • Luôn lưu artifacts runtime (stdout, file outputs, strace nếu dùng) vào thư mục evidence để làm bằng chứng cho biên tập viên kỹ thuật.
  1. Mẫu ngôn ngữ ghi chú trong phần caveats
  • Viết rõ: "Confidence A/B/C" và "evidence: path hoặc URL". Ví dụ: "Confidence B — docs include sample CSV but sample requires dataset not included; manual run not performed."

Kết luận ngắn

  • Áp dụng quy trình này cho mỗi tool: đọc README -> scan tĩnh -> thử chạy -> ghi evidence & confidence. Chỉ công bố điểm mạnh khi có evidence cấp A hoặc B kèm chú giới hạn. Dùng mẫu JSON/CSV/graph để minh họa output khi có A, và luôn ghi nguồn file:line hoặc issue link để người đọc kiểm chứng.

Kịch bản người dùng để gắn điểm mạnh với công việc cụ thể

Dưới đây là 8 kịch bản điển hình. Với mỗi kịch bản mình nêu: mục tiêu công việc, điểm mạnh thực dụng của loại công cụ tương ứng, và cách bạn/đội xác thực nhanh tính đúng nghĩa của điểm mạnh khi đánh giá repo.

  1. Audit hàng tuần (weekly on-page audit)
  • Mục tiêu: phát hiện regressions on-page trên tập URL quan trọng mỗi tuần.
  • Điểm mạnh thực dụng: chạy cục bộ nhanh, output machine-readable (CSV/JSON) cho diffs, tích hợp với CI để fail build khi số issue vượt ngưỡng.
  • Kiểm chứng nhanh: README có lệnh CLI ví dụ để chạy theo danh sách URL; có flag –output csv; sample report; test run trên 10 URL tạo CSV chứa columns như url, issue_code, severity.
  1. Crawl cho migration (migration-focused crawler)
  • Mục tiêu: crawl site hiện tại để thu thập URLs, status, canonical, hreflang, redirect chains trước migration.
  • Điểm mạnh thực dụng: khả năng giới hạn depth, respect robots, cấu hình dedupe query params, xuất redirect chain và sitemap-ready URL list.
  • Kiểm chứng nhanh: config mẫu chứa max_depth, dedupe rules; command sample crawl –max-pages N; output cung cấp redirect chain per URL hoặc sitemap.xml generator.
  1. Log analysis cho session stitching (server log -> session stitching)
  • Mục tiêu: nối các request thành session, map tới landing pages và conversion funnel, xuất cho attribution.
  • Điểm mạnh thực dụng: parser cho common log formats, configurable session window, join key để enrich với crawl data, output session-level CSV.
  • Kiểm chứng nhanh: repo có sample log fixture + script parse producing sessions.csv with fields sessionid, startts, pages[], useragent; config param sessiontimeout (s).
  1. Keyword harvest cục bộ (local n-gram / keyword extractor)
  • Mục tiêu: trích keyword từ content nội bộ để build topic clusters, không phụ thuộc công cụ API từ khóa trả phí.
  • Điểm mạnh thực dụng: chạy trên corpus local (HTML/text), options language/stopwords, min_freq thresholds, xuất candidate list ranked (tf-idf).
  • Kiểm chứng nhanh: có script extract –input path/to/html –min-freq 3 –out keywords.csv; keywords.csv gồm term, freq, ngram_length, score.
  1. SERP sampling (SERP scraper without paid API)
  • Mục tiêu: lấy snapshot SERP cho tập query nhỏ để so sánh thay đổi ranking hoặc kiểm tra SERP features.
  • Điểm mạnh thực dụng: scraping HTML SERP với proxy/headless optional, configurable geo/domain, sample-mode (n queries, 1 page), output normalized (position, url, snippet, feature flags).
  • Kiểm chứng nhanh: README nêu cách chạy sample queries list; sample output shows positions and feature flags; kiểm tra tool có tùy chọn proxy và rate-limit.
  1. Internal link audit (site graph / orphan detection)
  • Mục tiêu: xây dựng graph internal links, tìm orphan pages, compute in/out-degree để ưu tiên tối ưu nội bộ.
  • Điểm mạnh thực dụng: sản sinh graph format (GraphML/GEXF/edges CSV), canonicalization rules, dedupe query params, support seed URLs or sitemap input.
  • Kiểm chứng nhanh: command example generate-graph –site example.com –out graph.gexf; open graph.gexf hoặc edges.csv chứa source,target,anchor,no_follow.
  1. Schema generator / validator (JSON-LD)
  • Mục tiêu: tạo hoặc validate schema.org JSON-LD cho content hàng loạt, dễ tích hợp vào CMS pipeline.
  • Điểm mạnh thực dụng: templates per content type (Article/Product), validator offline, output injectable snippets and a report of missing required properties.
  • Kiểm chứng nhanh: repo có templates/*.jsonld, command generate –type Article –input content.md –out schema.jsonld; validator cli check –file schema.jsonld returns OK or list of missing fields.
  1. Image audit (alt text, size, srcset)
  • Mục tiêu: kiểm tra alt attr, detect missing alt, oversized images, and produce remediation suggestions.
  • Điểm mạnh thực dụng: batch scan local image folder or crawled images, compute dimensions/filesize, detect missing/duplicate alt, suggest srcset breakpoints, support lossless compression local tools.
  • Kiểm chứng nhanh: sample run image-audit –input path –out imagereport.csv; report columns: url, imgsrc, width, height, filesize, alt_text, suggestion.

Hướng dẫn ngắn để bạn gắn điểm mạnh trong bài

  • Mỗi tool trong bài nên có một câu ngắn nêu "điểm mạnh thực dụng" theo cấu trúc: chức năng chính + lợi ích cho workflow + evidence reference. Ví dụ: "Tool X – crawl migration: thu thập redirect chains và xuất sitemap-ready list (evidence: README#Usage + examples/redirects.json). Confidence: A/B."
  • Khi evidence chưa đủ (chỉ docs, không chạy được), ghi rõ confidence B hoặc C và yêu cầu kiểm thử 30 phút như checklist trong phân tích.

Mình đã gói đủ 8 kịch bản và cách diễn giải điểm mạnh thực dụng tương ứng để bạn dùng trực tiếp khi soạn phần công cụ theo loại.

Tiêu chí xếp hạng ưu tiên khi đưa vào bài

Mục tiêu: cung cấp khung chấm điểm minh bạch để sắp thứ tự repo đưa vào bài theo mức độ hữu dụng thực tế cho đội SEO, đồng thời tránh bias chỉ dựa vào stars. Mình đưa ra trọng số gợi ý, công thức điểm tổng và quy tắc tie-breaker cùng lưu ý vận hành.

  1. Các tiêu chí chính và trọng số (tổng chuẩn hóa = 100)
  • Tính ứng dụng thực dụng (Usefulness) – 40 điểm
  • Mức độ giúp giải việc SEO cụ thể – crawl, audit, log, v.v.
  • Bằng chứng: ví dụ lệnh/cli, sample output, workflow tích hợp.
  • Độ ổn định / duy trì (Stability & Maintenance) – 20 điểm
  • Last commit trong 24 tháng, issue response, test/CI presence.
  • Độ phổ biến và cộng đồng (Popularity) – 15 điểm
  • Stars, forks, contributors. Dùng để ưu tiên review, không là quyết định duy nhất.
  • Dễ dùng / trải nghiệm triển khai (Usability) – 15 điểm
  • Dockerfile, setup scripts, sample config, docs rõ ràng.
  • Rủi ro phụ thuộc API và pháp lý (Risk) – 10 điểm, tính theo chiều âm
  • Nếu repo yêu cầu API khoá bên thứ ba cho chức năng core hoặc có code bypass anti-bot, trừ điểm. Nếu không có API dependency, được cộng thấp.
  1. Công thức điểm (ví dụ)
  • Score = 0.4Usefulness_norm + 0.2Stabilitynorm + 0.15*Popularitynorm + 0.15Usability_norm – 0.1Risk_norm
  • Mỗi thành phầnnorm chuẩn hóa 0-100 trước khi nhân trọng số. Risknorm = 0 nếu không có rủi ro, 100 nếu rủi ro cao.
  1. Chi tiết cách đánh từng tiêu chí (phiên bản ngắn)
  • Usefulness: đánh 0-100 dựa trên evidence: có CLI + sample output (≥80), chỉ docs mô tả chức năng (40-60), không có ví dụ thực tế (≤30). Gắn thêm +10 nếu core feature chạy offline không cần API.
  • Stability: 100 nếu last commit trong 6 tháng, tests + CI pass, maintainer active; 50 nếu cập nhật trong 12-24 tháng; 0 nếu >24 tháng hoặc archived.
  • Popularity: hợp nhất stars (60%) và forks/contributors (40%). Nhưng clamp giá trị tối đa không vượt 30 điểm sau chuẩn hóa để tránh át các tiêu chí khác.
  • Usability: 100 khi có Dockerfile + examples + config mẫu + one-line install; giảm khi thiếu bước reproducible.
  • Risk: scan mã/CI tìm APIKEY, SDKs, endpoints api.*; nếu phát hiện API required cho core, Risknorm = 100. Nếu optional: 50. Nếu chỉ scraping public HTML và có politeness config, Risk_norm = 10.
  1. Cách tránh bias stars-only
  • Giới hạn trọng số Popularity nhỏ hơn Usefulness và Stability.
  • Khi sorting, nếu hai repo có score chênh < 3 điểm, ưu tiên repo có Usefulness cao hơn, không stars.
  • Trong trang xuất bản, hiển thị 3 chỉ số hàng đầu cho mỗi repo: Usefulness, Stability, Risk để độc giả thấy logic sắp xếp.
  • Prioritize manual spot-check cho repo ít sao nhưng score Usefulness cao.
  1. Tie-breaker và sàng lọc cuối
  • Tie-breaker: 1) Usefulness, 2) Stability, 3) lower Risk, 4) higher Usability, 5) stars.
  • Loại trừ tự động: license incompatible, archived > 24 tháng, hoặc core yêu cầu API key mà bài chủ yếu là "không API".
  1. Vận hành thực tế – hướng dẫn nhanh cho bạn
  • Giai đoạn thu thập: tính điểm sơ bộ tự động bằng script; lọc top 50 theo Score.
  • Giai đoạn kiểm thử: dành 30 phút/run cho mỗi repo top 50 theo checklist runtime; cập nhật các thành phần Usefulness, Risk, Usability thực tế.
  • Trạng thái xuất bản: chỉ đưa repo có Score ≥ threshold do bạn đặt và Risk_norm ≤ 40; với repo borderline, gắn note "needs verification".

Kết luận ngắn: khung này giúp bạn ưu tiên công cụ dựa trên giá trị thực dụng hơn là tiếng vang cộng đồng. Khi áp dụng, giữ log evidence cho mỗi field để biên tập có thể giải trình lựa chọn.

Cấu trúc dàn bài xuất bản cho phần tổng hợp

Mục tiêu ngắn gọn: cung cấp khung xuất bản rõ ràng để phần tổng hợp repo SEO không API có thể được viết, kiểm chứng và xuất bản nhanh. Dàn bài phân thành 5 khối chính: mở bài phương pháp, taxonomy + tiêu chí chọn, phần tổng hợp theo loại (per-type highlights), cảnh báo pháp lý/ToS, phụ lục evidence table. Mỗi khối kèm chỉ dẫn nội dung bắt buộc, bằng chứng cần đính kèm và mẫu ngắn để nhóm viết/biên tập dễ thực hiện.

  1. Intro + Methodology (khoảng 1 đoạn tóm tắt + 1 đoạn quy trình)
  • Nội dung: tóm tắt mục tiêu tổng hợp, định nghĩa vận hành "không có kết nối API" được dùng ở bài, phạm vi nền tảng (ưu tiên GitHub, mở rộng GitLab), mốc thời gian cập nhật (24 tháng).
  • Phương pháp tuyển chọn: liệt kê bước thu thập (queries GitHub), sàng lọc tự động (scan deps, .env, CI), quy trình kiểm tra 30 phút (static scan, curl test, run sample with network toggled), và cách đánh dấu [KHÔNG CHẮC] cho repo cần kiểm chứng sâu.
  • Bằng chứng bắt buộc: link script quét, log truy vấn, tiêu chí loại trừ tóm tắt. Viết rõ ai chịu trách nhiệm kỹ thuật để tái hiện.
  1. Taxonomy + Selection (khung phân loại và tiêu chí sàng lọc)
  • Danh sách loại công cụ (mỗi loại 1 dòng định nghĩa ngắn): crawl/thu thập, audit on-page, log analysis, keyword extraction, SERP scraper (HTML-based), internal-link analysis, schema tools, sitemap generator, image SEO tools, performance tools, utilities kỹ thuật.
  • Tiêu chí gán nhãn primary/secondary: entrypoint riêng, sample output, code density/module riêng, docs/tests. Ghi rõ điểm tối thiểu cần có để gán nhãn (ví dụ evidence_score ≥ 45).
  • Tiêu chí sàng lọc đầu vào: license (OSI-compatible), last pushed <= 24 months, không archived, buildable/runable (smoke test). Mẫu note nếu loại: vì phụ thuộc API / license / dead.
  • Trường lớp dữ liệu trong bảng sơ bộ để fill: repourl | primarytype | secondarytypes | apidependencyscore | lastcommit | license | run_status | reviewer.
  1. Per-type highlights (mỗi loại 1 đoạn, format tiêu chuẩn)
  • Mục đích: cho reader thấy trong mỗi loại có những repo tiêu biểu, và rút ra 2-3 điểm mạnh thực dụng gắn với workflow SEO.
  • Format bắt buộc cho mỗi loại:
  • Lead-in 1 câu nêu vai trò loại.
  • Top 3 tools (tên + repo_url) với 2 trường ngắn cho mỗi tool:
  • Điểm mạnh thực dụng (1-2 câu): nói rõ lợi ích cho practitioner, ví dụ "rút internal link graph nhanh giúp ưu tiên migrate canonical".
  • Hạn chế vận hành ngắn (1 câu): ví dụ "cần headless chrome để render JS".
  • Kịch bản ứng dụng điển hình (1-2 bullets): ai dùng, khi nào, output cần có.
  • Yêu cầu evidence: mỗi điểm mạnh phải kèm 1 bằng chứng trong repo (CLI example, sample output, issue report) và trích dẫn line/URL.
  1. Caveats pháp lý / Terms of Service (section ngắn nhưng sắc)
  • Nội dung bắt buộc: tóm tắt rủi ro ToS khi scraping; nguyên tắc vận hành an toàn (respect robots.txt, contact UA, rate-limit, không bypass CAPTCHA); lưu ý về dữ liệu cá nhân và privacy.
  • Hình thức: bullet checklist cho người dùng trước khi chạy tool (sandbox, proxy controlled, user-agent contact).
  • Mẫu disclaimer text ngắn để copy-paste vào bài và hướng dẫn biên tập ghi rõ: "Không phải tư vấn pháp lý; kiểm tra ToS mục tiêu trước khi chạy".
  1. Appendix: Evidence table + phương pháp kiểm chứng
  • Bảng dữ liệu chuẩn (máy đọc): mỗi dòng một repo; các trường bắt buộc:
  • repourl | primarytype | secondarytypes | lastcommit | license | apidependencyscore | runstatus (passed/failed/partial) | evidenceentrypoints (path/command) | evidenceoutputpath | reviewer | notes
  • Giải thích trường: apidependencyscore cách tính, run_status định nghĩa (passed = ran sample offline without API).
  • Kèm checklist kiểm thử 30 phút (timebox): 0-5m README + manifest scan; 5-10m rg scan for API tokens/SDK; 10-25m run sample with network on/off; 25-30m capture logs, classify.
  • Hướng dẫn lưu artifacts: stdout/stderr, lsof/strace snapshot, curl responses, và link lên storage nội bộ; tên file chuẩn hóa reposhortnameevidence_TIMESTAMP.zip.

Kết thúc và giao việc

  • Ở cuối bài, đính kèm short handover checklist cho nhóm viết: top 8 repo per type đã được xác thực, mẫu đoạn mô tả điểm mạnh (3 câu), links evidence, và tag rủi ro. Biên tập viên chỉ lấy từ Appendix table và copy template per-type highlights để hoàn thiện nội dung xuất bản.

Kịch bản kiểm thử chéo và cập nhật định kỳ

Mục tiêu: giữ độ chính xác của danh sách repo và các nhãn "KHÔNG-API" theo chu kỳ, phát hiện thay đổi yêu cầu API hoặc rủi ro vận hành, và lưu lịch sử kiểm chứng rõ ràng để biên tập/tech review tra lại.

Tổng quan quy trình định kỳ

  • Chu kỳ ngắn – 3 tháng: kiểm tra nhanh mọi repo trong danh sách 30 repo để phát hiện thay đổi rõ rệt (pushed:, CI workflow thay đổi, biến env mới xuất hiện, issues/PR gợi ý thay đổi). Thực hiện scan tự động + một lượt triage thủ công cho items flagged.
  • Chu kỳ dài – 6 tháng: kiểm tra sâu từng repo mục tiêu – chạy lại kịch bản 30 phút runtime test, kiểm tra full CI/Docker/README, cập nhật tag apidependencyscore và evidence. Ghi changelog kiểm chứng phiên bản.

Nguyên tắc vận hành

  • Automate first – triage second. Dùng script scan để tìm diff; chỉ gửi cho reviewer những repo có signal.
  • Giữ bằng chứng cho mọi lần kiểm chứng – lưu artifacts (logs, curl responses, kết quả rg, stdout run) có timestamp.
  • Phiên bản kiểm chứng là 1 chuỗi semantic-lite: verified-v{YYYYMMDD}-{checkerInitials}-{level}, level ∈ {quick, deep}. Ví dụ: verified-v20260719-MT-deep.
  1. Tự động re-scan hàng tháng (ci job)
  • Trigger: cron job nội bộ (ví dụ GitHub Actions scheduler) chạy script trên danh sách repo.
  • Script thực thi:
  • git ls-remote/pushed: check pushed:>= lastverifieddate for each repo.
  • rg scan trong tip commit cho patterns: API_KEY, secrets., imports của SDK tên thương hiệu, calls to api. domains, .env.example changes, .github/workflows changes.
  • CI manifests check: diff .github/workflows, docker-compose, Dockerfile for new ENV or curl/secret usage.
  • Output: JSON report per repo with fields: repourl, lastpushed, flags[], apidependencyscoredelta, artifactslinks.
  • Threshold: nếu flags > 0 hoặc score tăng >1 -> enqueue triage.
  1. Quy trình triage nhanh (3-months cycle – ~per repo 10-30 phút)
  • Người chịu trách nhiệm: reviewer kỹ thuật (dev reviewer).
  • Bước:
  1. Mở report JSON, xem flags và diff highlight.
  2. Mở PR/commit thay đổi gần nhất, kiểm tra .env.example, Dockerfile, .github/workflows. Nếu thấy biến env chứa KEY/TOKEN hoặc new SDK import -> classify as "needs-deep-check".
  3. Thực hiện quick runtime check: curl suspected endpoints (no auth), run sample CLI minimal with –dry-run or –help. Nếu xuất hiện 401/403/Authorization header usage -> mark "requires-API".
  4. Ghi log kết quả, cập nhật apidependencyscore và run_status.
  • Kết quả triage: accept (no API), warn (optional API/features), reject (requires API).
  • Thời gian mục tiêu: 10-30 phút/repo flagged. Repos không flagged: update timestamp verified with quick note.
  1. Quy trình kiểm thử sâu (6-months cycle – ~per repo 30-60 phút)
  • Người chịu trách nhiệm: dev reviewer + tech editor verification.
  • Bước:
  1. Clone repo, chạy static scan đầy đủ (regex list), build container nếu Dockerfile, chạy unit tests nếu có.
  2. Thực thi 30-minute runtime test: run Quickstart command(s) with network on, capture network calls (strace/tcpdump) để xác định outbound domains; rerun with network disabled to check offline path.
  3. Nếu tool có "offline mode" test đó, record evidence that core feature works without API keys. Nếu không, capture exact missing-key error text.
  4. Nếu code gọi external API chỉ ở optional path, document which feature needs key and mark core capability still "no-API".
  • Ghi chú: luôn chạy trong sandbox container; không gửi credential. Tất cả output, curl responses, strace logs nén và attach.
  1. Ghi phiên bản kiểm chứng và giữ changelog kiểm chứng
  • Phiên bản kiểm chứng format: verified-vYYYYMMDD-INITIALS-level.
  • Changelog kiểm chứng: một file kiểm chứng chung trong kho nội bộ (CSV/Markdown) có cột:
  • repourl | lastverified | verifiedversion | verifier | level | result (accept/warn/reject) | scorebefore | scoreafter | artifactslink | notes
  • Khi trạng thái thay đổi (ví dụ từ accept -> reject), tạo entry mới và gửi thông báo cho biên tập viên + team SEO.
  1. Cơ chế thông báo và hành động
  • Nếu triage hoặc deep-check phát hiện "requires API" cho repo đã từng được liệt kê là "no-API", thực hiện:
  • Ghi changelog entry và attach evidence.
  • Thay đổi trạng thái trong dataset: mark as removedfromnoapilist = true; thêm note public nếu cần.
  • Thông báo cho biên tập viên chịu phần đó để chỉnh nội dung bài trước khi xuất bản lại.
  • Nếu phát hiện rủi ro ToS (captcha bypass, explicit disable robots), escalate to legal/compliance.
  1. Báo cáo tổng kết định kỳ
  • Mỗi chu kỳ 3 tháng: tóm tắt số repo checked, số repo flipped status, top causes (CI changes, new SDK imports, new examples requiring keys).
  • Mỗi 6 tháng: audit report to stakeholders with sample artifacts, list of accepted/rejected, recommended actions.

Checklist ký tay cho reviewer khi hoàn thành 1 repo (tickbox)

  • [ ] Ran auto-scan and saved JSON report
  • [ ] Reviewed README and CI/Docker changes
  • [ ] Performed quick curl/run test with network on/off
  • [ ] Captured artifacts and stored link
  • [ ] Updated apidependencyscore and run_status
  • [ ] Wrote changelog entry with verified_version
  • [ ] Notified editor if status changed

Ghi chú vận hành ngắn

  • Mọi thay đổi trạng thái phải có artifacts. Không được thay đổi nhãn dựa vào cảm quan.
  • Lưu artifacts tối thiểu 12 tháng. Phiên bản kiểm chứng giúp audit và rollback nội dung nếu cần.

Cảnh báo tuân thủ pháp lý và ToS khi dùng scraping

Mục tiêu phần này là cung cấp cho bạn checklist vận hành để giảm rủi ro pháp lý và vi phạm điều khoản dịch vụ khi dùng công cụ scraping trong bộ tổng hợp. Nội dung nhắm vào quyết định vận hành: khi nào nên chạy, khi nào dừng, và cách ghi chú cảnh báo trong bài. Mình đưa ra checklist hành động, các tình huống cần xin phép, và mẫu wording disclaimer để chèn vào bài.

  1. Nguyên tắc cốt lõi
  • Tôn trọng robots.txt không tự động đồng nghĩa hợp pháp, nhưng là minimum hành xử kỹ thuật. Nếu robots.txt chặn một path, coi đó như giới hạn kỹ thuật cần tuân thủ trừ khi có sự cho phép rõ ràng.
  • Không cố gắng vượt cơ chế bảo vệ: bypass CAPTCHA, sử dụng dịch vụ giải CAPTCHA tự động, dùng kỹ thuật che giấu danh tính để né detection là hành vi rủi ro cao, dễ vi phạm ToS và có thể vi phạm luật trong một số vùng.
  • Phân biệt scraping HTML hợp lệ và gọi API kín: nếu mã gọi endpoint api.* hay yêu cầu API key, xử lý như dịch vụ bên thứ ba – cần kiểm tra điều khoản và có thể xin phép.
  1. Checklist pháp lý vận hành (trước khi chạy trên bất kỳ target nào)
  • Kiểm tra robots.txt:
  • Fetch và parse robots.txt cho domain trước khi chạy.
  • Tôn trọng Disallow, Crawl-delay khi có, và respect sitemap nếu nêu.
  • Nếu bạn có lý do hợp lý để bỏ qua robots.txt (ví dụ hợp tác với chủ site), chỉ làm khi có bằng chứng bằng văn bản.
  • Rate limits và concurrency:
  • Thiết lập rate limit mặc định: concurrency per domain <= 5 và delay ngẫu nhiên trung bình 2-3s; điều chỉnh thấp hơn nếu robots.txt quy định crawl-delay lớn hơn.
  • Dừng khi thấy 429/403 liên tiếp – pause và backoff exponential với jitter.
  • Ghi log mọi response 429/403 và số lần lặp lại trước khi dừng.
  • User-Agent và contact:
  • Đặt User-Agent rõ ràng: tên công cụ + phiên bản + contact email hoặc URL liên hệ, ví dụ "my-seotool/0.1 (+mailto:ops@example.com)".
  • Không giả mạo User-Agent trình duyệt để né detection; nếu bạn cần browser UA để xem nội dung, hãy dùng UA hợp lý và ghi chú rủi ro.
  • Cookie/session và login:
  • Nếu scraping yêu cầu login, chỉ dùng credentials hợp pháp do chủ site cung cấp. Không dùng account của người khác hay brute-force.
  • Nếu tool cần session token, hướng dẫn người dùng chạy trên account của họ và không lưu trữ credential trong repo công khai.
  • CAPTCHA và anti-bypass:
  • Tuyệt đối không tích hợp hoặc khuyến nghị dịch vụ giải CAPTCHA tự động (2captcha, anticaptcha…) cho tính năng mặc định.
  • Nếu repo chứa ví dụ hoặc plugin giải CAPTCHA, gắn nhãn rõ rủi ro và khuyến nghị xóa trước khi chạy.
  • Proxy và IP rotation:
  • Nếu dùng proxy, ưu tiên proxy trả phí có SLA; không dùng public proxy lists. Luôn kiểm tra terms-of-service của proxy provider.
  • Không chia sẻ proxy/credentials trong repo; cấu hình proxy phải do người dùng cung cấp.
  • Dữ liệu nhạy cảm và privacy:
  • Nếu thu thập dữ liệu cá nhân, tuân thủ luật bảo vệ dữ liệu địa phương (ví dụ GDPR). Án tử nghiệp: anonymize PII trước lưu trữ nếu có thể.
  • Ghi rõ mục đích, thời hạn lưu trữ và quyền truy cập dữ liệu trong tài liệu nội bộ.
  • Ghi lại và báo cáo:
  • Ghi log chi tiết cho mỗi lần chạy: domain, thời gian, requests, response codes, pause events.
  • Nếu chủ site yêu cầu ngừng, ngừng ngay và xóa dữ liệu theo yêu cầu.
  1. Khi nào cần xin phép chính thức
  • Khi scraping dữ liệu không công khai hoặc đòi hỏi đăng nhập tài khoản người dùng mà bạn không sở hữu.
  • Khi bạn định thu thập khối lượng lớn dữ liệu có thể gây tải đáng kể cho host (hàng ngàn request/ngày cho cùng domain).
  • Khi mục tiêu có rõ ràng trong ToS là cấm scraping hoặc khi robots.txt cấm phần bạn cần.
  • Khi dữ liệu thu thập có thể nhạy cảm (thông tin cá nhân, dữ liệu tài chính, nội dung có bản quyền).
  • Khi bạn sẽ sử dụng dữ liệu cho mục đích thương mại quy mô lớn hoặc chia sẻ công khai.
  1. Quy trình xin phép ngắn gọn
  • Xác định contact người quản lý site (ở domain contact, abuse@, webmaster@).
  • Soạn email ngắn gọn nêu:
  • mục đích thu thập, phạm vi (ví dụ số trang/ngày, paths), thời gian thử nghiệm, User-Agent bạn sẽ dùng, và thông tin liên hệ để phối hợp.
  • Đợi phản hồi bằng văn bản. Nếu có điều kiện (ví dụ rate limit), tuân thủ theo thỏa thuận.
  1. Mẫu wording disclaimer để chèn trong bài tổng hợp

Bạn có thể dùng đoạn sau để minh bạch cho độc giả:

"Mẫu disclaimer (thêm vào phần phương pháp) Mọi công cụ nêu trong bài được đánh giá theo tiêu chí ‘không yêu cầu khóa API để chạy chức năng chính’ tại thời điểm kiểm chứng. Trước khi chạy trên website thật, bạn nên:

  • kiểm tra robots.txt của site mục tiêu và tuân thủ mọi chỉ dẫn;
  • cấu hình rate limit và User-Agent có contact để giảm rủi ro bị block;
  • không sử dụng kỹ thuật bypass CAPTCHA hay chia sẻ credentials;
  • xin phép chủ sở hữu site khi thu thập khối lượng lớn hoặc dữ liệu không công khai.

Tác giả không chịu trách nhiệm cho hành vi vi phạm ToS hoặc pháp luật khi người đọc vận hành các công cụ này. Nếu bạn cần, hãy tham vấn bộ phận pháp chế hoặc liên hệ trực tiếp với chủ site."

  1. Gợi ý để ghi chú trong bảng repo
  • Thêm cột "ToS-risk" với mức Low/Medium/High dựa trên:
  • High: tool chuyên SERP scraping, có hướng dẫn proxy/CAPTCHA, hoặc mặc định concurrency cao.
  • Medium: tool dùng headless browser, cần login cho một số tính năng.
  • Low: tool làm việc trên file local hoặc chỉ audit local HTML.
  • Kèm link evidence: .env.example, README line, CI workflow reference.

Kết luận ngắn

  • Tuân thủ kỹ thuật (robots, rate limits, UA) và nguyên tắc không bypass là step tối thiểu để giảm rủi ro. Khi có nghi ngại pháp lý hoặc thương mại, xin phép bằng văn bản. Trong bài, luôn hiển thị disclaimer rõ ràng và gắn nhãn rủi ro cho từng repo để người đọc biết điểm cần kiểm chứng trước khi triển khai.

Tiêu chí rút ra "điểm mạnh thực dụng" và template viết ngắn

Mục tiêu phần này là cho bạn một khuôn mẫu rõ ràng để rút ra và viết ngắn gọn "điểm mạnh thực dụng" cho mỗi repo trong danh sách. Mình cung cấp: (A) nguyên tắc nhanh để quyết định một điểm mạnh là thực dụng, (B) template 3 câu bắt buộc dùng cho mọi repo, và (C) ví dụ thực tế sẵn sàng copy-paste cho các loại công cụ SEO phổ biến.

A. Nguyên tắc rút điểm mạnh thực dụng (quick rules)

  • Chỉ rút khi có bằng chứng cấp A hoặc B: lệnh CLI + output mẫu, hoặc sample config + example output, hoặc issue/PR thực tế kèm log. Nếu chỉ có mô tả trừu tượng, ghi nhãn [CẦN KIỂM CHỨNG].
  • Điểm mạnh phải gắn trực tiếp với một workflow SEO cụ thể: audit on-page, tạo sitemap, map internal links, enrich keyword list, phân tích log, v.v. Viết sao cho người đọc biết ngay được họ sẽ dùng tool vào bước nào trong quy trình.
  • Luôn kèm rủi ro/limit thực dụng: ví dụ cần headless browser, cần proxy, tốc độ mặc định cao, gọi API bên thứ ba, hoặc vấn đề ToS. Nếu rủi ro chưa rõ, dùng tag [KHÔNG CHẮC] và mô tả bước kiểm chứng tiếp theo.
  • Ngắn gọn và hành động: mỗi repo 3 câu, max 2 dòng mỗi câu. Câu 1: chức năng chính; Câu 2: ích lợi thực dụng cho workflow; Câu 3: rủi ro hoặc giới hạn vận hành.

B. Template chuẩn 3 câu (mẫu chung, fill-in)

  • Câu 1 (chức năng chính): "[Tên repo] là một công cụ để [động từ chính + mục tiêu], ví dụ: [một-line mô tả kỹ thuật ngắn]."
  • Câu 2 (lợi ích thực dụng): "Trong workflow SEO, nó giúp [việc cụ thể] bằng cách [cách output hỗ trợ công việc], do đó tiết kiệm/giảm thiểu [nỗi đau cụ thể]."
  • Câu 3 (rủi ro/limit): "Lưu ý: [rủi ro/chế độ giới hạn], nên khi dùng bạn nên [biện pháp giảm rủi ro hoặc điều kiện dùng]."

Ví dụ generic để dán vào trực tiếp (thay TÊN/điền chỗ phù hợp)

  • "seocrawl-tool là một crawler site nhỏ gọn để thu thập HTML và metadata từ danh sách seed URL. Trong workflow SEO, nó cho phép bạn nhanh chóng sinh bảng URL + tiêu đề + meta-description để chuẩn hóa audit trước migration. Lưu ý: mặc định không dùng proxy và concurrency cao, khuyên chạy với delay 1.5–3s và bật respect robots.txt."

C. Ví dụ cụ thể theo loại công cụ — copy-paste ready

  1. Crawl / site crawler
  • "mini-crawler là một spider đơn luồng thu thập HTML và trích xuất title/meta/h1 từ sitemap hoặc seed list. Trong quy trình SEO, dùng nó để tạo dataset đầu vào cho audit on-page và kiểm tra broken link nhanh trong bước tiền-migration. Lưu ý: mặc định concurrency thấp nhưng không xử lý JS; nếu site cần render JS thì phải bật headless và chuẩn bị Chrome binary."
  1. Audit on-page
  • "page-audit-cli chạy tập hợp kiểm tra on-page (title, meta description, H1, canonical, status code) cho danh sách URL. Nó giúp biên tập và SEO lead tạo report CSV trực tiếp để phân phối cho tác vụ chỉnh sửa nội dung. Lưu ý: rule mặc định cho tiếng Anh, với nội dung tiếng khác bạn cần cung cấp stopword/lang config."
  1. Log analysis
  • "log-parser là tool parse Apache/Nginx access logs để ghép session và tính organic landing pages. Trong workflow, dùng nó để nối click path vào data crawl để ưu tiên sửa trang có traffic thật. Lưu ý: cần sample log định dạng ‘combined’ hoặc mapping pattern; khuyên thử trên small sample trước khi scale."
  1. Keyword extractor / n-gram
  • "keyword-extract thu thập n-gram từ corpus HTML và cho điểm tf-idf, xuất CSV có volume estimate nếu bạn cung cấp file export. Dùng để nhanh làm seed keyword list cho cluster testing và content briefs. Lưu ý: mặc định dùng stopwords tiếng Anh; với nội dung tiếng Việt cần thay stopword list."
  1. SERP scraper (HTML, no API)
  • "serp-scrape lấy SERP HTML cho truy vấn nhất định và parse title/snippet/position mà không cần API key. Hữu ích cho sampling SERP nhanh trước khi quyết chiến lược content/feature snippets. Lưu ý: rủi ro ToS và bot detection cao; khuyên chạy low-rate và dùng geo/proxy có kiểm soát."
  1. Internal link analyzer
  • "link-mapper xây dựng graph internal link từ crawl output và xuất dạng graphml/CSV. Trong workflow, dùng nó để xác định orphan pages và tối ưu anchor text cho silos. Lưu ý: cần input crawl có cùng canonicalization rules; nếu site dùng nhiều query params, bật dedupe rules."
  1. Sitemap generator
  • "sitemap-gen sinh sitemap.xml từ danh sách URL dựa trên rules changefreq/priority. Dùng để tạo sitemap hợp lệ cho submit GSC sau migration. Lưu ý: mặc định không gzip; với >50k URLs bạn phải bật chunking theo chuẩn sitemap."
  1. Image SEO tool
  • "image-audit scan alt attributes và kích thước, xuất gallery các ảnh thiếu alt hoặc quá lớn. Giúp team front-end/SEO ưu tiên compress và thêm alt cho pages tác động cao. Lưu ý: nếu tool hỗ trợ compression bằng API, kiểm tra config vì tính năng nén có thể yêu cầu service ngoài."
  1. Performance tool (lightweight)
  • "perf-check chạy đo thời gian tải đơn giản bằng headless fetch và báo render time + kích thước tài nguyên chính. Dùng nó cho checkpoint performance pre/post thay đổi front-end. Lưu ý: nếu bật lighthouse mode cần local Chrome binary; phiên bản headless có thể khác kết quả PageSpeed Insights."
  1. Utilities (canonical checker, redirect tracer)
  • "redirect-checker theo dõi chuỗi redirect cho list URL và báo loop/chain dài. Hữu dụng trong audit migration để tìm redirect chain gây mất link equity. Lưu ý: mặc định followredirects=true và maxredirects=10; chỉnh nếu bạn muốn phát hiện chain dài hơn."

D. Ghi chú vận hành nhanh cho bạn khi viết phần repo

  • Luôn kèm 1 nguồn chứng cứ ngay sau 3 câu: trích 1 dòng README hoặc link issue/command (vd: Usage: seocrawl –start urls.txt). Nếu không có, thêm tag [CẦN KIỂM CHỨNG].
  • Nếu rủi ro là ToS/scraping, viết đủ để người đọc biết cách giảm rủi ro: delay, respect robots.txt, UA contact, proxy policy.
  • Dùng ngôn ngữ thực dụng, tránh thuật ngữ mơ hồ như "powerful" hoặc "comprehensive" nếu không có bằng chứng.

Kết thúc. Bạn có thể dùng template và các ví dụ trên copy-paste trực tiếp vào bảng repo; giữ nguyên cấu trúc 3 câu + 1 evidence link cho mỗi mục.

Kịch bản ngoại lệ khi không đủ repo cho một loại

Mục tiêu: khi không gom đủ ngưỡng tối thiểu cho một loại công cụ, chính sách ngoại lệ phải bảo đảm tính minh bạch, tái kiểm chứng và an toàn cho độc giả. Dưới đây là quy trình chặt chẽ bạn áp dụng để chấp nhận ngoại lệ, các điều kiện bắt buộc phải thỏa, và mẫu note công khai để ghi vào bảng tổng hợp.

  1. Nguyên tắc vận hành
  • Ngoại lệ chỉ được xét khi sau một chiến dịch tìm kiếm chuẩn hóa vẫn không tìm đủ repo hợp lệ cho loại đó. Không chấp nhận ngoại lệ vì thiếu công sức tìm kiếm.
  • Mọi ngoại lệ phải kèm evidence stack có thể tái lập bởi người thứ ba trong 30 phút.
  • Mọi ngoại lệ phải được review độc lập bởi ít nhất hai người, gồm một reviewer kỹ thuật và một reviewer nội dung/SEO.
  1. Khi xét ngoại lệ

Bạn được quyền đề xuất ngoại lệ khi một trong các tình huống sau có bằng chứng:

  • Loại công cụ thực sự ít xuất hiện public do rào cản pháp lý, ToS, hoặc tính chuyên ngành.
  • Các tính năng của loại đó thường xuất hiện như module trong repo đa năng thay vì repo chuyên biệt.
  • Có repo duy nhất nhưng có chất lượng cao, hoạt động tốt, và có khả năng được dùng làm ví dụ đại diện.
  1. Điều kiện bắt buộc để chấp nhận ngoại lệ

Mỗi đề xuất ngoại lệ phải kèm đầy đủ ba nhóm chứng cứ dưới đây. Thiếu một trong ba, đề xuất bị từ chối.

A. Search log (bắt buộc)

  • Ghi lại các truy vấn đã chạy (ít nhất 12 truy vấn biến thể trên GitHub + 4 trên GitLab hoặc 2 nguồn curated như Awesome lists).
  • Lưu kết quả thô: top 50 kết quả cho mỗi truy vấn, với lý do loại trừ cho các repo bị loại.
  • Đính kèm timestamp và tên người chạy truy vấn.

B. Evidence stack (bắt buộc)

  • Ít nhất hai loại evidence độc lập từ repo(s) được nêu: README usage example, sample output, test/CI job chạy, bin/entrypoint, hoặc demo reproducible trong container.
  • Nếu lựa chọn repo đa năng có module, phải có: entrypoint/module độc lập + sample output cho module đó + log chạy minh chứng.
  • Nếu thiếu sample runtime, phải có evidence thị trường giải thích tính hiếm (ví dụ báo cáo ToS, bài viết ngành, hoặc danh sách thương mại thay thế).
  • Tất cả evidence phải kèm đường dẫn file cụ thể trong repo hoặc snapshot stdout/stderr.

C. Reviewer sign-off (bắt buộc)

  • Reviewer A (kỹ thuật) xác nhận evidence stack tái lập được trên môi trường test trong tối đa 30 phút hoặc nêu rõ lỗi reproducible.
  • Reviewer B (nội dung/SEO) xác nhận công cụ thực sự phục vụ workflow SEO và ghi rõ phạm vi giới hạn.
  • Cả hai reviewer phải ký tên (initials) và ghi timestamp trong metadata.
  1. Quy tắc công khai minh bạch
  • Mọi ngoại lệ đưa vào bảng tổng hợp phải kèm một note ngắn công khai theo mẫu dưới đây. Note sẽ xuất hiện bên cạnh mục loại hiếm trong bài xuất bản.

Mẫu note công khai (copy-paste)

  • exception_note | Loại: <tên loại>; Repos tìm được: <n>; Lý do ngoại lệ: <ngắn gọn>; Evidence: <link1>, <link2>; Reviewers: <A initials> (<role>), <B initials> (<role>); Rủi ro: <tóm tắt rủi ro>; Recommendation: <hành động khuyến nghị>

Ví dụ thực tế:

  • exception_note | Loại: SERP scraper; Repos tìm được: 2; Lý do ngoại lệ: SERP scraping public ít do ToS và đa số giải pháp là thương mại; Evidence: https://github.com/owner/repoA#usage, https://github.com/owner/repoB#module; Reviewers: NT (tech), LM (editor); Rủi ro: bot detection, ToS; Recommendation: dùng để minh họa kỹ thuật, không khuyến nghị production.
  1. Hậu kiểm và cập nhật
  • Ngoại lệ được gắn nhãn EXCEPTION và review lại sau 3 tháng. Nếu trong chu kỳ review phát hiện thêm repo phù hợp, cập nhật bảng và gỡ nhãn EXCEPTION.
  • Mọi artifacts (search log, run logs, reviewer notes) lưu vào folder nội bộ kèm thời gian để cho phép audit về sau.

Kết luận ngắn

  • Chấp nhận ngoại lệ là hành động có chủ ý, cần bằng chứng tái lập được, ít nhất hai reviewer độc lập, và note công khai. Nếu thiếu bất kỳ điều kiện bắt buộc nào, không đưa ngoại lệ vào bài.

Checklist bàn giao cho nhóm viết và biên tập

Mình đưa checklist hoàn chỉnh để bạn dùng trực tiếp khi nhận repo đã sàng lọc. Mỗi mục là yêu cầu bắt buộc trước khi biên tập viết phần mô tả điểm mạnh thực dụng.

  • Tổng quan và bản đính kèm (bắt buộc)
  • Bảng repo đã fill: file CSV/Google Sheet với các cột tối thiểu: repourl | primarytype | secondarytypes | lastcommit | license | apidependencyscore | reviewerinitials | runstatus | exception_note.
  • Evidence files: thư mục evidence/ chứa screenshot README lines, snippets từ issues, sample output files, và link trực tiếp tới file trong repo.
  • Scripts used: thư mục scripts/ chứa script scan (rg/curl) và script chạy thử đã dùng để kiểm tra từng repo; kèm README ngắn cách chạy script.
  • Run logs: lưu run-logs/<repo>-<date>.log cho mọi thử nghiệm runtime (stdout/stderr) và network-capture nếu có.
  • Meta file: metadata.yml liệt kê reviewer, ngày kiểm chứng, profile chạy (A/B/C), và verdict (KHÔNG-API | CẦN-API | YÊU CẦU-LOGIN | YÊU CẦU-KIỂM-TRA).
  • Checklist kiểm chứng nội dung cho mỗi repo (từng hàng)
  • README quickcheck – copy dòng lệnh Usage nếu có, và paste vào evidence/readme-snippets.txt.
  • Dependencies scan – đính kèm output của rg trên manifests; highlight bất kỳ SDK tên thương hiệu nào.
  • Env/CI scan – đính kèm kết quả grep cho patterns API_KEY, secrets, .github/workflows references.
  • Endpoint scan – list các URL literal trích ra bởi script; gắn tag domain nếu là third-party (ví dụ serpapi.com).
  • Runtime test – ghi rõ command đã chạy, profile dùng, thời gian, kết quả (pass/fail) và file log tương ứng.
  • Network behavior – nếu repo claim không cần API, attach capture (strace/tcpdump) chứng minh không gọi third-party API; nếu có gọi, nêu host và classify.
  • Yêu cầu cho evidence khi rút điểm mạnh (mỗi claim phải có)
  • Claim tính năng core: phải có 1 trong {CLI command + sample output, test/CI job, example output file}. Ghi nguồn bằng đường dẫn file và dòng.
  • Claim "không cần API": phải có runtime evidence no-outbound OR code scan không thấy env/API SDK AND README không yêu cầu key. Nếu có nghi ngờ, gắn tag [KHÔNG CHẮC].
  • Claim “scale/performance”: cần có benchmark hoặc notes trong README hoặc issue. Nếu không, ghi cảnh báo "no benchmark".
  • Files and artifacts naming convention
  • Bảng: repos.csv
  • Evidence: evidence/<owner>-<repo>/<type>-<timestamp>.(png|txt|json)
  • Scripts: scripts/scan-repo.sh, scripts/run-profile-A.sh
  • Logs: run-logs/<owner>-<repo>-profileA-YYYYMMDD.log
  • Recommended profile to run (ghi rõ cho writer/tech reviewer)
  • Profile A – Polite minimal: concurrency=2, delay=random(1.0,3.0)s, maxpages=20, respectrobots=true. Dùng để kiểm tra core parsing.
  • Profile B – Functional quick: concurrency=4, delay=1s, maxpages=50, jsrender=false. Dùng để kiểm chứng feature detection.
  • Profile C – JS-heavy: headless=true, concurrency=1, max_pages=10, requires local headless binary. Dùng chỉ khi repo dùng puppeteer/playwright.
  • Ghi profile dùng bên cạnh run log.
  • Tone notes cho phần viết
  • Giữ tông practitioner: ngắn, cụ thể, hướng dẫn hành động. Dùng cấu trúc 3 câu cho mỗi tool: (1) công năng chính, (2) lợi ích thực dụng cho workflow SEO, (3) caveat / rủi ro vận hành.
  • Nếu repo có [KHÔNG CHẮC], dùng câu cảnh báo rõ ràng: "Cảnh báo: cần kiểm chứng thêm" và chỉ dẫn cụ thể để kiểm chứng.
  • Tránh so sánh giá trị với SaaS trả phí; nếu độc giả cần so sánh, chỉ nêu khả năng kỹ thuật, không nêu ROI.
  • Tags và nhãn bắt buộc trong bảng (standardized)
  • run_status: PASSED | FAILED | PARTIAL
  • apiflag: NOAPI | REQUIRESAPI | HYBRID | NEEDSLOGIN
  • evidence_level: A | B | C
  • risktag: TOSRISK | HEAVY_DEPENDENCY | UNMAINTAINED
  • review_state: DRAFT | VERIFIED | EXCEPTION
  • Nơi cần kiểm chứng thêm [KHÔNG CHẮC] – bắt buộc tag và note
  • Nếu có bất kỳ dấu hiệu động runtime (dynamic config, remote config fetch) mà script tĩnh không phát hiện, tag [KHÔNG CHẮC] và ghi bước kiểm chứng tiếp theo.
  • Nếu sample output không reproduce trong 30 phút thử nghiệm, tag [KHÔNG CHẮC] và attach run log lỗi.
  • Nếu repo có demo-mode hoặc fixtures dùng sẵn, ghi rõ "demo-mode detected" và note kiểm chứng: run with different input to confirm behavior.
  • Giao nhận và ký nhận
  • Trước khi giao cho biên tập, reviewer kỹ thuật ký vào metadata.yml với verdict và đính evidence zip.
  • Biên tập viên đối chiếu bảng repos.csv, mở evidence folder, và trong CMS chèn một hàng meta gồm evidence links + tone note + tags.
  • Lưu tất cả artifacts cuối cùng vào một commit riêng trong repo nội bộ: /handover/<date>-<initials>.zip
  • Checklist cuối trước khi publish
  • [ ] repos.csv complete và validated
  • [ ] evidence folder có run logs cho mọi repo đánh dấu VERIFIED
  • [ ] scripts runable và có README hướng dẫn
  • [ ] recommended profile noted per repo
  • [ ] tone notes + 3-sentence template cho mỗi repo
  • [ ] tags set, mọi [KHÔNG CHẮC] đều có bước kiểm chứng rõ ràng
  • [ ] metadata.yml có reviewer signatures

Mình đã chuẩn hóa tên file và mẫu để bạn copy-paste vào quy trình. Bạn dùng checklist này để kiểm nhận cuối cùng trước khi biên tập viết phần điểm mạnh cho từng công cụ.

Hướng dẫn an toàn vận hành khi thử chạy repo (sandbox)

Mục tiêu: cho bạn quy trình an toàn để thử chạy một repository không rõ nguồn gốc trong môi trường cách ly, ghi lại artifacts để làm bằng chứng, và có bước rollback an toàn nếu cần.

Chuẩn bị môi trường

  • Chạy trong container để cô lập filesystem và process.
  • Vô hiệu hóa network ở container khi muốn chắc chắn không có egress.
  • Nếu cần quan sát traffic, dùng proxy trung gian (mitmproxy) hoặc capture syscall (strace).
  • Luôn chuẩn bị snapshot hoặc layer để rollback container/image nhanh.

Bước 1 – Tạo container sandbox

  • Dùng Docker để chạy container với mạng tắt. Ví dụ chạy image base có sẵn môi trường dev:
docker run --rm -it --name repo-sandbox --network none -v "$PWD":/work -w /work python:3.10 bash
  • Giải thích ngắn:
  • –network none ngăn container kết nối internet.
  • mount dự án vào /work để chạy thử mã.
  • Nếu cần persistent state cho artifacts, mount thư mục host artifacts.

Bước 2 – Chuẩn bị capture và logging

  • Chuẩn bị ghi log lệnh và stdout/stderr vào file:
script -q -c "bash -lc 'your-start-command here'" run-output.log
  • Nếu muốn giám sát syscalls network, dùng strace (cần cài trong image):
strace -f -e trace=network -o /work/strace.log bash -c "your-start-command here"
  • Nếu muốn quan sát HTTP traffic có thể cho phép egress nhưng rút qua proxy kiểm soát:
  • Khởi chạy mitmproxy trên host và chạy container nối mạng nội bộ chỉ tới proxy; cấu hình HTTPPROXY/HTTPSPROXY cho container. Ghi chú: mitmproxy hiển thị request/response để bạn kiểm chứng endpoint.

Bước 3 – Chạy thử theo profile an toàn

  • Chạy ở chế độ "dry-run" hoặc với sample data khi repo hỗ trợ.
  • Nếu repo cung cấp profile config, override tham số concurrency/rate/limit để giữ mức nhẹ.
  • Chạy lệnh ví dụ và thu mọi artifacts: report.json, output/*.csv, logs, database file. Lưu vào thư mục artifacts/ trên host:
mkdir -p ../artifacts && cp run-output.log ../artifacts/
cp /work/path/to/report.json ../artifacts/ || true
cp /work/strace.log ../artifacts/ || true

Bước 4 – Kiểm tra network và phát hiện API/endpoint

  • Nếu container ở chế độ none thì mọi outbound bị ngăn, điều này sẽ lộ ra nếu mã phụ thuộc API vì lỗi xuất hiện nhanh.
  • Nếu cho phép proxy, xem mitmproxy để phát hiện domain, Authorization header, query param chứa key. Nếu thấy API key patterns, đánh dấu repo yêu cầu API.
  • Dùng ripgrep trên mã trước khi chạy để tìm biến môi trường kiểu API_KEY, hoặc import SDK rõ ràng.

Bước 5 – Lưu artifacts và metadata

  • Ghi metadata thử nghiệm: repo URL, commit SHA, thời gian, command dùng, profile config.
  • Ví dụ cấu trúc lưu:
  • artifacts/<repo>-<sha>/run-output.log
  • artifacts/<repo>-<sha>/strace.log
  • artifacts/<repo>-<sha>/mitmproxy.har
  • artifacts/<repo>-<sha>/metadata.json
  • metadata.json nên chứa: { "repo": "…", "sha": "…", "cmd": "…", "profile": "…", "result": "PASS/FAIL", "notes": "…" }

Bước 6 – Rollback và dọn dẹp

  • Nếu dùng Docker ephemeral: docker run –rm tự tidy up. Nếu dùng docker create/run với volumes, xóa container:
docker rm -f repo-sandbox || true
  • Xóa image tạm nếu cần:
docker image rm <temp-image> || true
  • Nếu đã thay đổi host (ví dụ cài tool global), revert bằng snapshot hoặc restore từ backup. Luôn ưu tiên làm mọi thay đổi trong container để không cần rollback host.

Cảnh báo vận hành và best practice

  • Luôn assume mã có thể âm thầm gọi network hoặc thực hiện I/O bất kỳ. Chạy first-pass với –network none.
  • Không chạy với quyền root trên host. Trong container, hạn chế quyền bằng user không root khi có thể.
  • Nếu phát hiện behavior đáng ngờ (call tới external API, upload data), dừng ngay, lưu artifacts, và chuyển mục đó vào queue “needs manual review”.
  • Ghi chú pháp lý: tránh gửi dữ liệu người dùng ra ngoài khi thử, và tuân thủ robots.txt, ToS khi kích hoạt chế độ có network.

Kết luận ngắn

  • Quy trình ngắn: clone vào host, chạy container –network none, chạy command với logging và strace, mở proxy chỉ khi cần quan sát, lưu artifacts và metadata, rồi xóa container. Nếu cần, mình có thể gửi script bash mẫu để tự động hóa các bước trên.

Lộ trình thực thi để gom >=30 repo hợp lệ

Mục tiêu ngắn: trong 4 tuần gom tối thiểu 30 repository công cụ SEO thỏa điều kiện "không cần API key để chạy chức năng chính", kèm bằng chứng kiểm chứng và bản nháp điểm mạnh thực dụng cho mỗi loại công cụ. Kết quả giao kèm: bảng dữ liệu hoàn chỉnh, 30 repo final có evidence, 8 loại công cụ có ít nhất 1 mẫu điểm mạnh đã soạn, và checklist bàn giao cho nhóm viết.

Tuần 1 – Query và thu thập 150 candidates (7 ngày)

  • Mục tiêu tuần: thu thập 150 repo candidates từ GitHub (ưu tiên), mở rộng GitLab nếu cần.
  • Hoạt động chính:
  1. Chuẩn bị queries chuẩn hóa (GitHub Advanced Search + topic lists + Awesome lists). Dùng template query đã thống nhất.
  2. Chạy các query, export kết quả thô (repo url, stars, last pushed, language, description).
  3. Loại trùng và lọc sơ bộ theo license và last pushed >= 24 tháng.
  4. Gán nhãn sơ bộ theo taxonomy (crawl, audit, log, keyword, serp, link, schema, sitemap, image, performance, utils).
  • Phân công:
  • 2 người searcher chạy queries và export (mỗi người 75 repo).
  • 1 người build sheet tổng hợp ban đầu và script dedupe.
  • Deliverables tuần:
  • Sheet candidates.csv (150 rows) + log truy vấn (queries.txt) để audit.
  • Script dedupe.sh hoặc gh CLI command list.
  • KPI tuần:
  • Số candidates >=150.
  • Tỉ lệ candidates với license OSI-approved >= 85%.
  • Log truy vấn đầy đủ và reproducible.

Tuần 2 – Triage tĩnh và static scan -> shortlist 60 (7 ngày)

  • Mục tiêu tuần: từ 150 xuống 60 bằng kiểm tra tĩnh, loại bỏ repo phụ thuộc API rõ rệt hoặc dead/archived.
  • Hoạt động chính:
  1. Chạy pipeline static scan tự động (script: rg + parsing package manifests + check .env.example + CI workflows). Output: apiflags, sdkflags, env_vars found.
  2. Manual review cho các trường hợp ambiguous. Mỗi repo có checklist tĩnh phải được tick: license OK, pushed >=24 months, no explicit API_KEY requirement, no SDK proprietary imports OR has offline mode doc.
  3. Gán score sàng lọc (0-10). Giữ 60 repo score cao nhất theo rule: priority = (no-api evidence, recent commit, runnable sample).
  • Phân công:
  • 3 reviewers tĩnh, mỗi người 20-25 repo; 1 kỹ sư viết script và aggregate results.
  • Deliverables tuần:
  • Shortlist60.csv với cột evidence static (files checked, matched patterns), api_flag boolean, notes.
  • Các artifact scan per-repo (json).
  • KPI tuần:
  • Tỉ lệ false positive static scan < 10% (đo bằng sampling manual).
  • Shortlist đạt 60 repo có evidence static pass >= 80% fields.

Tuần 3 – Runtime check 30 phút/repos -> final 30 (7 ngày)

  • Mục tiêu tuần: với mỗi repo trong shortlist, chạy quy trình kiểm thử runtime tối đa 30 phút/repo để phân loại thành FINAL_OK hoặc REJECT.
  • Quy trình 30 phút (tiêu chuẩn, timeboxed):

0-5m: đọc README Quickstart, xác định command mẫu và sample data. 5-10m: run static grep tiếp (APIKEY, sdk imports, endpoints). Nếu phát hiện dấu rõ ràng, mark REJECT. 10-25m: chạy sample command trong sandbox (docker –network none) hoặc với proxy/mitmproxy nếu muốn log egress. Ghi lại stdout/stderr, artifacts. 25-30m: phân tích logs: có outbound calls tới third-party API? có require env APIKEY? có produce usable output? Kết luận: PASS / HYBRID (core run without API but optional features use API) / FAIL.

  • Phân công và throughput:
  • Mỗi reviewer runtime đảm nhiệm 6 repo/ngày ~ 30 phút/repo -> 6 repos x 5 ngày = 30 repos tối đa per reviewer/week. Tổ team cần 2 reviewers runtime đồng thời để hoàn thành 60 trong 5 ngày. Thực tế bố trí 3 reviewer để buffer.
  • Deliverables tuần:
  • Final30.csv gồm 30 repo PASS, kèm artifacts folder per-repo: run-output.log, strace/mitm logs if any, metadata.json (cmd, sha, timestamp).
  • Reject list với lý do rõ ràng (apirequired, buildfail, archived).
  • KPI tuần:
  • Tỉ lệ PASS trên shortlist >= 50% để đạt 30 final. Nếu thấp, phải mở rộng shortlist nhanh.
  • Mỗi repo PASS có metadata đầy đủ và evidence file.

Tuần 4 – Soạn nháp điểm mạnh thực dụng + peer review, phân công công việc và KPIs (7 ngày)

  • Mục tiêu tuần: cho 30 repo final, soạn nháp điểm mạnh thực dụng theo template 3 câu, peer review 2 lượt, chuẩn bị giao cho nhóm viết.
  • Hoạt động chính:
  1. Viết nháp điểm mạnh cho mỗi repo theo template: chức năng chính | lợi ích thực dụng cho workflow SEO | rủi ro/limit. Mỗi nháp 40-60 từ.
  2. Peer review nội bộ: reviewer độc lập kiểm chứng bằng evidence artifacts; chấp nhận/requests changes.
  3. Tổ biên tập nhóm SEO chọn top 8 loại công cụ và viết phần tổng hợp điểm mạnh theo loại.
  4. Chuẩn bị checklist bàn giao cho nhóm viết: sheet final, artifacts, sample commands, recommended profile to run.
  • Phân công:
  • 3 copywriters mỗi người 10 repo nháp.
  • 2 peer reviewers (kỹ thuật + SEO lead) duyệt.
  • 1 PM tổng hợp và finalise deliverables.
  • Deliverables tuần:
  • Draft article section per-type with sample tools and points.
  • Handover package: final csv, artifacts zip, run scripts, review logs.
  • KPI tuần:
  • 100% repo final có nháp điểm mạnh và evidence link.
  • Peer review pass rate >= 90% (số nháp approved/total).
  • Handover hoàn tất sớm nhất 2 ngày trước deadline.

Cơ chế quản lý rủi ro và điều chỉnh tiến độ

  • Nếu tuần 3 không thu đủ 30 PASS, ngay lập tức lặp lại tuần 2 queries mở rộng + thêm 30 candidates và repeat triage ngắn trong 48 giờ.
  • Mỗi ngày báo cáo stand-up: số repo processed, PASS, REJECT, blockers. PM quyết chỉ đạo mở rộng nguồn nếu tỉ lệ PASS low.

KPIs chung dự án (4 tuần)

  • Candidates collected >=150.
  • Shortlist after static triage = 60.
  • Final PASS >=30.
  • Draft điểm mạnh cho 30 repo + peer review complete.
  • Evidence artifacts per-repo >= 3 files (readme snippet, run log, output file).
  • Tỉ lệ false inclusion < 10% (sau spot-check).

Giao nhận cuối cùng

  • Bảng final CSV với trường bắt buộc, artifacts zip, report ngắn nêu methodology, và checklist bàn giao cho nhóm viết. Mình khuyến khích bạn yêu cầu mỗi reviewer attach một "timestamped evidence pack" để audit sau này.

Ghi chú giới hạn phạm vi và nhãn [KHÔNG CHẮC]

Mình liệt kê rõ những giới hạn phân tích trong khung này, các trường hợp runtime-resolved endpoints khó phát hiện, và hành động follow-up bắt buộc khi gặp repo gắn nhãn [KHÔNG CHẮC]. Mục tiêu là giúp bạn đưa ra quyết định an toàn trước khi công bố hoặc đưa repo vào danh sách.

Giới hạn phương pháp luận

  • Phân tích ưu tiên GitHub, mở rộng GitLab nếu cần, trong phạm vi các repo public cập nhật trong 24 tháng gần nhất. Repo private, npm/pypi packages không public và artifacts binary không được quét tự động nằm ngoài phạm vi.
  • Định nghĩa "không API" là: chức năng chính chạy được mà không cần API key hoặc SDK bên thứ ba. Repo có tính năng tùy chọn dùng API vẫn chấp nhận nếu core chạy offline. Tuy nhiên điều này phụ thuộc vào logic runtime, nên đôi khi cần kiểm thử.
  • Quét tĩnh (grep/rg) + checks trên CI/Dockerfile thường bắt phần lớn dấu hiệu. Nhưng không thể khẳng định tuyệt đối nếu mã xây dựng endpoint tại runtime hoặc tải config từ remote.
  • Kiểm thử runtime 30 phút là trade-off giữa tốc độ và độ chính xác; một số dependency ẩn hoặc đường dẫn điều kiện hiếm có thể bị bỏ sót.

Trường hợp runtime-resolved endpoints khó phát hiện

  • Endpoint được tạo từ biến môi trường mà tên biến không chuẩn, chẳng hạn CONFIGURL hoặc ENDPOINTBASE, và giá trị được cung cấp từ CI hoặc secrets manager. Static grep dễ bỏ sót.
  • Endpoint được tải động từ remote config file/gist hoặc registry ở bước bootstrap. Mã không chứa URL tĩnh nên quét tĩnh không thấy.
  • Code sử dụng service discovery, DNS CNAME hoặc subdomain do deploy tạo ra, ví dụ host được ghép theo tenant hoặc environment, không thể phân biệt bằng regex.
  • Binary/vendored module hoặc submodule đóng gói endpoint trong mã nhị phân hoặc minified JS, gây khó đọc.
  • Chức năng bị lazy-loaded và chỉ kích hoạt qua flag ít dùng hoặc trong cron job; Quickstart không phủ tới code path ấy nên runtime test ngắn bỏ sót.
  • Proxying: repo gọi một host trung gian do tác giả quản lý, và host đó thực tế chuyển tiếp tới API bên thứ ba. Qua grep thấy host nội bộ, nhưng runtime vẫn cần service bên ngoài.

Hành động follow-up bắt buộc khi gặp nhãn [KHÔNG CHẮC]

  • Ghi lại evidence pack ngay: file README excerpt, kết quả grep, file CI/Docker liên quan, timestamp, commit SHA. Lưu vào folder evidence/ để audit.
  • Bắt buộc chạy thử mở rộng: 1) thực thi Quickstart trong container với –network none; 2) nếu pass, chạy lại với network enabled và giám sát outbound bằng strace hoặc mitmproxy để ghi tất cả host gọi ra. Ghi log network.
  • Nếu phát hiện outbound tới domain lạ hoặc api.* hoặc header Authorization dùng env var, treat as "requires API" và loại khỏi danh sách "không API".
  • Nếu không thể reproduce do thiếu binary hoặc môi trường, mở ticket "NEEDSDEEPREVIEW" và gán cho dev reviewer để làm full audit (tối đa 4 giờ).
  • Luôn thêm nhãn công khai trong bảng tổng hợp: "[KHÔNG CHẮC] – evidencepacklink – recommended_action" để người đọc biết cần thẩm định thêm.
  • Với các trường hợp chứa proxying hoặc runtime config tải từ remote, yêu cầu maintainer trả lời issue: xác nhận tính năng có thể chạy offline và cung cấp script demo offline; nếu maintainer không trả lời trong 7 ngày, loại bỏ khỏi bài hoặc đánh dấu rõ rủi ro ToS.

Ghi chú vận hành cuối

  • Mỗi repo gắn [KHÔNG CHẮC] phải qua 2 người kiểm chứng độc lập trước khi đổi nhãn thành "KHÔNG-API" hoặc "CẦN-API". Mọi quyết định thay đổi nhãn phải kèm evidence_pack và log runtime. Nếu bạn cần, mình có script mẫu tự động hóa phần grep + curl + strace để thu evidence.

Kết luận

Sau khi áp dụng khung này, bạn sẽ có một danh mục repo SEO chạy cục bộ đã được sàng lọc, phân loại và chứng thực đủ bằng chứng để đưa vào quy trình biên tập hoặc triển khai. Lưu ý thực tế quan trọng nhất: luôn chạy thử trong môi trường cô lập với mạng vô hiệu hóa trước khi cho phép bất kỳ repo nào truy cập ngoài, vì nhiều phụ thuộc chỉ lộ khi runtime.

Xây plugin quản lý Dashboard Widgets WordPress

Mình sẽ hướng dẫn bạn xây một plugin gọn và dễ bảo trì để thêm, sửa, xóa và sắp xếp Dashboard Widgets, ẩn hiện widget mặc định, bảo mật bằng capability và nonce, lưu cấu hình qua Options API, tôn trọng Screen Options. Ví dụ minh họa dùng Dashboard Widgets trên WordPress single-site hiện đại.

Xác lập phạm vi, giả định và mục tiêu

Trong toàn bộ hướng dẫn này, “phần hiển thị trên Dashboard” được hiểu chính xác là các Dashboard Widgets trên màn hình quản trị WordPress tại wp-admin/index.php. Đây là các meta box thuộc screen id dashboard, có context normal hoặc side, do core và plugin đăng ký. Những thành phần không phải meta box như Welcome panel hay admin_notices sẽ được nhắc riêng khi cần để tránh nhầm lẫn.

Giả định môi trường mặc định là single-site trên WordPress hiện đại. Bạn đã có kiến thức cơ bản PHP và WordPress: hiểu action, filter, cấu trúc plugin, cách làm việc với Options API, và quen tay với thao tác admin. Hướng dẫn không bao gồm cài đặt WordPress, xây theme hay hướng dẫn PHP nhập môn. Khi có khác biệt cho multisite, mình sẽ kèm lưu ý ngắn – ví dụ Network Admin dùng hook wpnetworkdashboardsetup và capability cấp mạng như managenetwork, còn User Admin dùng wpuserdashboardsetup. Bạn có thể kiểm tra ngữ cảnh nhanh qua isnetworkadmin hoặc isuser_admin trên máy mình.

Mục tiêu cuối cùng là tạo một plugin độc lập, có kiến trúc rõ ràng và dễ bảo trì, cho phép:

  • Thêm widget tùy chỉnh với tiêu đề, nội dung, capability, vị trí cột và ưu tiên hiển thị.
  • Sửa nội dung và thuộc tính hiển thị của widget an toàn, có kiểm tra xung đột khi lưu.
  • Xóa widget gọn gàng, có dọn dẹp dữ liệu liên quan.
  • Ẩn hoặc hiện các widget mặc định của WordPress theo cấu hình, không làm vỡ bố cục và tôn trọng lựa chọn người dùng khi phù hợp.

Các nguyên tắc bắt buộc để đạt độ an toàn và ổn định:

  • Đăng ký và gỡ widget đúng luồng wpdashboardsetup, dùng wpadddashboardwidget và removemeta_box đúng context – priority.
  • Kiểm soát phân quyền bằng currentusercan theo mục đích widget, không gọi callback tùy ý từ dữ liệu lưu trữ.
  • Chống CSRF bằng nonce, sanitize đầu vào, escape đầu ra.
  • Lưu cấu hình tập trung bằng Options API có versioning để cập nhật an toàn và dễ migrate.
  • Tôn trọng Screen Options và thứ tự kéo thả của người dùng, chỉ ép chính sách khi được cấu hình rõ.

Cách Dashboard widget hoạt động trong WordPress

Khi trang wp-admin/index.php được tải, WordPress khởi tạo WPScreen cho screen id dashboard, gọi hook wpdashboardsetup, rồi render meta box theo thứ tự và trạng thái ẩn hiện của từng người dùng. Ở bước wpdashboardsetup, core và plugin đăng ký các Dashboard widget bằng API wpadddashboardwidget. Mỗi widget thực chất là một meta box có id duy nhất, tiêu đề, và callback in nội dung. Sau khi đăng ký xong, WordPress đọc user meta để biết widget nào bị ẩn, thứ tự sắp xếp ra sao, rồi gọi dometaboxes để hiển thị.

Bạn cần nắm 3 mảnh ghép:

  • Đăng ký: dùng wpadddashboardwidget trong hook wpdashboard_setup. API này thêm widget vào context normal với priority core.
  • Lưu trữ tạm thời: mọi meta box đang có mặt được đặt trong mảng toàn cục $wpmetaboxes theo 4 tầng [screen][context][priority][id]. Riêng Dashboard dùng 2 context là normal và side. Priority phổ biến là high, core, default, low. Khi người dùng đã sắp xếp, core sẽ đổ chúng vào bucket sorted theo user meta.
  • Quy tắc người dùng: tab Screen Options ở góc phải thao tác trên 2 khóa user meta
  • metaboxhidden_dashboard: mảng ID widget đang bị ẩn.
  • meta-box-orderdashboard: thứ tự widget theo từng context. Khi tồn tại, thứ tự này được ưu tiên hơn priority trong $wpmeta_boxes.

Ví dụ đầy đủ: đăng ký một widget và đặt mặc định sang cột side với priority low. Mình chọn priority 99 để di chuyển sau khi đa số widget đã được thêm vào.

<?php
// file plugin hoặc mu-plugin
add_action('wp_dashboard_setup', function () {
    $widget_id = 'mydash_notes';

    // 1) Đăng ký widget - mặc định rơi vào normal-core
    wp_add_dashboard_widget(
        $widget_id,
        'Team notes',
        function () {
            echo '<p>Ghi chú nhanh cho team. Bạn có thể sửa nội dung trong phần cấu hình.</p>';
        }
    );

    // 2) Di chuyển mặc định sang cột side, priority low
    global $wp_meta_boxes;

    $page         = 'dashboard';
    $src_context  = 'normal';
    $dst_context  = 'side';
    $dst_priority = 'low';

    $possible_src = array('core', 'default', 'high', 'low');
    $box = null;

    foreach ($possible_src as $p) {
        if (isset($wp_meta_boxes[$page][$src_context][$p][$widget_id])) {
            $box = $wp_meta_boxes[$page][$src_context][$p][$widget_id];
            unset($wp_meta_boxes[$page][$src_context][$p][$widget_id]);
            break;
        }
    }

    if ($box) {
        // Bảo đảm mảng đích tồn tại
        if (!isset($wp_meta_boxes[$page][$dst_context])) {
            $wp_meta_boxes[$page][$dst_context] = array();
        }
        if (!isset($wp_meta_boxes[$page][$dst_context][$dst_priority])) {
            $wp_meta_boxes[$page][$dst_context][$dst_priority] = array();
        }

        // Cập nhật metadata trong box
        $box['context']  = $dst_context;
        $box['priority'] = $dst_priority;

        // Đặt vào side-low
        $wp_meta_boxes[$page][$dst_context][$dst_priority][$widget_id] = $box;
    }
}, 99);

Biến thể linh hoạt cùng ý tưởng, bạn chỉ cần đổi biến mục tiêu:

<?php
add_action('wp_dashboard_setup', function () {
    $widget_id   = 'sales_summary';
    $dst_context = 'side';   // 'normal' hoặc 'side'
    $dst_priority = 'low';   // 'high' | 'core' | 'default' | 'low'

    wp_add_dashboard_widget($widget_id, 'Sales summary', function () {
        echo '<p>Doanh số 7 ngày gần nhất.</p>';
    });

    global $wp_meta_boxes;
    $page = 'dashboard';
    $src_context = 'normal';
    foreach (array('core','default','high','low') as $p) {
        if (isset($wp_meta_boxes[$page][$src_context][$p][$widget_id])) {
            $box = $wp_meta_boxes[$page][$src_context][$p][$widget_id];
            unset($wp_meta_boxes[$page][$src_context][$p][$widget_id]);
            $box['context']  = $dst_context;
            $box['priority'] = $dst_priority;
            $wp_meta_boxes[$page][$dst_context][$dst_priority][$widget_id] = $box;
            break;
        }
    }
}, 99);

Điều quan trọng khi bạn di chuyển box trong $wpmetaboxes là tôn trọng lựa chọn người dùng. Nếu user đã từng kéo thả và core đã lưu meta-box-order_dashboard, core sẽ ưu tiên thứ tự trong user meta và bỏ qua vị trí mặc định bạn vừa đặt. Đây là hành vi đúng để không làm thay đổi dashboard cá nhân.

Ẩn hiện liên quan Screen Options hoạt động như sau: lần đầu người dùng vào Dashboard, WordPress dùng danh sách mặc định để xác định widget nào ẩn. Khi người dùng tick hoặc bỏ tick trong Screen Options, giá trị được lưu vào metaboxhiddendashboard và ngay từ lần tải sau, danh sách ẩn hiện sẽ tuân theo user meta này. Bạn có thể gỡ hẳn một widget nào đó khỏi giao diện bằng removemetabox trong cùng hook wpdashboard_setup khi cần, khi đó nó cũng biến mất khỏi Screen Options.

Cuối cùng, lưu ý rằng có thành phần trông giống widget nhưng không phải meta box. Điển hình là Welcome panel. Nó xuất hiện qua hook welcomepanel chứ không nằm trong $wpmetaboxes, nên removemetabox không tác dụng. Nếu muốn tắt, bạn cần gỡ hành vi in panel bằng removeaction trên hook đó:

<?php
add_action('wp_dashboard_setup', function () {
    remove_action('welcome_panel', 'wp_welcome_panel');
});

Nắm đúng luồng này giúp bạn thêm, sắp xếp hoặc ẩn widget một cách có kiểm soát, không phá vỡ lựa chọn cá nhân của người dùng và không nhầm lẫn giữa meta box với các khối đặc thù như Welcome panel.

Thiết kế kiến trúc plugin tách biệt, dễ bảo trì

Bạn nên đóng gói mọi thứ thành một plugin độc lập, có bootstrap rõ ràng, autoloader đơn giản, hook activation – deactivation – uninstall chuẩn, module hóa lớp lưu trữ, trang cấu hình, registry widget, rendering và security. Cách này giúp bật tắt plugin mà không đụng theme hay plugin khác, đồng thời dễ phát triển dài hạn.

Các bước thực hiện:

  1. Tạo cấu trúc thư mục gọn gàng
  • mydash/
  • mydash.php
  • uninstall.php
  • inc/
  • Autoload.php
  • Config.php
  • Storage.php
  • Registry.php
  • Rendering.php
  • Security.php
  • admin/
  • AdminPage.php
  • languages/
  • mydash.pot
  1. File plugin chính: bootstrap, autoloader, hook vòng đời, text domain, Settings link và khởi động module

Ví dụ hoàn chỉnh:

<?php
/**
 * Plugin Name: MyDash - Custom Admin Dashboard
 * Description: Quản lý Dashboard widgets tùy chỉnh: thêm, sửa, xóa và ẩn/hiện widget mặc định.
 * Version: 1.0.0
 * Author: Your Name
 * Text Domain: mydash
 * Domain Path: /languages
 */

defined('ABSPATH') || exit;

define('MYDASH_FILE', __FILE__);
define('MYDASH_DIR', plugin_dir_path(__FILE__));
define('MYDASH_URL', plugin_dir_url(__FILE__));
define('MYDASH_VER', '1.0.0');
define('MYDASH_CAP_MANAGE', 'manage_custom_dashboard');

// 1) Autoloader PSR-4 đơn giản cho namespace MyDash\
require_once MYDASH_DIR . 'inc/Autoload.php';
MyDash\Autoload::register('MyDash\\', MYDASH_DIR . 'inc/');

// 2) Nạp i18n
add_action('init', function () {
    load_plugin_textdomain('mydash', false, dirname(plugin_basename(__FILE__)) . '/languages');
});

// 3) Hook activation: cấp capability riêng, khởi tạo option cấu hình
register_activation_hook(__FILE__, function ($network_wide) {
    // Cấp quyền cho Administrator
    $grant_cap = function () {
        $role = get_role('administrator');
        if ($role && ! $role->has_cap(MYDASH_CAP_MANAGE)) {
            $role->add_cap(MYDASH_CAP_MANAGE);
        }
        // Khởi tạo option cấu hình mặc định nếu chưa có
        \MyDash\Storage::ensure_defaults();
    };

    if (is_multisite() && $network_wide) {
        $sites = get_sites(array('fields' => 'ids'));
        foreach ($sites as $blog_id) {
            switch_to_blog($blog_id);
            $grant_cap();
            restore_current_blog();
        }
    } else {
        $grant_cap();
    }
});

// 4) Hook deactivation: giữ dữ liệu, không làm gì để tránh mất cấu hình
register_deactivation_hook(__FILE__, function () {
    // Cố ý bỏ trống: giữ dữ liệu và capability để tránh gián đoạn khi kích hoạt lại
});

// 5) Link Settings ngay tại danh sách Plugins
add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
    $url = admin_url('options-general.php?page=mydash');
    $links[] = '<a href="' . esc_url($url) . '">' . esc_html__('Settings', 'mydash') . '</a>';
    return $links;
});

// 6) Khởi động các module khi plugin sẵn sàng
add_action('plugins_loaded', function () {
    // Trang cấu hình
    (new \MyDash\Admin\AdminPage())->init();

    // Registry widget và hook đăng ký widget vào Dashboard
    $registry = new \MyDash\Registry();
    add_action('wp_dashboard_setup', [$registry, 'register_widgets'], 10);

    // Bạn có thể thêm hook chuyên biệt cho Network/User Dashboard ở các phần tiếp theo
});
  1. Autoloader: tránh require thủ công và giảm rối rắm
<?php
// inc/Autoload.php
namespace MyDash;

defined('ABSPATH') || exit;

final class Autoload
{
    public static function register(string $prefix, string $base_dir): void
    {
        spl_autoload_register(function ($class) use ($prefix, $base_dir) {
            $len = strlen($prefix);
            if (strncmp($prefix, $class, $len) !== 0) {
                return;
            }
            $relative = substr($class, $len);
            $file = $base_dir . str_replace('\\', '/', $relative) . '.php';
            if (is_readable($file)) {
                require $file;
            }
        });
    }
}
  1. Module Config và Storage: một chỗ quyết định mặc định, một lớp bọc Options API
<?php
// inc/Config.php
namespace MyDash;

defined('ABSPATH') || exit;

final class Config
{
    // Tên option lưu toàn bộ cấu hình plugin
    public const OPTION_NAME = 'mydash_dashboard_widgets';

    // Cấu hình mặc định, autoload=no sẽ thiết lập ở lần add_option đầu tiên
    public static function defaults(): array
    {
        return [
            'schema_version' => 1,
            'rev' => 1,
            'widgets' => [
                // Ví dụ một widget mặc định
                'team_notes' => [
                    'id' => 'team_notes',
                    'title' => __('Team notes', 'mydash'),
                    'enabled' => true,
                    'type' => 'notes',
                    'context' => 'side',
                    'priority' => 'low',
                    'capability' => 'edit_posts',
                    'data' => [
                        'content' => __('Welcome to your custom dashboard.', 'mydash'),
                    ],
                    'updated_at' => gmdate('c'),
                ],
            ],
            'hidden_core' => [
                // ví dụ: 'dashboard_primary' => true
            ],
            'policy' => [
                'forced_hide' => [],
                'forced_show' => [],
            ],
        ];
    }
}
<?php
// inc/Storage.php
namespace MyDash;

defined('ABSPATH') || exit;

final class Storage
{
    public static function ensure_defaults(): void
    {
        $name = Config::OPTION_NAME;
        if (get_option($name, null) === null) {
            add_option($name, Config::defaults(), '', 'no'); // autoload=no
        }
    }

    public static function get(): array
    {
        $cfg = get_option(Config::OPTION_NAME, []);
        return is_array($cfg) ? $cfg : [];
    }

    public static function update(array $new): bool
    {
        return update_option(Config::OPTION_NAME, $new, false);
    }
}
  1. Security: gom hàm kiểm tra capability và nonce để dùng nhất quán
<?php
// inc/Security.php
namespace MyDash;

defined('ABSPATH') || exit;

final class Security
{
    public static function can_manage(): bool
    {
        return current_user_can(MYDASH_CAP_MANAGE);
    }

    public static function enforce_manage(): void
    {
        if (! self::can_manage()) {
            wp_die(esc_html__('You are not allowed to manage this dashboard.', 'mydash'), 403);
        }
    }

    public static function nonce_field(string $action, string $name = '_mydash_nonce'): void
    {
        wp_nonce_field($action, $name);
    }

    public static function verify_nonce(string $action, string $name = '_mydash_nonce'): void
    {
        check_admin_referer($action, $name);
    }
}
  1. Registry: nơi duy nhất khai báo các type và callback rendering, không đọc tên hàm từ DB
<?php
// inc/Registry.php
namespace MyDash;

defined('ABSPATH') || exit;

final class Registry
{
    private array $types;

    public function __construct()
    {
        $this->types = [
            // Mỗi type ánh xạ tới callable an toàn do plugin kiểm soát
            'notes' => [Rendering::class, 'render_notes'],
            // Bạn có thể bổ sung type mới tại đây
        ];
    }

    public function register_widgets(): void
    {
        if (! is_admin()) {
            return;
        }

        $cfg = Storage::get();
        if (empty($cfg['widgets']) || !is_array($cfg['widgets'])) {
            return;
        }

        foreach ($cfg['widgets'] as $id => $w) {
            if (empty($w['enabled'])) {
                continue;
            }
            // Kiểm tra capability người dùng hiện tại cho widget
            $cap = !empty($w['capability']) ? $w['capability'] : 'read';
            if (! current_user_can($cap)) {
                continue;
            }

            $type = $w['type'] ?? '';
            $cb   = $this->types[$type] ?? null;
            if (! is_callable($cb)) {
                continue;
            }

            // Đăng ký widget - vào normal/core mặc định
            wp_add_dashboard_widget(
                $id,
                $w['title'] ?? $id,
                function () use ($cb, $w) {
                    call_user_func($cb, $w);
                }
            );

            // Di chuyển sang context/priority mong muốn
            $context  = $w['context']  ?? 'normal';
            $priority = $w['priority'] ?? 'core';
            $this->move_box('dashboard', $id, $context, $priority);
        }
    }

    private function move_box(string $screen, string $widget_id, string $dst_context, string $dst_priority): void
    {
        global $wp_meta_boxes;
        $src_context = 'normal';
        foreach (['core','default','high','low'] as $p) {
            if (isset($wp_meta_boxes[$screen][$src_context][$p][$widget_id])) {
                $box = $wp_meta_boxes[$screen][$src_context][$p][$widget_id];
                unset($wp_meta_boxes[$screen][$src_context][$p][$widget_id]);

                if (!isset($wp_meta_boxes[$screen][$dst_context][$dst_priority])) {
                    $wp_meta_boxes[$screen][$dst_context][$dst_priority] = [];
                }
                $box['context']  = $dst_context;
                $box['priority'] = $dst_priority;
                $wp_meta_boxes[$screen][$dst_context][$dst_priority][$widget_id] = $box;
                break;
            }
        }
    }
}
  1. Rendering: nơi in nội dung widget, giữ an toàn mặc định
<?php
// inc/Rendering.php
namespace MyDash;

defined('ABSPATH') || exit;

final class Rendering
{
    public static function render_notes(array $widget): void
    {
        $content = $widget['data']['content'] ?? '';
        echo '<div class="mydash-notes">';
        echo wp_kses_post($content);
        echo '</div>';
    }
}
  1. Trang cấu hình admin: một menu dưới Settings và trang giao diện riêng, có kiểm tra capability
<?php
// admin/AdminPage.php
namespace MyDash\Admin;

use MyDash\Security;
use MyDash\Storage;

defined('ABSPATH') || exit;

final class AdminPage
{
    public function init(): void
    {
        add_action('admin_menu', [$this, 'register_menu']);
        add_action('admin_post_mydash_save_settings', [$this, 'handle_save']);
    }

    public function register_menu(): void
    {
        add_options_page(
            __('MyDash Dashboard', 'mydash'),
            __('MyDash Dashboard', 'mydash'),
            MYDASH_CAP_MANAGE,
            'mydash',
            [$this, 'render']
        );
    }

    public function render(): void
    {
        Security::enforce_manage();
        $cfg = Storage::get();
        ?>
        <div class="wrap">
            <h1><?php echo esc_html__('Custom Dashboard', 'mydash'); ?></h1>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <input type="hidden" name="action" value="mydash_save_settings" />
                <?php \MyDash\Security::nonce_field('mydash_save_settings'); ?>
                <p><?php echo esc_html__('This is a minimal settings page. The full CRUD UI will be added in later steps.', 'mydash'); ?></p>
                <p>
                    <label for="team_notes_content"><?php echo esc_html__('Team notes content', 'mydash'); ?></label>
                    <textarea id="team_notes_content" name="team_notes_content" class="large-text" rows="5"><?php
                        $content = $cfg['widgets']['team_notes']['data']['content'] ?? '';
                        echo esc_textarea($content);
                    ?></textarea>
                </p>
                <p><button class="button button-primary"><?php echo esc_html__('Save changes', 'mydash'); ?></button></p>
            </form>
        </div>
        <?php
    }

    public function handle_save(): void
    {
        \MyDash\Security::enforce_manage();
        \MyDash\Security::verify_nonce('mydash_save_settings');

        $cfg = Storage::get();
        $new = $cfg;

        $content = isset($_POST['team_notes_content']) ? wp_kses_post(wp_unslash($_POST['team_notes_content'])) : '';
        if (isset($new['widgets']['team_notes'])) {
            $new['widgets']['team_notes']['data']['content'] = $content;
            $new['widgets']['team_notes']['updated_at'] = gmdate('c');
            $new['rev'] = intval($new['rev'] ?? 1) + 1;
        }

        Storage::update($new);

        wp_safe_redirect(add_query_arg('updated', 1, admin_url('options-general.php?page=mydash')));
        exit;
    }
}
  1. Uninstall: dọn sạch option và thu hồi capability khỏi Administrator ở mọi site
<?php
// uninstall.php
defined('WP_UNINSTALL_PLUGIN') || exit;

require_once __DIR__ . '/inc/Config.php';

$delete_everything = function () {
    // Xóa option cấu hình chính
    delete_option(\MyDash\Config::OPTION_NAME);

    // Thu hồi capability tùy chỉnh
    $role = get_role('administrator');
    if ($role && $role->has_cap('manage_custom_dashboard')) {
        $role->remove_cap('manage_custom_dashboard');
    }
};

if (is_multisite()) {
    $sites = get_sites(array('fields' => 'ids'));
    foreach ($sites as $blog_id) {
        switch_to_blog($blog_id);
        $delete_everything();
        restore_current_blog();
    }
} else {
    $delete_everything();
}

Ghi chú áp dụng thực tế

  • Tên hàm, hằng và option đều có tiền tố mydash để tránh va chạm. Bạn có thể đổi slug theo dự án, miễn nhất quán.
  • Capability riêng managecustomdashboard chỉ cấp cho Administrator khi kích hoạt để kiểm soát trang cấu hình và các thao tác quản trị. Với multisite kích hoạt ở mức mạng, activation hook đã lặp mọi site để thêm cap đúng chỗ.
  • Text domain mydash đã được load qua init, thư mục languages để bạn xuất file .pot và dịch. Điều này phục vụ người đọc thứ cấp là quản trị viên nội bộ hay khách hàng cần giao diện Việt hóa hoặc đa ngôn ngữ.
  • Hook đăng ký widget đặt trong Registry qua wpdashboardsetup. Kiến trúc này đảm bảo khi vô hiệu hóa plugin, toàn bộ callback ngừng hoạt động ngay, không đụng chạm theme hoặc plugin khác.
  • Phần CRUD UI, chính sách ẩn hiện, kiểm soát priority và context sẽ nối tiếp trên nền các module này. Bạn đã có sẵn Security để dùng nonce và capability thống nhất, Storage để ghi đọc cấu hình một chỗ, Registry để kiểm soát callback an toàn và Rendering để giữ chuẩn sanitize khi hiển thị.

Mô hình lưu cấu hình bằng Options API, cấu trúc dữ liệu và versioning

Mình khuyến nghị lưu toàn bộ cấu hình widget vào một option duy nhất với autoload=no. Cách này giảm số truy vấn, cập nhật gần như nguyên tử ở cấp hàng, dễ kiểm soát version và migrate mà không rơi vào trạng thái dở dang. Dưới đây là mô hình dữ liệu, quy ước khóa, cơ chế khóa lạc quan bằng rev, kịch bản migrate an toàn và trường hợp bất khả kháng phải tách nhỏ.

Mô hình dữ liệu và quy ước khóa

  • Tên option: tiền tố theo plugin, ví dụ mydashdashboardwidgets. Không nhúng version vào tên option.
  • Autoload: no ngay từ lần add_option đầu tiên.
  • Quy ước id và slug:
  • id dùng làm meta box ID và key trong mảng widgets. Chỉ dùng a-z, 0-9 và gạch dưới. Chuẩn hóa bằng sanitizekey, ví dụ salessummary, team_notes.
  • slug để hiển thị URL hoặc nhãn trong UI quản trị, cho phép gạch ngang. Ví dụ team-notes. Có thể trùng về mặt ngữ nghĩa với id nhưng khác ký tự phân tách.
  • Dấu thời gian: ISO 8601 UTC, dùng gmdate(‘c’). Lưu createdat và updatedat để dễ kiểm soát thay đổi.
  • Cấu trúc đề xuất:
  • schema_version: version của schema option.
  • rev: số nguyên tăng dần mỗi lần ghi, dùng cho khóa lạc quan.
  • widgets: mảng kết hợp key là id. Mỗi widget có id, slug, title, type, capability, enabled, context, priority, data, createdat, updatedat.
  • hidden_core: map id widget core cần ẩn khỏi Dashboard ở tầm hệ thống.
  • forced_hide: danh sách id widget phải ẩn cho mọi người dùng.
  • forced_show: danh sách id widget phải hiện cho mọi người dùng.

Ví dụ payload hoàn chỉnh

<?php
$cfg = [
    'schema_version' => 2,
    'rev' => 7,
    'widgets' => [
        'team_notes' => [
            'id'         => 'team_notes',
            'slug'       => 'team-notes',
            'title'      => 'Team notes',
            'type'       => 'notes',
            'capability' => 'edit_posts',
            'enabled'    => true,
            'context'    => 'side',
            'priority'   => 'low',
            'data'       => [
                'content' => 'Welcome to your custom dashboard. Keep it tidy.',
            ],
            'created_at' => '2026-07-15T08:00:00Z',
            'updated_at' => '2026-07-17T10:20:00Z',
        ],
        'sales_summary' => [
            'id'         => 'sales_summary',
            'slug'       => 'sales-summary',
            'title'      => 'Sales summary',
            'type'       => 'report',
            'capability' => 'manage_woocommerce',
            'enabled'    => false,
            'context'    => 'normal',
            'priority'   => 'core',
            'data'       => [
                'range' => '7d',
            ],
            'created_at' => '2026-07-16T09:00:00Z',
            'updated_at' => '2026-07-16T09:00:00Z',
        ],
    ],
    'hidden_core' => [
        'dashboard_primary'     => true,
        'dashboard_quick_press' => false,
    ],
    'forced_hide' => [
        'dashboard_right_now',
    ],
    'forced_show' => [
        'team_notes',
    ],
];

Biến thể có biến số

<?php
$id = 'team_notes';
$cfg['widgets'][$id] = [
    'id'         => $id,
    'slug'       => str_replace('_', '-', $id),
    'title'      => 'Team notes',
    'type'       => 'notes',
    'capability' => 'edit_posts',
    'enabled'    => true,
    'context'    => 'side',
    'priority'   => 'low',
    'data'       => ['content' => '...'],
    'created_at' => gmdate('c'),
    'updated_at' => gmdate('c'),
];

Khởi tạo và bảo đảm autoload=no

<?php
// Gọi khi kích hoạt plugin hoặc lần đầu chạy
function mydash_ensure_defaults(): void {
    $name = 'mydash_dashboard_widgets';
    if (get_option($name, null) === null) {
        add_option($name, [
            'schema_version' => 2,
            'rev'            => 1,
            'widgets'        => [],
            'hidden_core'    => [],
            'forced_hide'    => [],
            'forced_show'    => [],
        ], '', 'no'); // autoload=no
    }
}

// Nếu option tồn tại nhưng từng để autoload=yes, đổi về no trong activation
function mydash_force_autoload_no(): void {
    $name = 'mydash_dashboard_widgets';
    $val  = get_option($name, null);
    if ($val !== null) {
        delete_option($name);
        add_option($name, $val, '', 'no');
    }
}

Cập nhật an toàn bằng khóa lạc quan rev

  • Ý tưởng: đọc rev hiện tại, người dùng chỉnh sửa trên form giữ rev đó. Khi submit, so sánh rev gửi lên và rev trong DB. Nếu khác, báo xung đột để tránh ghi đè thay đổi của người khác.
  • Mỗi lần lưu thành công, tăng rev +1 và cập nhật updated_at của bản ghi bị chỉnh.

Ví dụ handler tối giản

<?php
add_action('admin_post_mydash_save_cfg', function () {
    if (! current_user_can('manage_custom_dashboard')) {
        wp_die('Unauthorized', 403);
    }
    check_admin_referer('mydash_save_cfg', '_mydash_nonce');

    $stored = get_option('mydash_dashboard_widgets', []);
    $posted_rev = isset($_POST['rev']) ? (int) $_POST['rev'] : 0;
    $current_rev = isset($stored['rev']) ? (int) $stored['rev'] : 0;

    if ($posted_rev !== $current_rev) {
        wp_safe_redirect(add_query_arg('mydash_conflict', 1, admin_url('options-general.php?page=mydash')));
        exit;
    }

    // Ví dụ cập nhật nội dung team_notes
    $new = $stored;
    $content = isset($_POST['team_notes_content']) ? wp_kses_post(wp_unslash($_POST['team_notes_content'])) : '';
    if (isset($new['widgets']['team_notes'])) {
        $new['widgets']['team_notes']['data']['content'] = $content;
        $new['widgets']['team_notes']['updated_at'] = gmdate('c');
    }

    $new['rev'] = $current_rev + 1;
    update_option('mydash_dashboard_widgets', $new, false);

    wp_safe_redirect(add_query_arg('updated', 1, admin_url('options-general.php?page=mydash')));
    exit;
});

Tiêu chí tách thành nhiều option – và cách pointer swap

  • Cứ giữ một option duy nhất trừ khi:
  • Bạn có hàng trăm đến hàng nghìn widget cấu hình và chỉ cần nạp một phần nhỏ theo vai trò hay ngữ cảnh đặc thù.
  • Bạn có tiến trình nền ghi thường xuyên từng nhóm nhỏ và cần giảm va chạm ghi.
  • Nếu buộc phải tách:
  • Tạo một option chỉ mục autoload=no, ví dụ mydashwidgetsindex, trỏ tới phiên bản đang hoạt động.
  • Ghi cấu hình hoàn chỉnh vào khóa mới có hậu tố version, rồi đổi con trỏ trong chỉ mục – kỹ thuật pointer swap. Người đọc chỉ nhìn theo chỉ mục nên luôn thấy snapshot nhất quán.

Ví dụ pointer swap

<?php
// Ghi phiên bản mới v18
$new_ver = 'v18';
add_option("mydash_widgets_{$new_ver}", $cfg_new, '', 'no'); // hoặc update nếu đã tồn tại

// Đổi con trỏ active trong index
$idx = get_option('mydash_widgets_index', ['active' => 'v1', 'versions' => []]);
$idx['active'] = $new_ver;
$idx['versions'][$new_ver] = time();
update_option('mydash_widgets_index', $idx, false);

// Đọc cấu hình khi đăng ký widget
$idx = get_option('mydash_widgets_index', []);
$active = isset($idx['active']) ? $idx['active'] : 'v1';
$cfg = get_option("mydash_widgets_{$active}", []);

Kế hoạch migrate schema an toàn

  • Nguyên tắc: idempotent, có backup, xác minh sau khi ghi, chỉ chạy khi cần.
  • Thời điểm: trong activation hoặc admin_init khi truy cập trang cấu hình, có kiểm tra quyền.

Ví dụ migrate từ schema v1 sang v2 – tách policy cũ thành forcedhide và forcedshow ở top-level

<?php
function mydash_maybe_migrate_schema(): void {
    $name = 'mydash_dashboard_widgets';
    $cfg  = get_option($name, null);
    if (!is_array($cfg)) {
        return;
    }
    $sv = isset($cfg['schema_version']) ? (int) $cfg['schema_version'] : 1;
    if ($sv >= 2) {
        return;
    }

    // Backup
    add_option('mydash_dashboard_widgets_backup_v1', [
        'at'   => gmdate('c'),
        'data' => $cfg,
    ], '', 'no');

    // Chuyển đổi
    $cfg['forced_hide'] = isset($cfg['policy']['forced_hide']) && is_array($cfg['policy']['forced_hide'])
        ? array_values(array_map('sanitize_key', $cfg['policy']['forced_hide'])) : [];
    $cfg['forced_show'] = isset($cfg['policy']['forced_show']) && is_array($cfg['policy']['forced_show'])
        ? array_values(array_map('sanitize_key', $cfg['policy']['forced_show'])) : [];
    unset($cfg['policy']);

    // Chuẩn hóa widget keys
    if (!empty($cfg['widgets']) && is_array($cfg['widgets'])) {
        $normalized = [];
        foreach ($cfg['widgets'] as $id => $w) {
            $id = sanitize_key($w['id'] ?? $id);
            $w['id'] = $id;
            $w['slug'] = isset($w['slug']) ? sanitize_title($w['slug']) : str_replace('_', '-', $id);
            $w['updated_at'] = gmdate('c');
            $normalized[$id] = $w;
        }
        $cfg['widgets'] = $normalized;
    }

    $cfg['schema_version'] = 2;
    $cfg['rev'] = isset($cfg['rev']) ? ((int) $cfg['rev'] + 1) : 1;

    update_option($name, $cfg, false);

    // Xác minh
    $check = get_option($name, []);
    if (!is_array($check) || !isset($check['schema_version']) || (int) $check['schema_version'] !== 2) {
        // Khôi phục nếu cần
        $bk = get_option('mydash_dashboard_widgets_backup_v1', []);
        if (!empty($bk['data'])) {
            update_option($name, $bk['data'], false);
        }
    }
}

Ghi nhớ thực thi

  • Đọc: getoption một lần. Nếu trống, tạo mặc định bằng addoption với autoload=no.
  • Ghi: luôn kiểm tra quyền, nonce và rev. Tăng rev sau khi hợp lệ.
  • Không đưa dữ liệu động lớn vào option này. Số liệu báo cáo nên nạp khi render và nếu cần thì cache bằng transient riêng autoload=no.

Khi tuân thủ mô hình một option duy nhất, bạn được cấu hình gọn, truy vấn ít, kiểm soát phiên bản rõ ràng và nâng cấp an toàn. Khi thật sự phải tách, dùng chỉ mục và pointer swap để vẫn giữ nguyên tắc nhất quán khi đọc.

Tạo trang quản trị cấu hình bằng Settings API

Mình sẽ dựng một trang cấu hình trong wp-admin dùng Settings API để lưu chính sách ẩn hiện widget mặc định và hiển thị danh sách widget tùy chỉnh kèm nút thêm, sửa, xóa. Trang chỉ cho người có capability phù hợp truy cập, mọi form đều có nonce phòng CSRF, và thông báo theo chuẩn adminnotices. Cấu hình lưu trong một option duy nhất mydashdashboard_widgets như mô hình đã thống nhất.

Các bước thực hiện

  1. Tạo menu dưới Settings và đăng ký Settings API.
  2. Thiết kế form Settings để bật tắt widget mặc định bằng checkbox.
  3. Hiển thị bảng widget tùy chỉnh với nút Thêm, Sửa, Xóa.
  4. Viết handler admin-post cho thao tác thêm, sửa, xóa và dùng nonce.
  5. Tạo thông báo admin_notices cho thành công và lỗi.

Mẫu plugin đầy đủ để bạn chạy ngay

File chính của plugin, ví dụ wp-content/plugins/mydash/mydash.php:

<?php
/**
 * Plugin Name: MyDash - Dashboard Manager
 * Description: Trang cấu hình dashboard widgets, CRUD và ẩn hiện widget mặc định.
 * Version: 1.0.0
 * Author: Your Name
 * Text Domain: mydash
 */

if (!defined('ABSPATH')) {
    exit;
}

function mydash_cap(): string {
    // Có thể thay bằng capability riêng sau khi map cho role quản trị
    return 'manage_options';
}

// Bảo đảm option mặc định tồn tại và autoload=no
function mydash_bootstrap_defaults(): void {
    $name = 'mydash_dashboard_widgets';
    if (get_option($name, null) === null) {
        add_option($name, [
            'schema_version' => 2,
            'rev'            => 1,
            'widgets'        => [],
            'hidden_core'    => [],   // ['dashboard_primary' => true, ...]
            'forced_hide'    => [],
            'forced_show'    => [],
        ], '', 'no');
    }
}
register_activation_hook(__FILE__, 'mydash_bootstrap_defaults');

// 1) Menu dưới Settings
add_action('admin_menu', function () {
    add_options_page(
        __('MyDash Dashboard', 'mydash'),
        __('MyDash Dashboard', 'mydash'),
        mydash_cap(),
        'mydash',
        'mydash_render_settings_page'
    );
});

// 2) Đăng ký Settings API và field
add_action('admin_init', function () {
    if (!current_user_can(mydash_cap())) {
        return;
    }

    // Đăng ký option với sanitize
    register_setting('mydash_settings', 'mydash_dashboard_widgets', [
        'type'              => 'array',
        'sanitize_callback' => 'mydash_sanitize_settings',
        'default'           => [
            'schema_version' => 2,
            'rev'            => 1,
            'widgets'        => [],
            'hidden_core'    => [],
            'forced_hide'    => [],
            'forced_show'    => [],
        ],
    ]);

    // Section: Widget mặc định
    add_settings_section(
        'mydash_section_core',
        __('Widget mặc định của WordPress', 'mydash'),
        function () {
            echo '<p>' . esc_html__('Chọn widget mặc định cần ẩn. Thay đổi có hiệu lực trên Dashboard ở lần tải tiếp theo.', 'mydash') . '</p>';
        },
        'mydash'
    );

    add_settings_field(
        'mydash_field_hidden_core',
        __('Ẩn các widget', 'mydash'),
        'mydash_field_hidden_core',
        'mydash',
        'mydash_section_core'
    );

    // Section: Danh sách widget tùy chỉnh
    add_settings_section(
        'mydash_section_custom',
        __('Widget tùy chỉnh', 'mydash'),
        function () {
            echo '<p>' . esc_html__('Quản lý danh sách widget do bạn định nghĩa. Có thể thêm, sửa, xóa và bật tắt.', 'mydash') . '</p>';
        },
        'mydash'
    );

    add_settings_field(
        'mydash_field_widgets_table',
        __('Danh sách và thao tác', 'mydash'),
        'mydash_field_widgets_table',
        'mydash',
        'mydash_section_custom'
    );
});

// 3) Sanitize Settings: chỉ xử lý phần hidden_core và khóa lạc quan rev
function mydash_sanitize_settings($input) {
    if (!current_user_can(mydash_cap())) {
        return get_option('mydash_dashboard_widgets', []);
    }

    $stored = get_option('mydash_dashboard_widgets', []);
    $new    = $stored;

    // Khóa lạc quan: so rev
    $posted_rev   = isset($input['rev']) ? (int) $input['rev'] : 0;
    $current_rev  = isset($stored['rev']) ? (int) $stored['rev'] : 0;

    if ($posted_rev !== $current_rev) {
        add_settings_error('mydash', 'mydash_conflict', __('Dữ liệu đã thay đổi ở nơi khác. Vui lòng tải lại trang và thử lại.', 'mydash'), 'error');
        return $stored;
    }

    // Nhận checkbox ẩn widget core
    $allowed_core = [
        'dashboard_activity',
        'dashboard_primary',
        'dashboard_quick_press',
        'dashboard_site_health',
        'dashboard_right_now',
        'dashboard_recent_drafts',
        'dashboard_recent_comments',
        'dashboard_incoming_links', // có thể không còn trong bản mới, giữ để tương thích cũ
    ];

    $new_hidden = [];
    if (isset($input['hidden_core']) && is_array($input['hidden_core'])) {
        foreach ($allowed_core as $id) {
            $new_hidden[$id] = !empty($input['hidden_core'][$id]) ? true : false;
        }
    } else {
        foreach ($allowed_core as $id) {
            $new_hidden[$id] = false;
        }
    }

    $new['hidden_core'] = $new_hidden;
    $new['rev']         = $current_rev + 1;

    // Không ghi đè widgets ở đây. CRUD xử lý qua admin-post riêng.
    return $new;
}

// 4) Field renderer: checkbox ẩn widget core
function mydash_field_hidden_core() {
    $cfg = get_option('mydash_dashboard_widgets', []);
    $hidden = isset($cfg['hidden_core']) && is_array($cfg['hidden_core']) ? $cfg['hidden_core'] : [];

    $items = [
        'dashboard_activity'       => __('Hoạt động', 'mydash'),
        'dashboard_primary'        => __('Tin tức WordPress', 'mydash'),
        'dashboard_quick_press'    => __('Quick Draft', 'mydash'),
        'dashboard_site_health'    => __('Tình trạng sức khỏe trang', 'mydash'),
        'dashboard_right_now'      => __('At a Glance', 'mydash'),
        'dashboard_recent_drafts'  => __('Bản nháp gần đây', 'mydash'),
        'dashboard_recent_comments'=> __('Bình luận gần đây', 'mydash'),
    ];

    echo '<fieldset>';
    foreach ($items as $id => $label) {
        $checked = !empty($hidden[$id]) ? 'checked' : '';
        echo '<label style="display:block;margin:4px 0">';
        echo '<input type="checkbox" name="mydash_dashboard_widgets[hidden_core][' . esc_attr($id) . ']" value="1" ' . $checked . ' />';
        echo ' ' . esc_html($label) . ' <code>' . esc_html($id) . '</code>';
        echo '</label>';
    }
    echo '</fieldset>';
}

// 5) Field renderer: bảng widget và form CRUD
function mydash_field_widgets_table() {
    if (!current_user_can(mydash_cap())) {
        echo '<p>' . esc_html__('Bạn không có quyền xem mục này.', 'mydash') . '</p>';
        return;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $widgets = isset($cfg['widgets']) && is_array($cfg['widgets']) ? $cfg['widgets'] : [];
    $editing_id = isset($_GET['edit']) ? sanitize_key($_GET['edit']) : '';

    echo '<table class="widefat striped">';
    echo '<thead><tr>';
    echo '<th>' . esc_html__('ID', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Tiêu đề', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Type', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Cap', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Vị trí', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Ưu tiên', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Bật', 'mydash') . '</th>';
    echo '<th>' . esc_html__('Hành động', 'mydash') . '</th>';
    echo '</tr></thead><tbody>';

    if (!$widgets) {
        echo '<tr><td colspan="8">' . esc_html__('Chưa có widget tùy chỉnh.', 'mydash') . '</td></tr>';
    } else {
        foreach ($widgets as $id => $w) {
            $enabled  = !empty($w['enabled']);
            $context  = $w['context'] ?? 'normal';
            $priority = $w['priority'] ?? 'core';
            $title    = $w['title'] ?? $id;
            $type     = $w['type'] ?? 'unknown';
            $cap      = $w['capability'] ?? 'read';

            $edit_url = add_query_arg(['page' => 'mydash', 'edit' => $id], admin_url('options-general.php'));
            $del_url  = wp_nonce_url(
                admin_url('admin-post.php?action=mydash_delete_widget&wid=' . $id . '&rev=' . (int) ($cfg['rev'] ?? 0)),
                'mydash_delete_widget_' . $id
            );

            echo '<tr>';
            echo '<td><code>' . esc_html($id) . '</code></td>';
            echo '<td>' . esc_html($title) . '</td>';
            echo '<td><code>' . esc_html($type) . '</code></td>';
            echo '<td><code>' . esc_html($cap) . '</code></td>';
            echo '<td>' . esc_html($context) . '</td>';
            echo '<td>' . esc_html($priority) . '</td>';
            echo '<td>' . ($enabled ? '<span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>' : '—') . '</td>';
            echo '<td>';
            echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('Sửa', 'mydash') . '</a> ';
            echo '<a class="button button-small button-link-delete" href="' . esc_url($del_url) . '" onclick="return confirm(\'Xóa widget ' . esc_js($id) . '?\')">' . esc_html__('Xóa', 'mydash') . '</a>';
            echo '</td>';
            echo '</tr>';
        }
    }

    echo '</tbody></table>';

    // Form Thêm mới
    $rev = (int) ($cfg['rev'] ?? 0);
    echo '<h4 style="margin-top:16px">' . esc_html__('Thêm widget mới', 'mydash') . '</h4>';
    echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
    echo '<input type="hidden" name="action" value="mydash_add_widget" />';
    echo '<input type="hidden" name="rev" value="' . esc_attr($rev) . '" />';
    wp_nonce_field('mydash_add_widget', '_mydash_nonce');
    echo '<table class="form-table"><tbody>';
    echo '<tr><th><label for="mydash_id_new">' . esc_html__('ID', 'mydash') . '</label></th><td><input type="text" id="mydash_id_new" name="id" class="regular-text" placeholder="team_notes" required pattern="[a-z0-9_]+" /></td></tr>';
    echo '<tr><th><label for="mydash_title_new">' . esc_html__('Tiêu đề', 'mydash') . '</label></th><td><input type="text" id="mydash_title_new" name="title" class="regular-text" placeholder="Team notes" required /></td></tr>';
    echo '<tr><th><label for="mydash_type_new">' . esc_html__('Type', 'mydash') . '</label></th><td><select id="mydash_type_new" name="type"><option value="notes">notes</option><option value="report">report</option></select> <span class="description">' . esc_html__('Ánh xạ sang callback có sẵn trong plugin.', 'mydash') . '</span></td></tr>';
    echo '<tr><th><label for="mydash_cap_new">' . esc_html__('Capability', 'mydash') . '</label></th><td><input type="text" id="mydash_cap_new" name="capability" class="regular-text" value="edit_posts" /></td></tr>';
    echo '<tr><th><label for="mydash_ctx_new">' . esc_html__('Context', 'mydash') . '</label></th><td><select id="mydash_ctx_new" name="context"><option value="normal">normal</option><option value="side">side</option></select></td></tr>';
    echo '<tr><th><label for="mydash_pri_new">' . esc_html__('Priority', 'mydash') . '</label></th><td><select id="mydash_pri_new" name="priority"><option value="core">core</option><option value="high">high</option><option value="default">default</option><option value="low">low</option></select></td></tr>';
    echo '<tr><th><label for="mydash_enabled_new">' . esc_html__('Bật', 'mydash') . '</label></th><td><label><input type="checkbox" id="mydash_enabled_new" name="enabled" value="1" checked /> ' . esc_html__('Kích hoạt', 'mydash') . '</label></td></tr>';
    echo '<tr><th><label for="mydash_content_new">' . esc_html__('Nội dung HTML', 'mydash') . '</label></th><td><textarea id="mydash_content_new" name="content" class="large-text code" rows="5"><p>Welcome to your dashboard</p></textarea></td></tr>';
    echo '</tbody></table>';
    submit_button(__('Thêm widget', 'mydash'), 'primary', 'submit', false);
    echo '</form>';

    // Form Sửa nếu đang chọn
    if ($editing_id && isset($widgets[$editing_id])) {
        $w = $widgets[$editing_id];
        echo '<hr /><h4 style="margin-top:16px">' . sprintf(esc_html__('Sửa widget: %s', 'mydash'), esc_html($editing_id)) . '</h4>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="mydash_update_widget" />';
        echo '<input type="hidden" name="wid" value="' . esc_attr($editing_id) . '" />';
        echo '<input type="hidden" name="rev" value="' . esc_attr($rev) . '" />';
        wp_nonce_field('mydash_update_widget_' . $editing_id, '_mydash_nonce');
        echo '<table class="form-table"><tbody>';
        echo '<tr><th>' . esc_html__('ID', 'mydash') . '</th><td><code>' . esc_html($editing_id) . '</code></td></tr>';
        echo '<tr><th><label for="mydash_title_edit">' . esc_html__('Tiêu đề', 'mydash') . '</label></th><td><input type="text" id="mydash_title_edit" name="title" class="regular-text" value="' . esc_attr($w['title'] ?? $editing_id) . '" required /></td></tr>';
        echo '<tr><th><label for="mydash_type_edit">' . esc_html__('Type', 'mydash') . '</label></th><td><select id="mydash_type_edit" name="type">';
        $types = ['notes', 'report'];
        foreach ($types as $t) {
            $sel = (($w['type'] ?? '') === $t) ? 'selected' : '';
            echo '<option value="' . esc_attr($t) . '" ' . $sel . '>' . esc_html($t) . '</option>';
        }
        echo '</select></td></tr>';
        echo '<tr><th><label for="mydash_cap_edit">' . esc_html__('Capability', 'mydash') . '</label></th><td><input type="text" id="mydash_cap_edit" name="capability" class="regular-text" value="' . esc_attr($w['capability'] ?? 'edit_posts') . '" /></td></tr>';
        echo '<tr><th><label for="mydash_ctx_edit">' . esc_html__('Context', 'mydash') . '</label></th><td><select id="mydash_ctx_edit" name="context">';
        foreach (['normal', 'side'] as $ctx) {
            $sel = (($w['context'] ?? 'normal') === $ctx) ? 'selected' : '';
            echo '<option value="' . esc_attr($ctx) . '" ' . $sel . '>' . esc_html($ctx) . '</option>';
        }
        echo '</select></td></tr>';
        echo '<tr><th><label for="mydash_pri_edit">' . esc_html__('Priority', 'mydash') . '</label></th><td><select id="mydash_pri_edit" name="priority">';
        foreach (['core', 'high', 'default', 'low'] as $pri) {
            $sel = (($w['priority'] ?? 'core') === $pri) ? 'selected' : '';
            echo '<option value="' . esc_attr($pri) . '" ' . $sel . '>' . esc_html($pri) . '</option>';
        }
        echo '</select></td></tr>';
        $enabled_checked = !empty($w['enabled']) ? 'checked' : '';
        echo '<tr><th><label for="mydash_enabled_edit">' . esc_html__('Bật', 'mydash') . '</label></th><td><label><input type="checkbox" id="mydash_enabled_edit" name="enabled" value="1" ' . $enabled_checked . ' /> ' . esc_html__('Kích hoạt', 'mydash') . '</label></td></tr>';
        $content_val = isset($w['data']['content']) ? $w['data']['content'] : '';
        echo '<tr><th><label for="mydash_content_edit">' . esc_html__('Nội dung HTML', 'mydash') . '</label></th><td><textarea id="mydash_content_edit" name="content" class="large-text code" rows="5">' . esc_textarea($content_val) . '</textarea></td></tr>';
        echo '</tbody></table>';
        submit_button(__('Cập nhật widget', 'mydash'), 'primary', 'submit', false);
        echo ' <a class="button" href="' . esc_url(admin_url('options-general.php?page=mydash')) . '">' . esc_html__('Hủy', 'mydash') . '</a>';
        echo '</form>';
    }

    // Chèn hidden field rev để Settings API nhìn thấy
    $rev = (int) ($cfg['rev'] ?? 0);
    echo '<input type="hidden" name="mydash_dashboard_widgets[rev]" value="' . esc_attr($rev) . '" />';
}

// 6) Render trang Settings: gồm form Settings API và khu CRUD ở field
function mydash_render_settings_page() {
    if (!current_user_can(mydash_cap())) {
        wp_die(esc_html__('Bạn không có quyền truy cập trang này.', 'mydash'), 403);
    }
    $cfg = get_option('mydash_dashboard_widgets', []);
    echo '<div class="wrap">';
    echo '<h1>' . esc_html__('MyDash Dashboard', 'mydash') . '</h1>';

    // Thông báo chung của Settings API
    settings_errors('mydash');

    echo '<form method="post" action="options.php">';
    settings_fields('mydash_settings');    // nonce và option_page
    do_settings_sections('mydash');        // render 2 section và các field
    submit_button(__('Lưu thay đổi', 'mydash'));
    echo '</form>';

    echo '</div>';
}

// 7) Handlers admin-post cho Thêm, Cập nhật, Xóa
add_action('admin_post_mydash_add_widget', function () {
    if (!current_user_can(mydash_cap())) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    check_admin_referer('mydash_add_widget', '_mydash_nonce');

    $id         = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
    $title      = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
    $type       = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : 'notes';
    $cap        = isset($_POST['capability']) ? sanitize_text_field(wp_unslash($_POST['capability'])) : 'edit_posts';
    $context    = isset($_POST['context']) ? sanitize_key(wp_unslash($_POST['context'])) : 'normal';
    $priority   = isset($_POST['priority']) ? sanitize_key(wp_unslash($_POST['priority'])) : 'core';
    $enabled    = !empty($_POST['enabled']) ? true : false;
    $content    = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
    $posted_rev = isset($_POST['rev']) ? (int) $_POST['rev'] : 0;

    if (!$id || !$title) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'invalid'], admin_url('options-general.php')));
        exit;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $cur_rev = (int) ($cfg['rev'] ?? 0);
    if ($posted_rev !== $cur_rev) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'conflict'], admin_url('options-general.php')));
        exit;
    }

    if (!empty($cfg['widgets'][$id])) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'exists'], admin_url('options-general.php')));
        exit;
    }

    $cfg['widgets'][$id] = [
        'id'         => $id,
        'slug'       => str_replace('_', '-', $id),
        'title'      => $title,
        'type'       => $type,           // ánh xạ sang callback an toàn ở bước đăng ký widget
        'capability' => $cap,
        'enabled'    => $enabled,
        'context'    => in_array($context, ['normal', 'side'], true) ? $context : 'normal',
        'priority'   => in_array($priority, ['core', 'high', 'default', 'low'], true) ? $priority : 'core',
        'data'       => ['content' => $content],
        'created_at' => gmdate('c'),
        'updated_at' => gmdate('c'),
    ];
    $cfg['rev'] = $cur_rev + 1;

    update_option('mydash_dashboard_widgets', $cfg, false);
    wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'added'], admin_url('options-general.php')));
    exit;
});

add_action('admin_post_mydash_update_widget', function () {
    if (!current_user_can(mydash_cap())) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    $wid = isset($_POST['wid']) ? sanitize_key(wp_unslash($_POST['wid'])) : '';
    check_admin_referer('mydash_update_widget_' . $wid, '_mydash_nonce');

    $title      = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
    $type       = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : 'notes';
    $cap        = isset($_POST['capability']) ? sanitize_text_field(wp_unslash($_POST['capability'])) : 'edit_posts';
    $context    = isset($_POST['context']) ? sanitize_key(wp_unslash($_POST['context'])) : 'normal';
    $priority   = isset($_POST['priority']) ? sanitize_key(wp_unslash($_POST['priority'])) : 'core';
    $enabled    = !empty($_POST['enabled']) ? true : false;
    $content    = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
    $posted_rev = isset($_POST['rev']) ? (int) $_POST['rev'] : 0;

    $cfg     = get_option('mydash_dashboard_widgets', []);
    $cur_rev = (int) ($cfg['rev'] ?? 0);
    if ($posted_rev !== $cur_rev) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'conflict'], admin_url('options-general.php')));
        exit;
    }

    if (!$wid || empty($cfg['widgets'][$wid])) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'notfound'], admin_url('options-general.php')));
        exit;
    }

    // Giới hạn type vào danh sách hỗ trợ
    $allowed_types = ['notes', 'report'];
    if (!in_array($type, $allowed_types, true)) {
        $type = 'notes';
    }

    $w = $cfg['widgets'][$wid];
    $w['title']      = $title ?: ($w['title'] ?? $wid);
    $w['type']       = $type;
    $w['capability'] = $cap ?: 'edit_posts';
    $w['context']    = in_array($context, ['normal', 'side'], true) ? $context : 'normal';
    $w['priority']   = in_array($priority, ['core', 'high', 'default', 'low'], true) ? $priority : 'core';
    $w['enabled']    = $enabled;
    $w['data']['content'] = $content;
    $w['updated_at'] = gmdate('c');

    $cfg['widgets'][$wid] = $w;
    $cfg['rev'] = $cur_rev + 1;

    update_option('mydash_dashboard_widgets', $cfg, false);
    wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'updated'], admin_url('options-general.php')));
    exit;
});

add_action('admin_post_mydash_delete_widget', function () {
    if (!current_user_can(mydash_cap())) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    $wid = isset($_GET['wid']) ? sanitize_key(wp_unslash($_GET['wid'])) : '';
    check_admin_referer('mydash_delete_widget_' . $wid);

    $posted_rev = isset($_GET['rev']) ? (int) $_GET['rev'] : 0;
    $cfg     = get_option('mydash_dashboard_widgets', []);
    $cur_rev = (int) ($cfg['rev'] ?? 0);
    if ($posted_rev !== $cur_rev) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'conflict'], admin_url('options-general.php')));
        exit;
    }

    if (!$wid || empty($cfg['widgets'][$wid])) {
        wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'notfound'], admin_url('options-general.php')));
        exit;
    }

    unset($cfg['widgets'][$wid]);
    $cfg['rev'] = $cur_rev + 1;

    update_option('mydash_dashboard_widgets', $cfg, false);
    wp_safe_redirect(add_query_arg(['page' => 'mydash', 'mydash_msg' => 'deleted'], admin_url('options-general.php')));
    exit;
});

// 8) Thông báo admin_notices
add_action('admin_notices', function () {
    if (!is_admin()) {
        return;
    }
    if (!isset($_GET['page']) || $_GET['page'] !== 'mydash') {
        return;
    }
    $msg = isset($_GET['mydash_msg']) ? sanitize_key($_GET['mydash_msg']) : '';
    if (!$msg && empty($_GET['settings-updated'])) {
        return;
    }

    $map = [
        'added'    => ['class' => 'updated', 'text' => __('Đã thêm widget.', 'mydash')],
        'updated'  => ['class' => 'updated', 'text' => __('Đã cập nhật widget.', 'mydash')],
        'deleted'  => ['class' => 'updated', 'text' => __('Đã xóa widget.', 'mydash')],
        'exists'   => ['class' => 'error',   'text' => __('ID đã tồn tại. Chọn ID khác.', 'mydash')],
        'invalid'  => ['class' => 'error',   'text' => __('Thiếu dữ liệu bắt buộc.', 'mydash')],
        'notfound' => ['class' => 'error',   'text' => __('Không tìm thấy widget.', 'mydash')],
        'conflict' => ['class' => 'error',   'text' => __('Xung đột phiên bản. Vui lòng tải lại trang.', 'mydash')],
    ];

    if (!empty($_GET['settings-updated'])) {
        echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('Đã lưu cài đặt.', 'mydash') . '</p></div>';
        return;
    }

    if (isset($map[$msg])) {
        $c = $map[$msg]['class'] === 'updated' ? 'notice notice-success is-dismissible' : 'notice notice-error';
        echo '<div class="' . esc_attr($c) . '"><p>' . esc_html($map[$msg]['text']) . '</p></div>';
    }
});

Giải thích nhanh cách dùng

  • Vào Settings > MyDash Dashboard để mở trang cấu hình.
  • Khu đầu là checkbox ẩn hiện widget mặc định, lưu bằng Settings API. Mỗi lần lưu, plugin so sánh rev để tránh ghi đè nếu có người khác vừa sửa.
  • Khu thứ hai là bảng liệt kê widget tùy chỉnh cùng hai form:
  • Thêm mới: điền ID, tiêu đề, type, capability, vị trí, ưu tiên, bật tắt, nội dung HTML. Submit sẽ chuyển đến admin-post.php với action mydashaddwidget, có nonce và kiểm tra capability.
  • Sửa: click Sửa ở một dòng để show form sửa với dữ liệu đã điền sẵn. Submit đến action mydashupdatewidget.
  • Xóa: click Xóa sẽ bắn đến action mydashdeletewidget với nonce và xác nhận. Cả ba thao tác đều tăng rev sau khi ghi.

Ví dụ cấu hình mẫu sau khi thêm một widget Team notes thành công

  • Widget được lưu trong option với cấu trúc:
<?php
[
  'schema_version' => 2,
  'rev' => 3,
  'widgets' => [
    'team_notes' => [
      'id'         => 'team_notes',
      'slug'       => 'team-notes',
      'title'      => 'Team notes',
      'type'       => 'notes',
      'capability' => 'edit_posts',
      'enabled'    => true,
      'context'    => 'side',
      'priority'   => 'low',
      'data'       => ['content' => '<p>Welcome to your dashboard</p>'],
      'created_at' => '2026-07-17T10:20:00Z',
      'updated_at' => '2026-07-17T10:20:00Z',
    ],
  ],
  'hidden_core' => [
    'dashboard_primary' => true,
    'dashboard_quick_press' => false,
  ],
  'forced_hide' => [],
  'forced_show' => [],
];

Biến thể có biến số

  • Nếu bạn muốn tự động điền slug khác, hoặc lưu thêm trường data riêng cho type report, cập nhật ở handler mydashaddwidget và mydashupdatewidget bằng cách mở rộng mảng data. Ví dụ:
<?php
$range = isset($_POST['range']) ? sanitize_text_field(wp_unslash($_POST['range'])) : '7d';
$cfg['widgets'][$id]['data']['range'] = $range;

Gợi ý nhỏ để vận hành an toàn

  • Chỉ nhận các type bạn hỗ trợ và ánh xạ type sang callback cố định trong plugin khi đăng ký widget. Không gọi tên hàm từ dữ liệu người dùng.
  • Không ném dữ liệu lớn vào option. Nếu widget cần số liệu thời gian thực, lấy khi render và cache bằng transient riêng.
  • settingsfields đã tạo nonce cho form Settings API. Với form admin-post riêng, bạn luôn có wpnoncefield và checkadmin_referer tương ứng.
  • admin_notices dùng query args để hiển thị thông báo thân thiện cho người dùng nội bộ lẫn khách hàng quản trị. Mã hiển thị đã kiểm tra đúng page để không làm ồn trang khác.

Trang cấu hình này cho bạn một chỗ quản trị thống nhất: bật tắt widget core bằng Settings API và quản lý CRUD widget tùy chỉnh bằng handler riêng có nonce, capability và sanitize chặt chẽ. Khi kết hợp với bước nạp widget ở hook wpdashboardsetup, toàn bộ quy trình sẽ khép kín và dễ bảo trì.

Nạp và đăng ký widget từ cấu hình qua wpdashboardsetup

Mục tiêu là nạp option cấu hình, duyệt danh sách widget tùy chỉnh, ánh xạ type sang callback an toàn, kiểm tra capability theo từng widget trước khi đăng ký, sau đó di chuyển sang đúng cột và ưu tiên hiển thị. Đồng thời, gỡ các widget mặc định theo cấu hình ở priority rất muộn để chắc chắn có hiệu lực.

Các bước thực hiện

  1. Tạo bảng ánh xạ type sang callback cố định của plugin, tuyệt đối không gọi tên hàm lấy từ DB.
  2. Viết callback render cho từng type, nhận args để biết ID widget và lấy dữ liệu từ option.
  3. Viết helper di chuyển meta box sang context và priority mong muốn bằng cách thao tác global $wpmetaboxes ở priority 99.
  4. Viết hàm đăng ký widget chạy trên wpdashboardsetup để duyệt option, kiểm tra capability và gọi wpadddashboard_widget với callback và args đã ánh xạ.
  5. Viết hàm gỡ widget mặc định dựa trên hiddencore trong option, dùng removemetabox ở priority PHPINTMAX. Với Welcome panel thì removeaction welcome_panel.

Mẫu hoàn chỉnh có thể dán vào file plugin hiện có

<?php
// 1) Ánh xạ type -> callback an toàn
function mydash_widget_callback_map(): array {
    return [
        'notes'  => 'mydash_cb_render_notes',
        'report' => 'mydash_cb_render_report',
    ];
}

// 2) Callback render cho type "notes"
function mydash_cb_render_notes($post = null, $box = []) {
    $args = isset($box['args']) && is_array($box['args']) ? $box['args'] : [];
    $wid  = isset($args['id']) ? sanitize_key($args['id']) : '';

    $cfg = get_option('mydash_dashboard_widgets', []);
    $w   = isset($cfg['widgets'][$wid]) ? $cfg['widgets'][$wid] : null;
    if (!$w) {
        echo '<p>' . esc_html__('Không tìm thấy dữ liệu widget.', 'mydash') . '</p>';
        return;
    }
    if (!current_user_can($w['capability'] ?? 'read')) {
        echo '<p>' . esc_html__('Bạn không có quyền xem widget này.', 'mydash') . '</p>';
        return;
    }

    $content = isset($w['data']['content']) ? $w['data']['content'] : '';
    echo wp_kses_post($content);
}

// 2) Callback render cho type "report" - ví dụ placeholder
function mydash_cb_render_report($post = null, $box = []) {
    $args = isset($box['args']) && is_array($box['args']) ? $box['args'] : [];
    $wid  = isset($args['id']) ? sanitize_key($args['id']) : '';

    $cfg = get_option('mydash_dashboard_widgets', []);
    $w   = isset($cfg['widgets'][$wid]) ? $cfg['widgets'][$wid] : null;
    if (!$w) {
        echo '<p>' . esc_html__('Không tìm thấy dữ liệu widget.', 'mydash') . '</p>';
        return;
    }
    if (!current_user_can($w['capability'] ?? 'read')) {
        echo '<p>' . esc_html__('Bạn không có quyền xem widget này.', 'mydash') . '</p>';
        return;
    }

    $range = isset($w['data']['range']) ? sanitize_text_field($w['data']['range']) : '7d';
    echo '<p>' . sprintf(esc_html__('Báo cáo tóm tắt, khoảng thời gian: %s', 'mydash'), esc_html($range)) . '</p>';
    echo '<ul><li>' . esc_html__('Số liệu A: 123', 'mydash') . '</li><li>' . esc_html__('Số liệu B: 456', 'mydash') . '</li></ul>';
}

// 3) Helper di chuyển widget sang context và priority mong muốn
function mydash_place_widget(string $widget_id, string $dst_context, string $dst_priority, string $screen = 'dashboard'): void {
    global $wp_meta_boxes;

    $contexts   = ['normal', 'side'];
    $priorities = ['core', 'default', 'high', 'low'];

    $box = null;
    foreach ($contexts as $ctx) {
        foreach ($priorities as $pri) {
            if (isset($wp_meta_boxes[$screen][$ctx][$pri][$widget_id])) {
                $box = $wp_meta_boxes[$screen][$ctx][$pri][$widget_id];
                unset($wp_meta_boxes[$screen][$ctx][$pri][$widget_id]);
                break 2;
            }
        }
    }

    if (!$box) {
        return;
    }

    // Bảo đảm mảng đích tồn tại
    if (!isset($wp_meta_boxes[$screen][$dst_context])) {
        $wp_meta_boxes[$screen][$dst_context] = [];
    }
    if (!isset($wp_meta_boxes[$screen][$dst_context][$dst_priority])) {
        $wp_meta_boxes[$screen][$dst_context][$dst_priority] = [];
    }

    // Cập nhật metadata box
    $box['context']  = $dst_context;
    $box['priority'] = $dst_priority;

    // Đặt vào bucket đích, không ghi vào 'sorted'
    $wp_meta_boxes[$screen][$dst_context][$dst_priority][$widget_id] = $box;
}

// 4) Đăng ký widget từ option ở priority 99, đồng thời di chuyển vị trí
function mydash_register_dashboard_widgets(): void {
    // Chỉ chạy trên trang Dashboard site admin
    if (!is_admin() || !function_exists('get_current_screen')) {
        return;
    }
    $screen = get_current_screen();
    if (!$screen || $screen->id !== 'dashboard') {
        return;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $list = isset($cfg['widgets']) && is_array($cfg['widgets']) ? $cfg['widgets'] : [];
    if (!$list) {
        return;
    }

    $cb_map = mydash_widget_callback_map();

    foreach ($list as $id => $w) {
        // Bật tắt
        if (empty($w['enabled'])) {
            continue;
        }

        // Kiểm tra type hỗ trợ
        $type = isset($w['type']) ? sanitize_key($w['type']) : '';
        if (!$type || !isset($cb_map[$type])) {
            continue;
        }

        // Kiểm tra capability trước khi add
        $cap = isset($w['capability']) && is_string($w['capability']) ? $w['capability'] : 'read';
        if (!current_user_can($cap)) {
            continue;
        }

        // Chuẩn hóa tham số vị trí mong muốn
        $ctx = in_array($w['context'] ?? 'normal', ['normal', 'side'], true) ? $w['context'] : 'normal';
        $pri = in_array($w['priority'] ?? 'core', ['core', 'default', 'high', 'low'], true) ? $w['priority'] : 'core';

        // Đăng ký widget, callback args kèm ID để callback lấy dữ liệu
        $title = $w['title'] ?? $id;
        $cb    = $cb_map[$type];

        wp_add_dashboard_widget(
            $id,
            $title,
            $cb,
            null,
            ['id' => $id]
        );

        // Di chuyển sang context - priority theo cấu hình
        mydash_place_widget($id, $ctx, $pri, 'dashboard');
    }
}
add_action('wp_dashboard_setup', 'mydash_register_dashboard_widgets', 99);

// 5) Gỡ widget mặc định theo hidden_core ở priority rất muộn
function mydash_hide_core_dashboard_widgets(): void {
    // Chỉ áp dụng cho trang Dashboard site admin
    if (!is_admin() || !function_exists('get_current_screen')) {
        return;
    }
    $screen = get_current_screen();
    if (!$screen || $screen->id !== 'dashboard') {
        return;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $hidden = isset($cfg['hidden_core']) && is_array($cfg['hidden_core']) ? $cfg['hidden_core'] : [];
    if (!$hidden) {
        return;
    }

    foreach ($hidden as $id => $hide) {
        if (!$hide) {
            continue;
        }
        // Welcome panel không phải meta box
        if ($id === 'welcome_panel') {
            remove_action('welcome_panel', 'wp_welcome_panel');
            continue;
        }
        // Không chắc context, gỡ ở cả normal và side
        remove_meta_box($id, 'dashboard', 'normal');
        remove_meta_box($id, 'dashboard', 'side');
    }
}
add_action('wp_dashboard_setup', 'mydash_hide_core_dashboard_widgets', PHP_INT_MAX);

Giải thích nhanh để bạn vận hành

  • Callback map mydashwidgetcallback_map là lớp bảo vệ quan trọng để không bao giờ thực thi hàm tùy ý lấy từ DB. type trong option chỉ là khóa để tra map.
  • currentusercan được kiểm tra cho từng widget trước khi wpadddashboard_widget. Callback cũng kiểm tra lại để an toàn phòng trường hợp type được đổi cấu hình mà user vẫn còn DOM của widget cũ trong phiên.
  • Di chuyển vị trí diễn ra ngay sau khi đăng ký, ở cùng priority 99. Hàm mydashplacewidget chỉ ghi vào bucket context – priority, không đụng bucket sorted nên không can thiệp thứ tự người dùng đã lưu. Nếu người dùng đã từng kéo thả, WordPress sẽ ưu tiên order cá nhân của họ và bỏ qua bucket mặc định bạn đặt.
  • Gỡ widget mặc định chạy ở PHPINTMAX để chắc chắn sau tất cả nguồn đăng ký khác. removemetabox gọi cho cả normal và side để không cần đoán context. Với Welcome panel, dùng removeaction welcomepanel vì nó không phải meta box.
  • Hiệu năng ổn vì chỉ đọc một option và không truy vấn nặng khi đăng ký. Nội dung tốn tài nguyên hãy tính trong callback và có thể cache bằng transient riêng nếu cần.

Biến thể đa site, nếu bạn cần áp dụng tương tự ở Network Admin hoặc User Admin

  • Thêm các hook tương đương và thay screen id khi di chuyển:
<?php
add_action('wp_network_dashboard_setup', function () {
    // Kiểm tra quyền mạng phù hợp trước khi add
    // Đăng ký widget mạng của bạn...
    // Di chuyển với mydash_place_widget($id, $ctx, $pri, 'dashboard-network');
});

add_action('wp_user_dashboard_setup', function () {
    // Đăng ký widget cho User Admin...
    // Di chuyển với mydash_place_widget($id, $ctx, $pri, 'dashboard-user');
});

Khi ghép phần này vào plugin ở phần trước, bạn đã có quy trình nạp cấu hình, đăng ký widget tùy chỉnh có kiểm tra capability, định vị đúng cột và ưu tiên hiển thị, đồng thời ẩn widget mặc định theo chính sách một cách đáng tin cậy.

Thêm widget tùy chỉnh: schema, form tạo và đăng ký

Bạn cần một quy trình chuẩn để thêm widget mới: định nghĩa schema, tạo form nhập, validate và sanitize đầu vào, lưu vào option với rev tăng dần, ánh xạ type sang callback render và control, rồi đăng ký widget khi nạp Dashboard và đặt nó vào đúng cột – ưu tiên. Dưới đây là gói mã bạn có thể dán vào plugin đã dựng ở các phần trước.

Các bước thực hiện

  1. Khai báo type registry và callback cho widget mẫu html
  2. Tạo form thêm widget trong trang cài đặt của plugin
  3. Xử lý submit: validate, sanitize, lưu option và tăng rev
  4. Đảm bảo wpdashboardsetup đăng ký widget và đặt vị trí mong muốn
  1. Type registry và callback cho widget html
  • Type html cho phép nhập nội dung HTML, hiển thị bằng wpksespost, có control form con để sửa nhanh ngay trên widget.
<?php
// Registry: định nghĩa type -> render, control
function mydash_widget_type_registry(): array {
    return [
        'html' => [
            'label'   => 'HTML note',
            'render'  => 'mydash_cb_render_html',
            'control' => 'mydash_cb_control_html',
        ],
        // Bạn có thể thêm các type khác tại đây
    ];
}

// Bổ sung 'html' vào map render nếu bạn đang dùng mydash_widget_callback_map() ở phần trước
function mydash_widget_callback_map(): array {
    return [
        'notes'  => 'mydash_cb_render_notes',
        'report' => 'mydash_cb_render_report',
        'html'   => 'mydash_cb_render_html',
    ];
}

// Render: in nội dung đã kses
function mydash_cb_render_html($post = null, $box = []) {
    $args = isset($box['args']) && is_array($box['args']) ? $box['args'] : [];
    $wid  = isset($args['id']) ? sanitize_key($args['id']) : '';

    $cfg = get_option('mydash_dashboard_widgets', []);
    $w   = isset($cfg['widgets'][$wid]) ? $cfg['widgets'][$wid] : null;

    if (!$w) {
        echo '<p>' . esc_html__('Không tìm thấy dữ liệu widget.', 'mydash') . '</p>';
        return;
    }
    if (!current_user_can($w['capability'] ?? 'read')) {
        echo '<p>' . esc_html__('Bạn không có quyền xem widget này.', 'mydash') . '</p>';
        return;
    }

    $content = isset($w['data']['content']) ? (string) $w['data']['content'] : '';
    echo wp_kses_post($content);
}

// Control: form con để cập nhật nội dung nhanh trên Dashboard
function mydash_cb_control_html($post = null, $box = []) {
    $args = isset($box['args']) && is_array($box['args']) ? $box['args'] : [];
    $wid  = isset($args['id']) ? sanitize_key($args['id']) : '';

    $cfg = get_option('mydash_dashboard_widgets', []);
    $w   = isset($cfg['widgets'][$wid]) ? $cfg['widgets'][$wid] : null;
    if (!$w) {
        echo '<p>' . esc_html__('Không tìm thấy dữ liệu widget.', 'mydash') . '</p>';
        return;
    }
    if (!current_user_can('manage_options')) { // quyền chỉnh cấu hình plugin
        echo '<p>' . esc_html__('Bạn không có quyền chỉnh widget.', 'mydash') . '</p>';
        return;
    }

    $rev = intval($cfg['rev'] ?? 0);
    $content = isset($w['data']['content']) ? (string) $w['data']['content'] : '';
    ?>
    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="mydash_update_widget_inline">
        <input type="hidden" name="id" value="<?php echo esc_attr($wid); ?>">
        <input type="hidden" name="rev" value="<?php echo esc_attr($rev); ?>">
        <?php wp_nonce_field('mydash_update_widget_inline', 'mydash_nonce'); ?>
        <p>
            <label for="mydash_content_<?php echo esc_attr($wid); ?>">
                <?php esc_html_e('Nội dung HTML', 'mydash'); ?>
            </label>
            <textarea id="mydash_content_<?php echo esc_attr($wid); ?>" name="data[content]" class="widefat" rows="5"><?php echo esc_textarea($content); ?></textarea>
        </p>
        <p>
            <button type="submit" class="button button-primary"><?php esc_html_e('Lưu', 'mydash'); ?></button>
        </p>
    </form>
    <?php
}

// Handler lưu nhanh từ control
add_action('admin_post_mydash_update_widget_inline', function () {
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    check_admin_referer('mydash_update_widget_inline', 'mydash_nonce');

    $id  = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
    $rev = isset($_POST['rev']) ? intval($_POST['rev']) : 0;
    $cfg = get_option('mydash_dashboard_widgets', []);
    $cur = intval($cfg['rev'] ?? 0);

    if (!$id || !isset($cfg['widgets'][$id])) {
        wp_die(esc_html__('Widget không tồn tại.', 'mydash'), 400);
    }
    if ($rev !== $cur) {
        wp_safe_redirect(add_query_arg('mydash_err', 'conflict', admin_url('index.php')));
        exit;
    }

    $content = '';
    if (isset($_POST['data']['content'])) {
        $content = wp_kses_post(wp_unslash($_POST['data']['content']));
    }

    $cfg['widgets'][$id]['data']['content'] = $content;
    $cfg['widgets'][$id]['updated_at'] = gmdate('c');
    $cfg['rev'] = $cur + 1;

    update_option('mydash_dashboard_widgets', $cfg, false);
    wp_safe_redirect(add_query_arg('mydash_updated', 1, admin_url('index.php')));
    exit;
});
  1. Form thêm widget trong trang cài đặt
  • Form này thêm một record widget mới với id, title, capability, type, data, context, priority. Ví dụ dưới đây mặc định chọn cột side và priority low.
<?php
// Trong trang settings của plugin
$cfg = get_option('mydash_dashboard_widgets', []);
$rev = intval($cfg['rev'] ?? 0);
$types = mydash_widget_type_registry();
?>
<h2><?php esc_html_e('Thêm widget Dashboard mới', 'mydash'); ?></h2>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
    <input type="hidden" name="action" value="mydash_add_widget">
    <input type="hidden" name="rev" value="<?php echo esc_attr($rev); ?>">
    <?php wp_nonce_field('mydash_add_widget', 'mydash_nonce'); ?>

    <table class="form-table" role="presentation">
        <tr>
            <th scope="row"><label for="mydash_id"><?php esc_html_e('ID', 'mydash'); ?></label></th>
            <td>
                <input type="text" id="mydash_id" name="id" class="regular-text" required pattern="[a-z0-9_]+" placeholder="mydash_html_note">
                <p class="description"><?php esc_html_e('Chỉ chữ thường, số, gạch dưới. Phải duy nhất.', 'mydash'); ?></p>
            </td>
        </tr>
        <tr>
            <th scope="row"><label for="mydash_title"><?php esc_html_e('Tiêu đề', 'mydash'); ?></label></th>
            <td><input type="text" id="mydash_title" name="title" class="regular-text" required></td>
        </tr>
        <tr>
            <th scope="row"><label for="mydash_cap"><?php esc_html_e('Capability để xem', 'mydash'); ?></label></th>
            <td><input type="text" id="mydash_cap" name="capability" class="regular-text" value="read"></td>
        </tr>
        <tr>
            <th scope="row"><label for="mydash_type"><?php esc_html_e('Loại widget', 'mydash'); ?></label></th>
            <td>
                <select id="mydash_type" name="type">
                    <?php foreach ($types as $k => $def): ?>
                        <option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($def['label']); ?></option>
                    <?php endforeach; ?>
                </select>
            </td>
        </tr>
        <tr>
            <th scope="row"><?php esc_html_e('Vị trí', 'mydash'); ?></th>
            <td>
                <label><input type="radio" name="context" value="normal"> normal</label>
                <label><input type="radio" name="context" value="side" checked> side</label>
                <span style="margin-left:1em;">
                    <label><?php esc_html_e('Priority', 'mydash'); ?>
                        <select name="priority">
                            <option value="low" selected>low</option>
                            <option value="default">default</option>
                            <option value="core">core</option>
                            <option value="high">high</option>
                        </select>
                    </label>
                </span>
            </td>
        </tr>
        <tr>
            <th scope="row"><label for="mydash_data_content"><?php esc_html_e('Nội dung HTML', 'mydash'); ?></label></th>
            <td>
                <textarea id="mydash_data_content" name="data[content]" class="large-text code" rows="6"><p><strong>Xin chào</strong>, đây là ghi chú HTML.</p></textarea>
                <p class="description"><?php esc_html_e('Áp dụng cho type html. Nội dung sẽ được lọc an toàn.', 'mydash'); ?></p>
            </td>
        </tr>
    </table>

    <p><button type="submit" class="button button-primary"><?php esc_html_e('Thêm widget', 'mydash'); ?></button></p>
</form>
  1. Xử lý submit: validate, sanitize, lưu option và tăng rev
  • Mình dùng admin_post để xử lý, khóa lạc quan bằng rev để tránh ghi đè đồng thời.
<?php
add_action('admin_post_mydash_add_widget', 'mydash_handle_add_widget');
function mydash_handle_add_widget() {
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    check_admin_referer('mydash_add_widget', 'mydash_nonce');

    $id  = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
    $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
    $cap = isset($_POST['capability']) ? sanitize_key(wp_unslash($_POST['capability'])) : 'read';
    $type = isset($_POST['type']) ? sanitize_key(wp_unslash($_POST['type'])) : '';
    $context = isset($_POST['context']) && in_array($_POST['context'], ['normal','side'], true)
        ? $_POST['context'] : 'normal';
    $priority = isset($_POST['priority']) && in_array($_POST['priority'], ['core','default','high','low'], true)
        ? $_POST['priority'] : 'core';
    $rev = isset($_POST['rev']) ? intval($_POST['rev']) : 0;

    $types = mydash_widget_type_registry();
    if (!$id || !$title || !$type || !isset($types[$type])) {
        wp_safe_redirect(add_query_arg('mydash_err', 'invalid', admin_url('options-general.php?page=mydash')));
        exit;
    }

    // Sanitize data theo type
    $data = [];
    if ($type === 'html') {
        $raw = isset($_POST['data']['content']) ? wp_unslash($_POST['data']['content']) : '';
        $data['content'] = wp_kses_post($raw);
    }

    // Đọc và kiểm tra rev
    $cfg = get_option('mydash_dashboard_widgets', []);
    $cur = intval($cfg['rev'] ?? 0);
    if ($rev !== $cur) {
        wp_safe_redirect(add_query_arg('mydash_err', 'conflict', admin_url('options-general.php?page=mydash')));
        exit;
    }

    // Khởi tạo khung option nếu trống
    if (!isset($cfg['widgets']) || !is_array($cfg['widgets'])) {
        $cfg['widgets'] = [];
    }

    // Duy nhất ID
    if (isset($cfg['widgets'][$id])) {
        wp_safe_redirect(add_query_arg('mydash_err', 'duplicate', admin_url('options-general.php?page=mydash')));
        exit;
    }

    $cfg['widgets'][$id] = [
        'id'         => $id,
        'title'      => $title,
        'enabled'    => true,
        'context'    => $context,
        'priority'   => $priority,
        'capability' => $cap ?: 'read',
        'type'       => $type,
        'data'       => $data,
        'updated_at' => gmdate('c'),
    ];

    $cfg['rev'] = $cur + 1;

    update_option('mydash_dashboard_widgets', $cfg, false);
    wp_safe_redirect(add_query_arg(['mydash_updated' => 1], admin_url('options-general.php?page=mydash')));
    exit;
}
  1. Đăng ký widget khi nạp Dashboard và đặt vị trí
  • Nếu bạn đã có hàm đăng ký từ phần trước, chỉ cần bảo đảm map có type html và, nếu có control callback, truyền vào wpadddashboard_widget. Ví dụ đoạn thay thế cho phần add:
<?php
function mydash_register_dashboard_widgets(): void {
    if (!function_exists('get_current_screen')) {
        return;
    }
    $screen = get_current_screen();
    if (!$screen || $screen->id !== 'dashboard') {
        return;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $list = isset($cfg['widgets']) && is_array($cfg['widgets']) ? $cfg['widgets'] : [];
    if (!$list) {
        return;
    }

    $type_reg = mydash_widget_type_registry();

    foreach ($list as $id => $w) {
        if (empty($w['enabled'])) {
            continue;
        }
        $type = isset($w['type']) ? sanitize_key($w['type']) : '';
        if (!$type || !isset($type_reg[$type])) {
            continue;
        }
        $cap = isset($w['capability']) ? (string) $w['capability'] : 'read';
        if (!current_user_can($cap)) {
            continue;
        }

        $title = $w['title'] ?? $id;
        $render_cb  = $type_reg[$type]['render'];
        $control_cb = isset($type_reg[$type]['control']) ? $type_reg[$type]['control'] : null;

        wp_add_dashboard_widget(
            $id,
            $title,
            $render_cb,
            $control_cb,
            ['id' => $id]
        );

        // Đặt vào cột - ưu tiên mong muốn, ví dụ side + low
        $ctx = in_array($w['context'] ?? 'normal', ['normal','side'], true) ? $w['context'] : 'normal';
        $pri = in_array($w['priority'] ?? 'core', ['core','default','high','low'], true) ? $w['priority'] : 'core';
        mydash_place_widget($id, $ctx, $pri, 'dashboard');
    }
}
add_action('wp_dashboard_setup', 'mydash_register_dashboard_widgets', 99);

Kết quả mong đợi

  • Bạn tạo một widget type html với ví dụ nội dung, lưu vào option với rev tăng dần, và khi vào Dashboard nó sẽ hiển thị HTML đã được wpksespost. Với cấu hình mẫu ở form, widget sẽ nằm ở cột side với priority low mặc định. Nếu người dùng đã tự sắp xếp, WordPress sẽ tôn trọng thứ tự của họ.

Sửa widget tùy chỉnh: cập nhật an toàn và kiểm soát thay đổi

Mình sẽ triển khai luồng sửa một widget đã có theo hướng an toàn: nạp dữ liệu cũ vào form với escape đúng chỗ, khóa lạc quan bằng rev để phát hiện xung đột, chỉ cho phép sửa các trường an toàn như title, capability, context, priority và payload data, còn type và callback luôn bị giới hạn trong registry nội bộ của plugin. Khi lưu sẽ merge thay đổi, tăng rev, cập nhật option và phản hồi rõ ràng cho người dùng.

Các bước thực hiện

  1. Tạo form sửa, đổ sẵn dữ liệu cũ với escattr và esctextarea.
  2. Xử lý submit: kiểm tra nonce và capability, đối chiếu rev để phát hiện xung đột, merge thay đổi theo whitelist, tăng rev rồi update_option.
  3. Hiển thị thông báo thành công hoặc lỗi để người dùng hiểu chuyện gì đã xảy ra.

Ví dụ hoàn chỉnh: form sửa một widget cụ thể Đây là hàm in form sửa, chỉ định thẳng ID mydashhtmlnote. Bạn có thể dán vào trang Settings của plugin để có một form chạy ngay.

<?php
// In form sửa cho một widget cố định 'mydash_html_note'
function mydash_render_edit_widget_form_fixed(): void {
    $cfg = get_option('mydash_dashboard_widgets', []);
    $rev = intval($cfg['rev'] ?? 0);

    $id = 'mydash_html_note';
    $w  = isset($cfg['widgets'][$id]) && is_array($cfg['widgets'][$id]) ? $cfg['widgets'][$id] : null;
    if (!$w) {
        echo '<div class="notice notice-error"><p>' . esc_html__('Widget không tồn tại: mydash_html_note', 'mydash') . '</p></div>';
        return;
    }

    $title = (string) ($w['title'] ?? $id);
    $cap   = (string) ($w['capability'] ?? 'read');
    $type  = (string) ($w['type'] ?? 'html'); // chỉ hiển thị, không cho sửa
    $ctx   = in_array($w['context'] ?? 'normal', ['normal', 'side'], true) ? $w['context'] : 'normal';
    $pri   = in_array($w['priority'] ?? 'core', ['core','default','high','low'], true) ? $w['priority'] : 'core';
    $content = isset($w['data']['content']) ? (string) $w['data']['content'] : '';
    ?>
    <h2><?php esc_html_e('Sửa widget: mydash_html_note', 'mydash'); ?></h2>
    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="mydash_update_widget">
        <input type="hidden" name="id" value="<?php echo esc_attr($id); ?>">
        <input type="hidden" name="rev" value="<?php echo esc_attr($rev); ?>">
        <?php wp_nonce_field('mydash_update_widget', 'mydash_nonce'); ?>

        <table class="form-table" role="presentation">
            <tr>
                <th scope="row"><label for="mydash_title"><?php esc_html_e('Tiêu đề', 'mydash'); ?></label></th>
                <td><input type="text" id="mydash_title" name="title" class="regular-text" required value="<?php echo esc_attr($title); ?>"></td>
            </tr>
            <tr>
                <th scope="row"><label for="mydash_cap"><?php esc_html_e('Capability để xem', 'mydash'); ?></label></th>
                <td><input type="text" id="mydash_cap" name="capability" class="regular-text" value="<?php echo esc_attr($cap); ?>"></td>
            </tr>
            <tr>
                <th scope="row"><?php esc_html_e('Loại widget', 'mydash'); ?></th>
                <td>
                    <input type="text" class="regular-text" value="<?php echo esc_attr($type); ?>" readonly>
                    <p class="description"><?php esc_html_e('Type được giới hạn bởi plugin và không thể thay đổi tại đây.', 'mydash'); ?></p>
                </td>
            </tr>
            <tr>
                <th scope="row"><?php esc_html_e('Vị trí', 'mydash'); ?></th>
                <td>
                    <label><input type="radio" name="context" value="normal" <?php checked('normal', $ctx); ?>> normal</label>
                    <label style="margin-left:1em;"><input type="radio" name="context" value="side" <?php checked('side', $ctx); ?>> side</label>
                    <span style="margin-left:1em;">
                        <label><?php esc_html_e('Priority', 'mydash'); ?>
                            <select name="priority">
                                <option value="low" <?php selected('low', $pri); ?>>low</option>
                                <option value="default" <?php selected('default', $pri); ?>>default</option>
                                <option value="core" <?php selected('core', $pri); ?>>core</option>
                                <option value="high" <?php selected('high', $pri); ?>>high</option>
                            </select>
                        </label>
                    </span>
                </td>
            </tr>
            <?php if ($type === 'html'): ?>
                <tr>
                    <th scope="row"><label for="mydash_data_content"><?php esc_html_e('Nội dung HTML', 'mydash'); ?></label></th>
                    <td>
                        <textarea id="mydash_data_content" name="data[content]" class="large-text code" rows="8"><?php echo esc_textarea($content); ?></textarea>
                        <p class="description"><?php esc_html_e('Nội dung sẽ được lọc an toàn trước khi lưu.', 'mydash'); ?></p>
                    </td>
                </tr>
            <?php endif; ?>
        </table>

        <p><button type="submit" class="button button-primary"><?php esc_html_e('Lưu thay đổi', 'mydash'); ?></button></p>
    </form>
    <?php
}

Biến thể tổng quát: in form cho bất kỳ ID từ query Đoạn sau đọc tham số edit từ URL và in form tương ứng.

<?php
// Ví dụ gọi ở trang Settings của plugin
if (isset($_GET['edit'])) {
    $id = sanitize_key(wp_unslash($_GET['edit']));
    if ($id) {
        // Tái sử dụng logic ở hàm cố định bằng cách bóc tách ra hàm tổng quát
        mydash_render_edit_widget_form($id);
    }
}

// Bản tổng quát của hàm in form
function mydash_render_edit_widget_form(string $id): void {
    $cfg = get_option('mydash_dashboard_widgets', []);
    $rev = intval($cfg['rev'] ?? 0);

    $w  = isset($cfg['widgets'][$id]) && is_array($cfg['widgets'][$id]) ? $cfg['widgets'][$id] : null;
    if (!$w) {
        echo '<div class="notice notice-error"><p>' . esc_html(sprintf(__('Widget không tồn tại: %s', 'mydash'), $id)) . '</p></div>';
        return;
    }

    $title = (string) ($w['title'] ?? $id);
    $cap   = (string) ($w['capability'] ?? 'read');
    $type  = (string) ($w['type'] ?? '');
    $ctx   = in_array($w['context'] ?? 'normal', ['normal', 'side'], true) ? $w['context'] : 'normal';
    $pri   = in_array($w['priority'] ?? 'core', ['core','default','high','low'], true) ? $w['priority'] : 'core';

    $html_content = '';
    if ($type === 'html') {
        $html_content = isset($w['data']['content']) ? (string) $w['data']['content'] : '';
    }

    ?>
    <h2><?php echo esc_html(sprintf(__('Sửa widget: %s', 'mydash'), $id)); ?></h2>
    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="mydash_update_widget">
        <input type="hidden" name="id" value="<?php echo esc_attr($id); ?>">
        <input type="hidden" name="rev" value="<?php echo esc_attr($rev); ?>">
        <?php wp_nonce_field('mydash_update_widget', 'mydash_nonce'); ?>

        <table class="form-table" role="presentation">
            <tr>
                <th scope="row"><label for="mydash_title"><?php esc_html_e('Tiêu đề', 'mydash'); ?></label></th>
                <td><input type="text" id="mydash_title" name="title" class="regular-text" required value="<?php echo esc_attr($title); ?>"></td>
            </tr>
            <tr>
                <th scope="row"><label for="mydash_cap"><?php esc_html_e('Capability để xem', 'mydash'); ?></label></th>
                <td><input type="text" id="mydash_cap" name="capability" class="regular-text" value="<?php echo esc_attr($cap); ?>"></td>
            </tr>
            <tr>
                <th scope="row"><?php esc_html_e('Loại widget', 'mydash'); ?></th>
                <td>
                    <input type="text" class="regular-text" value="<?php echo esc_attr($type); ?>" readonly>
                    <p class="description"><?php esc_html_e('Type bị giới hạn trong danh sách được hỗ trợ và không nhận từ người dùng.', 'mydash'); ?></p>
                </td>
            </tr>
            <tr>
                <th scope="row"><?php esc_html_e('Vị trí', 'mydash'); ?></th>
                <td>
                    <label><input type="radio" name="context" value="normal" <?php checked('normal', $ctx); ?>> normal</label>
                    <label style="margin-left:1em;"><input type="radio" name="context" value="side" <?php checked('side', $ctx); ?>> side</label>
                    <span style="margin-left:1em;">
                        <label><?php esc_html_e('Priority', 'mydash'); ?>
                            <select name="priority">
                                <option value="low" <?php selected('low', $pri); ?>>low</option>
                                <option value="default" <?php selected('default', $pri); ?>>default</option>
                                <option value="core" <?php selected('core', $pri); ?>>core</option>
                                <option value="high" <?php selected('high', $pri); ?>>high</option>
                            </select>
                        </label>
                    </span>
                </td>
            </tr>
            <?php if ($type === 'html'): ?>
                <tr>
                    <th scope="row"><label for="mydash_data_content"><?php esc_html_e('Nội dung HTML', 'mydash'); ?></label></th>
                    <td>
                        <textarea id="mydash_data_content" name="data[content]" class="large-text code" rows="8"><?php echo esc_textarea($html_content); ?></textarea>
                    </td>
                </tr>
            <?php endif; ?>
        </table>

        <p><button type="submit" class="button button-primary"><?php esc_html_e('Lưu thay đổi', 'mydash'); ?></button></p>
    </form>
    <?php
}

Xử lý submit: kiểm tra rev, merge thay đổi, tăng rev và phản hồi Đoạn handler sau đảm bảo chỉ sửa trường hợp pháp, kiểm soát xung đột bằng rev và không bao giờ nhận callback từ người dùng.

<?php
add_action('admin_post_mydash_update_widget', 'mydash_handle_update_widget');
function mydash_handle_update_widget(): void {
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    check_admin_referer('mydash_update_widget', 'mydash_nonce');

    $id   = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
    $rev  = isset($_POST['rev']) ? intval($_POST['rev']) : 0;

    if (!$id) {
        wp_safe_redirect(add_query_arg('mydash_err', 'invalid_id', admin_url('options-general.php?page=mydash')));
        exit;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $cur = intval($cfg['rev'] ?? 0);
    if (!isset($cfg['widgets'][$id])) {
        wp_safe_redirect(add_query_arg('mydash_err', 'not_found', admin_url('options-general.php?page=mydash&edit=' . urlencode($id))));
        exit;
    }

    // Khóa lạc quan: phát hiện xung đột chỉnh sửa
    if ($rev !== $cur) {
        wp_safe_redirect(add_query_arg('mydash_err', 'conflict', admin_url('options-general.php?page=mydash&edit=' . urlencode($id))));
        exit;
    }

    $w = $cfg['widgets'][$id];

    // Chỉ cho sửa các trường an toàn
    $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : ($w['title'] ?? $id);
    $cap   = isset($_POST['capability']) ? sanitize_key(wp_unslash($_POST['capability'])) : ($w['capability'] ?? 'read');

    $ctx = isset($_POST['context']) && in_array($_POST['context'], ['normal','side'], true)
        ? $_POST['context'] : ($w['context'] ?? 'normal');

    $pri = isset($_POST['priority']) && in_array($_POST['priority'], ['core','default','high','low'], true)
        ? $_POST['priority'] : ($w['priority'] ?? 'core');

    // Type không nhận từ người dùng, lấy từ bản ghi cũ và kiểm tra trong registry
    $type = isset($w['type']) ? sanitize_key($w['type']) : '';
    $reg  = function_exists('mydash_widget_type_registry') ? mydash_widget_type_registry() : [];
    if (!$type || !isset($reg[$type])) {
        wp_safe_redirect(add_query_arg('mydash_err', 'unsupported_type', admin_url('options-general.php?page=mydash&edit=' . urlencode($id))));
        exit;
    }

    // Sanitize payload theo type
    $data = isset($w['data']) && is_array($w['data']) ? $w['data'] : [];
    if ($type === 'html') {
        $raw = isset($_POST['data']['content']) ? wp_unslash($_POST['data']['content']) : '';
        $data['content'] = wp_kses_post($raw);
    }

    // Merge thay đổi và tăng rev
    $cfg['widgets'][$id]['title']      = $title;
    $cfg['widgets'][$id]['capability'] = $cap ?: 'read';
    $cfg['widgets'][$id]['context']    = $ctx;
    $cfg['widgets'][$id]['priority']   = $pri;
    $cfg['widgets'][$id]['data']       = $data;
    $cfg['widgets'][$id]['updated_at'] = gmdate('c');

    $cfg['rev'] = $cur + 1;

    update_option('mydash_dashboard_widgets', $cfg, false);

    // Phản hồi rõ ràng
    $url = add_query_arg(
        ['mydash_updated' => 1, 'edited' => $id],
        admin_url('options-general.php?page=mydash')
    );
    wp_safe_redirect($url);
    exit;
}

Thông báo rõ ràng sau khi lưu hoặc khi xung đột Đặt notices sau vào trang Settings của plugin để người dùng luôn nhìn thấy kết quả thao tác.

<?php
add_action('admin_notices', function () {
    if (!is_admin() || !isset($_GET['page']) || $_GET['page'] !== 'mydash') {
        return;
    }
    if (isset($_GET['mydash_updated'])) {
        $id = isset($_GET['edited']) ? sanitize_key(wp_unslash($_GET['edited'])) : '';
        echo '<div class="notice notice-success is-dismissible"><p>' .
            esc_html(sprintf(__('Đã cập nhật widget: %s', 'mydash'), $id ?: '')) .
        '</p></div>';
    }
    if (isset($_GET['mydash_err'])) {
        $err = sanitize_key(wp_unslash($_GET['mydash_err']));
        $msg = '';
        if ($err === 'conflict') {
            $msg = __('Có thay đổi khác vừa được lưu. Vui lòng tải lại trang để hợp nhất trước khi tiếp tục.', 'mydash');
        } elseif ($err === 'invalid_id') {
            $msg = __('ID không hợp lệ.', 'mydash');
        } elseif ($err === 'not_found') {
            $msg = __('Không tìm thấy widget cần sửa.', 'mydash');
        } elseif ($err === 'unsupported_type') {
            $msg = __('Loại widget không được hỗ trợ.', 'mydash');
        } else {
            $msg = __('Có lỗi xảy ra. Vui lòng thử lại.', 'mydash');
        }
        echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($msg) . '</p></div>';
    }
});

Gợi ý thực hành an toàn

  • Không nhận tên callback từ form hay DB. Luôn ánh xạ type hợp lệ sang callback cố định trong plugin qua registry, ví dụ mydashwidgettype_registry. Nếu cần cho phép đổi type, chỉ hiển thị dropdown từ registry và bắt buộc sanitize về một giá trị có sẵn, đồng thời migrate dữ liệu data phù hợp. Mặc định, mình khuyên giữ type cố định khi sửa để giảm rủi ro.
  • Khi lưu, luôn kiểm tra rev. Nếu người khác đã lưu trước một nhịp, đừng overwrite. Hãy báo conflict và yêu cầu tải lại, như handler ở trên.
  • context và priority là ý định cấu hình. Việc đặt vị trí thật được thực hiện ở hook wpdashboardsetup bằng thao tác $wpmetaboxes mà bạn đã cài ở phần nạp widget. Form sửa chỉ thay đổi ý định này trong option.

Xóa widget tùy chỉnh: xác nhận, dọn dẹp và rollback

Khi cần xóa một widget khỏi Dashboard, mình triển khai quy trình an toàn gồm xác nhận có nonce, kiểm tra capability, xóa bản ghi khỏi option cấu hình, tăng rev để khóa lạc quan, dọn cache tạm liên quan, ghi log ngắn gọn, và cho phép Undo trong thời gian ngắn bằng snapshot tạm. Sau khi xóa, widget sẽ không còn được đăng ký ở lần nạp Dashboard tiếp theo vì danh sách đăng ký đọc từ option đã bị gỡ.

Các bước thực hiện

  1. Hiển thị form xác nhận xóa ở trang cấu hình plugin, gửi về admin-post.php với nonce và rev hiện tại.
  2. Handler xóa: kiểm tra capability, nonce, đối chiếu rev. Nếu hợp lệ thì gỡ widget khỏi mảng widgets, tăng rev rồi update_option. Xóa transient cache của widget nếu có. Lưu snapshot tạm để Undo. Ghi một log tối thiểu.
  3. Hiển thị thông báo có nút Undo. Nếu người dùng nhấn Undo trong thời gian hiệu lực, khôi phục snapshot và tăng rev một lần nữa.
  4. Người dùng tải lại trang Dashboard, widget đã bị gỡ sẽ không còn được đăng ký.

Ví dụ hoàn chỉnh: xác nhận xóa, handler xóa và Undo Đoạn code dưới đây hoạt động ngay trong trang Settings của plugin. ID thử nghiệm là mydashhtmlnote, option cấu hình là mydashdashboardwidgets, prefix transient cache là mydashw.

<?php
// 1) In form xác nhận xóa cho một widget
function mydash_render_delete_widget_confirm(string $id): void {
    $cfg = get_option('mydash_dashboard_widgets', []);
    $rev = intval($cfg['rev'] ?? 0);
    $w   = isset($cfg['widgets'][$id]) && is_array($cfg['widgets'][$id]) ? $cfg['widgets'][$id] : null;

    if (!$w) {
        echo '<div class="notice notice-error"><p>' . esc_html__('Không tìm thấy widget để xóa.', 'mydash') . '</p></div>';
        return;
    }

    $title = (string) ($w['title'] ?? $id);
    ?>
    <div class="notice notice-warning">
        <p><?php echo esc_html(sprintf(__('Bạn sắp xóa widget "%s". Hành động này sẽ gỡ widget khỏi Dashboard ở lần tải tiếp theo.', 'mydash'), $title)); ?></p>
    </div>
    <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="mydash_delete_widget">
        <input type="hidden" name="id" value="<?php echo esc_attr($id); ?>">
        <input type="hidden" name="rev" value="<?php echo esc_attr($rev); ?>">
        <?php wp_nonce_field('mydash_delete_widget', 'mydash_nonce'); ?>
        <p>
            <button type="submit" class="button button-secondary"><?php esc_html_e('Xóa vĩnh viễn', 'mydash'); ?></button>
            <a class="button" href="<?php echo esc_url(admin_url('options-general.php?page=mydash')); ?>">
                <?php esc_html_e('Hủy', 'mydash'); ?>
            </a>
        </p>
    </form>
    <?php
}

// 2) Handler xóa
add_action('admin_post_mydash_delete_widget', 'mydash_handle_delete_widget');
function mydash_handle_delete_widget(): void {
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    check_admin_referer('mydash_delete_widget', 'mydash_nonce');

    $id  = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
    $rev = isset($_POST['rev']) ? intval($_POST['rev']) : -1;

    if (!$id) {
        wp_safe_redirect(add_query_arg('mydash_err', 'invalid_id', admin_url('options-general.php?page=mydash')));
        exit;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $cur = intval($cfg['rev'] ?? 0);

    if (!isset($cfg['widgets'][$id])) {
        wp_safe_redirect(add_query_arg(['mydash_err' => 'not_found', 'id' => $id], admin_url('options-general.php?page=mydash')));
        exit;
    }

    if ($rev !== $cur) {
        wp_safe_redirect(add_query_arg(['mydash_err' => 'conflict', 'id' => $id], admin_url('options-general.php?page=mydash')));
        exit;
    }

    // Lưu snapshot tạm để Undo trong 15 phút
    $snap_key = 'mydash_undo_' . $id;
    set_transient($snap_key, [
        'id'     => $id,
        'widget' => $cfg['widgets'][$id],
        'rev'    => $cur,
        'at'     => gmdate('c'),
        'by'     => get_current_user_id(),
    ], 15 * MINUTE_IN_SECONDS);

    // Gỡ widget khỏi cấu hình và tăng rev
    unset($cfg['widgets'][$id]);
    $cfg['rev'] = $cur + 1;
    update_option('mydash_dashboard_widgets', $cfg, false);

    // Dọn transient cache liên quan nếu plugin có dùng
    delete_transient('mydash_w_' . $id);

    // Ghi log tối thiểu - giữ tối đa 20 mục
    $log = get_option('mydash_dashboard_widgets_log', []);
    if (!is_array($log)) { $log = []; }
    $log[] = ['t' => gmdate('c'), 'act' => 'delete', 'id' => $id, 'by' => get_current_user_id()];
    if (count($log) > 20) { $log = array_slice($log, -20); }
    update_option('mydash_dashboard_widgets_log', $log, false);

    // PRG
    wp_safe_redirect(add_query_arg(['mydash_deleted' => 1, 'id' => $id], admin_url('options-general.php?page=mydash')));
    exit;
}

// 3) Handler Undo
add_action('admin_post_mydash_undo_delete_widget', 'mydash_handle_undo_delete_widget');
function mydash_handle_undo_delete_widget(): void {
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized', 'mydash'), 403);
    }
    check_admin_referer('mydash_undo_delete_widget', 'mydash_nonce');

    $id = isset($_POST['id']) ? sanitize_key(wp_unslash($_POST['id'])) : '';
    if (!$id) {
        wp_safe_redirect(add_query_arg('mydash_err', 'invalid_id', admin_url('options-general.php?page=mydash')));
        exit;
    }

    $snap_key = 'mydash_undo_' . $id;
    $snap = get_transient($snap_key);
    if (!$snap || !is_array($snap) || empty($snap['widget'])) {
        wp_safe_redirect(add_query_arg(['mydash_err' => 'undo_expired', 'id' => $id], admin_url('options-general.php?page=mydash')));
        exit;
    }

    $cfg = get_option('mydash_dashboard_widgets', []);
    $cur = intval($cfg['rev'] ?? 0);

    // Không ghi đè nếu ai đó đã tạo lại cùng ID
    if (!isset($cfg['widgets'][$id])) {
        $cfg['widgets'][$id] = $snap['widget'];
        $cfg['widgets'][$id]['updated_at'] = gmdate('c');
        $cfg['rev'] = $cur + 1;
        update_option('mydash_dashboard_widgets', $cfg, false);
    }

    delete_transient($snap_key);

    // Log Undo
    $log = get_option('mydash_dashboard_widgets_log', []);
    if (!is_array($log)) { $log = []; }
    $log[] = ['t' => gmdate('c'), 'act' => 'undo_delete', 'id' => $id, 'by' => get_current_user_id()];
    if (count($log) > 20) { $log = array_slice($log, -20); }
    update_option('mydash_dashboard_widgets_log', $log, false);

    wp_safe_redirect(add_query_arg(['mydash_restored' => 1, 'id' => $id], admin_url('options-general.php?page=mydash')));
    exit;
}

// 4) Thông báo sau xóa và nút Undo
add_action('admin_notices', function () {
    if (!is_admin() || !isset($_GET['page']) || $_GET['page'] !== 'mydash') {
        return;
    }

    if (isset($_GET['mydash_deleted'], $_GET['id'])) {
        $id = sanitize_key(wp_unslash($_GET['id']));
        $snap_key = 'mydash_undo_' . $id;
        $has_snap = (bool) get_transient($snap_key);

        echo '<div class="notice notice-success"><p>';
        echo esc_html(sprintf(__('Đã xóa widget: %s', 'mydash'), $id));
        echo '</p>';

        if ($has_snap) {
            ?>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" style="display:inline-block;margin:8px 0;">
                <input type="hidden" name="action" value="mydash_undo_delete_widget">
                <input type="hidden" name="id" value="<?php echo esc_attr($id); ?>">
                <?php wp_nonce_field('mydash_undo_delete_widget', 'mydash_nonce'); ?>
                <button type="submit" class="button"><?php esc_html_e('Hoàn tác', 'mydash'); ?></button>
            </form>
            <?php
        }

        echo '</div>';
    }

    if (isset($_GET['mydash_restored'], $_GET['id'])) {
        $id = sanitize_key(wp_unslash($_GET['id']));
        echo '<div class="notice notice-success is-dismissible"><p>' .
            esc_html(sprintf(__('Đã phục hồi widget: %s', 'mydash'), $id)) .
        '</p></div>';
    }

    if (isset($_GET['mydash_err'])) {
        $err = sanitize_key(wp_unslash($_GET['mydash_err']));
        $msg = $err === 'conflict'
            ? __('Dữ liệu vừa được thay đổi ở nơi khác. Vui lòng tải lại và thử lại.', 'mydash')
            : ($err === 'not_found' ? __('Không tìm thấy widget.', 'mydash')
            : ($err === 'invalid_id' ? __('ID không hợp lệ.', 'mydash')
            : ($err === 'undo_expired' ? __('Không thể hoàn tác vì snapshot đã hết hạn.', 'mydash')
            : __('Có lỗi xảy ra.', 'mydash'))));
        echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($msg) . '</p></div>';
    }
});

Biến thể dùng biến tên option và prefix cache Nếu bạn đặt tên khác cho option và prefix transient, chuẩn hóa sớm ở đầu plugin rồi dùng nhất quán.

<?php
$mydash_option_name   = 'mydash_dashboard_widgets';
$mydash_cache_prefix  = 'mydash_w_';
$mydash_undo_prefix   = 'mydash_undo_';

// Ví dụ xóa cache theo prefix
function mydash_purge_widget_cache(string $id): void {
    global $mydash_cache_prefix;
    delete_transient($mydash_cache_prefix . $id);
}

Gợi ý thực hành

  • Luôn đối chiếu rev trước khi xóa để tránh ghi đè thay đổi đồng thời. Nếu lệch rev, báo conflict và yêu cầu tải lại.
  • Nếu bạn đang dùng multisite và cache theo phạm vi mạng, thay deletetransient bằng deletesite_transient cho các cache đặt ở cấp network.
  • Undo là tiện ích cho quản trị nội bộ. Đặt TTL ngắn để giảm rủi ro giữ snapshot quá lâu.

Ẩn hoặc vô hiệu hóa widget mặc định, thứ tự hook và chiến lược gỡ nguồn đăng ký

Bạn có hai hướng can thiệp khi muốn loại bỏ widget trên Dashboard: ẩn khỏi UI bằng removemetabox, hoặc gỡ tận gốc nơi đăng ký bằng removeaction. removemetabox an toàn, ít xâm lấn nhưng chỉ loại khỏi giao diện, logic khởi tạo của plugin khác vẫn chạy. Gỡ tận gốc bằng removeaction thì sạch và nhẹ hơn, nhất là khi bên thứ ba làm việc nặng trước khi add widget, nhưng bạn phải biết chính xác callback và priority. Trường hợp đặc biệt như Welcome panel không phải meta box, phải removeaction trên hook welcomepanel. Mấu chốt nữa là thứ tự hook: ẩn bằng removemetabox nên chạy rất muộn trong wpdashboardsetup, còn gỡ tận gốc thì chạy rất sớm, thậm chí ngay từ plugins_loaded.

Các bước áp dụng theo thứ tự hoạt động

  1. Ẩn widget core bằng removemetabox ở wpdashboardsetup với priority thật muộn.
  2. Tắt Welcome panel bằng remove_action vì nó không phải meta box.
  3. Nếu cần triệt tiêu widget của plugin khác, removeaction với callback đăng ký của họ ở priority sớm trong chính wpdashboardsetup, hoặc ở pluginsloaded để chặn ngay từ gốc.
  4. Trường hợp hiếm plugin tái đăng ký rất trễ, dọn lượt cuối ở adminhead-index.php bằng cách unset từ global $wpmeta_boxes.

Ví dụ hoàn chỉnh: ẩn widget core, tắt Welcome panel, chặn widget của bên thứ ba, và dọn dẹp cuối Đoạn sau chạy độc lập trong file plugin của bạn.

<?php
// 1) Chặn nguồn đăng ký của bên thứ ba thật sớm nếu biết callback
add_action('plugins_loaded', function () {
    // Ví dụ plugin Vendor thêm widget ở wp_dashboard_setup priority 10
    remove_action('wp_dashboard_setup', 'vendor_register_dashboard_widget', 10);

    // Nếu họ dùng class static hoặc tên có namespace
    remove_action('wp_dashboard_setup', array('Vendor_Plugin', 'register_dashboard_widget'), 20);
    // remove_action('wp_dashboard_setup', array('\Vendor\Plugin', 'register_dashboard_widget'), 20);
});

// 2) Chặn lại lần nữa ngay trong wp_dashboard_setup ở priority sớm để đảm bảo
add_action('wp_dashboard_setup', function () {
    remove_action('wp_dashboard_setup', 'vendor_register_dashboard_widget', 10);
    remove_action('wp_dashboard_setup', array('Vendor_Plugin', 'register_dashboard_widget'), 20);
}, 1);

// 3) Ẩn widget core và tắt Welcome panel thật muộn
add_action('wp_dashboard_setup', function () {
    // Danh sách widget core thường gặp
    $core = array(
        'dashboard_activity',
        'dashboard_primary',
        'dashboard_quick_press', // ID vẫn là quick_press dù UI ghi Quick Draft
        // Thêm nếu muốn: 'dashboard_site_health', 'dashboard_right_now' trên bản cũ
    );

    foreach ($core as $id) {
        // Khi không chắc context, gọi cho cả normal và side
        remove_meta_box($id, 'dashboard', 'normal');
        remove_meta_box($id, 'dashboard', 'side');
    }

    // Welcome panel không phải meta box
    remove_action('welcome_panel', 'wp_welcome_panel');

    // Tùy chọn: cũng tắt cờ hiển thị đã lưu của người dùng hiện tại
    if (is_user_logged_in()) {
        update_user_meta(get_current_user_id(), 'show_welcome_panel', 0);
    }
}, PHP_INT_MAX);

// 4) Dọn dẹp cuối trong tình huống hiếm khi plugin tái đăng ký rất trễ
add_action('admin_head-index.php', function () {
    // Chỉ chạy trên trang Dashboard chính
    if (!function_exists('get_current_screen')) {
        return;
    }
    $screen = get_current_screen();
    if (!$screen || $screen->id !== 'dashboard') {
        return;
    }

    $force_unset = array(
        'dashboard_activity',
        'dashboard_primary',
        'dashboard_quick_press',
        // Thêm ID widget của plugin khác nếu chúng cứ tái đăng ký
        // 'some_vendor_widget_id',
    );

    mydash_force_unset_dashboard_boxes($force_unset, 'dashboard');
}, PHP_INT_MAX);

/**
 * Hàm tiện ích: unset box khỏi $wp_meta_boxes theo ID, cho các context phổ biến.
 */
function mydash_force_unset_dashboard_boxes(array $ids, string $screen_id = 'dashboard'): void
{
    global $wp_meta_boxes;

    if (empty($wp_meta_boxes[$screen_id])) {
        return;
    }

    $contexts   = array('normal', 'side');
    $priorities = array('high', 'core', 'default', 'low');

    foreach ($ids as $id) {
        $id = sanitize_key($id);
        foreach ($contexts as $ctx) {
            if (empty($wp_meta_boxes[$screen_id][$ctx])) {
                continue;
            }
            foreach ($priorities as $prio) {
                if (isset($wp_meta_boxes[$screen_id][$ctx][$prio][$id])) {
                    unset($wp_meta_boxes[$screen_id][$ctx][$prio][$id]);
                }
            }
        }
    }
}

Biến thể với danh sách cấu hình riêng Bạn có thể trích danh sách cần ẩn từ option của plugin rồi dùng chung cho cả removemetabox và bước unset cuối.

<?php
add_action('wp_dashboard_setup', function () {
    $cfg = get_option('mydash_dashboard_widgets', array());
    $hide = isset($cfg['hidden_core']) && is_array($cfg['hidden_core']) ? array_keys(array_filter($cfg['hidden_core'])) : array();

    foreach ($hide as $id) {
        remove_meta_box($id, 'dashboard', 'normal');
        remove_meta_box($id, 'dashboard', 'side');
    }
    remove_action('welcome_panel', 'wp_welcome_panel');
}, PHP_INT_MAX);

add_action('admin_head-index.php', function () {
    $cfg = get_option('mydash_dashboard_widgets', array());
    $hide = isset($cfg['hidden_core']) && is_array($cfg['hidden_core']) ? array_keys(array_filter($cfg['hidden_core'])) : array();
    mydash_force_unset_dashboard_boxes($hide, 'dashboard');
}, PHP_INT_MAX);

Giải thích quan trọng để bạn nắm vững

  • Vì sao phải chạy removemetabox rất muộn. Hầu hết widget core và plugin khác được đăng ký ở wpdashboardsetup priority mặc định 10. Chạy ở 100 hoặc PHPINTMAX giúp bạn gỡ sau khi mọi thứ đã có mặt trong $wpmetaboxes.
  • Khi nào nên gỡ tận gốc bằng removeaction. Nếu widget không phải meta box như Welcome panel, nếu ID thay đổi động khó đoán, hoặc nếu callback của bên thứ ba thực thi nặng trước khi gọi wpadddashboardwidget thì removemetabox không giúp gì về hiệu năng. Lúc này removeaction ở wpdashboardsetup priority 1 hoặc ở pluginsloaded là cách đúng.
  • Về cơ chế WPHook. Danh sách callback được duyệt theo priority. Nếu bạn removeaction một callback có priority lớn hơn trong khi hook đang chạy, nó sẽ không còn được gọi ở bước đến lượt nó. Vì vậy removeaction ở wpdashboard_setup priority 1 đủ để chặn callback ở priority 10 của plugin khác.
  • Dọn dẹp cuối trong adminhead-index.php để đề phòng plugin cố tình tái đăng ký muộn. Đây là biện pháp an toàn cuối cùng. Bạn nên chỉ dùng khi đã thử removeaction và removemetabox mà vẫn không triệt để.

Mẹo thực hành để xác định đúng ID hoặc callback

  • Mở Dashboard, bật WPDEBUGLOG. Tạm thời vardump(arraykeys($wpmetaboxes[‘dashboard’][‘normal’][‘core’])) trong môi trường dev để xem ID chính xác rồi cập nhật danh sách. Xóa debug ngay sau khi xác định xong.
  • Nếu bạn không biết callback mà plugin dùng để đăng ký widget, tìm trong mã của họ chuỗi wpadddashboardwidget rồi lần ngược hàm bao ngoài. Sau đó removeaction chính hàm đó trên wpdashboardsetup với đúng priority mà họ add_action. Nếu chưa chắc priority, gỡ ở nhiều priority thường gặp như 10, 20.

Cách thường làm là chỉ removemetabox và nghĩ đã xong. Cách đúng là kết hợp đúng thứ tự hook: gỡ nguồn đăng ký thật sớm khi có thể, ẩn bằng removemetabox thật muộn để bảo đảm, và chỉ dùng unset $wpmetaboxes như phương án cuối cùng. Cách này vừa nhẹ, vừa chắc, và không gây tác dụng phụ khó đoán.

Điều khiển cột và ưu tiên hiển thị bằng $wpmetaboxes, tôn trọng order của người dùng

Sau khi wpadddashboardwidget chạy, widget mặc định rơi vào cột normal với priority core. Để đặt nó sang cột mong muốn và chọn priority hiển thị, cách ổn định là thao tác trực tiếp trên global $wpmetaboxes theo 4 tầng key: $wpmeta_boxes[‘dashboard’][context][priority][id]. Mục tiêu là di chuyển bản ghi box khỏi bucket nguồn sang bucket đích và cập nhật thuộc tính context, priority trong chính box. Quan trọng: không ghi vào bucket sorted vì đó là vùng do user meta điều khiển, đụng vào sẽ phá thứ tự mà người dùng đã sắp.

Các bước thực hiện chuẩn

  1. Đăng ký widget như bình thường bằng wpadddashboard_widget.
  2. Ngay sau đó, ở cùng hook wpdashboardsetup nhưng chạy muộn, lấy box theo id từ bucket nguồn và chuyển sang context – priority đích.
  3. Mặc định đặt side – low để tạo trải nghiệm gọn cho người chưa từng tùy biến. Nếu người dùng đã kéo thả và lưu order, core sẽ ưu tiên theo meta-box-order_dashboard và bỏ qua mặc định này – đây là hành vi cần tôn trọng.
  4. Chỉ khi thật sự phải ép vị trí mọi lúc, hãy loại id của widget khỏi user option meta-box-orderdashboard bằng filter getuseroptionmeta-box-order_dashboard, nhưng cân nhắc kỹ UX.

Ví dụ hoàn chỉnh – đăng ký và đặt mặc định side – low, tôn trọng order người dùng

<?php
// Di chuyển sau khi phần lớn widget đã đăng ký
add_action('wp_dashboard_setup', function () {
    $widget_id = 'acme_sales_widget';

    // 1) Đăng ký widget - rơi vào normal-core theo mặc định
    wp_add_dashboard_widget(
        $widget_id,
        'Sales snapshot',
        function () {
            echo '<p>Last 7 days revenue: <strong>$12,340</strong></p>';
        }
    );

    // 2) Di chuyển sang side-low
    global $wp_meta_boxes;

    $page = 'dashboard';
    $dst_context  = 'side';
    $dst_priority = 'low';

    // Tìm ở các khả năng nguồn hợp lý
    $src_contexts          = array('normal', 'side');
    $possible_src_priority = array('core', 'default', 'high', 'low');

    $box = null;
    foreach ($src_contexts as $src_context) {
        foreach ($possible_src_priority as $src_priority) {
            if (isset($wp_meta_boxes[$page][$src_context][$src_priority][$widget_id])) {
                $box = $wp_meta_boxes[$page][$src_context][$src_priority][$widget_id];
                unset($wp_meta_boxes[$page][$src_context][$src_priority][$widget_id]);
                break 2;
            }
        }
    }

    if ($box) {
        // Bảo đảm bucket đích tồn tại
        $wp_meta_boxes[$page][$dst_context] = $wp_meta_boxes[$page][$dst_context] ?? array();
        $wp_meta_boxes[$page][$dst_context][$dst_priority] = $wp_meta_boxes[$page][$dst_context][$dst_priority] ?? array();

        // Cập nhật metadata để core hiểu đúng vị trí
        $box['context']  = $dst_context;
        $box['priority'] = $dst_priority;

        $wp_meta_boxes[$page][$dst_context][$dst_priority][$widget_id] = $box;
    }
}, 99);

Biến thể theo biến cấu hình

<?php
add_action('wp_dashboard_setup', function () {
    $widget_id   = 'acme_news_widget';
    $dst_context = 'normal'; // 'normal' hoặc 'side'
    $dst_prio    = 'high';   // 'high' - 'core' - 'default' - 'low'

    wp_add_dashboard_widget($widget_id, 'Company news', function () {
        echo '<p>Welcome to Q3. All hands meeting at 10:00.</p>';
    });

    global $wp_meta_boxes;
    $page = 'dashboard';

    $box = null;
    foreach (array('normal', 'side') as $src_ctx) {
        foreach (array('core', 'default', 'high', 'low') as $src_prio) {
            if (isset($wp_meta_boxes[$page][$src_ctx][$src_prio][$widget_id])) {
                $box = $wp_meta_boxes[$page][$src_ctx][$src_prio][$widget_id];
                unset($wp_meta_boxes[$page][$src_ctx][$src_prio][$widget_id]);
                break 2;
            }
        }
    }
    if ($box) {
        $wp_meta_boxes[$page][$dst_context] = $wp_meta_boxes[$page][$dst_context] ?? array();
        $wp_meta_boxes[$page][$dst_context][$dst_prio] = $wp_meta_boxes[$page][$dst_context][$dst_prio] ?? array();
        $box['context']  = $dst_context;
        $box['priority'] = $dst_prio;
        $wp_meta_boxes[$page][$dst_context][$dst_prio][$widget_id] = $box;
    }
}, 99);

Vì sao cách trên tôn trọng người dùng

  • Với người chưa có user option meta-box-order_dashboard, core sẽ render theo các bucket priority mà bạn đã sắp – widget của bạn nằm ở side – low.
  • Khi người dùng đã kéo thả, core dùng danh sách trong user option để lấp đầy bucket sorted và lấy đó làm chuẩn. Box bạn vừa đặt ở side – low sẽ bị bỏ qua cho người dùng đó. Điều này đúng mong đợi và tránh việc widget của bạn cứ nhảy vị trí sau khi họ đã sắp xếp.

Ép vị trí mọi lúc – chỉ dùng khi thật sự cần

  • Cách nhẹ và an toàn là lọc user option trước khi core đọc, loại ID widget của bạn khỏi mọi context trong meta-box-orderdashboard. Như vậy core sẽ rơi về mặc định bạn đã set ở $wpmeta_boxes.
<?php
// Cân nhắc UX kỹ trước khi bật
add_filter('get_user_option_meta-box-order_dashboard', function ($value) {
    $force_id = 'acme_sales_widget';

    if (!is_array($value)) {
        return $value;
    }
    foreach ($value as $context => $csv) {
        if (!is_string($csv) || $csv === '') {
            continue;
        }
        $ids = array_filter(array_map('trim', explode(',', $csv)));
        $ids = array_values(array_diff($ids, array($force_id)));
        $value[$context] = implode(',', $ids);
    }
    return $value;
}, 10);

Lưu ý thực hành

  • Không ghi vào key sorted của $wpmetaboxes. Hãy luôn thêm box vào bucket theo context – priority như ví dụ.
  • Nếu cần làm tương tự ở Network Dashboard hoặc User Dashboard, thay page tương ứng thành dashboard-network hoặc dashboard-user và móc vào hook wpnetworkdashboardsetup hoặc wpuserdashboardsetup.
  • Khi debug, bạn có thể vardump các key của $wpmeta_boxes[‘dashboard’] trong môi trường dev để xác định id và bucket thực tế, sau đó tắt ngay đoạn debug.

Bảo mật và phân quyền: capability, nonce, sanitize và escape

Tất cả hành động thêm – sửa – xóa widget và cấu hình liên quan đều phải tuân thủ nguyên tắc ít quyền nhất, chống CSRF bằng nonce, lọc dữ liệu vào, và escape khi xuất. Mình tách thành bốn điểm then chốt để bạn áp dụng ngay vào plugin tùy biến Dashboard.

  1. Chọn capability đúng việc – không lạm dụng manage_options
  • Xem widget
  • Nội dung biên tập: edit_posts.
  • Widget điều phối bình luận: moderate_comments.
  • Widget cấu hình hệ thống: manage_options, nhưng chỉ dùng khi thật sự cần quyền quản trị cấp cao.
  • Tốt nhất: tự định nghĩa capability riêng cho plugin, ví dụ managecustomdashboard, rồi gán cho Administrator. Như vậy bạn dễ phân quyền về sau.
  • Cấu hình plugin và CRUD widget
  • Dùng capability riêng như managecustomdashboard cho trang cấu hình và các handler lưu xóa.
  • Khi đăng ký widget ở wpdashboardsetup, chỉ add khi currentusercan(capability của chính widget). Điều này tránh lộ dữ liệu cho người không đủ quyền.

Ví dụ thêm capability riêng cho Administrator khi kích hoạt plugin:

<?php
register_activation_hook(__FILE__, function () {
    $role = get_role('administrator');
    if ($role && !$role->has_cap('manage_custom_dashboard')) {
        $role->add_cap('manage_custom_dashboard');
    }
});
  1. Chống CSRF bằng nonce – kiểm tra trước khi đụng dữ liệu
  • Trong mọi form thêm – sửa – xóa, in nonce bằng wpnoncefield. Với AJAX, tạo nonce bằng wpcreatenonce và kiểm tra bằng checkajaxreferer.
  • Trên server, gọi checkadminreferer hoặc checkajaxreferer ngay đầu handler. Nếu fail thì dừng xử lý.
  • Với hành động xóa qua URL, dùng wpnonceurl để tạo link an toàn.
  1. Sanitize dữ liệu vào – chọn hàm phù hợp từng trường
  • Text ngắn như tiêu đề, id, slug
  • Title: sanitizetextfield.
  • ID hoặc key: sanitizekey để tránh ký tự lạ. Nếu cần slug hiển thị, có thể dùng sanitizetitle.
  • Nội dung HTML cho phép
  • Dùng wpksespost cho nội dung HTML dạng bài viết. Nếu bạn có whitelist riêng, dùng wpkses với mảng allowedhtml tùy chỉnh.
  • Số và boolean
  • Số nguyên: absint.
  • Boolean: ! empty($POST[‘flag’]) hoặc filtervar($val, FILTERVALIDATEBOOLEAN).
  • Nhớ wpunslash trước khi sanitize vì dữ liệu từ $POST có thể bị addslashes.
  1. Escape dữ liệu ra – luôn escape đúng ngữ cảnh
  • Nội dung văn bản: esc_html.
  • Thuộc tính HTML như value, data-*: esc_attr.
  • Nội dung trong textarea: esc_textarea.
  • HTML đã cho phép: wpksespost.
  • Với URL khi cần: esc_url.

Mẫu triển khai đầy đủ cho một widget có form lưu cấu hình qua admin-post.php

  • Đây là bản điền sẵn hoàn chỉnh, dùng capability riêng managecustomdashboard cho thao tác lưu, và minimal cho người xem là editposts. Dữ liệu nội dung HTML được sanitize bằng wpkses_post khi lưu, và escape đúng ngữ cảnh khi hiển thị.
<?php
// 1) Đăng ký widget nếu người dùng có quyền xem
add_action('wp_dashboard_setup', function () {
    if (! current_user_can('edit_posts')) {
        return;
    }
    $widget_id = 'acme_notes_widget';
    wp_add_dashboard_widget(
        $widget_id,
        'Team notes',
        'acme_notes_widget_render'
    );
});

// 2) Hàm hiển thị widget + form
function acme_notes_widget_render() {
    $cfg = get_option('acme_notes_widget_cfg', array(
        'title'   => 'Notes',
        'content' => '<p>Welcome to the Dashboard.</p>',
        'rev'     => 1,
    ));
    ?>
    <div class="acme-notes-widget">
        <h4><?php echo esc_html($cfg['title'] ?? 'Notes'); ?></h4>
        <div class="inside">
            <?php
            // Nội dung HTML đã được kses khi lưu
            echo wp_kses_post($cfg['content'] ?? '');
            ?>
        </div>

        <?php if ( current_user_can('manage_custom_dashboard') ) : ?>
            <hr>
            <form method="post" action="<?php echo esc_url( admin_url('admin-post.php') ); ?>">
                <input type="hidden" name="action" value="acme_save_notes_widget">
                <?php wp_nonce_field('acme_save_notes_widget', 'acme_nonce'); ?>
                <input type="hidden" name="rev" value="<?php echo esc_attr( (string) ($cfg['rev'] ?? 1) ); ?>">
                <p>
                    <label for="acme_title">Tiêu đề</label>
                    <input id="acme_title" name="title" type="text" class="widefat"
                           value="<?php echo esc_attr($cfg['title'] ?? ''); ?>">
                </p>
                <p>
                    <label for="acme_content">Nội dung HTML</label>
                    <textarea id="acme_content" name="content" class="widefat" rows="6"><?php
                        echo esc_textarea($cfg['content'] ?? '');
                    ?></textarea>
                </p>
                <p>
                    <button type="submit" class="button button-primary">Lưu</button>
                    <a class="button button-link-delete"
                       href="<?php echo esc_url( wp_nonce_url(
                           add_query_arg(array('action' => 'acme_delete_notes_widget'), admin_url('admin-post.php')),
                           'acme_delete_notes_widget',
                           'acme_nonce'
                       ) ); ?>"
                       onclick="return confirm('Xóa nội dung widget?');">
                        Xóa nội dung
                    </a>
                </p>
            </form>
        <?php endif; ?>
    </div>
    <?php
}

// 3) Handler lưu - kiểm nonce trước, rồi kiểm capability, sanitize và lưu
add_action('admin_post_acme_save_notes_widget', function () {
    check_admin_referer('acme_save_notes_widget', 'acme_nonce');

    if ( ! current_user_can('manage_custom_dashboard') ) {
        wp_die( esc_html__('Unauthorized', 'acme'), 403 );
    }

    $stored = get_option('acme_notes_widget_cfg', array('rev' => 1));

    $new = array();
    $new['rev'] = isset($_POST['rev']) ? absint( wp_unslash($_POST['rev']) ) : 0;
    if ($new['rev'] !== (int) ($stored['rev'] ?? 0)) {
        wp_safe_redirect( add_query_arg('acme_conflict', 1, admin_url('index.php')) );
        exit;
    }

    $title   = isset($_POST['title'])   ? sanitize_text_field( wp_unslash($_POST['title']) )   : '';
    $content = isset($_POST['content']) ? wp_kses_post(        wp_unslash($_POST['content']) ) : '';

    $saved = array(
        'title'   => $title,
        'content' => $content, // đã kses an toàn
        'rev'     => (int) ($stored['rev'] ?? 0) + 1,
    );

    update_option('acme_notes_widget_cfg', $saved, false);

    wp_safe_redirect( add_query_arg('acme_updated', 1, admin_url('index.php')) );
    exit;
});

// 4) Handler xóa - nonce qua URL
add_action('admin_post_acme_delete_notes_widget', function () {
    check_admin_referer('acme_delete_notes_widget', 'acme_nonce');

    if ( ! current_user_can('manage_custom_dashboard') ) {
        wp_die( esc_html__('Unauthorized', 'acme'), 403 );
    }

    $stored = get_option('acme_notes_widget_cfg', array('rev' => 1));
    $cleared = array(
        'title'   => 'Notes',
        'content' => '',
        'rev'     => (int) ($stored['rev'] ?? 0) + 1,
    );
    update_option('acme_notes_widget_cfg', $cleared, false);

    wp_safe_redirect( add_query_arg('acme_deleted', 1, admin_url('index.php')) );
    exit;
});

Biến thể lưu kép raw và sanitized để linh hoạt chính sách HTML về sau

  • Dùng khi bạn muốn có khả năng nới whitelist thẻ trong tương lai mà không mất dữ liệu người dùng.
<?php
add_action('admin_post_acme_save_notes_widget_raw', function () {
    check_admin_referer('acme_save_notes_widget_raw', 'acme_nonce');

    if ( ! current_user_can('manage_custom_dashboard') ) {
        wp_die( esc_html__('Unauthorized', 'acme'), 403 );
    }

    $raw_title   = isset($_POST['title'])   ? wp_unslash($_POST['title'])   : '';
    $raw_content = isset($_POST['content']) ? wp_unslash($_POST['content']) : '';

    $san_title   = sanitize_text_field($raw_title);
    $san_content = wp_kses_post($raw_content);

    update_option('acme_notes_widget_cfg_raw', array(
        'title' => $raw_title,
        'content' => $raw_content,
    ), false);

    update_option('acme_notes_widget_cfg', array(
        'title' => $san_title,
        'content' => $san_content,
    ), false);

    wp_safe_redirect( admin_url('index.php') );
    exit;
});

Gợi ý nhanh cho AJAX

  • Trên client gửi mynonce do wpcreatenonce(‘acmeajax’) cung cấp.
  • Server dùng checkajaxreferer(‘acmeajax’, ‘noncefield’), sau đó currentusercan, rồi sanitize và updateoption. Trả về JSON bằng wpsendjsonsuccess với dữ liệu đã được sanitize.

Với bốn lớp bảo vệ trên, bạn tránh được rủi ro lộ dữ liệu và thao tác trái phép: chỉ người có capability tối thiểu cần thiết mới thấy và lưu được, mọi request đều có nonce hợp lệ, dữ liệu vào được lọc đúng mục đích, và dữ liệu ra luôn được escape theo ngữ cảnh.

Giao tiếp dữ liệu widget: admin-post truyền thống và AJAX, chọn kênh phù hợp

Mình chọn kênh theo tiêu chí đơn giản hay tức thời. admin-post phù hợp form submit chuẩn, theo mẫu PRG để tránh double submit và không cần JS. AJAX phù hợp khi bạn muốn cập nhật ngay nội dung widget trên trang và giữ ngữ cảnh người dùng. Hai kênh ngang nhau về bảo mật nếu bạn luôn dùng nonce đúng cách và kiểm tra currentusercan trước khi ghi dữ liệu.

Quy tắc chọn nhanh

  • Cần lưu rồi reload toàn trang kèm thông báo đơn giản: adminpost{action}.
  • Cần cập nhật ngay phần hiển thị mà không reload: wpajax{action}.
  • Dù chọn kênh nào, bắt buộc: nonce hợp lệ, capability phù hợp, sanitize đầu vào và escape đầu ra.

Ví dụ 1 – Submit truyền thống với admin-post và PRG

  • Form đặt ngay trong callback widget. Handler dùng adminpost{action} để lưu rồi redirect về Dashboard.
<?php
// Đăng ký widget
add_action('wp_dashboard_setup', function () {
    if ( ! current_user_can('edit_posts') ) {
        return;
    }
    wp_add_dashboard_widget('acme_trad_widget', 'Team notes - truyền thống', 'acme_trad_widget_render');
});

// Hàm hiển thị widget + form submit truyền thống
function acme_trad_widget_render() {
    $cfg = get_option('acme_trad_widget_cfg', array('title' => 'Notes', 'content' => ''));
    ?>
    <div>
        <h4><?php echo esc_html($cfg['title']); ?></h4>
        <div class="inside"><?php echo wp_kses_post($cfg['content']); ?></div>

        <?php if ( current_user_can('manage_custom_dashboard') ) : ?>
            <hr>
            <form method="post" action="<?php echo esc_url( admin_url('admin-post.php') ); ?>">
                <input type="hidden" name="action" value="acme_trad_save_widget">
                <?php wp_nonce_field('acme_trad_save_widget', 'acme_nonce'); ?>
                <p>
                    <label for="acme_trad_title">Tiêu đề</label>
                    <input id="acme_trad_title" name="title" class="widefat" type="text"
                           value="<?php echo esc_attr($cfg['title']); ?>">
                </p>
                <p>
                    <label for="acme_trad_content">Nội dung HTML</label>
                    <textarea id="acme_trad_content" name="content" rows="5" class="widefat"><?php
                        echo esc_textarea($cfg['content']);
                    ?></textarea>
                </p>
                <p><button type="submit" class="button button-primary">Lưu</button></p>
            </form>
        <?php endif; ?>
    </div>
    <?php
}

// Handler lưu qua admin-post với PRG
add_action('admin_post_acme_trad_save_widget', function () {
    check_admin_referer('acme_trad_save_widget', 'acme_nonce');

    if ( ! current_user_can('manage_custom_dashboard') ) {
        wp_die( esc_html__('Unauthorized', 'acme'), 403 );
    }

    $title   = isset($_POST['title'])   ? sanitize_text_field( wp_unslash($_POST['title']) )   : '';
    $content = isset($_POST['content']) ? wp_kses_post(        wp_unslash($_POST['content']) ) : '';

    update_option('acme_trad_widget_cfg', array(
        'title' => $title,
        'content' => $content,
    ), false);

    wp_safe_redirect( add_query_arg('acme_trad_updated', 1, admin_url('index.php')) );
    exit;
});

Điểm mấu chốt

  • Form action trỏ tới admin-post.php, có hidden input name="action" để map vào hook adminpost{action}.
  • checkadminreferer kiểm nonce trước khi chạm dữ liệu. currentusercan bảo đảm phân quyền. Lưu xong redirect về Dashboard theo mẫu PRG.

Ví dụ 2 – AJAX tối giản cập nhật ngay trên trang

  • Cùng một dạng dữ liệu, nhưng gửi bằng fetch đến admin-ajax.php để cập nhật tức thì. ajaxurl đã sẵn có trong trang admin.
<?php
// Đăng ký widget AJAX
add_action('wp_dashboard_setup', function () {
    if ( ! current_user_can('edit_posts') ) {
        return;
    }
    wp_add_dashboard_widget('acme_ajax_widget', 'Team notes - AJAX', 'acme_ajax_widget_render');
});

// Render widget có nút Lưu qua AJAX
function acme_ajax_widget_render() {
    $cfg   = get_option('acme_ajax_widget_cfg', array('title' => 'Notes', 'content' => ''));
    $nonce = wp_create_nonce('acme_ajax_save_widget');
    ?>
    <div id="acme-ajax-wrap" data-nonce="<?php echo esc_attr($nonce); ?>">
        <h4 id="acme-ajax-title"><?php echo esc_html($cfg['title']); ?></h4>
        <div id="acme-ajax-content" class="inside"><?php echo wp_kses_post($cfg['content']); ?></div>

        <?php if ( current_user_can('manage_custom_dashboard') ) : ?>
            <hr>
            <p><input id="acme_ajax_title_inp" class="widefat" type="text"
                      value="<?php echo esc_attr($cfg['title']); ?>"></p>
            <p><textarea id="acme_ajax_content_inp" class="widefat" rows="5"><?php
                echo esc_textarea($cfg['content']);
            ?></textarea></p>
            <p><button id="acme_ajax_save_btn" class="button button-primary">Lưu bằng AJAX</button></p>

            <script>
            (function(){
                const btn   = document.getElementById('acme_ajax_save_btn');
                const wrap  = document.getElementById('acme-ajax-wrap');
                const title = document.getElementById('acme_ajax_title_inp');
                const cont  = document.getElementById('acme_ajax_content_inp');
                const h4    = document.getElementById('acme-ajax-title');
                const box   = document.getElementById('acme-ajax-content');
                btn.addEventListener('click', function(e){
                    e.preventDefault();
                    const body = new URLSearchParams({
                        action: 'acme_ajax_save_widget',
                        acme_nonce: wrap.getAttribute('data-nonce'),
                        title: title.value,
                        content: cont.value
                    });
                    fetch(ajaxurl, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                        body
                    })
                    .then(r => r.json())
                    .then(res => {
                        if (!res || !res.success) throw new Error(res && res.data && res.data.message || 'Lỗi');
                        // Cập nhật ngay UI
                        h4.textContent = res.data.title;
                        box.innerHTML  = res.data.content;
                    })
                    .catch(err => {
                        window.alert('Không lưu được: ' + err.message);
                    });
                });
            })();
            </script>
        <?php endif; ?>
    </div>
    <?php
}

// Handler AJAX
add_action('wp_ajax_acme_ajax_save_widget', function () {
    check_ajax_referer('acme_ajax_save_widget', 'acme_nonce');

    if ( ! current_user_can('manage_custom_dashboard') ) {
        wp_send_json_error(array('message' => __('Unauthorized', 'acme')), 403);
    }

    $title   = isset($_POST['title'])   ? sanitize_text_field( wp_unslash($_POST['title']) )   : '';
    $content = isset($_POST['content']) ? wp_kses_post(        wp_unslash($_POST['content']) ) : '';

    update_option('acme_ajax_widget_cfg', array(
        'title' => $title,
        'content' => $content,
    ), false);

    // Trả về dữ liệu đã sanitize để gán vào DOM an toàn
    wp_send_json_success(array(
        'title'   => $title,
        'content' => $content,
    ));
});

Gợi ý áp dụng

  • Một widget có thể hỗ trợ cả hai kênh. Bạn vẫn dùng cùng một policy bảo mật: nonce hợp lệ trước, rồi capability, sau đó sanitize và lưu. Với nội dung HTML có thể thay đổi whitelist trong tương lai, cân nhắc lưu kép raw và sanitized để có thể re-sanitize khi cần, còn khi render luôn dùng bản đã kses.

Chính sách ẩn – hiện theo người dùng và toàn cục, hợp nhất với user meta

Screen Options cho Dashboard lưu ẩn hiện từng widget theo từng người dùng trong user meta metaboxhiddendashboard. Để có chính sách toàn cục dễ bảo trì, mình tách rõ ba lớp và hợp nhất theo thứ tự ưu tiên: forcedhide thắng hết, forcedshow thắng tùy chọn người dùng còn lại, rồi tới userhidden. Nhờ vậy bạn vừa tôn trọng lựa chọn cá nhân, vừa giữ được ràng buộc bắt buộc ở cấp plugin.

Mục tiêu thực thi

  • Đặt mặc định ẩn cho người chưa từng lưu Screen Options.
  • Hợp nhất theo chính sách mỗi khi đọc user meta để render Dashboard.
  • Nếu cần khóa, chặn việc lưu trái chính sách khi người dùng bấm Screen Options.
  • Tránh gây bối rối bằng cách ẩn hoặc disable checkbox tương ứng trong Screen Options bằng CSS hoặc JS nhẹ.

Mẫu triển khai đầy đủ, điền sẵn

  1. Khai báo chính sách và hợp nhất khi đọc
  • Lưu chính sách trong một option autoload=no, ví dụ acmedashpolicy với 2 mảng forcedhide và forcedshow. Áp dụng vào:
  • defaulthiddenmeta_boxes để đặt mặc định cho người chưa có meta.
  • getuseroptionmetaboxhiddendashboard để hợp nhất ở mọi lần đọc.
<?php
// Helpers
function acme_dash_get_policy(): array {
    $opt = get_option('acme_dash_policy', array());
    $forced_hide = isset($opt['forced_hide']) && is_array($opt['forced_hide']) ? $opt['forced_hide'] : array();
    $forced_show = isset($opt['forced_show']) && is_array($opt['forced_show']) ? $opt['forced_show'] : array();

    // Chuẩn hóa ID widget
    $forced_hide = array_values(array_map('sanitize_key', $forced_hide));
    $forced_show = array_values(array_map('sanitize_key', $forced_show));

    // Nếu có trùng, forced_hide ưu tiên - loại khỏi forced_show
    if ($forced_hide && $forced_show) {
        $forced_show = array_values(array_diff($forced_show, $forced_hide));
    }

    return array(
        'forced_hide' => $forced_hide,
        'forced_show' => $forced_show,
    );
}

// Mặc định ẩn cho user chưa có meta
add_filter('default_hidden_meta_boxes', function ($hidden, $screen) {
    if (! $screen || $screen->id !== 'dashboard') {
        return $hidden;
    }
    $hidden = is_array($hidden) ? $hidden : array();
    $p = acme_dash_get_policy();

    // Thêm forced_hide, loại bỏ forced_show
    $hidden = array_unique(array_merge($hidden, $p['forced_hide']));
    if ($p['forced_show']) {
        $hidden = array_diff($hidden, $p['forced_show']);
    }
    // Chuẩn hóa
    return array_values(array_map('sanitize_key', $hidden));
}, 10, 2);

// Hợp nhất theo chính sách khi đọc user meta đã lưu
add_filter('get_user_option_metaboxhidden_dashboard', function ($hidden, $option, $user) {
    $hidden = is_array($hidden) ? $hidden : array();
    $p = acme_dash_get_policy();

    // Ép ẩn thắng hết
    $hidden = array_unique(array_merge($hidden, $p['forced_hide']));
    // Ép hiện - loại khỏi danh sách ẩn
    if ($p['forced_show']) {
        $hidden = array_diff($hidden, $p['forced_show']);
    }

    return array_values(array_map('sanitize_key', $hidden));
}, 10, 3);

Gợi ý giá trị option mẫu để chạy ngay

<?php
// Đặt mặc định chính sách
add_action('admin_init', function () {
    if (get_option('acme_dash_policy', null) === null) {
        add_option('acme_dash_policy', array(
            'forced_hide' => array('dashboard_quick_press', 'dashboard_primary'),
            'forced_show' => array('dashboard_activity', 'my_custom_widget'),
        ), '', 'no');
    }
});
  1. Khóa cập nhật trái chính sách khi lưu
  • Khi người dùng tick Screen Options, WordPress ghi metaboxhiddendashboard vào user meta. Nếu cần khóa cứng, bạn có thể chặn và điều chỉnh giá trị ngay ở thời điểm ghi bằng filter updateusermetadata. Lưu ý quan trọng: không gọi updateuser_meta trong filter này để tránh đệ quy vô hạn. Hãy ghi trực tiếp bằng $wpdb và làm sạch cache user meta.
<?php
add_filter('update_user_metadata', function ($check, $user_id, $meta_key, $meta_value, $prev_value) {
    if ($meta_key !== 'metaboxhidden_dashboard') {
        return $check; // Không đụng meta khác
    }

    // Dữ liệu mới từ request có thể là mảng hoặc không tồn tại
    $val = is_array($meta_value) ? $meta_value : array();
    $val = array_values(array_map('sanitize_key', $val));

    // Áp chính sách
    $p = acme_dash_get_policy();
    // Bắt buộc ẩn
    $val = array_unique(array_merge($val, $p['forced_hide']));
    // Bắt buộc hiện
    if ($p['forced_show']) {
        $val = array_values(array_diff($val, $p['forced_show']));
    }

    // Ghi trực tiếp, tránh gọi update_user_meta để không tái kích hoạt filter này
    global $wpdb;
    $table = $wpdb->usermeta;
    $serialized = maybe_serialize($val);

    // Kiểm tra meta đã tồn tại chưa
    $existing = get_user_meta($user_id, $meta_key, false); // mảng các giá trị
    if ($existing) {
        // Cập nhật giá trị đầu tiên
        $wpdb->update(
            $table,
            array('meta_value' => $serialized),
            array('user_id' => $user_id, 'meta_key' => $meta_key),
            array('%s'),
            array('%d', '%s')
        );
    } else {
        // Thêm mới
        $wpdb->insert(
            $table,
            array(
                'user_id'    => $user_id,
                'meta_key'   => $meta_key,
                'meta_value' => $serialized,
            ),
            array('%d', '%s', '%s')
        );
    }

    // Xóa cache để giá trị mới có hiệu lực ngay trong request
    wp_cache_delete($user_id, 'user_meta');

    return true; // Short-circuit: coi như update đã xong
}, 10, 5);
  • Cách này khiến mọi thao tác trái chính sách không thể ghi xuống DB, đồng thời trải nghiệm người dùng vẫn thấy đúng ngay sau khi reload nhờ hợp nhất ở thời điểm đọc. Nếu bạn chỉ muốn nhẹ nhàng hơn, có thể bỏ khối filter updateusermetadata và chỉ giữ hai filter đọc ở trên. Kết quả hiển thị vẫn tuân chính sách, chỉ khác là user meta có thể chứa giá trị không khớp nhưng luôn bị override lúc đọc.
  1. Ẩn hoặc vô hiệu hóa checkbox trong Screen Options bằng CSS hoặc JS nhẹ
  • Để giảm bối rối, bạn có thể disable các checkbox bị khóa. Trên Dashboard, các checkbox có name="metaboxhiddendashboard[]" và value bằng ID widget. Đoạn dưới chèn vào adminhead-index.php, đọc chính sách và disable các checkbox tương ứng, đồng thời thêm nhãn chú thích.
<?php
add_action('admin_head-index.php', function () {
    $p = acme_dash_get_policy();
    $forced_hide = wp_json_encode($p['forced_hide']);
    $forced_show = wp_json_encode($p['forced_show']);
    ?>
    <style>
    /* Làm mờ label của checkbox bị khóa để gợi ý trực quan */
    .acme-locked label { opacity: 0.6; }
    </style>
    <script>
    (function(){
        const fh = <?php echo $forced_hide; ?> || [];
        const fs = <?php echo $forced_show; ?> || [];
        // Khóa cả 2 nhóm: forced_hide và forced_show
        const locked = Array.from(new Set([].concat(fh, fs)));
        function lockBoxes() {
            locked.forEach(function(id){
                const sel = 'input[name="metaboxhidden_dashboard[]"][value="' + id + '"]';
                document.querySelectorAll(sel).forEach(function(inp){
                    // Với forced_hide, checkbox phải luôn checked
                    if (fh.indexOf(id) !== -1) {
                        inp.checked = true;
                    }
                    // Với forced_show, checkbox phải luôn unchecked
                    if (fs.indexOf(id) !== -1) {
                        inp.checked = false;
                    }
                    inp.disabled = true;
                    const label = inp.closest('label') || inp.parentElement;
                    if (label && !label.classList.contains('acme-locked')) {
                        label.classList.add('acme-locked');
                        label.insertAdjacentHTML('beforeend', ' <em>(bị khóa bởi chính sách)</em>');
                    }
                });
            });
        }
        // Chạy ngay và sau khi người dùng mở Screen Options
        document.addEventListener('DOMContentLoaded', lockBoxes);
        document.addEventListener('click', function(e){
            if (e.target && e.target.id === 'show-settings-link') {
                setTimeout(lockBoxes, 0);
            }
        });
    })();
    </script>
    <?php
});

Lưu ý áp dụng thực tế

  • Nếu bạn thực sự muốn loại bỏ hoàn toàn một widget khỏi Dashboard và cả Screen Options, hãy dùng removemetabox ở wpdashboardsetup. forced_hide chỉ ẩn qua cơ chế user meta, checkbox vẫn tồn tại nếu không ẩn bằng CSS hoặc JS.
  • forcedshow không thể làm xuất hiện một widget đã bị gỡ khỏi hệ thống bằng removemetabox ở một nơi khác. Hãy thống nhất chính sách: hoặc quản bằng removemetabox, hoặc quản bằng metaboxhiddendashboard, đừng trộn mâu thuẫn.
  • Trên multisite, logic metaboxhidden_dashboard áp cho Site Dashboard. Network Dashboard và User Dashboard có screen id khác, bạn cần chính sách song song riêng nếu muốn áp ở đó.

Cách thường làm là chỉ hợp nhất ở thời điểm đọc vì đơn giản, ít xâm lấn và đủ để đảm bảo kết quả hiển thị đúng. Cách đúng khi bạn cần tính tuân thủ chặt là kết hợp thêm lớp khóa khi ghi như ở filter updateusermetadata và chặn thao tác UI để người dùng không nghĩ rằng tùy chọn của họ có hiệu lực. Với hai lớp này, Dashboard luôn hiển thị nhất quán và không có dữ liệu user meta lạc nhịp với chính sách.

Kiểm thử, hiệu năng và tương thích phiên bản

Mình đóng gói quy trình kiểm thử như một ma trận chạy trên staging để bạn bắt được lỗi sớm, giữ widget nhẹ và không phá Dashboard của các plugin khác. Mục tiêu là đo được thời gian render từng widget, xác nhận chính sách forced luôn áp dụng đúng, và đảm bảo tương thích WordPress 6.x trở lên.

Ma trận kiểm thử trên staging

  • Vai trò người dùng
  • Administrator: toàn quyền cấu hình, thấy đủ widget được phép.
  • Editor: chỉ xem widget có capability phù hợp, không thấy trang cấu hình nếu không đủ quyền.
  • Author hoặc Contributor: xác nhận widget đòi quyền cao hơn bị ẩn.
  • Tài khoản mới tinh: kiểm tra mặc định ẩn hiện và vị trí khi chưa có user meta.
  • Tình huống chức năng
  • Thêm – sửa – xóa widget tùy chỉnh: form có nonce, đổi title, đổi capability, đổi context – priority, payload data.
  • Ẩn – hiện widget mặc định qua cấu hình plugin và chính sách forced: forcedhide thắng, forcedshow loại khỏi danh sách ẩn.
  • Kéo thả sắp xếp: thay đổi vị trí, reload lại để xem user meta lưu đúng, forced còn hiệu lực.
  • Đăng ký – gỡ widget qua wpdashboardsetup: ưu tiên gọi removemetabox rất muộn để gỡ widget mặc định hoặc của plugin khác.
  • Tình huống lỗi và bảo mật
  • Submit form thiếu hoặc sai nonce: từ chối và log.
  • Người dùng không đủ capability truy cập trang cấu hình: bị chặn.
  • Cập nhật đồng thời: kiểm tra cơ chế rev khóa lạc quan phát hiện xung đột.
  • Tình huống hệ thống
  • Kích hoạt – vô hiệu hóa plugin phổ biến có widget Dashboard: WooCommerce, Yoast SEO, Jetpack, Wordfence. Kiểm tra không bị widget tái đăng ký chồng sau khi remove.
  • Bật persistent object cache như Redis để xem hành vi transient và xóa cache.

Bật log và công cụ theo dõi

  • Cấu hình trong wp-config.php trên staging để bắt cảnh báo và viết log vào file:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true);
define('WP_ENVIRONMENT_TYPE', 'staging');
  • Cài Query Monitor để xem:
  • Tổng thời gian trang admin, số lượng – thời gian truy vấn DB, truy vấn trùng lặp.
  • HTTP API calls phát sinh trong admin, mã trạng thái, thời lượng.
  • Hooks đang chạy và thứ tự gọi cơ bản.

Đo thời gian render từng widget để bắt điểm nghẽn

  • Bọc callback hiển thị bằng một lớp đo microtime và đếm truy vấn trước – sau. Log vào debug.log, chỉ kích hoạt khi WP_DEBUG bật.
<?php
// Đăng ký widget kèm wrapper đo thời gian
add_action('wp_dashboard_setup', function () {
    $id = 'acme_perf_demo';
    wp_add_dashboard_widget($id, 'ACME - Perf Demo', function () use ($id) {
        acme_render_with_timer($id, function () {
            global $wpdb;
            // Ví dụ nội dung có 1 truy vấn nhẹ
            $count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish'");
            echo '<p>Bài viết đã xuất bản: ' . esc_html($count) . '</p>';
        });
    });
}, 20);

// Wrapper đo thời gian và số truy vấn
function acme_render_with_timer(string $id, callable $real_cb): void {
    if (! defined('WP_DEBUG') || ! WP_DEBUG) {
        $real_cb();
        return;
    }
    global $wpdb;
    $start_time = microtime(true);
    $start_q    = isset($wpdb->num_queries) ? (int) $wpdb->num_queries : 0;

    $real_cb();

    $t      = round((microtime(true) - $start_time) * 1000, 2); // ms
    $q_diff = isset($wpdb->num_queries) ? ((int) $wpdb->num_queries - $start_q) : 0;
    error_log(sprintf('[ACME PERF] widget=%s time=%sms queries=%d', $id, $t, $q_diff));
    do_action('acme_dash_widget_rendered', $id, $t, $q_diff);
}
  • Ngưỡng gợi ý để soi kỹ hơn: thời gian render > 50 ms hoặc số truy vấn > 5 cho một widget đơn. Với widget đụng API ngoài, tổng thời gian không nên vượt 200 ms nếu đã có cache.

Không chạy truy vấn nặng hoặc API chậm trong callback – thêm cache ngắn hạn

  • Với dữ liệu đổi chậm, hãy bọc bằng transient và thoát nhanh khi có cache. Dùng timeout ngắn và hạ cấp tử tế khi API lỗi.
<?php
function acme_render_sales_widget() {
    $cfg = get_option('acme_dash_cfg', array());
    $rev = isset($cfg['rev']) ? (int) $cfg['rev'] : 0;

    $cache_key = 'acme_sales_summary_v' . $rev; // khóa có version để auto-bust khi cấu hình đổi
    $data = get_transient($cache_key);

    if ($data === false) {
        // Gọi API ngoài có timeout thấp, tránh chặn Dashboard
        $resp = wp_remote_get('https://api.example.com/sales?range=7d', array(
            'timeout' => 2,
            'redirection' => 2,
            'user-agent' => 'ACME-Dash/1.0; ' . home_url(),
        ));
        if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) !== 200) {
            // Hạ cấp: hiển thị dữ liệu cũ hơn nếu còn, hoặc thông báo nhẹ
            echo '<p>Không tải được dữ liệu mới. Thử lại sau vài phút.</p>';
            return;
        }
        $body = wp_remote_retrieve_body($resp);
        $json = json_decode($body, true);
        $data = array(
            'total' => isset($json['total']) ? (int) $json['total'] : 0,
            'updated_at' => gmdate('c'),
        );
        // Cache 5 phút
        set_transient($cache_key, $data, 5 * MINUTE_IN_SECONDS);
    }

    echo '<p>Doanh số 7 ngày: <strong>' . esc_html(number_format_i18n($data['total'])) . '</strong></p>';
    echo '<p><small>Cập nhật: ' . esc_html($data['updated_at']) . '</small></p>';
}
  • Lưu ý khi thay đổi cấu hình có ảnh hưởng nội dung, tăng rev trong option và dùng nó để tạo khóa transient. Cách này tự động bỏ qua cache cũ mà không phải xóa hàng loạt.
  • Khi bạn biết chính xác các khóa cache, hãy xóa khi lưu cấu hình:
<?php
function acme_after_save_settings(int $new_rev, array $known_ids) {
    foreach ($known_ids as $id) {
        delete_transient('acme_' . $id . '_v' . ($new_rev - 1));
    }
}
  • Với persistent object cache, transients nằm trong cache tầng ngoài. Không dùng wildcard xóa. Hoặc dùng rev-salt như trên, hoặc lưu danh mục khóa để xóa có chủ đích.

Checklist tương thích WordPress 6.x trở lên và plugin phổ biến

  • Phiên bản lõi
  • Kiểm tra trên 6.0, 6.4, 6.6. Xác nhận:
  • removemetabox ở wpdashboardsetup chạy ở priority rất muộn vẫn gỡ được widget mặc định và widget của plugin thông dụng.
  • Di chuyển widget qua $wpmetaboxes vẫn đúng khóa context – priority mà không chạm bucket sorted.
  • Screen Options lưu và đọc metaboxhidden_dashboard như kỳ vọng, filter hợp nhất chính sách hoạt động.
  • Plugin có widget Dashboard
  • WooCommerce, Yoast SEO, Jetpack, Wordfence: bật lần lượt để xem:
  • Có widget nào đăng ký rất muộn không. Nếu có, ưu tiên remove ở PHPINTMAX và thêm một lượt dọn ở adminhead-index.php bằng cách unset từ $wpmeta_boxes khi cần.
  • Gỡ hẳn nguồn đăng ký bằng remove_action nếu plugin đó tự thêm lại sau khi bị gỡ khỏi UI.
  • Môi trường cache
  • Bật Redis Object Cache hoặc Memcached: xác nhận transient hoạt động bình thường, đo lại thời gian render khi có cache và khi hết hạn.
  • Bật plugin page cache: thông thường admin không bị cache HTML, nhưng vẫn kiểm thử để chắc chắn không dính JS delayed làm chậm Screen Options.

Kịch bản hiệu năng cần soi và cách sửa

  • Callback chạy truy vấn không có LIMIT hoặc không dùng chỉ mục. Sửa bằng:
  • Tránh SELECT *. Chỉ lấy cột cần thiết.
  • Bắt buộc LIMIT cho các danh sách. Ví dụ activity gần đây chỉ lấy 5 – 10 bản ghi.
  • Đẩy tổng hợp nặng vào WP Cron để tính sẵn, widget chỉ đọc kết quả đã cache.
  • Gọi HTTP bên ngoài không đặt timeout và không có cache. Sửa bằng set timeout 1 – 3 giây và bọc transient 2 – 10 phút tùy mức tươi.
  • Lạm dụng get_option nhiều lần. Sửa bằng đọc option cấu hình một lần và truyền vào các lớp con hoặc dùng biến tĩnh trong request.

Kiểm soát và dọn cache khi thay đổi cấu hình

  • Luôn tăng rev khi lưu option cấu hình. Sinh khóa cache theo rev để auto-bust.
  • Nếu phải xóa cache cũ, lưu danh sách khóa đã phát sinh trong một option phụ autoload=no rồi lặp xóa có chủ đích. Tránh wpcacheflush vì ảnh hưởng toàn hệ thống.

Ghi chú báo cáo cho người dùng nội bộ

  • Tạo một trang trạng thái nhỏ trong trang cấu hình hiển thị:
  • Thời gian render trung bình gần đây cho từng widget nếu bạn gom log bằng hook acmedashwidget_rendered.
  • Số lần cache hit – miss nếu bạn đếm bằng counter trong transient hoặc user meta admin. Điều này giúp khách hàng hiểu widget đã tối ưu và lý do đôi khi số liệu chậm 2 – 5 phút.

Với quy trình trên, bạn có thể chỉ ra widget nào chậm, chứng minh bằng số liệu thời gian – số truy vấn, sửa đúng điểm bằng cache ngắn hạn và cài đặt timeout, đồng thời đảm bảo mọi thay đổi cấu hình không bị đè bởi cache cũ. Staging là nơi bắt xung đột với plugin phổ biến và kiểm tra lại toàn bộ đường đi hook của Dashboard trước khi lên production.

Ghi chú Multisite: Network, User, Site Dashboard và capability khác biệt

Trong môi trường Multisite, Dashboard có 3 khu vực tách biệt và mỗi khu vực có hook riêng để thêm hoặc gỡ widget. Nếu bạn dùng sai hook hoặc kiểm tra nhầm capability theo site ở Network Admin, widget sẽ không hiển thị hoặc gây lỗi quyền. Cách an toàn là đăng ký theo đúng hook của từng khu vực, gắn screen id chuẩn khi removemetabox, và kiểm tra capability tương ứng theo ngữ cảnh.

Bản đồ nhanh để bạn bám vào:

  • Site Dashboard
  • URL: /wp-admin/index.php
  • Hook: wpdashboardsetup
  • Screen id: dashboard
  • Capability: tùy nội dung widget theo site, ví dụ editposts, moderatecomments, manage_options
  • Network Admin Dashboard
  • URL: /wp-admin/network/index.php
  • Hook: wpnetworkdashboard_setup
  • Screen id: dashboard-network
  • Capability: cấp mạng như managenetwork hoặc cụ thể hơn managenetworkusers, managenetworkplugins, managenetwork_sites
  • User Admin Dashboard
  • URL: /wp-admin/user/index.php
  • Hook: wpuserdashboard_setup
  • Screen id: dashboard-user
  • Capability: tối thiểu read hoặc quyền phù hợp với nội dung hiển thị cho người dùng

Ví dụ đầy đủ: đăng ký 1 widget cho mỗi khu vực, kèm capability đúng và minh họa gỡ widget mặc định theo screen id tương ứng.

<?php
/**
 * Plugin Name: ACME Multisite Dash Notes
 * Description: Demo thêm - gỡ widget cho Site, Network, User Dashboard trong Multisite.
 * Version: 1.0.0
 */

// 1) Site Dashboard
add_action('wp_dashboard_setup', function () {
    if ( ! current_user_can('edit_posts') ) {
        return;
    }

    $id = 'acme_site_widget';
    wp_add_dashboard_widget($id, 'ACME - Site Widget', function () {
        echo '<p>Nội dung dành cho Site Dashboard.</p>';
    });

    // Gỡ một widget mặc định ở Site Dashboard nếu muốn
    // Chạy muộn để chắc đã đăng ký xong
}, 20);

add_action('wp_dashboard_setup', function () {
    // Gỡ ở priority rất muộn để thắng phần lớn plugin khác
    remove_meta_box('dashboard_primary', 'dashboard', 'side');
    // Khi không chắc context
    remove_meta_box('dashboard_primary', 'dashboard', 'normal');
}, PHP_INT_MAX);

// 2) Network Admin Dashboard
add_action('wp_network_dashboard_setup', function () {
    // Không dùng manage_options ở đây vì đó là quyền theo site
    if ( ! current_user_can('manage_network') ) {
        return;
    }

    $id = 'acme_network_overview';
    wp_add_dashboard_widget($id, 'ACME - Network Overview', function () {
        echo '<p>Thống kê cấp mạng. Chỉ hiện với Super Admin.</p>';
    });

    // Di chuyển widget sang cột side - low trên Network Admin
    global $wp_meta_boxes;
    $page = 'dashboard-network';
    $src_context = 'normal';
    $dst_context = 'side';
    $dst_priority = 'low';
    $candidates = array('core', 'default', 'high', 'low');

    $box = null;
    foreach ($candidates as $p) {
        if (isset($wp_meta_boxes[$page][$src_context][$p][$id])) {
            $box = $wp_meta_boxes[$page][$src_context][$p][$id];
            unset($wp_meta_boxes[$page][$src_context][$p][$id]);
            break;
        }
    }
    if ($box) {
        $wp_meta_boxes[$page][$dst_context] = $wp_meta_boxes[$page][$dst_context] ?? array();
        $wp_meta_boxes[$page][$dst_context][$dst_priority] = $wp_meta_boxes[$page][$dst_context][$dst_priority] ?? array();
        $box['context'] = $dst_context;
        $box['priority'] = $dst_priority;
        $wp_meta_boxes[$page][$dst_context][$dst_priority][$id] = $box;
    }
}, 20);

add_action('wp_network_dashboard_setup', function () {
    // Ví dụ gỡ một widget nếu biết ID. Gỡ sai ID thì vô hại.
    remove_meta_box('dashboard_primary', 'dashboard-network', 'side');
    remove_meta_box('dashboard_primary', 'dashboard-network', 'normal');
}, PHP_INT_MAX);

// 3) User Admin Dashboard
add_action('wp_user_dashboard_setup', function () {
    // Cho mọi user đăng nhập đều thấy
    if ( ! current_user_can('read') ) {
        return;
    }

    $id = 'acme_user_notes';
    wp_add_dashboard_widget($id, 'ACME - User Notes', function () {
        echo '<p>Gợi ý nhanh dành cho tài khoản người dùng.</p>';
    });

    // Nếu cần gỡ widget nào đó trên User Dashboard theo ID đã biết
}, 20);

add_action('wp_user_dashboard_setup', function () {
    // Gọi cho cả normal và side khi không chắc context
    remove_meta_box('dashboard_primary', 'dashboard-user', 'side');
    remove_meta_box('dashboard_primary', 'dashboard-user', 'normal');
}, PHP_INT_MAX);

Mẹo phân nhánh và phạm vi nạp plugin:

  • Plugin kích hoạt ở mức mạng sẽ được nạp trong cả Network Admin, User Admin và tất cả Site Dashboard. Vì vậy bạn càng nên bám đúng 3 hook chuyên biệt để tránh chạy sai ngữ cảnh.
  • Plugin chỉ kích hoạt theo site thì không chạy trong Network Admin. Nếu bạn cần widget ở Network Admin, hãy network-activate hoặc chuyển thành MU plugin.
  • Dù có thể dùng isnetworkadmin() hoặc isuseradmin() để rẽ nhánh, cách gọn gàng và an toàn hơn là gắn callback vào đúng wpnetworkdashboardsetup, wpuserdashboardsetup và wpdashboardsetup. Khi cần gỡ widget, luôn truyền screen id chuẩn: dashboard cho Site, dashboard-network cho Network, dashboard-user cho User.

Lưu ý về capability theo ngữ cảnh:

  • Network Admin: dùng managenetwork hoặc các capability network cụ thể theo chức năng widget. Tránh manageoptions vì đây là quyền của site con và thường không đại diện đúng cho Super Admin ở màn hình mạng.
  • User Admin: dùng read để bao phủ mọi tài khoản, hoặc cap cao hơn nếu nội dung nhạy cảm. Không nên dựa vào manage_options tại đây.
  • Site Dashboard: dùng cap theo nghiệp vụ thực sự của widget, ví dụ editposts cho nội dung biên tập, moderatecomments cho bình luận, manage_options cho cấu hình cấp cao.

Cuối cùng, nếu bạn thao tác trực tiếp $wpmetaboxes để di chuyển widget, hãy nhớ key cấp 1 là screen id tương ứng: dashboard, dashboard-network, dashboard-user. Khi gỡ bằng removemetabox, gọi cho cả normal và side để chắc chắn loại khỏi UI trong mọi trường hợp. Cách tổ chức này giúp plugin của bạn sạch ngữ cảnh, đúng quyền và không va chạm giữa 3 Dashboard khác nhau của Multisite.

Lỗi thường gặp và cách khắc phục an toàn

Khi tùy biến Dashboard, một lỗi nhỏ ở hook, quyền hay lưu trữ cũng có thể gây hỏng giao diện hoặc lộ dữ liệu. Mình tổng hợp các bẫy phổ biến và cách xử lý an toàn để bạn áp dụng ngay, giữ code sạch và dễ bảo trì.

  • removemetabox chạy quá sớm nên không gỡ được widget của plugin khác
  • Nguyên nhân: đa số widget được đăng ký ở wpdashboardsetup priority 10. Bạn gỡ ở priority mặc định nên chạy trước, không thấy ID để gỡ.
  • Cách khắc phục:
  • Gỡ thật muộn trong wpdashboardsetup, ưu tiên PHPINTMAX. Khi không chắc context, gọi cho cả normal và side.
  • Nếu bên thứ ba cố tình đăng ký muộn hoặc tái đăng ký, chặn từ gốc bằng remove_action chạy thật sớm.
  • Tình huống bất trị, dọn lần cuối ở adminhead-index.php bằng cách unset trong $wpmeta_boxes trước khi render.
// Gỡ muộn và chắc
add_action('wp_dashboard_setup', function () {
    remove_meta_box('dashboard_primary', 'dashboard', 'side');
    remove_meta_box('vendor_widget_id', 'dashboard', 'normal');
    remove_meta_box('vendor_widget_id', 'dashboard', 'side');
}, PHP_INT_MAX);

// Chặn nguồn đăng ký của bên thứ ba
add_action('wp_dashboard_setup', function () {
    remove_action('wp_dashboard_setup', 'vendor_register_widget', 10);
}, 1);

// Phương án cuối cùng ngay trước khi render
add_action('admin_head-index.php', function () {
    global $wp_meta_boxes;
    unset($wp_meta_boxes['dashboard']['side']['core']['vendor_widget_id']);
});
  • Quên kiểm tra capability và nonce ở form cấu hình widget
  • Hệ quả: lộ dữ liệu, ai có link cũng có thể gửi POST hoặc AJAX.
  • Cách khắc phục: kiểm tra quyền sớm, rồi kiểm tra nonce, sau đó mới xử lý dữ liệu. Sanitize khi lưu, escape khi hiển thị.
add_action('admin_post_acme_save_widget', function () {
    if (! current_user_can('manage_options')) {
        wp_die('Unauthorized', 403);
    }
    check_admin_referer('acme_save_widget', 'acme_nonce');

    $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
    $html  = isset($_POST['html']) ? wp_kses_post(wp_unslash($_POST['html'])) : '';

    update_option('acme_dash_widget', ['title' => $title, 'html' => $html], false);
    wp_safe_redirect(admin_url('index.php?acme_saved=1'));
    exit;
});
  • Lưu option với autoload=yes gây bloat bộ nhớ
  • Nguyên nhân: cấu hình Dashboard chỉ cần khi vào admin, không nên được nạp ở mọi request frontend.
  • Cách khắc phục: dùng một option duy nhất với autoload=no. Nếu lỡ tồn tại với autoload=yes, đổi bằng delete + add trong activation.
register_activation_hook(__FILE__, function () {
    $val = get_option('acme_dash_cfg', null);
    if ($val !== null) {
        delete_option('acme_dash_cfg');
        add_option('acme_dash_cfg', $val, '', 'no');
    } else {
        add_option('acme_dash_cfg', ['schema_version' => 1, 'rev' => 1, 'widgets' => []], '', 'no');
    }
});
  • Lưu thẳng tên hàm callback trong DB rồi calluserfunc
  • Hệ quả: mở đường thực thi tùy ý nếu dữ liệu bị chèn. Ngay cả khi chỉ admin sửa cũng rủi ro dài hạn.
  • Cách khắc phục: chỉ lưu type hoặc slug, ánh xạ sang whitelist callback trong code. Không bao giờ gọi tên hàm lấy từ DB.
$type_map = [
    'note'  => 'acme_widget_note_render',
    'stats' => 'acme_widget_stats_render',
];
$type = $cfg['widgets'][$id]['type'] ?? 'note';
$cb   = $type_map[$type] ?? 'acme_widget_note_render';
wp_add_dashboard_widget($id, $title, $cb);
  • Ghi vào bucket sorted của $wpmetaboxes làm hỏng thứ tự người dùng
  • Nguyên nhân: sorted là vùng do user meta điều khiển. Viết thẳng vào đó phá order đã lưu.
  • Cách khắc phục: di chuyển box vào bucket context priority như side low hoặc normal high. Không chạm sorted.
global $wp_meta_boxes;
$box = $wp_meta_boxes['dashboard']['normal']['core'][$id] ?? null;
if ($box) {
    unset($wp_meta_boxes['dashboard']['normal']['core'][$id]);
    $box['context']  = 'side';
    $box['priority'] = 'low';
    $wp_meta_boxes['dashboard']['side']['low'][$id] = $box;
}
  • Ép vị trí widget bất chấp user meta
  • Hệ quả: người dùng kéo thả nhưng luôn bị bạn ép lại, trải nghiệm tệ.
  • Cách khắc phục: đặt default bằng side low như trên, tôn trọng order người dùng. Chỉ khi thật sự cần cưỡng chế, lọc user meta khi đọc để loại ID khỏi mọi cột, buộc rơi về default mong muốn.
add_filter('get_user_option_meta-box-order_dashboard', function ($order) {
    $id = 'acme_widget';
    $order = is_array($order) ? $order : [];
    foreach (['normal', 'side', 'column3', 'column4'] as $col) {
        if (!empty($order[$col])) {
            $list = array_filter(explode(',', $order[$col]), fn($x) => $x !== $id);
            $order[$col] = implode(',', $list);
        }
    }
    return $order;
});
  • Dùng manage_options ở Network Admin khiến kiểm tra quyền luôn fail
  • Nguyên nhân: manage_options là quyền theo site, không đại diện cho Super Admin ở màn hình mạng.
  • Cách khắc phục: tại Network Admin dùng capability cấp mạng như managenetwork, và gắn vào hook wpnetworkdashboardsetup.
add_action('wp_network_dashboard_setup', function () {
    if (! current_user_can('manage_network')) {
        return;
    }
    wp_add_dashboard_widget('acme_network', 'ACME Network', 'acme_network_cb');
});
  • Ghi đè cấu hình cùng lúc làm mất thay đổi của nhau
  • Nguyên nhân: nhiều admin cùng sửa, update_option ghi đè không kiểm tra.
  • Cách khắc phục: dùng khóa lạc quan bằng trường rev trong option. So sánh rev từ form với rev trong DB trước khi ghi, nếu lệch thì báo xung đột và yêu cầu tải lại rồi hợp nhất thay đổi.

Với các biện pháp trên, bạn vừa gỡ trúng, vừa đăng ký đúng, vừa bảo vệ dữ liệu và trải nghiệm người dùng. Chốt lại: đặt priority hợp lý, kiểm tra quyền và nonce ở mọi lối vào, lưu một option autoload=no có rev, không bao giờ gọi callback từ dữ liệu người dùng, và tuyệt đối tôn trọng lựa chọn đã lưu của người dùng trừ khi chính sách của bạn đòi hỏi cưỡng chế.

Kết luận

Sau khi áp dụng, bạn sẽ có một plugin gọn nhẹ cho phép thêm, sửa, xóa, sắp xếp và ẩn hiện Dashboard Widgets có kiểm soát, lưu cấu hình an toàn bằng Options API và vẫn tôn trọng Screen Options của từng người dùng. Lưu ý quan trọng: đừng lưu tên hàm callback trong DB, hãy ánh xạ từ type đã được whitelist và luôn kiểm tra nonce cùng capability trước khi ghi dữ liệu.

Tạo nhanh sale page tĩnh cho sức khỏe và làm đẹp

Mình trình bày lộ trình kỹ thuật và checklist để bạn thiết kế một ứng dụng PC local-first giúp marketer chọn template (8 mẫu minh họa: 4 sức khỏe, 4 làm đẹp), nhập nội dung, preview responsive và xuất HTML/CSS tĩnh. Có đề xuất kiến trúc Electron/Tauri, pipeline media, validation và export.

Mục tiêu sản phẩm và luồng người dùng

Mục tiêu MVP: một ứng dụng PC để người dùng nội bộ hoặc marketer nhanh tạo sale page responsive từ nội dung có sẵn, chọn từ thư viện template chủ đề sức khỏe và làm đẹp, tùy chỉnh nhanh (hình, văn bản, màu, font), preview chính xác trên nhiều kích thước, xác thực các ràng buộc bắt buộc, và xuất ra bộ file HTML/CSS/asset tĩnh sẵn deploy. Ứng dụng lưu dự án local theo định dạng JSON + folder assets, có export ZIP cho deploy. Actors chính: developer chịu kỹ thuật và tích hợp, product manager (PM) điều phối yêu cầu và kiểm tra QA, marketer nhập nội dung và xuất bản.

Luồng người dùng chính (high-level)

  1. Tạo project mới
  2. Chọn template theo chủ đề
  3. Nhập và chỉnh nội dung (form + inline + markdown)
  4. Preview responsive (desktop/tablet/mobile)
  5. Validate (schema, media, compliance heuristics)
  6. Export tĩnh (folder + ZIP) hoặc save draft local

Luồng hành vi chi tiết để dev/PM dùng làm checklist

  1. Create project
  • UI: màn hình New Project with fields: project name, slug (auto gợi ý), target country, export mode (light/self-contained).
  • Local state: create folder structure in workspace/projects/<slug>/ with project.json, assets/.
  • Checklist dev:
  • create atomic save API (saveProject, loadProject) with versioned project.json (field projectVersion).
  • ensure file locking / concurrency safe for autosave.
  • surface immediate validation errors if slug invalid.
  1. Choose template
  • UI: Template Library filtered by category (Sức khỏe, Làm đẹp), search, preview thumbnails, tags.
  • Action: user selects template -> app clones template package into project/templates/active/ and loads template manifest.
  • Checklist dev/PM:
  • manifest validation: template must include template.json with requiredFields and themeVariables.
  • import logic must copy partials, css, preview assets.
  • if template requires compliance review, mark project.settings.requiresCompliance = true.
  1. Nhập nội dung
  • Modes available:
  • Structured form: right-side panel lists fields defined in template manifest. Types: text, richtext (markdown), image, list, boolean, url.
  • Inline editing: click on canvas to edit text; drag-drop images into placeholders.
  • Bulk import: upload JSON/CSV to map fields (mapping UI with preview).
  • UX rules:
  • every edit updates in-memory project.model and triggers debounce autosave to disk.
  • image drops copy file to assets/ and create placeholder derivatives for preview.
  • Checklist dev:
  • implement two-way binding between form fields and template context paths.
  • sanitize and normalize pasted HTML; store rich text as sanitized HTML or markdown.
  • on image import, capture intrinsic width/height and store metadata in project.json.
  • provide undo/redo stack per project session.
  1. Preview
  • UI: central canvas showing rendered template with toolbar to switch breakpoints: Desktop/Tablet/Mobile and toggle show/hide overlays (grid, spacing, alt-text warnings).
  • Behavior:
  • render pipeline uses template engine to compile with current project.model and themeVariables.
  • preview must reflect exactly what export will produce for HTML structure and CSS variables.
  • Checklist dev:
  • isolated render sandbox separate from editor to avoid style bleed.
  • implement viewport simulation and device emulation for densities.
  • include visual indicators for missing required fields inline.
  • ensure preview uses same image srcset logic as export.
  1. Validate (preflight)
  • Types of validation:
  • Schema validation: project.model against template manifest/JSON Schema; list missing required fields.
  • Media validation: check image minimum dimensions/aspect ratios per manifest, file size warnings.
  • Content heuristics: keyword scanner for risky health claims, presence of mandatory disclaimer for health templates.
  • Export readiness: all relative asset refs resolvable.
  • UI: preflight panel with two sections: blocking issues and warnings. Blocking issues must be fixed before final export unless user forces export with explicit acknowledgment.
  • Checklist dev/PM:
  • implement ajv or similar JSON Schema validator and map errors to UI paths.
  • define clear severities: BLOCKER, WARNING, INFO.
  • produce actionable messages: field path, expected type, remediation suggestion.
  • log preflight runs for audit (timestamp, user id, issues).
  1. Export
  • Options: Export Light (references external CDN fonts/images), Export Self-contained (includes all assets, fonts, webp variants).
  • Steps executed by app:
  • run final render to produce index.html,
  • generate srcset/webp variants from stored assets,
  • inject meta tags and theme CSS variables,
  • minify HTML/CSS if selected,
  • package folder and create ZIP.
  • Post-export: show summary (files included, size, warnings), and provide open-folder button.
  • Checklist dev:
  • ensure consistent pathing (relative paths) so exported site runs when hosted statically.
  • include meta comment with templateId and templateVersion for traceability.
  • fail export if blocking preflight issues remain unless force override with explicit UX.

Project states and local persistence (state machine for dev/PM)

  • New → Draft: initial created project, autosave enabled.
  • Editing: user makes changes; dirty flag true.
  • Validated: ran preflight with zero blockers; can export.
  • Warning: preflight yields warnings but no blockers.
  • Blocked: preflight yields blockers; export disabled unless force override.
  • Exported: final ZIP produced; project.savedExportPoints[] appended with metadata.
  • Archived: user marks project archived; app may compress or read-only it.

Checklist to hand to dev/PM as behavior flow diagram (sequence checkpoints)

  1. On New Project create files and initial manifest snapshot.
  2. On Template Select validate template manifest; copy template package.
  3. On Content Change update model, autosave, run lightweight validation (required presence, image metadata).
  4. On Preview request ensure render uses same pipeline as Export.
  5. On Validate run full preflight; categorize issues and block/present remediation.
  6. On Export check export options, run asset pipeline (generate srcset), render final HTML, minify, package zip, write export log.
  7. On Save/Close ensure state persisted and user warned if unsaved changes.

Dùng checklist trên, developer và PM sẽ có một sơ đồ hành vi chi tiết để kiểm tra mỗi bước implement, các ràng buộc validation cần xử lý, và trạng thái dự án lưu local để đảm bảo trải nghiệm marketer ổn định và tránh lỗi export.

Yêu cầu chức năng bắt buộc (MVP)

Mục tiêu chức năng của MVP là cung cấp một ứng dụng PC cho phép marketer hoặc nhân viên content nhanh dựng và xuất sale page tĩnh từ nội dung có sẵn, với trải nghiệm chọn template theo chủ đề, tùy chỉnh nhanh, preview responsive và xuất HTML/CSS/asset sạch. Dưới đây là danh sách tính năng bắt buộc, kèm các ràng buộc vận hành và validation bắt buộc trước khi cho phép xuất.

  1. Nhập nội dung có sẵn (input)
  • Hỗ trợ import:
  • JSON: import file project.json theo schema template.
  • CSV: map cột sang fields (mapping UI).
  • Markdown: import block markdown cho vùng rich text.
  • Paste: dán văn bản hoặc HTML; app sẽ sanitize và chuyển về markdown/HTML an toàn.
  • Lưu ý UX: cho phép preview mapping trước khi áp data vào template.
  1. Chọn template theo chủ đề
  • Thư viện template có phân nhóm chủ đề (Sức khỏe, Làm đẹp).
  • Template package phải chứa manifest (template.json) với các trường tối thiểu: templateId, version, requiredFields, themeVariables, mediaRequirements.
  • Chức năng preview template trước khi dùng; khi chọn, app clone template vào dự án.
  1. Tùy chỉnh nhanh (editing)
  • Thay hình: drag-and-drop vào vùng ảnh, upload từ file system; ảnh được lưu vào thư mục project assets.
  • Thay văn bản: hai chế độ
  • Form fields có cấu trúc (title, subtitle, list items).
  • Inline editing trên preview cho các vùng ngắn.
  • Màu và font: palette theme với tối thiểu 6 biến màu và 2 font (heading, body); thay đổi cập nhật trực tiếp preview bằng CSS variables.
  • Undo/redo và autosave local.
  1. Preview responsive
  • Preview 3 kích thước tối thiểu: desktop, tablet, mobile.
  • Preview phản ánh đúng HTML/CSS xuất ra (đừng chỉ là mock). Preview dùng cùng pipeline render như export.
  • Hiển thị cảnh báo visual nếu một trường tràn (long text) hoặc ảnh không đúng aspect ratio.
  1. Lưu project local
  • Project lưu dưới dạng folder: project.json (versioned), /assets/, /templates/active/.
  • Autosave định kỳ và save as exportable .zip project.
  • Cho phép open recent projects and import project.zip.
  1. Export HTML/CSS tĩnh
  • Tạo bộ file index.html + css/ + assets/ + optional js/ theo cấu trúc relative paths.
  • Hai chế độ export:
  • Light: fonts/images tham chiếu CDN bên ngoài (nhỏ gọn).
  • Self-contained: tất cả assets và font được đóng gói.
  • Export phải đảm bảo:
  • index.html chứa meta viewport, charset.
  • images xuất kèm srcset + sizes và width/height attributes.
  • css scope bằng root class để tránh override global.
  • Gói kết quả thành ZIP để người dùng download hoặc lưu.
  1. Form lead capture
  • Trình tạo form đơn giản kèm các field cơ bản và honeypot chống spam.
  • Submit options cấu hình: webhook URL, Zapier webhook, Formspree/Getform endpoints. App xuất snippet form action phù hợp.
  • Form handler không mặc định gửi data ra internet; mọi tích hợp network phải do user cấu hình rõ ràng.
  1. Manifest template và quản lý template
  • Mỗi template phải có manifest hợp lệ.
  • Chức năng import/export template dưới dạng zip package.
  • Hỗ trợ versioning template (templateId + version) để trace khi export.
  1. Validation pre-export (preflight)
  • Bắt buộc chạy preflight trước export; preflight phân loại vấn đề: BLOCKER, WARNING, INFO.
  • Kiểm tra tối thiểu:
  • Schema validation (JSON Schema / ajv) với requiredFields.
  • Media validation: file tồn tại, kích thước tối thiểu, aspect ratio phù hợp.
  • HTML sanitization cho rich text.
  • Kiểm tra form action hợp lệ (valid URL) nếu form tồn tại.
  • Kiểm tra rằng index.html không phụ thuộc runtime backend để hiển thị nội dung chính.
  • BLOCKER phải được sửa trước export trừ khi user explicit override qua confirm.
  1. Mapping import/export dữ liệu và báo lỗi rõ ràng
  • Khi import JSON/CSV, app hiển thị bản đồ mapping (CSV column → template field).
  • Khi có mismatch, app liệt kê các field missing và gợi ý sửa hoặc map thủ công.
  1. Asset/media handling cơ bản
  • Khi upload ảnh, app lưu bản gốc và tạo preview nhanh; app auto-generate metadata: intrinsic width/height.
  • Quy tắc mặc định cho prototype: chặn upload file > 10 MB; cảnh báo nếu ảnh nhỏ hơn threshold cho hero (ví dụ width < 800 px).
  • App phải lưu metadata srcset widths chuẩn: 480, 768, 1200, 1920.
  1. Các ràng buộc API nội bộ và hạn chế cần tuân thủ
  • Tên IPC / internal API calls phải định danh rõ và không blocking UI: ví dụ ipc.render.invoke(“project:save”), ipc.render.invoke(“export:run”).
  • Mọi call nặng (image batch optimize, zip create) phải chạy off-main thread hoặc process riêng, trả progress qua events.
  • Không tự động upload assets hoặc dữ liệu người dùng ra mạng mà không có opt-in rõ ràng; network calls phải qua API được bật bởi user.
  • Hạn mức hiệu năng prototype:
  • Single export job should not block UI; cung cấp progress + cancel.
  • Giới hạn mặc định file assets per project để tránh OOM trên máy yếu; ví dụ warn khi assets total > 200 MB.
  • Không vượt quyền filesystem ngoài workspace project dir trừ khi user explicit chọn folder khi export.
  1. Các validation UX và báo cáo lỗi
  • Mọi lỗi validation phải hiển thị rõ ràng: field path, loại lỗi, gợi ý khắc phục.
  • Lưu log export (timestamp, templateId, templateVersion, preflight result) trong project để audit.

Phần này là những yêu cầu tối thiểu bắt buộc để prototype vận hành an toàn và hữu dụng. Các điều chỉnh sâu hơn (ví dụ chuyển đổi ảnh chất lượng cao offline, tích hợp CMS/CRM tự động) là các feature mở rộng và phải tôn trọng các ràng buộc API nội bộ nêu trên.

Đề xuất kiến trúc kỹ thuật tổng quan

Mục tiêu kiến trúc: một ứng dụng PC cross-platform cho phép marketer chọn template, nhập nội dung, preview responsive và xuất HTML/CSS tĩnh. Dưới đây là so sánh hai lựa chọn ưu tiên Electron và Tauri theo các tầng chính, quy ước IPC/command, và đề xuất mặc định cho prototype.

  1. Tầng UI renderer
  • Electron: renderer là SPA (React hoặc Vue 3) chạy trong Chromium embedded. Lợi: dev speed rất cao, đầy đủ toolchain (Vite/webpack), dễ tích hợp WYSIWYG editors (TipTap), state libs (Zustand/Pinia). Hạn chế: tiêu thụ RAM, package lớn.
  • Tauri: frontend cũng là SPA tương tự; chạy trên WebView hệ thống. Lợi: binary nhỏ hơn, startup nhanh hơn. Hạn chế: cần cấu hình thêm để gọi API native, đôi khi debug phức tạp hơn.
  1. Template engine layer
  • Đề xuất: Handlebars (Node) cho prototype. Rationale: syntax đơn giản, partials/helpers rõ ràng, dễ compile server-side/local.
  • Triển khai:
  • Electron: render template bằng Handlebars trong main process hoặc dedicated worker process để tránh blocking UI.
  • Tauri: render template có thể thực hiện trong frontend JS (client-side compile) hoặc bằng command Rust gọi runtime Node script; khuyến nghị render bằng thư viện JS trên client để giữ pipeline đơn giản.
  1. Media processing
  • Yêu cầu: preview nhanh, export chất lượng, tạo srcset/webp.
  • Kiến trúc hybrid:
  • Immediate client-side preprocessing để show preview: browser canvas / Pica / browser-image-compression. Tạo preview và minimal srcset metadata.
  • Background native optimization cho export: sharp (libvips) chạy trong native context.
  • Electron: dễ dàng tích hợp sharp trực tiếp (Node native module) trong main process hoặc worker. Tiện cho batch optimize và tạo AVIF/WebP.
  • Tauri: không có sharp JS; để đạt chất lượng tương đương cần viết command Rust dùng libvips bindings hoặc spawn và bundle binary (sharp CLI or cwebp). Nếu team không có Rust skill, Tauri làm chậm dev.
  1. File IO và project storage
  • Cần read/write project JSON + assets folder + zip export.
  • Electron: dùng fs Node trực tiếp trong main process; renderer giao tiếp qua contextBridge IPC để tránh NodeIntegration bật trực tiếp.
  • Tauri: dùng tauri::api::fs hoặc viết command Rust; cần cấu hình allowlist cho file APIs. Lợi ích: security-by-default; nhược điểm: phải thêm code Rust nếu file ops phức tạp.
  1. Export pipeline
  • Luồng đề xuất cho prototype:
  1. UI builds project JSON and assets references.
  2. Validate schema (ajv) in renderer.
  3. Send “export:prepare” IPC to native layer with data + assets metadata.
  4. Native layer runs image optimization tasks if requested, compiles Handlebars templates to index.html, injects CSS variables, generates srcset and sizes attributes, minifies assets (cssnano/terser/html-minifier), writes files to export folder.
  5. Native layer zips folder and returns path via IPC.
  • IPC naming conventions: “project:save”, “project:load”, “export:prepare”, “media:optimize”, “export:zip” for clarity.
  1. IPC / command boundaries
  • Nguyên tắc: keep heavy, filesystem, and native-processing out of renderer.
  • Renderer responsibilities: UI, preview rendering (client-side compile for speed), input validation, minor image preview transforms.
  • Native responsibilities: durable storage, batch image transforms, final template render for export (ensures deterministic output), minify/zip and long tasks with progress events.
  • Progress reporting: native emits events “export:progress” and “media:progress” so UI shows progress and allows cancel.
  1. Trade-offs chính
  • Tốc độ phát triển
  • Electron win nếu team JS/TS muốn prototype nhanh, tận dụng sharp, npm libs.
  • Tauri win nếu team chấp nhận Rust và ưu tiên gói nhẹ.
  • Kích thước binary
  • Electron lớn hơn (Chromium bundled). Nếu gói nhẹ quan trọng sớm, Tauri có lợi.
  • Xử lý ảnh native
  • Electron thuận lợi: sharp sẵn, ít rào cản kỹ thuật.
  • Tauri khả thi nhưng cần kỹ năng Rust hoặc bundling của native binaries, làm tăng dev time.
  1. Đề xuất mặc định cho prototype
  • Chọn Electron nếu bạn cần prototype nhanh, muốn tích hợp sharp để đảm bảo chất lượng ảnh export và có dev JS sẵn. Kiến trúc: React + Vite renderer, Handlebars rendering trong main worker, sharp cho media, fs trong main process, IPC qua contextBridge.
  • Nếu bạn bắt buộc phải có binary nhỏ từ đầu và có Rust resource, chọn Tauri và thiết kế render pipeline tương tự nhưng chuẩn bị viết command Rust cho image pipeline hoặc plan dùng cloud optimization khi export.
  1. Một vài quy ước kỹ thuật cần tuân thủ
  • Template package structure: template.json (manifest), layout.hbs, partials/, assets/, styles.css.
  • Template manifest phải khai báo requiredPaths, mediaConstraints (minWidth, aspectRatio), themeVariables.
  • Export must include width/height attributes, srcset, sizes, and aspect-ratio CSS on containers to avoid CLS.
  • Long-running native jobs must be cancellable and return progress via IPC.

Kết luận kiến trúc: cho prototype nội bộ với yêu cầu xử lý ảnh native và tốc độ dev cao, Electron là lựa chọn pragmatic. Nếu mục tiêu là build sản phẩm nhẹ, an toàn hơn cho release, chuyển sang Tauri sau khi prototype ổn định là đường đi hợp lý.

Stack frontend và công nghệ UI đề xuất

Mục tiêu phần này là chỉ rõ một stack frontend thực tế để bạn nhanh triển khai prototype editor/preview cho sale page, đồng thời tổ chức mã sao cho dễ port sang Tauri sau này. Mình nêu lựa chọn ưu tiên, thư viện WYSIWYG/Markdown, CSS strategy, testing visual và pattern tổ chức renderer.

Lựa chọn framework và tooling cơ bản

  • React + Vite (ưu tiên cho prototype): nhanh, hệ sinh thái phong phú, dễ tích hợp tiptap và react-markdown. Vite cho HMR rất nhanh khi dev UI. Nếu đội bạn thích Vue thì Vue 3 + Vite là thay thế ngang năng lực.
  • Language: TypeScript để có schema-safe state và props, giúp mapping JSON → template an toàn hơn.

State management và data flow

  • Dùng state nhẹ: Zustand hoặc useReducer + Context. Tránh Redux nặng. Mô hình đơn giản:
  • projectStore: metadata + content JSON + assets list
  • uiStore: preview viewport, selected field, dirty flags
  • Luồng dữ liệu: editor updates → projectStore → preview component thực hiện client-side render của template (Handlbars compiled) để show WYSIWYG.

Editor WYSIWYG / rich text / markdown

  • Rich text: TipTap (ProseMirror-based) cho inline editing, block controls, paste handling. TipTap có plugin ecosystem thuận tiện cho sanitization.
  • Markdown: react-markdown + rehype/remark plugins cho sanitize và render. Lưu Markdown trong JSON cho các block dài.
  • Kết hợp: dùng cả hai. Ví dụ: short copy inline editable trên canvas với TipTap; article block cho markdown.

Template preview và rendering

  • Client-side preview: compile Handlebars templates in-memory for instant preview. Điều này giúp preview không cần native layer và dễ test visual.
  • Final export: run same Handlebars render in native layer (Electron main hoặc Tauri command) để đảm bảo deterministic output. Không rely chỉ vào client render.

CSS / design system

  • Hai lựa chọn hợp lệ:
  • Tailwind CSS: nhanh khi xây components, nhưng production build cần purge cẩn thận. Phù hợp nếu bạn muốn nhiều biến thể layout nhanh.
  • CSS Modules / SCSS + design tokens: phù hợp nếu muốn CSS scoped rõ ràng và dễ export sạch.
  • Pattern đề xuất: template CSS scoped dưới root class, ví dụ .sp-template-<id> để tránh tên lớp toàn cục. Theme variables lưu dưới :root hoặc injected inline khi export:
  • –brand-1, –accent, –text, –container-max
  • Always include utility CSS rules to prevent paste-borne inline styles breaking layout: img { max-width:100%; height:auto; object-fit:cover } and .rich-text { word-break:break-word; white-space:normal }.

Media handling in renderer

  • Renderer chỉ làm preview-thumbnails và light client-side resize using canvas / Pica. Không thực hiện heavy batch convert ở renderer.
  • Metadata: whenever user drops an image, record intrinsic width/height and create a preview variant. Store these in projectStore so export can use them.

Organizing renderer code for easy port to Tauri

  • Keep renderer pure web: avoid requiring Node APIs or electron modules in UI code.
  • Abstraction boundary:
  • Define an IPC service interface (TypeScript types) with methods like saveProject, loadProject, optimizeImages, exportProject. Implement one adapter for Electron (via contextBridge IPC) and a stub/mock adapter for web dev.
  • Example: src/services/nativeApi.ts exports interface NativeApi { saveProject(p): Promise; exportProject(opts): Promise<string>; } and implementations src/adapters/electronNative.ts, src/adapters/mockNative.ts.
  • Build with Vite in SPA mode so same bundle can be loaded by Electron or served to Tauri WebView.
  • Avoid direct imports of node-only libs in UI. All filesystem and heavy lifting go through the nativeApi adapter.

Visual testing and QA

  • Use Playwright or Puppeteer for automated visual regression. Workflow:
  • For each template and sample JSON, render pages in headless Chromium at multiple viewports and capture screenshots.
  • Compare with baseline via pixel-diff (or Playwright snapshots). Integrate into CI to catch layout regressions from content changes.
  • Unit / integration:
  • Test Handlebars helpers and render pipeline with Jest and jsdom.
  • Test TipTap content sanitization and paste scenarios.

Developer ergonomics and build

  • Local dev: run Vite dev server for renderer and start main process in dev mode. Expose mock native adapter for fast UI-only dev.
  • Production build: Vite build outputs static assets. For Electron, copy these into app resources. For Tauri, use same build artifacts and bundle with Rust binary.

Tóm tắt hành động kỹ thuật ngắn

  • Start with React + Vite + TypeScript.
  • Use TipTap for inline editing, react-markdown cho markdown.
  • Scope CSS per-template, prefer CSS Modules or Tailwind with careful purge.
  • Implement a nativeApi adapter boundary to isolate Node/native calls, để dễ port sang Tauri.
  • Add Playwright visual tests per-template as part of CI.

Phần này cung cấp đủ để bạn hoặc đội dev bắt tay scaffolding renderer, đồng thời giữ mã dễ chuyển sang Tauri khi cần.

Template engine và mapping nội dung

Mình chọn Handlebars làm engine ưu tiên cho prototype vì cú pháp rõ ràng, hỗ trợ partials và helpers dễ, và có runtime Node để render cả ở preview lẫn export. Nếu bạn thích Liquid vì compatibility với một số CMS, ý tưởng mapping tương tự áp được. Phần này mô tả cách tổ chức templates/partials, manifest template.json, quy tắc mapping JSON → template path, helper cần thiết, cách render local/server-side, và cách scope CSS bằng root class name. Cuối phần mình giao một manifest mẫu và ví dụ mapping thực tế.

  1. Cấu trúc template và partials
  • Mỗi template là một folder độc lập:
  • template.json – metadata và schema (manifest)
  • layout.hbs – entry layout, dùng partials
  • partials/ – hero.hbs, benefits.hbs, testimonial.hbs, footer.hbs, …
  • assets/css/ – styles.css (scoped bằng root class)
  • assets/img/ – sample images, thumbnails
  • Partial nhỏ, single-responsibility: mỗi partial chỉ render data theo schema đã định. Điều này cho phép tái dùng giữa templates.
  1. manifest: template.json (mẫu hoàn chỉnh)
  • manifest định nghĩa:
  • id, name, category, version
  • requiredFields: list JSON paths bắt buộc
  • fields: mô tả các field để UI sinh form input
  • themeVariables: danh sách biến màu/font có thể override
  • preview: thumbnail path, default viewport
  • Ví dụ manifest hoàn chỉnh:
{
  "id": "health-hero-01",
  "name": "Clinical Offer - Hero Right Form",
  "category": "suc-khoe",
  "version": "1.0.0",
  "requiredFields": [
    "meta.title",
    "hero.title",
    "hero.image",
    "hero.cta[0].label"
  ],
  "fields": {
    "meta.title": { "type": "text", "label": "Title" },
    "hero.title": { "type": "text", "label": "Hero title" },
    "hero.subtitle": { "type": "text", "label": "Hero subtitle" },
    "hero.image": { "type": "image", "label": "Hero image", "aspectRatio": "16:9", "minWidth": 1200 },
    "hero.cta": { "type": "list", "itemSchema": { "label": "text", "href": "url" } },
    "testimonials": { "type": "list", "itemSchema": { "name": "text", "text": "html", "photo": "image" } }
  },
  "themeVariables": {
    "brandColor": "#D6336C",
    "accentColor": "#F59E0B",
    "fontHeading": "Inter, sans-serif"
  },
  "preview": {
    "thumbnail": "assets/preview.png",
    "viewport": "desktop"
  }
}
  1. Quy tắc mapping JSON → template path
  • JSON data dùng path dot notation tương ứng manifest:
  • Primitive fields: meta.title → {{meta.title}}
  • Arrays: benefits[] → iterate with #each: {{#each benefits}}{{> benefitPartial this}}{{/each}}
  • Optional fields: partials phải support conditional rendering: {{#if hero.subtitle}}<p>{{hero.subtitle}}</p>{{/if}}
  • Mapping rules cụ thể:
  1. Template chỉ đọc các paths được khai báo trong manifest.fields hoặc dùng safe helpers để tránh undefined output.
  2. Nếu field là list, manifest có itemSchema; UI chỉ cho phép nhập theo itemSchema để tránh type mismatch.
  3. Luôn lưu metadata ảnh: original width/height và generated srcset entries để export tạo attribute width/height và srcset.
  1. Helpers cần thiết (Handlebars)
  • imageSrcset(context) – trả về <picture> hoặc srcset HTML dựa trên metadata image object (webp + fallback). Example usage: {{{imageSrcset hero.image “hero”}}}
  • safeHtml(htmlString) – sanitize và trả raw HTML an toàn. Example: {{{safeHtml testimonial.text}}}
  • formatPrice(value, currency) – format tiền tệ cho phần product templates.
  • ifEqual / unlessEqual – so sánh strings để lựa variant.
  • loopWithIndex – helper trả index trong each nếu cần.
  • registerHelpers khi khởi tạo engine trước render preview/export.
  1. Cách render – client-side preview và server-side / local export
  • Client-side preview (fast, non-blocking)
  • Compile template in-memory with Handlebars.compile(layout).
  • Use same helpers but for image processing preview only use generated thumbnails.
  • Preview should not perform heavy image re-encoding. Use metadata to show correct aspect-ratio and placeholder variants.
  • Local export (deterministic final output)
  • Run render in Node context (Electron main or build script) to produce full HTML string.
  • During export, replace theme variables into CSS, inject critical CSS if needed, build picture elements using full srcset files that were generated by image pipeline.
  • Validate manifest.requiredFields before render; fail export with explicit messages if missing.
  1. Scope CSS bằng root class name
  • Mỗi template root element có class duy nhất, pattern: sp-template-<id> e.g. <body class=”sp-template-health-hero-01″>
  • CSS trong assets/styles.css viết dưới scope:
  • .sp-template-health-hero-01 h1 { … }
  • .sp-template-health-hero-01 .hero { … }
  • Khi export, replace :root variables per template theme:
  • :root { –brand-color: #D6336C; –accent: #F59E0B; }
  • Kết quả: template CSS không va chạm giữa templates và tránh global selectors như h1 { }.
  1. Ví dụ mapping ngắn: JSON → Handlebars usage
  • JSON snippet:
{
  "hero": {
    "title": "Tư vấn chuyên khoa trực tuyến",
    "subtitle": "Gặp bác sĩ trong 24h",
    "image": { "path":"assets/img/hero.jpg", "width":2400, "height":1350, "srcset":["assets/img/hero-480.webp","assets/img/hero-1200.webp"] },
    "cta": [{ "label":"Đăng ký", "href":"#form" }]
  }
}
  • layout.hbs snippet usage:
<body class="sp-template-health-hero-01">
  <header class="hero">
    <div class="hero-text">
      <h1>{{hero.title}}</h1>
      {{#if hero.subtitle}}<p>{{hero.subtitle}}</p>{{/if}}
      {{#each hero.cta}}
        <a class="btn btn-primary" href="{{href}}">{{label}}</a>
      {{/each}}
    </div>
    <div class="hero-media">
      {{{imageSrcset hero.image "hero"}}}
    </div>
  </header>
  {{> testimonialsPartial testimonials=testimonials}}
</body>

Tóm tắt thực thi nhanh

  • Kiểm tra manifest.requiredFields trước mọi render.
  • Dùng partials nhỏ, helpers chuẩn để tránh logic trong template.
  • Preview nhẹ, export chạy render trên Node/Electron main để đảm bảo srcset/width/height được inject.
  • Scope CSS bằng root class name để tránh specificity và inline styles từ content phá layout.

Thiết kế hệ thống template và component tái dùng

Mục tiêu: xây một hệ thống template/partial/component có thể tái dùng giữa nhiều mẫu sale page, dễ quản lý, và tạo được per-template CSS bundles nhỏ để giảm CSS footprint. Mình mô tả phân lớp template, design tokens, CSS variables, theme variants, component API (props JSON), danh sách partial chuẩn, và cách quản lý CSS để build per-template bundles. Các ví dụ là mẫu hoàn chỉnh bạn có thể dùng ngay.

  1. Nguyên tắc phân lớp template
  • Layout (page shell): cấu trúc HTML chung của trang, chứa head/meta, header, footer, và vị trí đặt sections.
  • Sections: khối lớn trên trang, ví dụ hero, features, testimonials, faq, cta. Sections ghép từ các partials nhỏ.
  • Partials (components): unit nhỏ, single responsibility, tái dùng across sections. Ví dụ: hero-media, hero-content, benefit-card, testimonial-card, faq-item, cta-bar.
  • Tách rõ: layout dùng sections; sections dùng partials. Khi render, Handlebars/engine chỉ ghép layout + sections cần thiết.
  1. Danh sách partial chuẩn (số lượng và tên đề xuất)
  • header.hbs
  • footer.hbs
  • hero/content.hbs
  • hero/media.hbs
  • benefits/list.hbs
  • benefit/item.hbs
  • testimonials/list.hbs
  • testimonial/item.hbs
  • faq/list.hbs
  • faq/item.hbs
  • cta/bar.hbs
  • gallery/carousel.hbs
  • product/feature.hbs
  • disclaimer/block.hbs

Tổng 14 partials là chuẩn để dựng 8 mẫu minh họa; mở rộng thêm partial khi cần.

  1. Design tokens và CSS variables
  • Lưu tokens ở tập tin JSON global (design-tokens.json) và export vào CSS custom properties khi build template.
  • Token nhóm chính:
  • colors: brand-1, brand-2, accent, text, bg
  • spacing: space-1..space-6
  • typography: font-base, font-heading, size-h1..p
  • radii, shadows, breakpoints
  • Khi export template, inject :root hoặc body.root-class { –brand-1: #… } để override per-template.

Ví dụ tokens (mẫu hoàn chỉnh):

{
  "colors": {
    "brand": "#D6336C",
    "accent": "#F59E0B",
    "text": "#1F2937",
    "bg": "#FFFFFF"
  },
  "typography": {
    "fontHeading": "Inter, sans-serif",
    "fontBody": "Noto Sans, sans-serif",
    "h1Size": "2rem",
    "bodySize": "1rem"
  },
  "spacing": {
    "sm": "8px",
    "md": "16px",
    "lg": "24px"
  }
}
  1. Theme variants
  • Mỗi template có một or vài theme variants (ví dụ light, dark, promo). Biến thể là bộ giá trị token thay đổi. UI cho phép chọn variant; engine inject tương ứng.
  • Implement bằng CSS variables. Khi user đổi màu nhanh, app chỉ cập nhật variables mà không rebuild CSS.
  1. Component API (props JSON)
  • Mỗi partial định nghĩa schema props để UI tự động sinh form. Đây là ví dụ hoàn chỉnh cho hero partial:
{
  "component": "hero",
  "props": {
    "title": { "type": "text", "required": true },
    "subtitle": { "type": "text", "required": false },
    "image": { "type": "image", "required": true, "aspectRatio": "16:9", "minWidth": 1200 },
    "cta": {
      "type": "list",
      "itemSchema": {
        "label": { "type": "text", "required": true },
        "href": { "type": "url", "required": true },
        "variant": { "type": "enum", "options": ["primary","ghost"], "default": "primary" }
      }
    },
    "layoutVariant": { "type": "enum", "options": ["media-left","media-right","full-width"], "default": "media-right" }
  }
}
  • UI đọc schema để validate input, show image aspect guidelines, và map JSON vào template path (hero.title, hero.image…).
  1. Pattern tái dùng để giảm CSS footprint
  • Component-scoped CSS: mỗi partial có file CSS riêng, tên hóa theo component, ví dụ hero.css. Không include style global.
  • Root scoping: mỗi template export gắn class root .sp-template-<id>. Mọi selector bắt đầu bằng .sp-template-<id> để tránh global collision.
  • Build per-template bundle: khi export, detect partials dùng trong template, concat chỉ CSS của các partials đó + base reset + token vars. Không ship toàn bộ component library.
  • Avoid global utilities: hạn chế utility classes toàn cục; thay vì ship Tailwind compile toàn bộ, dùng tiny utility subset hoặc generate per-template utilities.
  • CSS modularization: keep component rules minimal and rely on CSS variables for visual differences. Ví dụ benefit-card chỉ chứa layout and spacing; colors come from variables.
  1. Kỹ thuật tạo per-template CSS bundles
  • Đặt mỗi component CSS ở src/components/<name>/styles.css.
  • At export time:
  1. Parse template manifest to list used partials.
  2. Concatenate base.css + tokens.css + used partials’ styles.
  3. Run PostCSS pipeline: autoprefixer, cssnano (safe settings).
  4. Optionally run PurgeCSS against rendered HTML to strip unused rules, with safelist patterns for dynamic classes.
  • Store resulting styles as assets/css/styles.css referenced by index.html.
  1. Ví dụ hoàn chỉnh nhỏ: hero partial + usage
  • Handlebars snippet:
<section class="hero {{layoutVariant}}" aria-label="Hero">
  <div class="hero-content">
    <h1>{{hero.title}}</h1>
    {{#if hero.subtitle}}<p class="hero-sub">{{hero.subtitle}}</p>{{/if}}
    {{#each hero.cta}}<a class="btn btn-{{variant}}" href="{{href}}">{{label}}</a>{{/each}}
  </div>
  <div class="hero-media">
    {{{imageSrcset hero.image "hero"}}}
  </div>
</section>
  • CSS (component-scoped excerpt):
.sp-template-health-hero-01 .hero { display:flex; gap:var(--space-lg); align-items:center; padding:var(--space-lg); }
.sp-template-health-hero-01 .hero.media-right { flex-direction:row; }
.sp-template-health-hero-01 .hero-media img { width:100%; height:auto; object-fit:cover; display:block; }
  • Khi export, only this CSS plus base/token files are bundled.
  1. Quy tắc vận hành và kiểm soát
  • Mỗi partial phải: a) có props JSON schema, b) có CSS riêng, c) có test snapshot. Điều này cho phép build per-template bundles chính xác.
  • Purge/run-on-render: purge CSS only after rendering HTML with actual data to avoid removing dynamic classes.
  • Safelist prefixes: is-, has-, btn- để bảo toàn lớp động.
  • Giữ helper imageSrcset dùng metadata do pipeline ảnh tạo để injection width/height và srcset tránh CLS.

Kết luận tóm tắt hành động

  • Triển khai template folder pattern, partial CSS files, token system, và export pipeline that picks only used partial CSS. Dùng schema-driven props để UI validate và mapping dữ liệu. Giải pháp này vừa tái dùng component vừa hạn chế CSS footprint cho mỗi template xuất.

Bộ 8 mẫu minh họa (4 sức khỏe, 4 làm đẹp)

Dưới đây là 8 mẫu template minh họa dành cho prototype. Mỗi mục nêu mục đích, cấu trúc section chính, danh sách biến cần thay (title, hero, benefits, testimonials, CTA, disclaimers, v.v.), và lưu ý compliance cụ thể cho ngành sức khỏe / làm đẹp.

  1. Clinical Offer – Tư vấn chuyên khoa
  • Mục đích: thu lead cho buổi tư vấn y tế trực tuyến hoặc đặt lịch khám.
  • Sections: head/meta, hero (text + form), credibility badges, benefits/features, doctor profiles, testimonials, FAQ, footer.
  • Biến cần thay: meta.title, hero.title, hero.subtitle, hero.image, hero.formFields, badges[], benefits[], doctors[], testimonials[], ctaPrimary, footer.links, disclaimer.text.
  • Compliance: bắt buộc trường disclaimer.visible và disclaimer.text mặc định “Không phải lời khuyên y tế”; cấm nội dung khẳng định chữa khỏi; form thu dữ liệu sức khỏe phải kèm checkbox đồng ý xử lý dữ liệu.
  1. Wellness Program – Chương trình chăm sóc dài hạn
  • Mục đích: đăng ký chương trình coaching/chăm sóc sức khỏe.
  • Sections: hero (full-bleed), program timeline, package cards, pricing + CTA, social proof, FAQ, signup modal.
  • Biến: program.title, timeline[], package[].title, price, benefits[], testimonials[], cta.label, cta.href, disclaimer.
  • Compliance: không cho phép minh chứng lâm sàng không kiểm chứng; bắt show disclaimer và chỗ để upload chứng nhận chuyên gia nếu yêu cầu.
  1. Supplement Product – Sản phẩm bổ sung
  • Mục đích: single-product page cho viên uống/ thực phẩm chức năng.
  • Sections: hero (product gallery + buy CTA), product highlights, ingredients, how-it-works, scientific notes, before/after (optional), testimonials, FAQ, footer.
  • Biến: product.title, gallery[], ingredients[], benefits[], dosage, researchLinks[], testimonials[], ctaBuy.
  • Compliance: luôn hiển thị disclaimer về tuyên bố sức khỏe; nếu có claims, gắn nguồn tham chiếu; cấm copy text dạng “chữa bệnh” nếu không có chứng cứ pháp lý.
  1. Clinic Event – Workshop / Sự kiện sức khỏe
  • Mục đích: đăng ký tham gia sự kiện / hội thảo.
  • Sections: hero with countdown, speakers, schedule, ticket tiers, location/map, testimonials, register form, footer.
  • Biến: event.title, date, countdown.enabled, speakers[], schedule[], tickets[], ctaRegister, disclaimer.event.
  • Compliance: nếu quảng cáo liên quan can thiệp y tế, yêu cầu thông tin ràng buộc về độ tuổi/điều kiện tham gia; thêm field consent nếu thu dữ liệu sức khỏe.
  1. Skincare Product Launch – Ra mắt sản phẩm chăm sóc da
  • Mục đích: bán sản phẩm mỹ phẩm, nhấn visual và công thức.
  • Sections: hero carousel, product features, ingredients spotlight, routine steps, before/after gallery, testimonials, CTA voucher, footer.
  • Biến: product.title, hero.images[], features[], ingredients[], routine[], beforeAfter[], testimonials[], promoCode, cta.label.
  • Compliance: trước/sau phải kèm xác nhận consent; cấm ngôn từ implying medical treatment; cung cấp phần “Lưu ý” cho phản ứng phụ.
  1. Salon Service – Đặt lịch dịch vụ làm đẹp
  • Mục đích: đặt lịch cho salon/nghệ nhân; hiển thị portfolio.
  • Sections: hero + booking widget, service list, price table, gallery, team, reviews, booking CTA.
  • Biến: salonName, hero.image, services[], prices[], gallery[], team[], testimonials[], bookingLink.
  • Compliance: hiển thị thông tin chứng chỉ nhân viên nếu claim chuyên môn; giới hạn ngôn từ tránh hứa hẹn kết quả y tế.
  1. Cosmetic Procedure Info – Thủ thuật thẩm mỹ thông tin + lead capture
  • Mục đích: cung cấp thông tin thủ thuật và thu lead tư vấn.
  • Sections: hero + risk panel, procedure overview, before/after slider, patient stories, FAQ, contact form, mandatory disclaimer.
  • Biến: procedure.title, riskSummary, benefitsList, beforeAfterPairs[], patientTestimonials[], ctaConsult, disclaimer.risk.
  • Compliance: bắt buộc block “risk” hiển thị; yêu cầu đồng ý trước khi tải hình trước/sau; rõ ràng không được quảng cáo chữa bệnh.
  1. Beauty Course – Khóa học make-up / chăm sóc
  • Mục đích: bán khóa học, hiển thị chương trình học và giảng viên.
  • Sections: hero (video preview), curriculum accordion, instructor bios, testimonials, pricing tiers, enroll CTA, FAQ.
  • Biến: course.title, promoVideo, modules[], instructors[], testimonials[], pricing[], ctaEnroll.
  • Compliance: nếu dùng hình mẫu khách hàng, đảm bảo quyền sử dụng ảnh và không quảng cáo kết quả y tế.

Ghi chú dùng chung cho tất cả mẫu

  • Mọi template ngành sức khỏe/ thẩm mỹ phải có trường disclaimer cấu hình bắt buộc và vùng upload chứng nhận/nguồn tham khảo.
  • Template manifest cần khai báo requiredFields và media.requirements (aspectRatio, minWidth) để tránh lỗi responsive khi xuất.
  • Ứng dụng phải cảnh báo nếu văn bản chứa claim y tế bằng rule-based keyword scan trước khi cho phép export ở chế độ “publish”.

Mẫu dữ liệu đầu vào và JSON Schema mẫu

Dưới đây là một JSON mẫu hoàn chỉnh dùng cho một template sale page. Đây là bản điền sẵn để bạn có thể dán vào editor hoặc làm sample data cho rendering, export và kiểm tra preflight.

{
  "meta": {
    "title": "Khỏe Đẹp Tự Tin - Chương trình 6 tuần",
    "slug": "khoe-dep-6-tuan",
    "description": "Chương trình 6 tuần giúp cải thiện dinh dưỡng và làn da. Đăng ký nhận tư vấn miễn phí.",
    "author": "Clinic A",
    "locale": "vi",
    "theme": {
      "brandColor": "#D6336C",
      "accentColor": "#F59E0B",
      "fontHeading": "Inter",
      "fontBody": "Noto Sans"
    }
  },
  "hero": {
    "title": "Làm chủ sức khỏe - đẹp từ bên trong",
    "subtitle": "Chương trình 6 tuần với chuyên gia dinh dưỡng và thẩm mỹ",
    "image": {
      "src": "assets/img/hero-1600.jpg",
      "width": 1600,
      "height": 900,
      "srcset": [
        {"src":"assets/img/hero-480.webp","w":480},
        {"src":"assets/img/hero-1024.webp","w":1024},
        {"src":"assets/img/hero-1600.webp","w":1600}
      ],
      "alt": "Hình minh họa chương trình"
    },
    "cta": [
      {"label": "Đăng ký tư vấn", "type": "primary", "href": "#signup"}
    ]
  },
  "benefits": [
    {"title": "Kế hoạch ăn uống cá nhân", "desc": "Lộ trình dinh dưỡng cá nhân hóa cho từng thể trạng", "icon": "assets/icons/meal.svg"},
    {"title": "Chăm sóc da chuyên sâu", "desc": "Quy trình chăm sóc da theo chuẩn bác sĩ da liễu", "icon": "assets/icons/skin.svg"},
    {"title": "Hỗ trợ trực tiếp", "desc": "Tư vấn online trong suốt 6 tuần", "icon": "assets/icons/support.svg"}
  ],
  "testimonials": [
    {"name": "Lan H.", "text": "Da mình cải thiện rõ sau 4 tuần.", "photo": "assets/img/t1.jpg"},
    {"name": "Minh T.", "text": "Chế độ ăn dễ thực hiện, hiệu quả.", "photo": "assets/img/t2.jpg"}
  ],
  "faq": [
    {"q": "Chương trình kéo dài bao lâu?", "a": "6 tuần, gồm 3 buổi tư vấn trực tiếp và hỗ trợ online."},
    {"q": "Có hoàn tiền không?", "a": "Xem chính sách hoàn tiền trong phần điều khoản."}
  ],
  "ctaBlocks": [
    {"text": "Đăng ký ngay để nhận ưu đãi 20%", "button": {"label": "Đăng ký", "href": "#signup"}}
  ],
  "footer": {
    "company": "Clinic A",
    "links": [{"label": "Chính sách", "href": "/policy"}]
  },
  "settings": {
    "layoutVariant": "hero-left",
    "formAction": "https://hooks.example.com/collect",
    "requireDisclaimer": true,
    "disclaimer": "Nội dung chỉ mang tính tham khảo, không thay thế tư vấn y tế chuyên môn."
  }
}

Hướng dẫn viết JSON Schema cho template (AJV)

  • Mục đích: validate dữ liệu người dùng trước khi bind vào template và trước khi export.
  • Nguyên tắc: mỗi template kèm schema riêng. Schema bao gồm required fields theo manifest.requiredFields (xem phần manifest bên dưới). Dùng ajv để validate; bật strictTypes.

Ví dụ JSON Schema (rút gọn, Handlebars template “standard” schema):

{
  "$id": "https://example.com/schemas/salepage.v1.json",
  "type": "object",
  "required": ["meta", "hero", "settings"],
  "properties": {
    "meta": {
      "type": "object",
      "required": ["title", "slug"],
      "properties": {
        "title": {"type": "string"},
        "slug": {"type": "string", "pattern": "^[a-z0-9\\-]+$"},
        "description": {"type": "string"},
        "author": {"type": "string"},
        "locale": {"type": "string"}
      },
      "additionalProperties": true
    },
    "hero": {
      "type": "object",
      "required": ["title", "image"],
      "properties": {
        "title": {"type": "string"},
        "subtitle": {"type": "string"},
        "image": {
          "type": "object",
          "required": ["src", "width", "height"],
          "properties": {
            "src": {"type": "string"},
            "width": {"type": "integer", "minimum": 1},
            "height": {"type": "integer", "minimum": 1},
            "alt": {"type": "string"},
            "srcset": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["src", "w"],
                "properties": {"src": {"type": "string"}, "w": {"type": "integer"}}
              }
            }
          }
        },
        "cta": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["label"],
            "properties": {"label": {"type": "string"}, "href": {"type": "string"}}
          }
        }
      }
    },
    "benefits": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["title", "desc"],
        "properties": {"title": {"type": "string"}, "desc": {"type": "string"}, "icon": {"type": "string"}}
      }
    },
    "testimonials": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name", "text"],
        "properties": {"name": {"type": "string"}, "text": {"type": "string"}, "photo": {"type": "string"}}
      }
    },
    "faq": {
      "type": "array",
      "items": {"type": "object", "required": ["q", "a"], "properties": {"q": {"type": "string"}, "a": {"type": "string"}}}
    },
    "ctaBlocks": {
      "type": "array",
      "items": {"type": "object", "required": ["text", "button"], "properties": {"text": {"type": "string"}, "button": {"type": "object"}}}
    },
    "settings": {
      "type": "object",
      "required": ["layoutVariant", "formAction", "requireDisclaimer"],
      "properties": {
        "layoutVariant": {"type": "string"},
        "formAction": {"type": "string"},
        "requireDisclaimer": {"type": "boolean"},
        "disclaimer": {"type": "string"}
      }
    }
  },
  "additionalProperties": false
}

Ví dụ validate bằng ajv (Node)

const Ajv = require("ajv");
const ajv = new Ajv({allErrors: true, strict: true});
const schema = require("./salepage.v1.json");
const data = require("./sample-data.json");

const validate = ajv.compile(schema);
const ok = validate(data);
if (!ok) {
  console.error("Validation errors:", validate.errors);
} else {
  console.log("Data is valid");
}

Gợi ý xử lý lỗi validate

  • Nếu thiếu required field: báo chính xác JSON path (ajv.errors cung cấp instancePath). Dùng thông báo rõ ràng trong UI: field path, expected type, sample value.
  • Nếu additionalProperties: cho phép extraFields: true chỉ khi template manifest cho phép mở rộng.

manifest.requiredFields để preflight check

  • Mỗi template kèm một manifest JSON nhỏ để preflight. Ví dụ minimal manifest:
{
  "templateId": "health-hero-01",
  "version": "1.0.0",
  "title": "Clinical Offer",
  "category": "health",
  "requiredFields": [
    "meta.title",
    "meta.slug",
    "hero.title",
    "hero.image.src",
    "hero.image.width",
    "hero.image.height",
    "settings.formAction",
    "settings.requireDisclaimer"
  ],
  "media": {
    "hero.image": {"aspectRatio": "16:9", "minWidth": 1200}
  }
}

Preflight logic (kết hợp schema + manifest)

  1. Run AJV schema validate. Nếu fail, show errors and block export.
  2. Check manifest.requiredFields: ensure each JSON path exists and non-empty. Nếu any missing, mark as blocking.
  3. Check media rules: validate image width/height and aspectRatio. Nếu image too small, warn or block depending manifest policy.
  4. Generate preflight report JSON with lists: blockingIssues[], warnings[]; present to user before export.

Kết luận thực hành nhanh

  • Sử dụng per-template JSON Schema + manifest.requiredFields là cơ sở chắc để phát hiện mismatch trước render/export.
  • Luôn validate image metadata (width/height/srcset) và lưu metadata khi user import ảnh.
  • Trong UI, hiển thị lỗi preflight rõ ràng với path, lý do và ví dụ sửa.

Giao diện chọn và quản lý template, import/export

Mục tiêu giao diện: cho phép bạn duyệt, xem trước, cài, xuất và quản lý templates theo chủ đề nhanh chóng; cho phép admin quản lý phiên bản, phân quyền và nhập/xuất dưới dạng package (.zip) chuẩn để chia sẻ hoặc backup.

Giao diện chính

  • Bar trên cùng: search, filter theo chủ đề (Sức khỏe, Làm đẹp, Tất cả), chế độ grid/list toggle.
  • Thư viện template (grid): mỗi card có thumbnail, tiêu đề, tags, version, badges (public / private / draft).
  • Action trên card: Use / Preview / Duplicate / More (menu).
  • Preview modal: hiển thị full-size preview ở 3 viewport (desktop / tablet / mobile) có toggle, live sample data switch (sample A / sample B), nút “Use this template” và nút “Open template editor” nếu bạn có quyền.
  • Template detail panel (slide-over): metadata (id, version, author, changelog), required fields summary, manifest validation status, last updated, permissions.

Preview modal behavior

  • Pre-render HTML using current sample data to show exact output; cho phép bật overlay hiển thị JSON path khi hover một vùng để liên kết nhanh tới field editor.
  • Cho phép download preview as static HTML zip for quick review.
  • Khi preview phát hiện blocking preflight issues, hiển thị tag cảnh báo kèm link tới preflight report.

Quy trình import / export template

  • Import:
  • UI: nút Import template ZIP, drag-and-drop area, validation summary.
  • App unzip package to temp, validate manifest.json + schema, check for required partials/assets. Nếu lỗi, show detailed report và reject.
  • On success, prompt chọn category và quyền mặc định (private/public/team).
  • Export:
  • UI: menu Export -> options: Export Package (.zip) or Export Minimal (partials + manifest).
  • Package bao gồm template folder, manifest.json, partials, assets, README.md, changelog.
  • Support exporting a template snapshot tied to a specific version tag.

Versioning và lifecycle

  • Template manifest includes templateId, version (semver), changelog, supported schemaVersion.
  • UI supports: publish new version, rollback to earlier version, view diff between versions (partials diff).
  • When user imports a template with same templateId but higher version, show upgrade dialog: preview changes, migrate samples if migration script present.
  • Templates can be forked to create variant without affecting original.

Permissions and roles

  • Roles: Viewer, Editor, Template Admin.
  • Viewer: browse, preview, use template for projects but cannot edit or export template package.
  • Editor: create projects from templates, customize per-project, suggest template changes (creates draft).
  • Template Admin: import/export templates, publish versions, set category, manage ACL, approve suggested edits.
  • UI: per-template ACL panel to add/remove users or teams, set default project sharing settings.

Các hành động UI cần có (quick list)

  • Browse by category/tag/search
  • Preview modal with viewport switcher and JSON-path overlay
  • Use template -> create project from template
  • Duplicate template -> fork
  • Edit template -> open template editor (partials/CSS/manifest)
  • Import template ZIP -> validate -> register
  • Export template ZIP -> choose includeAssets? includeSamples?
  • Publish version / Rollback / View changelog
  • Set ACL and download audit log

File structure cho template package (bắt buộc, máy đọc)

  • templates/<templateId>/ | title: Clinical Offer | version: 1.0.0 | category: health
  • templates/<templateId>/manifest.json | required: true | schemaVersion: 1
  • templates/<templateId>/layout.hbs | type: layout | required: true
  • templates/<templateId>/partials/header.hbs | type: partial
  • templates/<templateId>/partials/hero.hbs | type: partial
  • templates/<templateId>/partials/benefits.hbs | type: partial
  • templates/<templateId>/styles/styles.css | type: stylesheet | tokens: variables.css
  • templates/<templateId>/assets/logo.svg | type: asset
  • templates/<templateId>/assets/img/preview-thumbnail.png | type: asset
  • templates/<templateId>/samples/sample-data.v1.json | type: sample | schemaVersion: 1
  • templates/<templateId>/migrations/ (optional) | scripts to upgrade older sample shapes
  • templates/<templateId>/README.md | license, usage, complianceNotes

Ghi chú ngắn

  • Manifest phải liệt kê requiredFields và media rules để preflight nhanh.
  • Import/Export phải validate manifest + schema trước khi chấp nhận.
  • Quyền quản lý template nằm riêng so với quyền chỉnh dự án, để marketing dùng template mà không sửa template gốc.

Trình soạn thảo nội dung và validation

Mục tiêu phần này: mô tả chi tiết các chế độ soạn thảo (inline editing, form fields, markdown), cách tổ chức UX để marketer thao tác nhanh, và hệ thống validation thời gian thực cần có để đảm bảo templates nhận nội dung có sẵn chạy an toàn trên layout responsive. Đồng thời nêu cách logging mapping các trường thiếu và trả về báo cáo rõ ràng trước khi export.

  1. Các chế độ soạn thảo và khi dùng
  • Inline editing (WYSIWYG nhẹ)
  • Dùng cho các vùng ngắn: hero title, subtitle, CTA label, card title. Triển khai bằng contentEditable hoặc TipTap/ProseMirror.
  • UX: double-click để edit, Enter commit, Esc hủy; inline toolbar nhỏ (bold/italic/link) xuất hiện trên selection.
  • Ưu: trực quan, nhanh cho marketer. Hạn chế: không dùng cho block markdown phức tạp.
  • Form fields (structured editor)
  • Bảng bên phải liệt kê tất cả field theo schema template (text, richtext, list, image, url, boolean).
  • Dùng khi cần nhập nhiều mục có cấu trúc (benefits[], testimonials[]). Hỗ trợ bulk paste từ CSV/Excel cho list.
  • UX: focus jumps to preview vùng tương ứng khi click field; có nút “populate sample” để test layout.
  • Markdown + preview
  • Dùng cho bài dài, disclaimers, long copy. Lưu markdown trong JSON và render sanitized HTML cho preview.
  • Live preview cạnh nhau; hỗ trợ basic remark plugins cho links và image embedding.
  • Chế độ kết hợp
  • Cho phép marketer chuyển đổi: ví dụ double-click hero text mở form field tương ứng để chỉnh chi tiết (giữ single source of truth).
  1. UX tiết kiệm thao tác cho marketer
  • Inline edits auto-sync về JSON data + autosave (debounce 800ms). Undo/redo per-project.
  • Smart paste: khi paste text có nhiều dòng vào single-line field, app nhắc “Bạn có muốn dán vào block nhiều dòng?” để chuyển qua markdown.
  • Drag-and-drop image vào vùng preview để upload và auto-generate srcset variants.
  • Quick actions: replace all images, swap color palette, apply sample data.
  • Field linking: map một field JSON vào nhiều vùng (ví dụ meta.title và hero.title) với toggle “sync”.
  • Inline validation hints: hiển thị icon cảnh báo ngay trên vùng nếu lỗi; click mở panel mô tả lỗi và hướng sửa.
  1. Validation thời gian thực cần có

Thực thi validation ở hai lớp: client-side nhanh cho UX; preflight toàn diện trước export. Các loại kiểm tra:

A. Required fields

  • Kiểm tra theo template manifest.requiredFields (list of JSON paths).
  • Hiển thị warning khi missing; blocking export nếu field vòng “required” là blocking.
  • Ví dụ: manifest.json có “requiredFields”: [“hero.title”,”hero.image”,”cta[0].label”]

B. Image dimensions / aspect ratio / file size

  • Khi user upload: read intrinsic width/height và file size.
  • Rules example:
  • Hero: minWidth = 1200 px, aspect = 16:9 preferred. Nếu width < 800 px: warning; nếu width < 600 px: blocking for hero.
  • Thumbnails: minWidth = 400 px.
  • Max file size before optimize: 5 MB; recommend compress < 300 KB for hero mobile.
  • UX: nếu image không đạt, hiển thị badge trên preview với suggested crop button, or auto-crop to aspect ratio with preview.
  • Technical: ensure exported HTML includes width/height attributes and aspect-ratio CSS fallback.

C. URL format

  • Validate using robust regex or URL constructor. Example quick check (concept):
try { new URL(value); } catch(e) { invalid }
  • Additionally check for supported schemes (http, https, mailto, tel). Warn if link missing protocol.

D. Length limits and text patterns

  • Meta title length <= 70 chars; meta description <= 160 chars (show counter).
  • Section titles: recommend <= 60 chars; card text <= 200 chars with live truncation preview.
  • Detect long unbreakable strings (e.g., long base64 or long slug with no spaces) and insert soft-wrap or warn.
  • For lists, enforce max items per template variant (e.g., benefits max 6) or layout will break.

E. Rich text sanitization and disallowed inline styles

  • Sanitize pasted HTML to remove style attributes, script tags, style tags, and potentially dangerous HTML. Use DOMPurify with allowed tags set.
  • Normalize inline styles: remove px font-sizes inserted by MS Word paste.

F. Accessibility and contrast quick checks

  • For chosen palette, compute color contrast ratio and warn if below 4.5:1 for body text.
  1. Validation flow and blocking policy
  • Real-time layer: quick checks on edit, non-blocking warnings except for clearly required or corrupt assets.
  • Preflight layer (before export): run full validation and produce a report:
  • Blocking issues: missing required fields, hero image below min_width, links invalid.
  • Non-blocking issues: color contrast low, long text, images large.
  • Export blocked if any blocking issues exist. Provide “export anyway” only for admin role with explicit confirmation and audit log.
  1. Logging mapping missing fields and báo cáo trước export
  • Khi render template, engine instrumentation records which JSON paths were accessed and their values. Nếu path undefined or null, add to mapping report.
  • Report format (JSON example for UI and audit):
{
  "templateId": "clinic-hero",
  "version": "1.0.0",
  "checkedAt": "2026-07-16T10:00:00Z",
  "missingFields": [
    {"path":"hero.image","reason":"missing","severity":"blocking"},
    {"path":"testimonials[0].photo","reason":"missing","severity":"warning"}
  ],
  "typeMismatches": [
    {"path":"benefits","expected":"array","actual":"string"}
  ],
  "mediaWarnings": [
    {"path":"hero.image","issue":"width=640 < minWidth=1200","suggestedAction":"replace or run Optimize images"}
  ],
  "advice": [
    "Meta description longer than 160 chars: consider shortening for SEO",
    "Color contrast low for CTA: consider darker text"
  ]
}
  • UI shows report grouped by blocking/non-blocking, with direct buttons to jump to field, auto-fix suggestions (crop, compress, replace), and “download report” for audit.
  1. Technical implementation notes (practical)
  • Use JSON Schema (ajv) per template for structural validation. Example fragment for hero:
{
  "type":"object",
  "properties": {
    "hero": {
      "type":"object",
      "properties": {
        "title":{"type":"string"},
        "image":{"type":"string","format":"uri"}
      },
      "required":["title","image"]
    }
  },
  "required":["hero"]
}
  • On render, produce AST-path extraction (from Handlebars parse) to know which paths template references; compare to provided JSON to catch unmapped fields early.
  • For image processing: immediate client-side generate quick srcset via canvas for preview; background worker (sharp) for production-quality variants on export.
  • For URL validation use try/catch with URL; for pattern checks use configurable regex list stored in template manifest.
  1. UX patterns for error handling
  • Inline badges with short sentence and link to fix.
  • Preflight modal with summary count and expand/collapse per issue class.
  • Suggested one-click fixes: “replace hero image”, “auto-resize and convert to webp”, “truncate text to 140 chars”.
  • Audit log: record export attempts that were blocked and user actions taken.

Kết luận ngắn gọn về phạm vi kỹ thuật: triển khai editor phải kết hợp structured form + inline WYSIWYG + markdown với autosave và immediate validation; trước export chạy preflight dùng JSON Schema + AST-driven mapping để tìm missing/path mismatches; logging trả về JSON report có severity để marketer và dev dễ xử lý.

Trình soạn thảo nội dung và validation

Mục tiêu phần này: mô tả chi tiết các chế độ soạn thảo (inline editing, form fields, markdown), cách tổ chức UX để marketer thao tác nhanh, và hệ thống validation thời gian thực cần có để đảm bảo templates nhận nội dung có sẵn chạy an toàn trên layout responsive. Đồng thời nêu cách logging mapping các trường thiếu và trả về báo cáo rõ ràng trước khi export.

  1. Các chế độ soạn thảo và khi dùng
  • Inline editing (WYSIWYG nhẹ)
  • Dùng cho vùng ngắn: hero title, subtitle, CTA label, card title.
  • Triển khai bằng contentEditable hoặc TipTap/ProseMirror để có undo/redo và toolbar nhỏ.
  • UX: double-click để edit, Enter commit, Esc bỏ thay đổi; toolbar xuất hiện trên selection với bold/italic/link.
  • Ưu/nhược: trực quan, nhanh cho marketer. Hạn chế với nội dung dài hoặc paste từ Word.
  • Form fields (structured editor)
  • Panel bên phải liệt kê tất cả field theo schema template (text, richtext, list, image, url, boolean).
  • Dùng khi cần nhập nhiều mục có cấu trúc như benefits[], testimonials[]. Hỗ trợ bulk paste từ CSV/Excel cho list.
  • UX: click field focus vùng tương ứng trên preview; có nút “populate sample” để test layout.
  • Markdown + preview
  • Dùng cho bài dài, disclaimers, long copy. Lưu markdown trong JSON; render sanitized HTML cho preview.
  • Live preview cạnh nhau; react-markdown hoặc unified/remark cho parsing, hỗ trợ image embedding.
  • Chế độ kết hợp
  • Cho phép chuyển giữa inline và form. Ví dụ double-click hero text mở form field để chỉnh chi tiết. Mọi thay đổi đồng bộ vào cùng một nguồn dữ liệu (project JSON).
  1. UX tiết kiệm thao tác cho marketer
  • Autosave debounce (ví dụ 800 ms) và undo/redo per project.
  • Smart paste: nếu paste nhiều dòng vào single-line field, app gợi ý chuyển về markdown hoặc form list.
  • Drag-and-drop ảnh trực tiếp lên preview; ảnh được copy vào project assets và tạo derivative nhanh cho preview.
  • Quick actions: apply sample data, swap palette, replace all images.
  • Field linking: cho phép sync giữa meta.title và hero.title với toggle “sync”.
  • Inline validation badges: cảnh báo ngay trên vùng nếu lỗi; click dẫn tới panel chi tiết.
  1. Validation thời gian thực cần có

Hai lớp: client-side nhanh cho UX, và preflight toàn diện trước export.

A. Required fields

  • Dùng manifest.requiredFields trong template để kiểm tra presence. Missing required được đánh severity blocking.
  • Ví dụ manifest:
{
  "requiredFields": ["hero.title","hero.image","cta[0].label"]
}

B. Image rules

  • Khi upload, đọc intrinsic width/height + file size.
  • Quy tắc gợi ý:
  • Hero: minWidth 1200 px, aspect 16:9 preferred; nếu width < 800 px → warning; width < 600 px → blocking.
  • Thumbnails: minWidth 400 px.
  • Max upload raw file: 5 MB.
  • UX: nếu ảnh không đạt, show badge với action “auto-crop” hoặc “optimize now”.

C. URL validation

  • Validate bằng constructor URL để tránh regex rườm rà.
function isValidUrl(value) {
  try {
    const u = new URL(value);
    return ['http:', 'https:', 'mailto:', 'tel:'].includes(u.protocol);
  } catch (e) {
    return false;
  }
}

D. Length and content patterns

  • Meta title <= 70 chars; meta description <= 160 chars with counter.
  • Section titles recommend <= 60 chars; card text <= 200 chars. Hiển thị preview truncation.
  • Detect long unbreakable strings and warn to insert soft-breaks.

E. Rich text sanitization

  • Dùng DOMPurify hoặc tương đương để remove style attributes, script tags, style tags. Cho phép whitelist tags cần thiết.
  • Bình thường remove inline style px để tránh override CSS template.

F. Accessibility quick checks

  • Contrast ratio check cho palette; nếu < 4.5:1 với body text hiển thị cảnh báo.
  • Kiểm tra alt attributes cho ảnh.
  1. Validation flow và blocking policy
  • Real-time: hiển thị warnings không chặn thao tác, trừ các lỗi rõ ràng như missing required hoặc media corrupted.
  • Preflight (trước export): chạy full validation, tạo báo cáo phân loại blocking / non-blocking. Export bị chặn nếu có blocking issues. Có tùy chọn “Export anyway” chỉ cho admin với audit log.
  1. Logging mapping missing fields và báo cáo trước export
  • Khi render preview/export, runtime ghi lại các JSON path được truy cập và kết quả. Nếu path undefined/null thì đưa vào report.
  • Report có cấu trúc machine-readable để UI hiển thị và lưu audit. Ví dụ:
{
  "templateId": "clinic-hero",
  "version": "1.0.0",
  "checkedAt": "2026-07-16T10:00:00Z",
  "missingFields": [
    {"path":"hero.image","reason":"missing","severity":"blocking"},
    {"path":"testimonials[0].photo","reason":"missing","severity":"warning"}
  ],
  "typeMismatches": [
    {"path":"benefits","expected":"array","actual":"string"}
  ],
  "mediaWarnings": [
    {"path":"hero.image","issue":"width=640 < minWidth=1200","suggestedAction":"replace or Optimize images"}
  ],
  "advice": [
    "Meta description longer than 160 chars",
    "CTA contrast low vs background"
  ]
}
  • UI actions: click item → focus trực tiếp vào trường tương ứng trong editor; cung cấp one-click fixes khi có thể (auto-resize, compress, crop, truncate).
  1. Kỹ thuật triển khai thực tế
  • Structural validation: duy trì JSON Schema per-template và validate bằng ajv trước render.
  • AST-driven mapping: parse Handlebars template để thu danh sách paths tham chiếu. So sánh với data để catch missing keys trước render runtime.
  • Image pipeline: quick client-side derivatives cho preview (canvas / Pica), production-quality variants bằng sharp trong background worker trước export.
  • Proxy instrumentation: dùng Proxy như kiểm tra bổ sung trong dev mode nhưng không phụ thuộc hoàn toàn vào nó. AST + schema mới là primary detection.
  1. Quy ước UX cho lỗi và cảnh báo
  • Đánh dấu rõ severity: blocking (màu đỏ), recommended (màu vàng), info (xanh).
  • Không hide cảnh báo; bắt user confirm fix hoặc acknowledge trước export blocked.
  • Lưu audit log của mọi export attempt bị block và mọi lần override export.

Kết luận ngắn: editor phải kết hợp inline + structured + markdown với autosave, immediate validation, và preflight kiểm tra dựa trên JSON Schema cộng AST-driven mapping. Report mapping phải machine-readable để dev và marketer sửa nhanh, và pipeline media phải phối hợp với validation để tránh lỗi responsive do ảnh.

Responsive design system và breakpoints + lỗi thường gặp

Mục tiêu phần này: cung cấp một hệ thống responsive thực dụng cho sale page, quy ước breakpoint, mẫu grid/flex cho các section thường dùng, quy tắc ảnh để tránh CLS, liệt kê lỗi responsive hay gặp khi nhét content có sẵn và cách phòng tránh, kèm checklist test responsive trước khi export.

  1. Breakpoints đề xuất (dễ áp dụng và phổ biến)
  • xs (mobile small): 320 – 480 px
  • sm (mobile): 481 – 767 px
  • md (tablet): 768 – 991 px
  • lg (desktop small): 992 – 1199 px
  • xl (desktop): 1200 px trở lên

Ghi chú: đây là khung khởi điểm. templates có thể tinh chỉnh theo thiết kế, nhưng giữ các ngưỡng này giúp nhất quán.

  1. Grid / flex patterns cho các section chính
  • Hero
  • Desktop: 2-column 60/40 hoặc 50/50 – text + image. Dùng CSS grid: grid-template-columns: minmax(0, 1fr) minmax(0, 1fr).
  • Tablet: vẫn 2-column nhưng chuyển thành 1-column nếu image quá cao.
  • Mobile: stacked – image dưới hoặc trên tùy UX. Giữ CTA luôn hiển thị cao trong viewport.
  • Kỹ thuật: use align-items: center; gap: 24px; image container min-height bằng CSS variable theo breakpoint.
  • Features / Benefits
  • Desktop: grid 3 columns (repeat(3, 1fr)).
  • Tablet: grid 2 columns.
  • Mobile: 1 column.
  • Kỹ thuật: use grid-auto-rows: 1fr cho cards cùng chiều cao; card nội bộ dùng flex column để footer CTA luôn bottom-aligned.
  • Testimonials
  • Desktop: 3-column card grid hoặc centered carousel.
  • Tablet: 2-column grid hoặc carousel showing 2.
  • Mobile: single card carousel / stacked list.
  • Kỹ thuật: nếu dùng carousel, đảm bảo kích thước và aria controls, và fallback stacked list nếu JS absent.
  1. Quy tắc aspect-ratio và <img> attributes để tránh CLS (practices)
  • Luôn chèn intrinsic width và height attributes trên <img> nếu biết kích thước gốc. Nếu không, áp dụng CSS aspect-ratio trên container, ví dụ .hero-media { aspect-ratio: 16 / 9; } và img { width:100%; height:100%; object-fit:cover; display:block; }.
  • Sử dụng <picture> + srcset + sizes để cho browser chọn kích thước phù hợp. Ví dụ srcset=”img-480.webp 480w, img-1200.webp 1200w” sizes=”(max-width:767px) 100vw, 50vw”.
  • Luôn dùng loading=”lazy” cho ảnh không xuất hiện trên first viewport, ngoại trừ hero nếu cần hiển thị ngay.
  • Đặt display:block cho <img> để tránh inline baseline gaps.
  • Nếu dùng responsive background-image, reserve space bằng một pseudo-element với padding-top theo tỷ lệ để tránh shift.
  1. Các lỗi responsive thường gặp khi nhét content có sẵn và cách phòng tránh
  • Ảnh không có width/height hoặc tỉ lệ khác kỳ vọng
  • Hậu quả: hero phình to, CLS cao.
  • Khắc phục: khi import ảnh, đọc intrinsic size; auto-generate srcset và aspect-ratio placeholder; warn nếu ảnh nhỏ hơn ngưỡng.
  • Rich text paste kèm inline styles
  • Hậu quả: override CSS template, phá layout mobile.
  • Khắc phục: sanitize HTML, strip style attributes, chuyển paste thành plain text hoặc markdown, có nút “Keep formatting” tùy chọn.
  • Long unbreakable strings (URLs, emojis, base64)
  • Hậu quả: horizontal overflow.
  • Khắc phục: css rule word-break: break-word; overflow-wrap: anywhere; add zero-width-space vào strings nếu cần.
  • Fixed heights / absolute positioned elements
  • Hậu quả: cắt nội dung hoặc chồng lấp.
  • Khắc phục: tránh height:px trên content; dùng min-height / max-height với units tương đối; sử dụng flow layout.
  • Inline styles / high specificity từ content
  • Hậu quả: template CSS bị ghi đè.
  • Khắc phục: namespace toàn bộ template với .sp-<template> root class; dùng CSS variables và !important chỉ ở edge-case; sanitize inline style hoặc scope nó.
  • Missing container fallbacks khi component optional
  • Hậu quả: khoảng trắng lớn khi testimonials/benefits thiếu.
  • Khắc phục: template phải có conditional rendering và CSS fallback cho số lượng phần tử khác nhau.
  1. Checklist test responsive (phải chạy trước export)
  • Viewport check: render và kiểm tra tại 320, 375, 414, 768, 1024, 1366.
  • CLS check: measure cumulative layout shift; đảm bảo hero và images có width/height hoặc aspect-ratio.
  • Image quality/sizes: verify srcset selects appropriate file; file size cảnh báo cho hero > 300 KB desktop.
  • Overflow check: no horizontal scroll, test long words & pasted HTML.
  • Forms and CTA: buttons visible above fold on mobile; touch target >= 44px.
  • Font loading: font-display: swap; check FOIT/FOUT.
  • Accessibility quick checks: alt attributes, logical heading order, contrast >= 4.5:1 cho body text.
  • JS-free fallback: kiểm tra page render cơ bản nếu client JS không chạy.
  • Export integrity: mở exported index.html local và kiểm tra assets path relative đúng.

Kết luận ngắn: hệ thống responsive hiệu quả dựa trên breakpoints chuẩn, pattern grid linh hoạt, và quy tắc bắt buộc cho ảnh. Khi nhận content có sẵn, tự động hóa chuẩn hóa ảnh và sanitize rich text là chìa khóa để tránh lỗi. Hãy chạy checklist test responsive trước mỗi export để kịp sửa.

Export static HTML/CSS: build pipeline và packaging

Mục tiêu phần này là cung cấp pipeline thực tế để từ template + data sinh ra site tĩnh: index.html + css + assets, tối ưu responsive và nhỏ gọn, cuối cùng đóng gói thành ZIP theo hai chế độ export. Những bước dưới đây là thứ tự thao tác mà bạn hiện thực trong ứng dụng PC (Electron/Tauri) như một command export.

  1. Tổng quan pipeline (tóm tắt các bước)
  2. Render template với dữ liệu để tạo HTML tĩnh.
  3. Inject theme variables (CSS custom properties hoặc biến SCSS) vào stylesheet.
  4. Sinh responsive srcset cho ảnh và cập nhật markup <picture> / <img> với width/height và sizes.
  5. Tùy chọn: extract và inline critical CSS cho phần above-the-fold.
  6. Minify assets: HTML (html-minifier-terser), CSS (cssnano), JS (terser).
  7. Copy/convert assets (images, fonts, icons) vào thư mục export.
  8. Tạo zip package hoặc folder export.
  9. Kiểm tra integrity và báo cáo lỗi/warnings (missing images, undersized hero, broken links).
  1. Bước chi tiết thực hiện (implementable)
  2. Render template
  • Dùng Handlebars.compile(templateString) hoặc engine bạn chọn. Context = project JSON + theme variables.
  • Kết quả: rawHtml string. Lưu temp file render/index.raw.html để pipeline bước sau truy xuất.
  • Kiểm tra: đảm bảo head có meta viewport và charset trước khi tiếp tục.
  1. Inject theme variables
  • Thiết kế templates dùng CSS variables: :root { –brand: #… } hoặc inject inline style trong head.
  • Lấy theme từ project.meta.theme và generate nhỏ file css/themes.css hoặc inject <style> trong head với custom properties.
  • Nếu template dùng SCSS, compile với node-sass/postcss trước khi tiếp tục.
  1. Generate responsive srcset
  • Quy ước: với mỗi ảnh, tạo variants widths = [480, 768, 1200, 1920] nếu ảnh gốc >= các kích thước đó; còn không, tạo theo kích thước gốc.
  • Nếu dùng sharp (Electron): run sharp(source).resize(width).toFile(dest).
  • Ghi metadata vào project JSON: { “image”: { “src”: “assets/hero-1200.jpg”, “srcset”: [“hero-480.webp 480w”, …], “width”: 1200, “height”: 675 } }.
  • Replace markup: use <picture> with <source type=”image/webp” srcset=”…”> and fallback <img src=”…” width=”…” height=”…” loading=”lazy” style=”object-fit:cover;display:block;”>
  • Ensure sizes attribute suitable: sizes=”(max-width:767px) 100vw, 50vw”.
  1. Inline critical CSS (optional)
  • Run a critical extractor per-template or use a precomputed critical CSS for each template variant.
  • Use penthouse or critical libraries to compute critical CSS for given viewport set, or for prototype, manually author minimal critical CSS (< 14 KB).
  • If enabled: inject critical CSS into <head> in a <style> tag; load rest.css with rel=”preload” + onload swap pattern:
<link rel="preload" href="assets/css/styles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="assets/css/styles.css"></noscript>
  1. Minify HTML/CSS/JS
  • HTML: html-minifier-terser with options keepClosingSlash true, collapseWhitespace true, removeComments true, minifyJS false/true depending on your in-page scripts.
  • CSS: postcss + cssnano with safe options (disable dangerous transforms if you use custom properties extensively).
  • JS: terser for any behavioral JS (form handler, accordion). For prototype, avoid bundling heavy frameworks into exported JS.
  • Example npm scripts:
"scripts": {
  "minify:html": "html-minifier-terser --collapse-whitespace --remove-comments --minify-css true --minify-js true -o export/index.html export/index.raw.html",
  "minify:css": "postcss export/assets/css/styles.css -u cssnano -o export/assets/css/styles.min.css",
  "minify:js": "terser export/assets/js/main.js -c -m -o export/assets/js/main.min.js"
}
  1. Copy and convert assets
  • Copy generated srcset files and original assets into export/assets/img.
  • Fonts: if self-contained export, copy woff2 files into export/assets/fonts and update @font-face urls. For light export, reference Google Fonts link in head.
  • SVG icons: inline critical SVGs into HTML when small; keep large SVG files external.
  1. Create ZIP and manifest
  • Build folder structure:
  • export/<slug>/index.html
  • export/<slug>/assets/css/styles.min.css
  • export/<slug>/assets/js/main.min.js
  • export/<slug>/assets/img/*
  • export/<slug>/assets/fonts/*
  • Create manifest.json inside export that includes export mode, templateId, dataVersion, generatedAt, asset list.
  • ZIP creation options:
  • Node: use archiver package to create zip.
  • CLI: on mac/linux use zip -r output.zip folder/.
zip -r mypage.zip export/mypage/
  1. Integrity checks (final)
  • Verify index.html references exist in assets list.
  • Check hero image width >= recommended threshold (warn if < 800 px).
  • Ensure index.html contains meta viewport.
  • Report file sizes and total zip size; warn if self-contained > threshold.
  1. Hai chế độ export: light và self-contained
  • Light (external refs)
  • CSS: reference external CDN or Google Fonts.
  • Images: reference external URLs (if user used cloud-hosted images) or include optimized small variants only.
  • Kích thước package: nhỏ, suitable khi bạn muốn upload to Netlify/Vercel quickly.
  • Pros: small ZIP, faster transfer. Cons: depends on external availability.
  • Self-contained (all assets)
  • All images, fonts, scripts, CSS included locally.
  • Inline critical CSS optional to reduce HTTP requests.
  • Pros: works fully offline, easy to hand to client. Cons: larger ZIP; be careful with font licensing.
  • Implementation tip: allow user to pick mode in export dialog, show estimated sizes for each mode before creating ZIP.
  1. Hướng dẫn deploy lên Netlify / Vercel / S3 (ngắn, thực tế)
  • Netlify
  • Kéo thả: unzip ZIP, drag-and-drop folder vào Netlify Sites – Deploys area. Hoặc push to Git and connect repo.
  • CLI deploy: install netlify-cli and run:
npx netlify deploy --dir=export/mypage --prod
  • Nếu forms: Netlify Forms nhận form action nếu form có data-netlify=”true”; else configure webhook.
  • Vercel
  • Vercel supports static folder deploy as well:
npx vercel deploy export/mypage --prod
  • Vercel auto-detects static site and serves index.html with correct headers. For redirects or headers, include vercel.json.
  • S3 + CloudFront
  • Upload to S3 bucket configured for static hosting. Example AWS CLI:
aws s3 sync export/mypage/ s3://my-bucket/mypage/ --acl public-read
  • Set correct Content-Type and Cache-Control headers. If using CloudFront, invalidate distribution after upload:
aws cloudfront create-invalidation --distribution-id XXXXX --paths "/mypage/*"
  • For form handling, configure action to external webhook or Lambda endpoint.
  1. Cảnh báo và best-practices
  • Relative paths: export must use relative paths ./assets/… để site hoạt động khi mở file:// hoặc khi host trên subpath.
  • Fonts licenses: do not include paid fonts unless license allows redistribution.
  • srcset and width/height: always include width and height or container aspect-ratio to avoid CLS.
  • Test exported package by opening export/index.html locally and on a staging host; verify images, forms, and meta tags.
  • Show final report to user with warnings: missing required fields, undersized images, large file sizes, and compliance flags if template health-related.
  1. Quick checklist để tích hợp vào UI export
  • [ ] Render success, head contains viewport/charset.
  • [ ] Theme variables injected and effective in preview.
  • [ ] All image srcset variants generated and present.
  • [ ] Critical CSS inlined if option ON.
  • [ ] HTML/CSS/JS minified without errors.
  • [ ] Assets copied and referenced with relative paths.
  • [ ] Manifest.json created with metadata.
  • [ ] ZIP created and size reported.
  • [ ] Pre-deploy diagnostics passed or user acknowleges warnings.

Kết luận ngắn: pipeline này là flow thực tế để đảm bảo exported sale page tĩnh vừa responsive, vừa nhẹ, và dễ deploy. Trong ứng dụng PC, implement pipeline làm thành task background với progress logs, cho phép cancel và re-run từng bước nếu có lỗi.

SEO cơ bản, performance và accessibility checklist

Mục tiêu: khi bạn xuất một sale page tĩnh từ chương trình, trang phải có metadata chuẩn để search/social, tải nhanh trên mạng di động, và đáp ứng yêu cầu truy cập cơ bản cho người dùng khuyết tật. Dưới đây là checklist thực thi từng bước để tích hợp vào pipeline export và preflight.

  1. Meta tags tối thiểu
  • <meta charset=”utf-8″> và <meta name=”viewport” content=”width=device-width,initial-scale=1″>
  • <title> hợp lệ và mô tả ngắn; meta description 50–160 ký tự.
  • canonical link khi có nhiều đường dẫn cùng nội dung.
  • robots meta khi cần block trang thử nghiệm.
  1. Open Graph và Twitter Card
  • OG tags cơ bản: og:title, og:description, og:image, og:url, og:type.
  • Twitter: twitter:card (summary hoặc summarylargeimage), twitter:title, twitter:description, twitter:image.
  • Ảnh OG: cung cấp kích thước chuẩn (ví dụ 1200×630) trong assets và tham chiếu trực tiếp. Nếu không có ảnh lớn, tránh để og:image trống.
  1. Structured data (JSON-LD) — tùy chọn nhưng khuyến nghị
  • Hỗ trợ schema.org cơ bản: Organization, Product hoặc Event tùy template.
  • Đặt script type=”application/ld+json” trong head với dữ liệu có cấu trúc từ project.meta.
  • Không bịa claim y tế trong structured data; nếu là content sức khỏe chỉ dùng Organization/FAQ/LocalBusiness.
  1. Hình ảnh và loading
  • Luôn cung cấp width và height attribute hoặc CSS aspect-ratio cho <img> để giảm CLS.
  • Sinh srcset và sizes cho hình lớn. Ví dụ variants 480w, 768w, 1200w, 1920w.
  • Dùng <picture> với <source type=”image/webp”> và fallback <img>.
  • Thêm loading=”lazy” cho ảnh ngoài above-the-fold; critical hero nên không lazy nếu cần hiển thị ngay.
  • Kiểm tra: hero desktop < 300 KB tối ưu; mobile variant < 150 KB nếu có thể.
  1. Fonts
  • Sử dụng font-display: swap cho @font-face để tránh FOIT.
  • Nếu self-host font, subset glyphs cho Latin hoặc ngôn ngữ mục tiêu; không inline toàn bộ font trong CSS mặc định.
  • Nếu dùng Google Fonts, cân nhắc preload cho font chính: <link rel=”preload” as=”font”>.
  1. CSS, critical CSS và render-blocking
  • Đặt critical CSS inline cho phần above-the-fold, kích thước lý tưởng < 14 KB.
  • Load phần CSS còn lại với rel=”preload” → onload swap pattern hoặc tách file CSS chính.
  • Giảm render-blocking bằng cách loại bỏ đồng thời các script chặn (defer/async cho JS không cần thiết).
  • Minify CSS và loại bỏ CSS không dùng nếu purge an toàn theo quy trình render-before-purge.
  1. JavaScript tối thiểu
  • Xuất page tĩnh cố gắng không phụ thuộc framework client-side nặng. Nếu có script chỉ dùng để xử lý form/accordion, giữ < 20 KB gzipped.
  • Defer/async scripts không quan trọng cho initial render.
  1. Accessibility cơ bản (kiểm tra trước export)
  • Ảnh có alt text mô tả; nếu ảnh trang trí, alt=””.
  • Các nút và liên kết có accessible name; dùng aria-label khi cần.
  • Tiến trình tab đúng, focus states rõ ràng.
  • Kiểm tra contrast: text chính ít nhất 4.5:1, large text 3:1.
  • Các form có label liên kết với input; thông báo lỗi accessible.
  • Kiểm tra với tool tự động (axe-core) và manual spot checks.
  1. Performance targets cho prototype
  • Time to First Byte không thể kiểm soát kỹ từ export static nhưng khuyến nghị host trên CDN.
  • Largest Contentful Paint (LCP) mục tiêu: < 2.5s trên mobile thực tế; prototype chấp nhận < 3.5s.
  • Cumulative Layout Shift (CLS): < 0.1 với việc nêu rõ width/height hoặc aspect-ratio.
  • Total page weight (light export): aim < 500 KB gzipped (không bao gồm fonts lớn); self-contained có thể lên đến 2–3 MB nhưng cần cảnh báo.
  1. Kiểm tra cuối cùng trước export (preflight)
  • Kiểm tra presence của meta tags quan trọng, og image tồn tại, all images have width/height, srcset present.
  • Run automated accessibility scan and report top 5 issues.
  • Tính toán tổng size export và cảnh báo nếu vượt ngưỡng.
  • Ghi lại manifest metadata (templateId, dataVersion, generatedAt) trong export để trace.

Thực hiện những bước này trong pipeline export và đưa thành warnings/errors rõ ràng trong UI để người dùng fix trước khi public.

Form và lead capture integration options

Mục tiêu: đưa form thu lead hoạt động trên trang tĩnh xuất ra từ chương trình, cho phép marketer cấu hình action dễ dàng và đảm bảo dữ liệu gửi đúng định dạng để xử lý bởi hệ thống bên ngoài.

Tùy chọn tích hợp (mỗi mục có mô tả ngắn, pros/cons, khi dùng)

  1. Webhook trực tiếp
  • Mô tả: form gửi POST JSON hoặc form-encoded tới URL do người dùng cấu hình.
  • Pros: đơn giản, realtime, không cần bên thứ ba; dễ tích hợp với backend nội bộ.
  • Cons: người dùng cần endpoint server; bảo mật phải xử lý (HMAC signature, rate limit).
  • Khi dùng: khách hàng có endpoint xử lý lead.
  1. Zapier / Make (Integromat)
  • Mô tả: form action trỏ tới webhook do Zapier/Make cung cấp; Zap/Scenario xử lý và đẩy tiếp vào CRM, email, Google Sheet.
  • Pros: không cần dev backend; hỗ trợ nhiều tích hợp sẵn; dễ cho marketer.
  • Cons: phụ thuộc dịch vụ bên thứ ba, giới hạn miễn phí, tiềm ẩn latency.
  • Khi dùng: muốn nhanh triển khai nhiều tích hợp mà không code.
  1. Formspree / Getform
  • Mô tả: dịch vụ nhận form POST và forward email hoặc webhook.
  • Pros: thiết lập nhanh, phù hợp với static sites, thường hỗ trợ spam protection cơ bản.
  • Cons: chi phí khi volume lớn, hạn chế tuỳ biến phức tạp.
  • Khi dùng: export landing page tĩnh và cần gửi form về email/Slack nhanh.
  1. Client-side validation + honeypot
  • Mô tả: kiểm tra trước khi gửi; thêm field ẩn chống bot (honeypot) và timestamp.
  • Pros: giảm spam, cải thiện UX, không phụ thuộc server.
  • Cons: không đủ chống bot tinh vi; vẫn cần server-side validation ở endpoint.
  • Khi dùng: luôn bật cả với webhook và services ở trên.
  1. Optional SMTP upload (local send)
  • Mô tả: app cho phép cấu hình SMTP và gửi submission trực tiếp từ máy người dùng khi export hoặc qua server-side run.
  • Pros: không cần endpoint ngoài, có thể dùng tài khoản email doanh nghiệp.
  • Cons: cấu hình phức tạp, deliverability thấp nếu máy cá nhân, cần bảo mật credential.
  • Khi dùng: chỉ cho trường hợp đặc biệt khi không có backend.

Cách export form snippet và mẫu payload

  • Export cung cấp hai dạng snippet: HTML form + minimal JS for fetch, và JSON schema mô tả trường.
  • Ví dụ HTML snippet (POST JSON to webhook):
<form id="leadForm">
  <input name="name" required />
  <input name="email" type="email" required />
  <input name="phone" />
  <input type="hidden" name="projectId" value="{{projectId}}">
  <div style="display:none"><input name="hp_field" /></div> <!-- honeypot -->
  <button type="submit">Gửi</button>
</form>
<script>
document.getElementById('leadForm').addEventListener('submit', async e=>{
  e.preventDefault();
  const data = Object.fromEntries(new FormData(e.target));
  if (data.hp_field) return; // honeypot
  await fetch('{{WEBHOOK_URL}}', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)});
  // show success
});
</script>
  • Ví dụ mẫu payload JSON:
{
  "projectId": "health-hero-01",
  "submittedAt": "2026-07-16T10:00:00Z",
  "name": "Nguyen Van A",
  "email": "a@example.com",
  "phone": "+84123456789",
  "utm": {"source":"facebook","campaign":"summer"}
}

Gợi ý bảo mật và vận hành nhanh

  • Luôn kèm server-side validation cho mọi endpoint. Với webhook, cung cấp header HMAC để verify nguồn. Với Zapier/Formspree, bật CAPTCHA hoặc honeypot. Trong export UI, cho marketer chọn integration type, nhập endpoint/keys, và export snippet kèm ví dụ payload và hướng dẫn server-side verification.

Testing, QA và visual regression

Mục tiêu: đảm bảo mỗi template xuất ra hoạt động đúng trên các trình duyệt và viewport mục tiêu, không bị regress khi sửa template, và ngăn xuất trang có lỗi responsive, accessibility hoặc asset. Dưới đây là checklist và quy trình QA thực tế để bạn tích hợp vào workflow dev và export.

  1. Pre-export checklist (kiểm tra cục bộ trước khi cho phép export)
  • Schema validation: chạy JSON Schema (ajv) cho data project; block export nếu required field missing.
  • Media checks:
  • mọi <img> có intrinsic width/height metadata trong project manifest;
  • hero images >= 800 px width cảnh báo, < 400 px block export;
  • generated srcset entries tồn tại cho các breakpoint.
  • Content sanity:
  • no unbreakable long-strings trong fields; apply word-break rules or warn;
  • rich-text sanitized (no <style> hoặc <script>);
  • CTA URLs valid (http(s) or mailto).
  • Accessibility quick checks:
  • all images have alt text;
  • color contrast check for brand colors against body text; warn if < 4.5:1;
  • form inputs have labels.
  • Export integrity:
  • index.html contains meta viewport and charset;
  • asset paths are relative and present in export folder;
  • form action configured.
  1. Cross-browser matrix (minimum for prototype)
  • Desktop: Chrome latest, Firefox latest, Safari latest, Edge Chromium latest.
  • Mobile: iOS Safari latest, Chrome on Android latest.
  • If customer targets older browsers, add legacy targets and run separate legacy build.
  1. Responsive viewport list (for automated and manual checks)
  • Mobile small: 320×568
  • Mobile medium: 375×812
  • Mobile large: 414×896
  • Tablet portrait: 768×1024
  • Tablet landscape / small desktop: 1024×1366
  • Desktop common: 1366×768 and 1920×1080

Use these sizes for screenshot baselines, interactive testing, and Lighthouse runs.

  1. Visual regression approach
  • Recommended stack:
  • Playwright for rendering pages and capturing screenshots across viewports and browsers.
  • For pixel comparison: Percy if you want hosted diff + UI; or use open-source pixeldiff / pixelmatch for CI-run diffs.
  • Workflow:
  1. On template change branch, generate rendered HTML for canonical sample data and capture baseline screenshots for each viewport.
  2. Compare against main branch baselines. Fail CI on visual delta exceeding threshold (e.g., 0.5% of pixels).
  3. If diffs intentional, update baselines via PR with reviewed change.
  • Tips:
  • Use per-component screenshots (hero, benefits grid) to isolate diffs.
  • Store baselines per template variant and per theme to avoid noisy diffs.
  1. Automated accessibility
  • Integrate axe-core:
  • run axe-core checks in Playwright tests for each rendered page and report violations.
  • Classify violations: critical (must fix), serious (ideally fix), minor (advisory).
  • Also automate contrast checks and ensure keyboard navigation works for interactive components.
  • Block export or mark template as “needs attention” if critical violations present.
  1. Unit tests for template rendering
  • Tests to include:
  • Render template with minimal valid JSON returns non-empty HTML and no exceptions.
  • Render with optional/missing blocks renders fallback behavior (collapsed section, placeholder).
  • Helpers: unit tests for custom Handlebars helpers, verifying output for edge inputs.
  • Snapshot tests: lightweight HTML snapshot for structural regressions (not pixel-perfect).
  • Tooling: Jest for unit tests; use jsdom or Handlebars compile in Node.
  1. CI gating rules for template changes
  • Protect main branch with required checks:
  • Linting and unit test pass.
  • Template schema validation passes against sample data.
  • Rendering unit tests and snapshot tests pass.
  • Visual regression checks pass or have approved baseline updates.
  • Accessibility critical violations = fail.
  • PR requirements:
  • If a template file (layout, partial, css) changed, the PR must include updated sample data and at least one manual review sign-off from a reviewer.
  • Changes to template manifest.requiredFields must increment manifest version and include migration notes.
  1. Manual QA checklist for release
  • Walk through responsive preview list, verify no horizontal scroll and CTAs visible without overlap.
  • Verify images do not cause CLS: load page with slow network throttling.
  • Test forms with configured webhook and third-party providers.
  • Check exported zip deploys to a static host and behaves identically to local preview.
  1. Reporting and developer feedback loop
  • Failures in CI should generate:
  • screenshot diffs,
  • axe violation report with selectors,
  • render logs including missing fields and image warnings.
  • Require attaching these artifacts to PR for faster debugging.

Thực hiện theo quy trình này sẽ giảm hỏng layout do nội dung có sẵn, bắt regress sớm, và giữ export template luôn có chất lượng đủ dùng cho marketing.

Hướng dẫn triển khai prototype và packaging cho Windows/macOS

Mục tiêu phần này: hướng dẫn từng bước để bạn build prototype desktop (Electron hoặc Tauri), đóng gói installer cho Windows và macOS, tạo artifacts để thử nghiệm nội bộ, ghi chú auto-update, và checklist bản phát hành cho testers. Giả định project dùng frontend SPA (React/Vite hoặc Vue/Vite), Handlebars cho template rendering, và xử lý ảnh bằng sharp (Electron) hoặc pipeline client/Rust (Tauri).

  1. Chuẩn bị môi trường dev
  • Yêu cầu chung:
  • Node 16+ (hoặc LTS phù hợp), npm/yarn/pnpm
  • Git repo với hai folder chính: src/renderer (UI) và src/main (Electron main hoặc Tauri frontend + Rust backend)
  • Cài dev deps (example Node):
npm install
# hoặc
pnpm install
  1. Quy trình phát triển nhanh cho Electron (dev run)
  • Khởi tạo project với Vite + React:
npm init @vitejs/app my-app --template react
cd my-app
npm install
  • Thêm Electron skeleton: trong package.json scripts:
"scripts": {
  "dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
  "build:renderer": "vite build",
  "start": "electron ."
}
  • Main process basic (src/main/main.js): mở window và load URL dev server hoặc built file.
  • Chạy dev:
npm run dev
  • Hot reload: bật HMR cho renderer; restart Electron main on file changes bằng electron-reload hoặc nodemon.
  1. Export pipeline và render templates trong app
  • Thực hiện render Handlebars trong main process hoặc workstation worker để tạo index.html: compile template + inject context JSON + theme CSS.
  • Sau render, minify HTML/CSS with html-minifier-terser and cssnano before writing to export folder.
  • Generate responsive images via sharp in main process:
// pseudocode
await sharp(input).resize(1920).toFile('assets/hero-1920.webp');
  1. Packaging Electron với electron-builder (Windows NSIS, macOS dmg)
  • Cài electron-builder:
npm install --save-dev electron-builder
  • Thêm config tối thiểu vào package.json:
"build": {
  "appId": "com.yourcompany.salepage",
  "files": ["dist/**", "main.js", "package.json", "templates/**"],
  "mac": {
    "target": ["dmg"]
  },
  "win": {
    "target": ["nsis"]
  }
}
  • Build renderer trước, sau đó builder:
npm run build:renderer
npm run build // script chạy electron-builder
  • Kết quả: installer .dmg cho macOS và .exe/.msi NSIS cho Windows trong thư mục dist/.
  1. Tauri alternative (lightweight, requires Rust)
  • Cài Rust toolchain và Tauri CLI:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install tauri-cli
  • Tạo frontend bundle:
npm run build // tạo dist
  • Tauri build:
cargo tauri build
  • Kết quả: native bundles; nếu cần xử lý ảnh nặng bằng Rust, viết Tauri command sử dụng crates (image, libvips binding) hoặc gọi external binary.
  1. Deliverables để thử nghiệm nội bộ
  • Windows installer (.exe/.msi) và macOS .dmg
  • Portable export zip sample: một template xuất HTML/CSS tĩnh với assets sẵn
  • Project sample file (.sp-project.json) plus assets để reviewer mở trong app
  • Release notes ngắn: build version, nội dung test chính, known issues
  • Automated smoke test script (sử dụng Playwright) chạm vào: mở app, load project, render preview, xuất zip
  1. Auto-update note
  • Electron: tích hợp electron-updater + Squirrel/NSIS or GitHub releases. Cấu hình publisher và feed URL. Trong prototype, bật auto-update ở chế độ opt-in cho testers; test bằng release draft trên GitHub.
  • Tauri: dùng tauri-updater hoặc host update JSON; requires signed releases on macOS. Với prototype, prefer manual update to avoid notarization steps.
  1. Checklist release cho testers (pre-release)
  • Build artifacts:
  • Installer cho Windows và macOS tersedia
  • Sample project và sample export zip kèm
  • Release notes + checklist test
  • QA quick checks:
  • Installer chạy và cài app, app mở, không crash first-run
  • Mở sample project, all fields hiển thị trong preview
  • Thay ảnh hero bằng drag-and-drop, preview phản hồi và tạo srcset
  • Export static site: unzip export, mở index.html offline, assets hiển thị, meta viewport có mặt
  • Form test: gửi đến webhook mẫu verify nhận payload
  • Security & privacy:
  • App lưu project local trong folder user documents; không gửi dữ liệu lên mạng mặc định
  • Nếu tích hợp auto-update, confirm updates được ký (code signing) hoặc có disclaimer
  • Performance / resource:
  • Test export speed with 10 images; record time; report if > threshold người dùng chấp nhận
  • Testers report template:
  • OS/version, installer used, steps to reproduce, expected vs actual, logs (if any), screenshot or screen recording
  1. Ghi chú build ops ngắn
  • Windows signing and macOS notarization not mandatory cho internal test, nhưng cần cho public release.
  • Sử dụng CI để produce artifacts: pipeline chạy build renderer, run tests, build installers; attach artifacts for testers.

Các bước trên cung cấp luồng build rõ ràng để bạn triển khai prototype, đóng gói cho Windows/macOS, tạo artifacts thử nghiệm nội bộ và checklist kiểm tra release cho testers.

Security, privacy và lưu trữ local-first

Mục tiêu: app mặc định lưu mọi dự án và media hoàn toàn local, giảm rủi ro rò rỉ dữ liệu, và chỉ cho phép đưa dữ liệu lên cloud sau khi người dùng chủ động bật tính năng và đồng ý. Dưới đây là chính sách vận hành và các biện pháp kỹ thuật cần áp dụng cho prototype.

Nguyên tắc chung

  • Local-first by default: tạo project mới thì mọi file, ảnh, JSON project, history đều lưu trong thư mục user (ví dụ Documents/YourApp/projects) và không gửi ra mạng. Không bật sync hay telemetry mặc định.
  • Opt-in cloud sync: tính năng đồng bộ cloud phải rõ ràng, có modal xác nhận, hiển thị scope dữ liệu sẽ upload, và yêu cầu người dùng nhập credentials. Không bí mật gửi token hay dữ liệu.

Lưu trữ token và credentials

  • Không lưu token plain-text. Nếu app dùng Electron, dùng keychain hệ điều hành (ví dụ Keytar) để lưu token; nếu dùng Tauri, dùng OS credential store via tauri plugin hoặc Rust crate tương đương.
  • Khi cần, encrypt token/credential trước khi viết vào disk bằng AES-GCM với key được derive từ OS-provided secret hoặc key stored in keychain. Không hard-code keys trong binary.
  • Xóa token khi user logout hoặc revoke sync.

Media và quyền riêng tư

  • Không upload ảnh tự động. Khi user thêm ảnh vào project, app chỉ copy ảnh vào thư mục dự án local và thực hiện preprocessing local (preview, generate srcset). Nếu có tùy chọn “Upload for CDN/optimize”, luôn hiển thị checklist gồm: files to upload, destination, chi phí/điều khoản, và checkbox đồng ý.
  • Bật cảnh báo bản quyền: khi user import ảnh, hiển thị notice rõ ràng nhớ kiểm tra quyền sử dụng ảnh, và cung cấp checklist: có bản quyền? có consent cho before/after? nếu ảnh từ stock, yêu cầu user dán license id hoặc nguồn. Không tự động mặc định cho phép sử dụng.

Secure IPC và sandboxing

  • Electron: tắt NodeIntegration trong renderer. Dùng preload + contextBridge để expose safe API surface. Thiết kế API allowlist: chỉ cho phép các hành động cần thiết từ renderer như readProjectList, saveProject, exportZip, openFileDialog. Validate từng request ở main process, kiểm tra path traversal, whitelist extensions khi ghi file, và sanitize input. Ghi log lỗi cho audit nhưng không log nhạy cảm.
  • Tauri: cấu hình allowlist chỉ bật API cần thiết (fs, dialog, shell nếu cần). Viết command Rust nhỏ gọn cho các thao tác file hoặc image processing nặng. Tránh bật toàn bộ quyền native.

Validation, encryption at-rest và backups

  • Dữ liệu local có thể được encrypted at-rest tùy chọn: cho phép user bật project-level encryption với passphrase. Khi bật, toàn bộ assets + project JSON được lưu dưới dạng encrypted bundle. Hiển thị rõ cảnh báo về mất passphrase.
  • Tạo backup local automatic optional (kept in same device) và cung cấp export encrypted package để chuyển giữa máy.

Logging, telemetry và third-party

  • Mặc định không bật telemetry. Nếu cần, làm opt-in rõ ràng và minify logs. Khi tích hợp dịch vụ third-party (CDN, image service), hiện rõ tên nhà cung cấp và chính sách lưu trữ; không lưu token của bên thứ ba nếu không cần.

UI & UX an toàn

  • Trong editor, hiển thị badges/warnings khi nội dung chứa thông tin nhạy cảm (ví dụ ảnh before/after) và khi user bật upload. Tránh hành vi tự động gửi hoặc preview live lên dịch vụ bên ngoài.

Những quy tắc này đủ để prototype an toàn: local-first mặc định, token được encrypt/kept in OS keychain khi cần sync, ảnh không upload tự động, cảnh báo bản quyền rõ ràng, và IPC/allowlist được thu hẹp để giảm bề mặt tấn công.

Extensibility: marketplace, plugin helpers, migration và versioning

Mục tiêu: làm cho hệ thống template có thể mở rộng an toàn, cho phép marketplace chia sẻ template, cho phép cài helper bổ sung cho Handlebars, và quản lý version/migration sao cho project cũ vẫn render được. Dưới đây là thiết kế thực dụng để bạn triển khai nhanh.

  1. Template registry – cấu trúc và manifest
  • Mỗi template là folder zip chứa:
  • template.json – metadata và schema
  • layout.hbs + partials/
  • assets/ (css, js, images)
  • migrations/ (scripts chuyển đổi data cũ)
  • template.json mẫu:
{
  "id": "clinic-offer-v1",
  "version": "1.0.0",
  "title": "Clinic Offer",
  "category": "health",
  "requiredFields": ["hero.title","hero.image","cta[0].label"],
  "schemaVersion": 1,
  "compatibility": { "minAppVersion": "0.9.0" },
  "author": "template-studio",
  "license": "proprietary"
}
  • Registry local: SQLite hoặc JSON index trong app data lưu metadata, tags, installedVersion, source (marketplace/local).
  1. Template marketplace – import/export API
  • Export package: zip with template folder structure + manifest signature (HMAC using publisher key). Provide endpoint server-side optional to host packages.
  • Import flow in app:
  • Validate manifest signature or hash.
  • Validate template.json schema against app template manifest schema.
  • Run preflight rendering using sample data to detect missing helpers or assets.
  • Marketplace API endpoints (server side example):
  • GET /templates – list with metadata
  • GET /templates/:id/download – package zip
  • POST /templates/publish – upload package + metadata
  • Client verifies publisher identity and shows changelog and required permissions (e.g., needs custom helper).
  1. Plugin helpers – custom Handlebars helpers
  • Design:
  • Helpers are isolated JS modules implementing a small interface: name, version, apiVersion, init(options), safeTemplatesAllowed.
  • Discovery: helpers can be bundled into app/plugins/ or installed via marketplace. Manifest for plugin lists allowed templates or global.
  • Security:
  • In Electron, load helper in a sandboxed worker process or run under VM with limited APIs. Never eval arbitrary code in renderer.
  • In Tauri, implement helper as WASM/Rust plugin or run Node microservice.
  • API example (pseudo):
  • registerHelper({name:”formatPrice”, fn: (value,opts)=>”…”, version:”1.0.0″})
  • Backward compatibility: helpers carry apiVersion so template engine can reject incompatible helper versions with clear error.
  1. Template versioning và migration scripts
  • Versioning: use SemVer for template versions. Store template.schemaVersion integer for data schema.
  • Migration strategy:
  • Keep migration scripts in migrations/ as ordered files named 001-to-002.js etc. Each script receives project JSON and returns transformed JSON.
  • On template install/update, run migrations whose target > project.schemaVersion in sequence in a sandboxed process.
  • Example migration file signature:
module.exports = async function migrate(data) {
  // mutate and return new data
  if (data.hero && data.hero.titleV2) {
    data.hero.title = data.hero.titleV2;
    delete data.hero.titleV2;
  }
  data.schemaVersion = 2;
  return data;
}
  • Test and rollback: run migrations on a copy; store before/after snapshot; provide rollback if migration fails.
  1. Backward compatibility strategy
  • Render-time fallback: templates should check fields via helpers like hasValue and provide fallbacks. Registry enforces manifest.requiredFields for blocking exports.
  • Compatibility layer: app maintains a mapping table of deprecated fields and common renames to attempt auto-mapping during import.
  • CI for template updates: when publishing new template version, run automated render tests against a set of sample projects to detect breaking changes.

Tóm tắt hành động kỹ thuật

  • Định nghĩa template.json manifest bắt buộc.
  • Hỗ trợ import/export zip với signature.
  • Cho phép plugins helpers nhưng chỉ chạy trong sandboxed process.
  • Migrations là scripts có versioned ordering, chạy tự động trên copy, có rollback.
  • CI kiểm tra rendering across versions để giữ backward compatibility.

Compliance workflow cho nội dung sức khỏe và làm đẹp

Mô tả ngắn: cung cấp một quy trình nhiều lớp để giảm rủi ro vi phạm quảng cáo y tế khi người dùng tạo sale page. Quy trình kết hợp ràng buộc trên template, khối disclaimer bắt buộc, kiểm tra ngôn ngữ theo quy tắc cấu hình được, chế độ xuất draft vs publish, luồng phê duyệt có người kiểm, và hệ thống ghi log/audit cùng UI giúp reviewer thấy rõ cụm từ rủi ro.

  1. Template-level constraints
  • Mỗi template trong nhóm sức khỏe/làm đẹp có manifest khai báo:
  • requiredFields: các trường bắt buộc (ví dụ hero.title, disclaimer.text).
  • prohibitedFields: danh sách trường không cho phép chứa certain claim fields (ví dụ “cureclaim”, “beforeafter_unverified”).
  • contentPolicies: flag bật các kiểm tra cụ thể (e.g., “nomedicalclaims”, “requireconsentfor_images”).
  • mediaRules: yêu cầu kích thước, metadata consent checkbox cho before/after images.
  • Cơ chế áp dụng: khi user chọn template, app enforce UI controls theo manifest. Nếu template yêu cầu consent cho ảnh trước/sau thì trường upload ảnh kèm trường checkbox bắt buộc; nếu thiếu, export bị block.
  1. Mandatory disclaimer block
  • Mỗi template health-themed có vùng disclaimer bắt buộc hiển thị ở vị trí nổi bật (đầu trang hoặc ngay trước CTA) với nội dung template cung cấp mặc định và cho phép tuỳ chỉnh có giới hạn ký tự.
  • App chèn disclaimer cả vào HTML export và metadata JSON; export loại “publish” không cho phép bỏ hoặc ẩn disclaimer.
  • Mẫu disclaimer mặc định phải rõ ràng, ví dụ: “Nội dung mang tính tham khảo. Không phải lời khuyên y tế. Vui lòng tham khảo chuyên gia y tế.” Trường hợp cần điều chỉnh theo luật địa phương, manifest có thể chứa biến locale để chọn text phù hợp.
  1. Rule-based phrase scanner (configurable)
  • Cốt lõi: engine scan text fields theo bộ quy tắc configurable gồm:
  • keyword lists: danh sách từ/cụm từ cấm hoặc rủi ro (ví dụ “chữa khỏi”, “đảm bảo khỏi”, “không cần thuốc”).
  • regex patterns: bắt các cấu trúc như “giảm X% trong Y tuần” hoặc “được chứng minh”.
  • contextual rules: kết hợp trường (nếu field = testimonial và chứa “cure” thì higher risk).
  • Behaviour:
  • mỗi kết quả scan được gán mức rủi ro: low/medium/high và lý do.
  • rules có thể được bật/tắt theo template hoặc theo vùng pháp lý (user chọn target country).
  • rules lưu cấu hình trong file JSON có versioning; admin có thể cập nhật rule set.
  • Hạn chế: đây là lớp cảnh báo tự động, không thay thế phê duyệt con người.
  1. Publish vs Draft export
  • Draft export:
  • Dùng cho review nội bộ; không áp dụng block mạnh. Export có watermark “DRAFT” và không được dùng cho chạy quảng cáo.
  • Publish export:
  • Kiểm tra toàn bộ rules; nếu có high-risk findings hoặc missing required disclaimers thì block export.
  • Publish export ghi log vào audit trail và tạo nhiệm vụ phê duyệt nếu cần.
  • UX: khi user chọn “Publish”, app hiển thị preflight report tóm tắt issues; xuất sẽ bị chặn cho đến khi các blocking issues được giải quyết hoặc admin override kèm lý do.
  1. Human-in-the-loop approval flow
  • Khi scan tìm thấy medium/high risk hoặc template manifest yêu cầu review:
  • Tạo reviewer task trong app: page preview + list highlight cụm từ rủi ro (xem phần UI).
  • Reviewer có 3 hành động: Approve, Request changes (commented), Reject.
  • Approve có thể kèm điều kiện: e.g., “approve for Vietnam only” nếu có điều kiện địa lý.
  • Audit: mọi quyết định lưu với reviewer id, timestamp, reason, và snapshot của nội dung tại thời điểm phê duyệt.
  1. Logging / audit trail
  • Ghi lại:
  • tất cả export attempts (user, templateId, exportMode, result).
  • kết quả phrase scan (ruleId, matchedText, position).
  • reviewer actions (approve/request/reject) với comment.
  • media consents (user provided checkbox, file metadata).
  • Lưu trữ: log dạng append-only local DB (SQLite) trong app data; option export audit log as CSV/JSON cho compliance team.
  • Retention: policy mặc định 180 ngày, cấu hình được.
  1. UI để highlight risky phrases cho reviewer
  • Trong editor preview:
  • những cụm từ bị rule đánh dấu sẽ được highlight inline bằng màu theo mức rủi ro (yellow cho medium, red cho high).
  • tooltip trên highlight hiển thị rule name + suggested remediation (ví dụ: replace “chữa khỏi” bằng “hỗ trợ cải thiện”).
  • sidebar “Compliance Panel” liệt kê tất cả findings, cho phép nhảy tới vị trí text, thêm comment, và tạo task yêu cầu sửa.
  • Export preflight report:
  • tóm tắt counts by severity, list blocking issues, và direct buttons: Fix in editor / Mark as reviewed with justification (admin only).

Thiết lập mặc định cho prototype

  • Bật rule sets cơ bản tiếng Việt/English; mẫu keyword lists cung cấp sẵn.
  • Mặc định health templates require disclaimer + human review if any high-risk match.
  • Không tự động xóa hoặc sửa text của user; chỉ gợi ý và cho phép block export cho publish.
  • Ghi rõ disclaimer rằng app cung cấp hỗ trợ tuân thủ kỹ thuật, không đảm bảo tuân thủ pháp lý; final responsibility thuộc người xuất bản.

Kết thúc phần này bằng cấu trúc hành động: implement manifest-driven constraints, build rule scanner with savable rule sets, add compliance panel and preflight gating, and implement audit logging and reviewer workflow trong app.

Risk register và mitigation kế hoạch

Mình liệt kê các rủi ro chính cho prototype, mức độ ảnh hưởng, likelihood (cao/trung bình/thấp) và hành động mitigation cụ thể để giảm rủi ro khi phát triển và xuất templates.

  1. Templates không responsive (layout vỡ trên mobile)
  • Ảnh hưởng: Cao — trải nghiệm người dùng tồi, mất lead.
  • Likelihood: Cao.
  • Mitigation:
  • Quy định bắt buộc cho mọi template: include width+height attributes, aspect-ratio CSS fallback, object-fit:cover, max-width:100% cho img.
  • Tạo validation pre-export: kiểm tra mọi vùng hình và text overflow; block export nếu hero image missing hoặc image width < threshold (ví dụ 800 px cho hero) và cảnh báo cho marketer.
  • Implement preview automated checks: render tại các breakpoints (320, 480, 768, 1024) và chạy visual diff cơ bản; highlight vùng bị tràn.
  • Thiết kế template theo components responsive chuẩn (single-column fallback) và cung cấp guidelines cho content length (max chars cho tiêu đề/subtitle).
  1. Claims vi phạm pháp luật cho ngành sức khỏe/làm đẹp
  • Ảnh hưởng: Rất cao — rủi ro pháp lý và uy tín.
  • Likelihood: Trung bình cao.
  • Mitigation:
  • Template manifest cho nhóm health/beauty phải include mandatory disclaimer block và require_consent flag cho before/after.
  • Rule-based scanner tích hợp sẵn kiểm tra keyword/regex (configurable) và gắn severity. Block publish export nếu có high-risk matches.
  • Publish flow có hai bước: Draft export (không public) và Publish export (phải qua human-in-the-loop review nếu có high-risk findings).
  • Lưu audit trail cho mọi export publish (user, timestamp, snapshot) để phục vụ compliance review.
  • Hiển thị inline highlight và suggested rephrases cho nội dung bị báo nguy.
  1. Hình ảnh thiếu bản quyền hoặc metadata không đủ
  • Ảnh hưởng: Cao — rủi ro bản quyền, có thể phải gỡ page.
  • Likelihood: Trung bình.
  • Mitigation:
  • Khi user import ảnh, yêu cầu điền nguồn/owner/consent checkbox cho ảnh before/after; lưu metadata trong project JSON.
  • Cảnh báo rõ ràng khi dùng ảnh từ clipboard/link không rõ nguồn; cung cấp placeholders gợi ý và liên kết tới nguồn stock trả phí ngoài app.
  • Trong preflight, nếu bất kỳ ảnh nào không có metadata consent, mark as warning/block publish cho templates health.
  • Hướng dẫn người dùng cách chọn và ghi nhận license; cung cấp mẫu text for licensing.
  1. Kích thước package quá lớn khi export self-contained
  • Ảnh hưởng: Trung bình — khó gửi, deploy chậm.
  • Likelihood: Trung bình cao.
  • Mitigation:
  • Export modes: light (references CDN fonts/images) vs self-contained (include assets). Warn user rõ ràng về kích thước for self-contained.
  • Client-side immediate image preprocessing tạo srcset (480, 768, 1200, 1920) và tạo WebP; optional background native batch optimize (sharp) trước khi export.
  • Limit in-project originals bằng mặc định (auto-resize on import) và cung cấp report kích thước tổng trước zipping.
  • Minify CSS/HTML, optionally remove unused CSS per-template using render-then-purge with safelist rules.
  1. CSS/JS conflicts giữa templates hoặc inline styles từ content
  • Ảnh hưởng: Trung bình.
  • Likelihood: Trung bình.
  • Mitigation:
  • Namespace root class .sp-template-<id> cho toàn bộ CSS của từng template.
  • Sanitize pasted HTML, strip inline style attributes by default; allow advanced override only in template editor with warning.
  • Build per-template CSS bundles; run scoped purge against rendered HTML.
  1. Image processing failures or long blocking tasks
  • Ảnh hưởng: Trung bình.
  • Likelihood: Trung bình.
  • Mitigation:
  • Use hybrid pipeline: fast client-side preview transforms, optional background native optimization. Run heavy tasks off main UI thread and show progress.
  • Retry logic and graceful degrade: if native binary not available, fallback to JS resize and mark for later optimization.

Tổng kết hành động ưu tiên

  • Áp ngay template manifest với required fields và media rules.
  • Bật preflight validation và rule-based scanner cho health templates.
  • Thiết lập image ingestion pipeline hybrid và export modes rõ ràng.
  • Scope CSS bằng namespace và sanitize nội dung.

Roadmap phát triển và milestones cho prototype → production

Mục tiêu: đưa một ứng dụng PC prototype có thể tạo và xuất sale page responsive (ít nhất 8 mẫu minh họa) từ content có sẵn, rồi tiến tới bản beta có authoring template và quy trình tuân thủ, cuối cùng là sản phẩm production với marketplace và analytics. Dưới đây là lộ trình 3 pha, mốc thời gian đề xuất và deliverables cụ thể cho mỗi mốc. Mốc thời gian là khung tham khảo cho một team nhỏ full-stack; bạn điều chỉnh theo nguồn lực thực tế.

Giai đoạn 1 — MVP (8 tuần)

  • Thời gian đề xuất: 6–8 tuần.
  • Mục tiêu chính: có app chạy trên PC (Electron hoặc Tauri) cho phép chọn template, nhập nội dung, preview responsive và xuất HTML/CSS tĩnh cho 8 mẫu minh họa.
  • Deliverables:
  1. Core app infrastructure: project open/save local, assets folder, user project JSON schema.
  2. Template library cơ bản: 8 mẫu minh họa (4 sức khỏe, 4 làm đẹp) dưới dạng template package với template.json manifest.
  3. Editor cơ bản: form-based field editor + inline editing đơn giản + markdown block support.
  4. Preview responsive: switcher desktop/tablet/mobile, live render bằng Handlebars.
  5. Media pipeline cơ bản: client-side image normalization + generate srcset small/medium/large + WebP conversion (JS/canvas) cho preview.
  6. Export static: produce index.html, styles.css, assets/ và zip export. Export mode: light (external fonts) và self-contained.
  7. Validation preflight: required fields check, image size warnings.
  8. Packaging: dev build + Windows/macOS test installers.
  • Kiểm tra hoàn tất: render tất cả 8 templates với sample JSON, test export and deploy to static host manually.

Giai đoạn 2 — Beta (12 tuần)

  • Thời gian đề xuất: 10–12 tuần sau MVP.
  • Mục tiêu chính: mở rộng khả năng authoring template, nâng cấp xử lý media, thêm compliance workflow và QA tự động.
  • Deliverables:
  1. Template authoring UI cho admin: edit partials, upload CSS, define manifest.requiredFields, versioning templates.
  2. Hybrid media pipeline: integrate native optimizer (sharp/libvips) as optional background task for higher-quality WebP/AVIF variants.
  3. Compliance tooling: template flags for health/beauty, mandatory disclaimer block, rule-based scanner (keyword/regex) và publish vs draft export flow with manual approval gate.
  4. Advanced export optimizations: per-template CSS purge (render-then-purge), minify pipeline, critical CSS extraction.
  5. Automated QA: visual regression snapshots for templates + responsive checks across key viewports using Playwright/Puppeteer.
  6. Form integrations: webhook templates, Zapier/Make guides, exportable form payload examples.
  7. Beta packaging and internal distribution + user feedback collection workflow.
  • Kiểm tra hoàn tất: run automated visual tests; compliance scanner tuned on sample corpus; onboard small group of marketers for usability feedback.

Giai đoạn 3 — Production (16–24 tuần)

  • Thời gian đề xuất: 4–6 tháng after Beta.
  • Mục tiêu chính: ổn định, scale, và mở marketplace templates + multi-language + analytics.
  • Deliverables:
  1. Marketplace & template registry: import/export zip, signatures, rating/review, paid/free flags.
  2. Multi-language support: i18n schema, per-template locale variants, RTL handling if cần.
  3. Analytics & A/B: integrate lightweight analytics snippets and export option for A/B variants tracking; dashboard summary in-app for publish stats.
  4. CDN/upload workflow (optional): integrations to upload assets during export to S3/Cloudinary for self-hosted optimized delivery.
  5. Security/compliance hardening: audit logs for publish actions, opt-in telemetry, data encryption for cloud sync.
  6. Installer & release pipelines: automated CI/CD to produce signed installers for Windows/macOS/Linux, update mechanism.
  7. Documentation & onboarding: template author guide, compliance playbook, export/deploy guide for marketers.
  • Kiểm tra hoàn tất: run production-scale tests, marketplace onboarding of third-party templates, finalize legal checklist.

Timeline tổng quát tham khảo: MVP 2 months, Beta 3 months, Production 4–6 months. Mỗi mốc kèm checklist QA và release notes. Prioritize feedback loop giữa marketers và compliance team trong Beta để giảm rủi ro pháp lý trước khi lên Production.

Tài liệu cần viết kèm và checklist handover

Mục tiêu: cung cấp bộ tài liệu đầy đủ để handover dự án prototype ứng dụng PC tạo sale page. Dưới đây là danh sách tài liệu cần giao kèm và outline mẫu cho mỗi file. Mỗi outline là bản điền sẵn để bạn chuyển thành tài liệu hoàn chỉnh.

  1. template.json spec — mục đích: chuẩn hóa package template
  • Header: templateId, version, name, category, author, license
  • fields: danh sách trường với schema (path, type, required, default, validation: maxLength, pattern, aspectRatio)
  • partials: tên partials và file path
  • themeTokens: list variables (color, font, spacing) và giá trị mặc định
  • assets: required asset list với minWidth/minHeight/kind (image/svg)
  • exportRules: breakpoints, criticalCssHints, inlineCritical boolean
  • compliance: flags (healthTemplate boolean, requiredDisclaimer boolean)
  • example: một template.json hoàn chỉnh mẫu cho “Clinical Offer”
  1. Contributor guide cho template authors — mục đích: hướng dẫn tạo template đúng chuẩn
  • Quickstart: cấu trúc thư mục template, naming convention
  • Handlebars conventions: helpers allowed, partial usage, escaping rules
  • CSS scoping: root class pattern .sp-template-<id>, use of CSS variables
  • Images: recommended srcset sizes, naming, metadata fields in manifest
  • Accessibility checklist per template
  • Versioning and changelog format
  • Example: step-by-step tạo một partial hero.hbs và cập nhật template.json
  1. Runbook export — mục đích: quy trình xuất/khắc phục lỗi export
  • Preflight checklist: schema validation, image warnings, compliance flags, missing fields
  • Export steps: render → generate srcset → inject theme → minify → package zip
  • Commands: node scripts / CLI commands kèm ví dụ lệnh
  • Error handling: common errors và cách fix (missing asset, purgeCSS removed classes)
  • Rollback: restore last valid export, where tìm log
  • Example: log snippet và mẫu đầu ra index.html structure
  1. User help cho marketer — mục đích: hướng dẫn sử dụng nhanh
  • Creating project, choosing template, replacing images, editing text (form + inline)
  • Quick tips: recommended hero image sizes, CTA best practices, disclaimer insertion
  • Export options explained: light vs self-contained, webhook form setup
  • Troubleshooting FAQ: image too large, CTA not visible on mobile
  • Example: checklist publish-ready (meta, og image, form action)
  1. QA checklist — mục đích: kiểm tra trước khi giao file cho khách
  • Functional: all CTAs clickable, forms post correctly, links not 404
  • Responsive: list viewports to test (320, 375, 768, 1024, 1366)
  • Visual: compare against reference mock, no layout overflow, images not pixelated
  • Performance/accessibility: meta viewport, alt text, color contrast, lazy-load
  • Export integrity: assets present, paths relative, zip content validation
  • Example: mẫu báo cáo QA pass/fail cho một template
  1. Build + release guide cho DevOps — mục đích: build, package, release
  • Dev prerequisites: Node, Rust (if Tauri), native binaries list (sharp/libvips)
  • Build steps: dev run, production build commands, environment variables
  • Packaging: electron-builder or tauri bundler config snippets, codesign notes
  • CI pipeline: tests to run, artifact storage, auto-update setup
  • Release checklist: notarization, release notes, smoke test
  • Example: sample CI YAML snippet và artifact layout

Kèm mỗi doc: một bản mẫu hoàn chỉnh (filled example) để dev có thể chạy thử ngay. Các file cấu trúc phải versioned và lưu trong repo docs/ với quy ước tên rõ ràng để dễ tra cứu.

Kết luận

Mình tin rằng sau khi bạn áp dụng khung kỹ thuật và checklist này, đội phát triển sẽ có lộ trình rõ ràng để dựng một ứng dụng PC local-first giúp marketer tạo và xuất sale page tĩnh an toàn, nhất quán và dễ kiểm soát. Lưu ý thực tế quan trọng nhất: hãy giữ ranh giới rõ ràng giữa preview client-side và export server-side/native để đảm bảo preview nhanh cho marketer trong khi export vẫn tạo ra HTML/CSS/asset deterministically và tuân thủ mọi kiểm tra preflight.

Plugin WordPress để tăng tốc Google index

Mình trình bày thiết kế và hướng dẫn triển khai plugin WordPress giúp rút ngắn thời gian Google index nội dung và để bạn có bằng chứng vận hành. Ví dụ minh hoạ dùng chiến lược sitemap-first làm mặc định và mô tả khi nào nên bật Google Indexing API.

Mục tiêu đo được của plugin

Mục tiêu của plugin là tạo các tín hiệu có thể đo được để rút ngắn thời gian Google index nội dung quan trọng và để bạn có bằng chứng vận hành. Ba KPI chính cần theo dõi, cách đo và threshold thành công đề xuất:

  • Time-to-index (TtI)
  • Định nghĩa: thời gian từ lúc post trạng thái publish tới khi URL xuất hiện trong index của Google.
  • Cách đo: lấy timestamp publish từ WP; kiểm tra trạng thái index qua Search Console API hoặc URL Inspection API theo mẫu; tính distribution (median, p90).
  • Threshold khuyến nghị: median TtI cho URL ưu tiên <= 24 giờ; p90 <= 48 giờ. Làm configurable theo site.
  • Tỷ lệ URL requested vs indexed
  • Định nghĩa: trong một khoảng thời gian (ví dụ 7 ngày), phần trăm URL mà plugin đã gửi/submit (requests) và sau đó thực sự được index.
  • Cách đo: từ log gửi của plugin (URL, timestamp, response) đối chiếu với dữ liệu index từ Search Console (có index hay không) sau 24h, 72h, 7d.
  • Threshold khuyến nghị: >= 70% indexed sau 7 ngày cho URL ưu tiên; nếu thấp hơn cần điều tra nguyên nhân (cache race, noindex, redirect).
  • Crawl frequency cho URL ưu tiên
  • Định nghĩa: tần suất Googlebot truy cập các URL ưu tiên (crawl per day/week).
  • Cách đo: dùng Search Console crawl stats, server access logs lọc Googlebot UA và IP ranges, hoặc dùng URL Inspection API lịch sử crawl khi khả dụng.
  • Threshold khuyến nghị: tăng gấp tối thiểu 2x crawl rate cho nhóm URL ưu tiên so với trước khi bật plugin, hoặc đạt trung bình >= 1 crawl/ngày cho trang chiến lược.

Cách triển khai đo đạc chung

  • Lưu log gửi (request id, URL, method, payload, response code, timestamp) vào bảng riêng; lưu trạng thái verify trước khi gửi.
  • Lập job so sánh định kỳ giữa log gửi và dữ liệu Search Console để cập nhật KPI.
  • Alert khi: median TtI tăng > 2x baseline, hoặc tỷ lệ requested vs indexed < threshold, hoặc crawl rate không tăng sau 48 giờ.
  • Làm tất cả cấu hình threshold có thể tùy chỉnh trong UI để phù hợp đặc thù site.

Kiến trúc cao cấp của plugin

Mình mô tả kiến trúc cao cấp để bạn và team phát triển có kho hình dung rõ: file chính, autoloading PSR-4, namespace, cấu trúc thư mục chuẩn, composer, bảng tuỳ chỉnh cho queue và logs, cùng hook kích hoạt/hủy và migration. Thiết kế hướng đến testability, dễ bảo trì và an toàn khi lưu credentials.

  1. File chính và bootstrap
  • Tệp chủ: wp-content/plugins/fast-index/fast-index.php — chỉ chứa comment header plugin và tải autoloader rồi khởi tạo App singleton.
  • Ví dụ logic tối giản trong file chủ: tạo instance của \FastIndex\App::bootstrap().
  1. PSR-4 autoloading và composer
  • Thư mục package chuẩn:
  • composer.json
  • src/ (PSR-4 namespace)
  • bin/
  • assets/, views/, tests/
  • composer.json mẫu:
{
  "name": "yourvendor/fast-index",
  "autoload": {
    "psr-4": { "YourVendor\\FastIndex\\": "src/" }
  },
  "require": {
    "php": ">=7.4"
  }
}
  • Tại plugin bootstrap gọi require DIR . ‘/vendor/autoload.php’;
  1. Namespace và lớp chính
  • Namespace gốc: YourVendor\FastIndex
  • Lớp entry points: App, Activator, Deactivator, Migrator, QueueManager, IndexingClient, SitemapManager, AdminController, Logger.
  1. Cấu trúc thư mục đề xuất
  • fast-index/
  • composer.json
  • fast-index.php
  • vendor/
  • src/
  • Admin/
  • Api/
  • DB/
  • Migrations.php
  • Schema.php
  • Queue/
  • Services/
  • Utils/
  • views/
  • assets/
  • tests/
  1. Custom table schema cho queue và logs
  • Bảng queue (wpfiqueue):
CREATE TABLE wp_fi_queue (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  url VARCHAR(2048) NOT NULL,
  action VARCHAR(32) NOT NULL,
  payload JSON NULL,
  status ENUM('pending','processing','sent','failed') NOT NULL DEFAULT 'pending',
  attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
  available_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  last_error TEXT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NULL,
  UNIQUE KEY ux_url_action (url(768), action)
) ENGINE=InnoDB CHARSET=utf8mb4;
  • Bảng logs (wpfilogs):
CREATE TABLE wp_fi_logs (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  queue_id BIGINT UNSIGNED NULL,
  event_type VARCHAR(64) NOT NULL,
  message TEXT,
  meta JSON NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX (queue_id)
) ENGINE=InnoDB CHARSET=utf8mb4;
  • Lưu ý: index url(768) để tránh quá dài, dùng JSON cho payload để mở rộng.
  1. Activation / deactivation / migration hooks
  • Trong Activator::activate():
  • chạy migrations an toàn, tạo bảng nếu chưa có, tạo default options, verify permissions cho storage credential path.
  • Trong Deactivator::deactivate():
  • dọn cron hooks, clear transient, không xóa dữ liệu production mặc định.
  • Migrator::migrate($fromVersion, $toVersion):
  • thực hiện thay đổi schema có transaction khi có thể, ghi log migration, backup schema trước khi thay đổi.
  • Đăng ký hook:
register_activation_hook(__FILE__, [\YourVendor\FastIndex\Activator::class, 'activate']);
register_deactivation_hook(__FILE__, [\YourVendor\FastIndex\Deactivator::class, 'deactivate']);
  1. Lưu ý vận hành
  • Không lưu credentials trong wp_options plaintext; chỉ lưu đường dẫn tới file ngoài document root hoặc khóa mã hoá.
  • Méthod migration phải idempotent để tránh partial-run trên multi-node.
  • QueueManager phải hỗ trợ cả DB-backed và Redis-backed; trong DB mode dùng row-level locking và available_at để tránh race trong multi-node.

Thiết kế này cho phép bạn phát triển tính năng gửi index, hàng đợi, logging và UI quản trị mà vẫn giữ an toàn và khả năng scale khi cần.

Danh sách chức năng tổng quan và luồng dữ liệu

Dưới đây là toàn bộ chức năng chính mà plugin cần có, mỗi chức năng kèm mô tả ngắn về luồng dữ liệu: input → processing → output. Mình viết theo ngôn ngữ kỹ thuật để bạn và team dev có thể chuyển thẳng thành code/đầu mục.

  • Sitemap XML động
  • Input: sự kiện publish/update/delete hoặc cron rebuild.
  • Processing: query posts theo filter, tạo/ghi sitemap shard, cập nhật sitemap index, purge cache nếu cần.
  • Output: sitemap shard XML + sitemap index, HTTP 200 cho GET sitemap.
  • Push tới Google (Indexing API hoặc ping)
  • Input: job enqueue của URL ưu tiên.
  • Processing: xác thực credential nếu dùng API, kiểm tra pre-flight (HEAD), rate-limit via token-bucket, gửi request, parse response.
  • Output: response status (ok/queued/error) + log entry.
  • Feed URL ưu tiên và metadata index
  • Input: rule engine (post_type, taxonomy, custom fields) hoặc admin override.
  • Processing: xây payload metadata (lastmod, priority, crawl-allow), nhóm theo batch nếu cần.
  • Output: feed endpoint (JSON/XML) trả metadata; batch jobs cho gửi API hoặc sitemap update.
  • Tự động ping Google khi post/update/delete
  • Input: transitionpoststatus / save_post hooks.
  • Processing: debounce/coalesce, enqueue verify job, purge caches, gọi https://www.google.com/ping?sitemap=…
  • Output: ping response + log.
  • Quản lý canonical và redirect
  • Input: page render hoặc post meta changes.
  • Processing: validate rel=canonical, detect redirect chains, auto-suggest redirect rules or apply redirect header.
  • Output: canonical link in HTML, 301/302 as configured.
  • Hỗ trợ structured data (schema)
  • Input: post data + template rules.
  • Processing: render JSON-LD, validate (Rich Results Test), attach to head.
  • Output: injected JSON-LD markup in HTML.
  • HTTP headers cấu hình cho crawl
  • Input: per-site/per-post header settings.
  • Processing: hook send_headers, set X-Robots-Tag, Cache-Control, Vary.
  • Output: HTTP headers delivered to clients and bots.
  • Khóa/mở index (lock/unlock)
  • Input: post meta or global toggle.
  • Processing: block auto-send for locked items, mark in queue as ignore, enforce noindex header.
  • Output: noindex headers/meta, queue skip behavior.
  • Bộ lọc chọn URL ưu tiên
  • Input: admin rules, auto-rules, canary settings.
  • Processing: evaluate rule engine at event time, tag job priority, push to high/low queue.
  • Output: prioritized job enqueues.
  • Lịch sử / logs / DLQ
  • Input: mọi hành động gửi/ping/response.
  • Processing: persist structured logs (no secrets), aggregate metrics, move failed jobs to DLQ.
  • Output: admin log UI, alerts, CSV export.

Luồng tổng quát giữa các chức năng: sự kiện publish → rule engine chọn URL → tạo/điều chỉnh sitemap + purge cache → enqueue job vào queue trung tâm → worker verify (HEAD, sitemap presence, canonical, structured data) → rate-limit send (API hoặc ping) → persist result vào logs và cập nhật dashboard.

Tạo và tinh chỉnh sitemap XML động

Mục tiêu: sinh sitemap index + shards sao cho dễ cập nhật từng phần, an toàn khi ghi file, và đảm bảo CDN/cache được purge trước khi plugin gửi tín hiệu tới Google.

  1. Kiến trúc sitemap
  • Dùng sitemap index chứa nhiều sitemap shard. Mỗi shard giữ tối đa 50.000 URL hoặc <= 50 MB chưa nén.
  • Chia shard theo quy tắc hợp lý cho site bạn: post_type, year/month, hoặc hash(bucket) trên post ID. Mục tiêu là hạn chế scope thay đổi khi 1 URL mới xuất hiện.
  1. Format metadata
  • lastmod: ISO 8601, ví dụ 2026-07-16T12:34:56+00:00. Nếu chỉ chính xác tới ngày, dùng YYYY-MM-DD.
  • priority: float 0.0 tới 1.0, 1 chữ số thập phân thường là đủ, ví dụ 0.5 hoặc 1.0.
  • Ví dụ một entry:
<url>
  <loc>https://example.com/post/123</loc>
  <lastmod>2026-07-16T12:34:56+00:00</lastmod>
  <priority>0.8</priority>
</url>
  1. Ghi file atomic
  • Sinh nội dung vào file tạm trên cùng filesystem, fsync đảm bảo ghi xong, rồi rename (atomic move) tới đường dẫn chính. Tránh ghi trực tiếp để không tạo sitemap bị nửa-chưa-kịp.
  • Nén sitemap sang .gz sau khi ghi xong; cập nhật sitemap index tham chiếu đến .gz nếu dùng.
  1. Cache headers và phân phối
  • Trả Content-Type: application/xml; charset=utf-8 và Cache-Control hợp lý cho sitemap (ví dụ Cache-Control: public, max-age=300) tùy chiến lược purge.
  • Ghi ETag và Last-Modified để CDN/edge có thể nhanh kiểm tra.
  1. Purge CDN và verify propagation
  • Khi shard cập nhật: gọi API purge CDN cho cụ thể đường dẫn shard + sitemap index. Nếu CDN cung cấp status propagation, poll đến khi hoàn tất hoặc timeout cấu hình (ví dụ 60s).
  • Nếu CDN không có propagation status, thực hiện sampling HEAD requests từ một vài public vantage points hoặc vendor API region nếu có, kiểm tra HEAD trả 200 và ETag/Last-Modified mới.
  • Chỉ sau khi xác thực propagation thành công mới cập nhật sitemap index và gửi ping/Indexing API.
  1. Cập nhật sitemap index (atomic)
  • Sinh sitemap index tạm, fsync, rename vào vị trí chính. Mỗi <sitemap> entry:
<sitemap>
  <loc>https://example.com/sitemaps/posts-2026-07-16.xml.gz</loc>
  <lastmod>2026-07-16T12:35:10+00:00</lastmod>
</sitemap>
  • Nếu chỉ cập nhật một shard, thay vì ghi lại toàn bộ index, thay file shard rồi atomically replace index after verifying shard propagation.
  1. Hành vi khi publish/update/delete
  • Flow đề xuất: event -> regenerate affected shard -> atomic write -> purge CDN -> verify edges -> update sitemap index atomically -> ping Google (hoặc enqueue Indexing API) only after verification.

Ghi nhớ: luôn test trên staging với same CDN config; giữ hành vi atomic + verify trước khi gửi tín hiệu để tránh race giữa sitemap chưa phân phối và thông báo tới Google.

Gửi thông báo tới Google: hai lựa chọn và khuyến nghị mặc định

Tóm tắt kết luận chính: ưu tiên cập nhật sitemap và ping trước, chỉ dùng Google Indexing API cho một tập URL ưu tiên khi bạn có credential an toàn và quota phù hợp.

  1. Sitemap + ping (truyền thống)
  • Cách hoạt động: plugin sinh hoặc cập nhật sitemap XML, sau đó gọi endpoint ping của Google hoặc để Google crawl theo lịch. Không cần credential để ping; để submit sitemap programmatically bạn có thể dùng Search Console API nhưng đó là bước tuỳ chọn.
  • Ưu điểm: đơn giản, ít rủi ro bảo mật, tương thích với mọi loại nội dung, phù hợp cho khối lượng lớn, không cần quản lý key.
  • Nhược điểm: không có phản hồi per-URL tức thời; Google quyết định thời điểm crawl; ping sitemap cho nhiều thay đổi không giúp index tức thời.
  • Khi dùng: mặc định cho hầu hết plugin, đặc biệt site có nhiều URL hoặc không muốn lưu credential.
  1. Google Indexing API (direct)
  • Cách hoạt động: gửi yêu cầu API để báo Google về URL mới hoặc thay đổi. Yêu cầu xác thực bằng Google Cloud credentials (service account) và trang phải thuộc quyền sở hữu Search Console cho account đó.
  • Yêu cầu credential: tạo Google Cloud project, tạo service account, cấp quyền phù hợp, lưu file JSON trên server an toàn (outside webroot, perms chặt). Không commit file vào repo.
  • Ưu điểm: phản hồi per-request, thích hợp cho URL quan trọng cần index nhanh; dễ log và audit.
  • Nhược điểm: không áp dụng cho mọi loại nội dung theo chính sách Google; có quota và rate limit; nếu credential lỏng lẻo sẽ tạo rủi ro bảo mật; vận hành phức tạp hơn (rate limiter, retry, DLQ).
  • Khi dùng: chỉ cho URL ưu tiên (ví dụ landing page chiến dịch, job postings nếu API hỗ trợ), khi bạn có quy trình quản lý credential, rate limiting và monitoring.

Khuyến nghị mặc định

  • Chế độ mặc định của plugin: sitemap-first. Tự động cập nhật sitemap và ping Google; chỉ đưa Indexing API vào cho nhóm URL được admin đánh dấu ưu tiên. Bật API cần bước kiểm tra credential, bật chế độ canary (ví dụ 5% hoặc chỉ admin), và có circuit breaker khi nhận nhiều lỗi 4xx/429. Chuyển hoàn toàn sang API khi: loại nội dung được API hỗ trợ, bạn có credential an toàn, quota đủ và hệ thống hàng đợi + rate limit hoạt động ổn định.

Tạo feed URL ưu tiên và metadata index

Mục tiêu: cung cấp một endpoint feed (JSON hoặc XML) liệt kê các URL ưu tiên kèm metadata để hệ thống nội bộ hoặc bên thứ ba (crawler/worker) dễ đọc và quyết định gửi tới Google. Feed phải idempotent, paginated và có lastmod global.

Thiết kế endpoint

  • Path gợi ý: /wp-json/myindexer/v1/priority-feed hoặc /priority-feed.xml
  • HTTP: GET, trả 200, cacheable (Cache-Control) và hỗ trợ conditional GET (ETag / Last-Modified)

Trường metadata cần có (per-URL)

  • url: string đầy đủ
  • priority: number 0.0 – 1.0
  • lastmod: ISO 8601 datetime
  • crawl_allow: boolean (true = cho crawl; false = blocked)
  • source: string (post_type hoặc rule id)
  • reason: short string (ví dụ “high-traffic”, “campaign”)

Ví dụ JSON hoàn chỉnh

{
  "generated_at": "2026-07-16T12:00:00+00:00",
  "page": 1,
  "per_page": 100,
  "urls": [
    {
      "url": "https://example.com/blog/post-123",
      "priority": 0.9,
      "lastmod": "2026-07-16T11:45:00+00:00",
      "crawl_allow": true,
      "source": "post",
      "reason": "editorial-feature"
    }
  ]
}

Ví dụ XML hoàn chỉnh

<?xml version="1.0" encoding="utf-8"?>
<priorityFeed generated_at="2026-07-16T12:00:00+00:00" page="1" per_page="100">
  <url>
    <loc>https://example.com/blog/post-123</loc>
    <priority>0.9</priority>
    <lastmod>2026-07-16T11:45:00+00:00</lastmod>
    <crawl_allow>true</crawl_allow>
    <source>post</source>
    <reason>editorial-feature</reason>
  </url>
</priorityFeed>

Cách admin set rules

  1. Rule editor UI: rule = {name, enabled, priority, scope, conditions, ttl}
  • scope: posttype / taxonomy / customfield / URL regex
  • conditions: ví dụ posttype = product AND taxonomy = “featured” OR meta[ispromoted] = true
  1. Priority logic: rules được đánh thứ tự ưu tiên; cao nhất thắng. Admin có thể override trên từng post với meta box “Index priority” và checkbox “Bypass rules”
  2. Sampling & limits: cho phép canary percent và per-rule daily cap để tránh gửi hàng loạt
  3. Expose test panel: nhập URL và xem rule match, effective metadata trả về

Ghi chú vận hành ngắn: feed phải cung cấp pagination và checksum; khi rules thay đổi, invalidate cache feed để các worker thấy metadata mới.

Tự động ping Google khi post/update/delete

Mô tả ngắn: bắt các sự kiện publish/update/delete, gom các event trong cửa sổ thời gian, đẩy vào hàng đợi có retry và rate-limit, trước khi gửi thực hiện các kiểm tra xác thực (HEAD, canonical, noindex) và purge cache/sitemap nếu cần.

Các hook để bắt event

  • Use transitionpoststatus để phát hiện chuyển sang publish và từ publish đi ra:
  • kiểm tra: $oldstatus !== ‘publish’ && $newstatus === ‘publish’ để chỉ gửi khi thực sự public.
  • dùng save_post để bắt cập nhật metadata quan trọng (cần debounce).
  • dùng wptrashpost và delete_post để bắt xóa/trashed events.
  • nếu hỗ trợ REST publish, lắng nghe restafterinsert_post hoặc hook phù hợp cho REST workflow.

Debounce / coalesce logic (điểm quan trọng)

  1. Khi sự kiện tới, không gửi ngay. Thay vào đó upsert vào queue key bằng cách coalesce theo URL/post ID.
  2. Sử dụng transient hoặc Redis key per-post: setex(“indexing:pending:{$postid}”, $delay, jsonmeta). Nếu key đã tồn, cập nhật dữ liệu nhưng giữ same expiry để gom các update trong cửa sổ (ví dụ default 10s).
  3. Sau expiry, worker lấy job từ queue chung và gửi. Điều này giảm duplicate và race với cache/CDN.

Enqueue job pattern

  • Producer (hook) chỉ do: validate basic, write/upsert job to persistent queue (Redis list or custom DB table) with fields: postid, url, action, attempts=0, nextrun_at.
  • Consumer: long-running worker (supervisord/systemd) pop job atomically, acquire global rate-token (token-bucket in Redis), process job.

Xác thực trước khi gửi

  • Purge caches first: call CDN API + local fullpage cache purge; wait hoặc poll vendor purge status if available.
  • Verify URL by HEAD/GET (follow limited redirects):
  • expect final HTTP 200
  • ensure no X-Robots-Tag: noindex header
  • ensure canonical link element (or header) points to expected URL
  • Verify sitemap shard contains URL or sitemap index updated and reachable (HEAD sitemap URL with new Last-Modified or ETag)
  • If any check fail, requeue with exponential backoff and jitter; after max attempts move to DLQ and notify admin.

Ví dụ luồng publish → enqueue → send (pseudocode)

add_action('transition_post_status', function($new, $old, $post){
  if($old !== 'publish' && $new === 'publish'){
    $url = get_permalink($post);
    upsert_pending_index_job($post->ID, $url, 'publish', $delay_seconds = 10);
  }
}, 10, 3);

Worker pseudocode (simplified)

while($job = pop_due_job()){
  if(!acquire_rate_token()) { requeue_with_delay($job, 5); continue; }
  purge_caches($job->url);
  if(!verify_url_ok($job->url)) { retry_or_dlq($job); continue; }
  send_index_request($job->url); // Indexing API or ping sitemap
  record_log($job, result);
}

Ghi chú vận hành ngắn

  • Default delay 10s; config cho site với CDN chậm tăng lên 30s.
  • Luôn có max_retries + DLQ; log an toàn (không lưu credential). Kiểm tra quyền người dùng có thể trigger manual send.

Quản lý canonical và redirect để tránh nội dung trùng lặp

Mục tiêu: đảm bảo mỗi URL gửi tới Google là phiên bản chính xác, không bị redirect vòng, và có canonical rõ ràng. Mình trình bày cách plugin xác minh, phát hiện chuỗi redirect, gợi ý sửa, và hành vi khi canonical không đúng.

Cách plugin xác minh canonical/rel

  • Fetch URL nội bộ bằng HEAD/GET (theo redirect tối đa 5 lần) và parse thẻ <link rel=”canonical”> trong HTML, đồng thời kiểm tra header Link và rel=canonical.
  • So sánh canonical value với URL hiện tại: nếu khác domain hoặc path, đánh dấu mismatch.
  • Kiểm tra header X-Robots-Tag cho noindex hoặc nofollow.

Detect redirect chains

  • Khi fetch, lưu toàn bộ redirect trace: [request_url | status | location header].
  • Nếu số bước > 1 hoặc có redirect loop, gắn cảnh báo chaindetected và record finalstatus_code.
  • Nếu finalstatuscode là 3xx và canonical khác final URL, đánh dấu conflict.

Gợi ý sửa và hành vi tự động

  • Nếu canonical trỏ sang self nhưng trang trả non-200: hoãn gửi, purge cache, re-verify; requeue với backoff.
  • Nếu canonical trỏ sang khác trong cùng domain: đề nghị admin chỉnh canonical trên post meta hoặc tạo 301 từ URL cũ về canonical. UI hiển thị:
  • Khuyến nghị 1: cập nhật <link rel=”canonical”> thành URL mong muốn.
  • Khuyến nghị 2: nếu URL nên chuyển hẳn, thiết lập server 301 permanent và update sitemap shard.
  • Nếu canonical trỏ ra domain khác hoặc staging: mặc định block auto-send và chuyển job sang DLQ, show urgent admin alert.

Mẫu pseudocode kiểm tra (ví dụ để worker dùng)

$response = http_get($url, ['max_redirects'=>5]);
$trace = $response->redirect_chain; // array of {url, status, location}
$final = $response->final_url;
$canonical = parse_canonical($response->body) ?: header_link_canonical($response->headers);
if($response->status != 200) { retry_or_dlq(); }
if($canonical && normalize($canonical) !== normalize($final)) { flag_mismatch($url, $canonical, $final); }

Hỗ trợ structured data cơ bản

Mục tiêu: chèn JSON-LD hợp lệ, an toàn, có thể bật/tắt theo post type, preview trong admin và validate trước khi render ra frontend.

Nguyên tắc chính

  • Sinh JSON bằng mảng/obj PHP rồi encode bằng wpjsonencode. Tuyệt đối không nối chuỗi thủ công từ input chưa lọc.
  • Chỉ inject trong frontend khi post public và không có meta robots noindex.
  • Cho phép bật/tắt per post type trong trang cài đặt plugin; theo dõi capability manage_options.

Mẫu JSON-LD (complete ready-to-use)

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Tiêu đề bài viết",
  "datePublished": "2026-07-16T12:00:00+00:00",
  "author": {
    "@type": "Person",
    "name": "Tên tác giả"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Tên site",
    "logo": {
      "@type": "ImageObject",
      "url": "https://example.com/logo.png"
    }
  }
}

Kỹ thuật chèn an toàn (pattern)

  • Tạo builder function build_schema($post) trả array.
  • Trước echo trong wp_head dùng:
echo '<script type="application/ld+json">' . wp_json_encode($schema) . '</script>';
  • Kiểm tra quyền: if ( ! applyfilters(‘mypluginschemaenabledfor_post’, true, $post) ) return;

Validate trước khi render

  • Server-side: kiểm tra wpjsonencode không false, bắt lỗi JSON.
  • Optional: gọi Google Rich Results Test hoặc use third-party schema validator trong CI / staging; trong plugin cung cấp nút “Validate” gọi URL Inspection API hoặc Rich Results Test (tuỳ quyền) cho preview URL.

Preview trong admin

  • Meta box hiển thị generated JSON-LD; nút Preview gửi request tới REST endpoint nội bộ trả HTML-fragment chứa script tag – REST endpoint yêu cầu nonce và currentusercan.
  • Cho phép edit trường cơ bản (title, author, date) trong meta box và xem JSON-LD cập nhật tức thì bằng JS (client-side preview) trước lưu.

Bật/tắt per post type

  • Cài đặt plugin: checklist các post type; lưu option; khi render kiểm tra post_type có bật hay không.
  • Cung cấp per-post override checkbox “Disable schema for this post” trong meta box.

Bảo mật và rủi ro ngắn

  • Không include dữ liệu nhạy cảm (IDs, internal URLs). Khi dùng user-provided fields, sanitizetextfield và giới hạn chiều dài. Không ghi credential vào schema.

Header HTTP liên quan tới crawl có thể cấu hình

Mục đích phần này là mô tả những header HTTP quan trọng ảnh hưởng tới hành vi của bot tìm kiếm, cách plugin cung cấp cấu hình ở các scope site/global/post-level, và ví dụ code minh họa hook vào sendheaders để thiết lập header an toàn, idempotent, và kiểm soát được. Những header chính cần quan tâm: X-Robots-Tag, Cache-Control, Vary. Bạn phải đảm bảo header được set trước khi PHP bắt đầu xuất nội dung (hook sendheaders) và không ghi credential hay thông tin nhạy cảm vào header.

  1. Quy tắc thiết kế ngắn
  • Scope cấu hình: site-wide default, per-post-type / post meta override, và admin UI cho phép set global policy. Thang ưu tiên: post-level > post-type > global.
  • Thực thi: set header trong action send_headers với priority cao (ví dụ 20), nhưng trước khi output buffer flush.
  • An toàn: mọi giá trị header phải sanitize; không dùng dữ liệu do user bỏ vào trực tiếp mà không lọc; chỉ admin role mới thay đổi cấu hình global; per-post override chỉ hiển thị cho roles có capability phù hợp.
  • Idempotency: khi nhiều plugin cùng set header, hãy merge logic và không ghi header mâu thuẫn (ví dụ không cùng lúc gửi X-Robots-Tag: noindex và meta robots index).
  1. X-Robots-Tag
  • Vai trò: chỉ định cho crawler (kể cả non-HTML resources như PDF) về index/follow ở level HTTP.
  • Giá trị phổ biến: noindex, nofollow, noarchive, nosnippet, none (tương đương noindex,nofollow).
  • Ví dụ use-cases:
  • Site under maintenance: global X-Robots-Tag: noindex, nofollow (chỉ khi bạn chắc chắn).
  • Post nháp/private: per-post X-Robots-Tag: noindex.
  • Resource non-public (PDF nội bộ): X-Robots-Tag: noindex
  • Lưu ý quan trọng: X-Robots-Tag overrides HTML meta robots chỉ ở mức HTTP; nếu cả hai tồn tại, Google có thể kết hợp nhưng header HTTP có thể mạnh hơn cho non-HTML. Tránh vô tình set X-Robots-Tag: noindex cho toàn site.
  1. Cache-Control và Vary
  • Cache-Control: điều hướng cache behavior cho CDN, reverse proxy, browser. Các giá trị thường dùng:
  • Cache-Control: no-cache, no-store, max-age=0, public, private, s-maxage.
  • Hướng dẫn:
  • Đối với trang public muốn quick index: dùng Cache-Control: public, max-age=60 cho trang có nội dung ổn định trong ngắn; tuy nhiên cần cân nhắc CDN propagation. Nếu muốn khả năng purge dễ hơn, set s-maxage cùng phù hợp với CDN.
  • Đối với trang có session/user-specific content: Cache-Control: private, no-cache.
  • Vary: chỉ rõ biến thể cache (User-Agent, Accept-Encoding). Đừng thêm Vary: User-Agent để phân biệt Googlebot vì làm tăng cache fragmentation; chỉ thêm Vary khi thực sự cần (ví dụ Vary: Accept-Encoding cho gzip).
  1. Cách set header qua hook send_headers và scope
  • Mẫu code đầy đủ, an toàn, với per-post override và global option. Đặt file key/option bằng UI admin, nhưng code sau minh họa logic set header. Chú ý sanitize và capability checks như trong comments.
<?php
namespace MyPlugin\IndexingHeaders;

defined('ABSPATH') || exit;

class Headers {
    public static function init() {
        add_action('send_headers', [__CLASS__, 'maybe_set_index_headers'], 20);
    }

    // Lấy giá trị có thứ tự ưu tiên: post meta -> post type option -> global option
    protected static function get_effective_header_values() {
        $defaults = [
            'x_robots' => get_option('myplugin_headers_x_robots', ''), // e.g. 'index, follow'
            'cache_control' => get_option('myplugin_headers_cache_control', ''), // e.g. 'public, max-age=60'
            'vary' => get_option('myplugin_headers_vary', ''), // e.g. 'Accept-Encoding'
        ];

        if (is_singular()) {
            $post_id = get_queried_object_id();
            if ($post_id) {
                $pm = get_post_meta($post_id, '_myplugin_headers', true);
                if (! empty($pm) && is_array($pm)) {
                    // Only override keys that are non-empty strings
                    foreach (['x_robots','cache_control','vary'] as $k) {
                        if (! empty($pm[$k]) && is_string($pm[$k])) {
                            $defaults[$k] = sanitize_text_field($pm[$k]);
                        }
                    }
                }
            }
        }

        // Normalize empty strings to null
        foreach ($defaults as &$v) {
            $v = strlen($v) ? $v : null;
        }

        return $defaults;
    }

    public static function maybe_set_index_headers() {
        // Early bail-outs: admin area, logged-in users for preview, REST API requests
        if ( is_admin() || (defined('REST_REQUEST') && REST_REQUEST) ) {
            return;
        }

        $vals = self::get_effective_header_values();

        // X-Robots-Tag
        if ($vals['x_robots']) {
            // Allow list check: only permit safe directives
            $allowed = ['index','noindex','follow','nofollow','noarchive','nosnippet','unavailable_after','noimageindex'];
            $parts = array_map('trim', explode(',', strtolower($vals['x_robots'])));
            $parts = array_values(array_intersect($parts, $allowed));
            if (! empty($parts)) {
                header('X-Robots-Tag: ' . implode(', ', $parts));
            }
        }

        // Cache-Control
        if ($vals['cache_control']) {
            // Minimal validation: disallow "private" on public pages unless explicitly set
            $cc = trim($vals['cache_control']);
            header('Cache-Control: ' . $cc);
        }

        // Vary
        if ($vals['vary']) {
            header('Vary: ' . trim($vals['vary']));
        }
    }
}

Headers::init();
  1. Test và kiểm tra
  • Kiểm tra bằng curl từ nhiều vantage points, không chỉ server:
# Kiểm tra header từ server public
curl -I -L https://example.com/some-post
# Kiểm tra presence of X-Robots-Tag and Cache-Control
  • Kiểm tra Google behavior: trong Search Console -> URL Inspection kiểm tra cách Googlebot nhìn thấy header. Nếu dùng Indexing API, vẫn verify headers.
  1. Nhắc về race và cache propagation
  • Khi bạn publish và ngay lập tức purge CDN, hãy nhớ rằng CDN propagation có thể delay; nếu plugin set header trên origin nhưng CDN vẫn phục vụ cached copy có header cũ, Google có thể nhận giá trị cũ. Cách giảm rủi ro:
  • Purge cache tập trung cho URL và sitemap shard trước khi gửi index signal.
  • Kết hợp delay-enqueue-verify: enqueue job với delay ngắn (5-30s), trong job verify header và HTTP 200 từ edge/POP hoặc kiểm tra CDN purge status.
  1. Bảo mật và rủi ro
  • Không cho phép editor tự do nhập X-Robots-Tag bất kỳ vì có thể gây de-index. Chỉ admin có capability manage_options có quyền set global defaults; per-post override nên require explicit meta box với capability authorize.
  • Sanitize mọi input; log header changes but never log full credentials.
  • Không expose header-setting endpoints công khai. REST endpoints cần permission_callback và nonce.

Kết luận ngắn

  • X-Robots-Tag, Cache-Control, và Vary là công cụ mạnh để điều khiển crawl/index. Thiết kế plugin phải cung cấp scope cấu hình (global, post-type, post), implement via send_headers, validate/sanitize mọi giá trị, và kết hợp purge + verify để tránh race với CDN. Test đa vùng và monitor Search Console để đảm bảo header thực tế mà Google thấy khớp với kỳ vọng.

Khóa/tạm khóa index cho trang nháp/private và mở index khi public

Mục tiêu: cho phép gắn cờ per-post để chặn mọi thông báo index tự động cho các trạng thái không public, và tự động mở khi post chuyển sang public, kèm tuân thủ quyền quản trị và bulk ops.

  1. Data model
  • Post meta key: mypluginindex_locked (values: ‘1’ locked, ‘0’ or absent unlocked).
  • Post meta boolean khác: mypluginapi_eligible (đánh dấu thủ công nếu muốn gửi qua Indexing API).
  1. Hook workflow cơ bản
  • Dùng transitionpoststatus để bắt thay đổi trạng thái:
  • Nếu $newstatus !== ‘publish’ thì setpostmeta($postid, ‘mypluginindex_locked’, ‘1’).
  • Nếu $newstatus === ‘publish’ và existing lock === ‘1’, removepost_meta(…) hoặc update to ‘0’ rồi enqueue verify job.
  • Trong job gửi: luôn kiểm tra post meta mypluginindex_locked trước khi gửi; nếu locked, abort.
  1. UI/Admin
  • Meta box per-post: toggle “Tạm khóa index” (capability currentusercan(‘manage_options’) hoặc custom capability).
  • Bulk operations: trong list table thêm row action and bulk action to lock/unlock selected posts. Bulk actions phải check capability and log actions.
  • Settings: global default auto-unlock on publish (checkbox). Provide dry-run and preview.
  1. Safety checks trước gửi
  • Before enqueue/send:
  • verify post_status === ‘publish’
  • verify mypluginindex_locked not set
  • verify canonical and HTTP HEAD return 200
  • If any check fails, requeue with backoff and notify admin after N failures.
  1. Pseudocode mẫu (hook)
add_action('transition_post_status', function($new, $old, $post){
    if ($new !== 'publish') {
        update_post_meta($post->ID, '_myplugin_index_locked', '1');
        return;
    }
    // becoming public
    if ($old !== 'publish' && get_post_meta($post->ID, '_myplugin_index_locked', true) === '1') {
        delete_post_meta($post->ID, '_myplugin_index_locked');
        // enqueue verification job that will check CDN/HEAD before actual send
        MyPlugin\Queue::enqueue_verify_and_send($post->ID);
    }
}, 10, 3);

Ghi chú vận hành ngắn: luôn tắt auto-send trên staging; audit bulk unlock vì dễ gây gửi hàng loạt; log mọi unlock action để trace.

Bộ lọc chọn URL ưu tiên

Mục tiêu: cung cấp engine chọn lọc rõ ràng, có thể mở rộng và audit được để quyết định URL nào được gửi ưu tiên tới Google. Kết quả giao được: rule engine cấu hình được (post_type, taxonomy, tag, custom fields, URL pattern), override từng URL trong UI, cơ chế canary sampling, và hàm validator per-URL chạy trước khi enqueue gửi.

  1. Mô hình dữ liệu (tối thiểu)
  • rules table: id | name | priority | enabled | rulejson (mảng điều kiện) | action (api|sitemap|ignore) | createdby | updated_at
  • overrides table (per-post): postid | overrideaction | reason | overridden_by | timestamp
  • canary config: percentage (0-100) | scope (global|posttype|ruleid)
  • log: postid | ruleid | decision | validator_result | timestamp
  1. Cấu trúc rule cơ bản (logical)
  • Điều kiện hỗ trợ: posttype IN […], taxonomy IN […], tag IN […], customfield match (equals/exists/regex), URL pattern (glob/regex), lastmod within N days, author capability, numeric priority threshold.
  • Các rule kết hợp theo thứ tự priority giảm dần; first-match wins hoặc merge actions theo cấu hình.
  1. Admin override
  • Per-post meta box: radio {auto, forcesend, blocksend} + note. Thao tác phải require capability manage_options hoặc custom capability.
  • Bulk unlock/force via list table với confirm modal showing estimate number of requests.
  1. Canary sampling
  • Khi rule chọn “api” và canary enabled, apply sampling: roll = hash(postid + today) % 100 < canarypercentage. Những URL trúng canary đi API, phần còn lại fallback sitemap-only. Log sampling decision.
  1. Per-URL eligibility validator (run inside delayed job before send)

Các bước kiểm tra, nếu bất kỳ fail thì abort hoặc requeue:

  • checkoverride: if override == blocksend -> abort.
  • HTTP HEAD (follow redirects, limit 3) -> final status 200 or 3xx to allowed target.
  • check headers: no X-Robots-Tag: noindex; check meta robots in HTML for noindex.
  • canonical: parse rel=canonical in HTML; canonical must match expected URL or be allowed mapping.
  • sitemap membership: ensure sitemap shard includes URL or mark for sitemap update first.
  • content-type: text/html or expected MIME for that content.
  • anti-SSRF: resolve host and ensure not private IP; enforce allowlist domain matching site domain.
  • DB/read consistency: if infrastructure uses read replicas, verify source of truth or delay until master reflects row.
  1. Pseudocode quyết định (ngắn)
if (get_post_meta($id,'_override') === 'block') return 'abort';
$rule = RuleEngine::find_matching($post);
if (!$rule) return 'skip';
if ($rule->action === 'api' && Canary::sample($post,$rule)) {
  if (Validator::check($url)) Queue::enqueueApi($url);
} else {
  if (Validator::check($url)) Queue::enqueueSitemap($url);
}
  1. Vận hành và an toàn
  • Default policy: conservative – only specific whitelisted post_types enabled for auto API sends.
  • Audit trail for every decision; expose UI filter to list posts selected by each rule and export sample.
  • Khi thay rule lớn (affects many URLs) show estimate and require explicit confirmation.

Thiết kế này giúp bạn kiểm soát tỷ lệ gửi, audit lại quyết định, và tránh gửi nhầm URL chưa ready.

Lịch sử/log sự kiện và dashboard quản trị

Mục tiêu: ghi lại mọi sự kiện liên quan tới gửi tín hiệu index, phản hồi từ Google, trạng thái queue và DLQ, để audit, debug và vận hành an toàn. Log không lưu credential hoặc token thô.

Ví dụ schema bảng chính (MySQL/InnoDB)

CREATE TABLE wp_idx_events (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  post_id BIGINT UNSIGNED NULL,
  url VARCHAR(2048) NOT NULL,
  event_type VARCHAR(32) NOT NULL, -- publish | update | delete | sitemap_submit | manual
  action_target VARCHAR(16) NOT NULL, -- api | sitemap
  rule_id INT NULL,
  status VARCHAR(32) NOT NULL, -- pending | processing | success | failed | dlq
  attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
  last_error TEXT NULL, -- truncated, sanitized, no secrets
  http_status SMALLINT NULL,
  response_snippet VARCHAR(1024) NULL, -- short excerpt of response, redacted
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  INDEX (post_id),
  INDEX (status),
  INDEX (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DLQ (dead-letter) table

CREATE TABLE wp_idx_dlq (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  event_id BIGINT UNSIGNED NOT NULL,
  post_id BIGINT UNSIGNED NULL,
  url VARCHAR(2048) NOT NULL,
  failed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  failure_reason TEXT NULL,
  metadata JSON NULL,
  INDEX (failed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Chính sách retention

  • Logs trạng thái success: keep 90 days.
  • Logs failed / dlq: keep 365 days or until manual purge.
  • Auto-purge job chạy nightly, ghi audit entry trước khi xóa.
  • Không ghi credential, token; nếu response chứa token, sanitize trước khi persist.

Dashboard quản trị – tính năng giao diện

  • Table list: cột id | time | url | post title | eventtype | actiontarget | status | attempts | httpstatus | lasterror (truncated).
  • Filter: date range, status, eventtype, actiontarget, posttype, ruleid, text search URL.
  • Sortable by created_at, attempts, status.
  • Row actions: view full sanitized log (no secrets), requeue (if not dlq), move to dlq, mark resolved, export single row JSON.
  • Bulk actions: requeue selected, move selected to dlq, export CSV.
  • Export: CSV/JSON export of selected rows; export omits any field flagged sensitive.
  • DLQ view: timeline of failures, failure_reason grouping, retry suggestion, manual retry button with dry-run toggle.
  • Health panel: counts by status (pending, processing, success, failed, dlq), queue length, last successful send, recent 429/403 rate.
  • Audit trail: who performed manual actions (user id, ip, timestamp) stored separately in admin_audit table.

Operational notes (ngắn)

  • Logs phải write-append; batch inserts for throughput.
  • Sanitize function must strip Authorization headers, private_key, email addresses in response bodies.
  • Provide API endpoint for exports protected by capability and nonce; rate-limit exports to avoid data leakage.

UI/UX quản trị plugin

Giao diện admin tập trung cho bạn phải rõ ràng, an toàn và có khả năng hành động nhanh. Thiết kế bao gồm các khu chính: trang cài đặt tổng quan, bảng log, công cụ test, và controls từng chức năng.

Trang cài đặt tổng quan: hiển thị trạng thái hệ thống (credential connected, queue length, last send), nút bật/tắt global auto-send, chế độ vận hành (Off | Sitemap-only | API-enabled), canary percentage slider và dry-run checkbox. Mỗi thay đổi yêu cầu capability manage_options và có modal xác nhận.

Bảng log: list event với cột time | url | eventtype | actiontarget | status | attempts | httpstatus | lasterror (truncated). Filter theo status, post_type, date; hành động dòng: requeue (dry-run), move to DLQ, export CSV; giữ audit trail user/ip cho mọi hành động manual.

Công cụ test: form test URL thực hiện pre-flight checks (HEAD, check X-Robots-Tag, canonical, structured data) và hiện kết quả chi tiết; nút test credential kiểm tra auth với Google và hiển thị scopes + expiry.

Toggle từng chức năng: sitemap generation, auto-purge CDN, send-on-publish, structured-data injection, x-robots headers. Mỗi toggle có trạng thái canary và ghi log thay đổi.

Preview dry-run và canary controls: bảng preview liệt kê URLs sẽ gửi trong dry-run với checkbox chọn thủ công; canary cho phép mẫu ngẫu nhiên X% trước khi bật toàn diện.

Ví dụ cấu hình mẫu (filled):

{
  "mode": "API-enabled",
  "auto_send": true,
  "canary_pct": 5,
  "dry_run": false,
  "rate_rpm": 5
}

Quy trình triển khai kỹ thuật cơ bản (pseudocode + hooks)

Mô tả ngắn: đây là luồng sự kiện tối thiểu để từ lúc một post được public tới lúc plugin xác nhận và gửi tín hiệu cho Google. Mỗi bước là điểm kiểm tra nội bộ và có thể enqueue để xử lý bất đồng bộ.

Các hooks chính để bắt sự kiện

  • transitionpoststatus: dùng để phát hiện đúng lúc post chuyển trạng thái, kiểm tra $oldstatus và $newstatus.
  • save_post: dùng để bắt lưu post bổ sung, nhưng cần kiểm tra điều kiện để không gửi sớm.
  • wptrashpost: khi post bị vào thùng rác, cần gửi remove/delete signal.
  • delete_post: khi bị xóa hoàn toàn, xử lý xóa khỏi sitemap và gửi signal.

Pseudocode luồng cơ bản

// on transition_post_status
if ($old_status !== 'publish' && $new_status === 'publish') {
  // debounced enqueue to allow caches/replication to settle
  enqueue_job('handle_publish', ['post_id' => $post_id], delay = config.publish_delay);
}

// worker: handle_publish
function handle_publish($job) {
  $url = get_permalink($job.post_id);

  // 1. rebuild or update sitemap shard that contains this URL
  sitemap_update($job.post_id);

  // 2. purge caches synchronously where possible (CDN, Varnish, local full-page cache)
  purge_caches_for($url);
  purge_caches_for(sitemap_shard_url($job.post_id));

  // 3. verify readiness: HEAD/GET url and HEAD sitemap shard
  if (!verify_url_ready($url) || !verify_sitemap_contains($url)) {
    retry_with_backoff_or_move_to_dlq($job);
    return;
  }

  // 4. send: either Indexing API call OR ping/submit sitemap per config
  if (config.use_indexing_api && is_api_eligible($job.post_id)) {
    send_indexing_api($url);
  } else {
    submit_sitemap_or_ping(sitemap_index_url());
  }

  // 5. record log and metrics
  log_event('sent', $url, response_metadata);
}

Ghi chú vận hành ngắn

  • Always debounce and verify to avoid race giữa publish và CDN/replica propagation.
  • Enqueue jobs in a central queue (Redis/DB) so multi-node không duplicate.
  • Kiểm tra và tuân thủ rate limiting trong sendindexingapi.

Hệ thống hàng đợi và rate limiting

Mục tiêu: đảm bảo gửi tín hiệu tới Google có độ tin cậy, tránh spike gây throttle hoặc block. Bạn cần chọn queue phù hợp với khối lượng và độ tin cậy, rồi áp chính sách rate limiting tập trung.

  • So sánh ngắn:
  • WP-Cron / transient/option: dễ triển khai, phù hợp site nhỏ; yếu về độ tin cậy, dễ duplicate và phụ thuộc traffic để chạy.
  • Redis / SQS / RabbitMQ: phù hợp scale trung-cao; hỗ trợ atomic pop, visibility timeout, dead-letter queue, dễ điều phối across nodes.
  • Token-bucket pattern: duy trì một bucket token trung tâm (Redis) refill theo rate. Trước mỗi gửi token phải được lấy; nếu không có token, job bị delay.
  • Central rate limiter: lưu token bucket keyed theo project_id hoặc egress IP để tránh cộng dồn trên shared IP. Dùng Lua script Redis để đảm bảo atomic.
  • Batch size đề xuất: API-critical URLs: 1–10 per request; sitemap submits or internal batching: 50–500 per batch tùy API.
  • Retry policy: exponential backoff với base 5–30s, multiplier 2, max attempts 4–6, thêm randomized jitter (±30–100%) để giảm thundering herd.

Bảo mật và rủi ro bảo mật chi tiết

Mục tiêu: liệt kê rủi ro chính và các biện pháp thực thi cụ thể để plugin không làm lộ credential, không cho phép kẻ xấu kích hoạt gửi hàng loạt, và không gây SSRF hoặc lộ thông tin nhạy cảm trong logs.

Rủi ro chính

  • Credentials leak (service account JSON, OAuth token)
  • Public endpoints / cron triggers bị gọi lạm dụng
  • Capability bypass: user không đủ quyền có thể trigger hành động nhạy cảm
  • SSRF / open-redirect qua tính năng kiểm tra URL nội bộ
  • Logging sensitive data (tokens, private_key)

Biện pháp thực thi (practical)

  • Secret storage: đặt file key ngoài document root; file perms 600, owner là user chạy PHP; hoặc dùng GCP Secret Manager / Vault; không commit vào repo; không lưu plaintext trong wp_options.
  • Access control: mọi REST route phải có permissioncallback kiểm tra currentusercan(‘manageoptions’) hoặc custom capability; form/JS phải dùng wpnoncefield + checkadminreferer. External webhooks dùng HMAC signature với secret lưu ở vault.
  • Input validation & SSRF protection: chỉ cho phép hostname whitelist; resolve DNS then block RFC1918, loopback, link-local; giới hạn follow-redirects và tổng redirect depth.
  • Rate limiting & idempotency: centralized token-bucket (Redis) keyed theo project_id/egress IP; idempotency key per-URL to avoid duplicates; circuit-breaker on repeated 429/403.
  • Logging & redaction: mask Authorization headers and private_key fields before lưu; retention ngắn; audit trail chỉ chứa event meta, không chứa secrets.
  • Operational: rotate keys regularly, test credential with dry-run, require explicit admin enable on production only, and alert admin on repeated failures or unusual API usage.

Kiểm thử và staging trước production

Mục tiêu: đảm bảo plugin gửi tín hiệu index an toàn, không gây race condition, và hoạt động với cache/CDN và kiến trúc DB phân tán.

Thiết lập staging

  • Tách môi trường staging với domain/hosts khác và credentials riêng; mặc định auto-send tắt.
  • Dùng sandbox Google project hoặc dry-run mode để giả lập Indexing API.

Unit test (logic)

  • PHPUnit cho rule engine, eligibility, debounce, backoff, idempotency key generation.
  • Tập test: chuyển trạng thái post, coalesce nhiều update, serialize/deserialize job payload.

Integration test (webhook/API)

  • Chạy test end-to-end trên staging: publish -> sitemap rebuild -> purge cache -> verify URL -> mock Indexing API call (ngrok hoặc local mock server) hoặc dùng Google sandbox.
  • Test các phản hồi 200, 4xx, 429, 5xx và xác nhận retry/backoff, circuit breaker.

Dry-run và canary

  • Dry-run: thực hiện mọi bước ngoại trừ gọi Google; log request body và decision.
  • Canary rollout: bật tự động cho X% URL (ví dụ 5%) trong 24-72 giờ; kiểm tra metrics.

Test cases quan trọng

  • Cache/CDN propagation: publish, purge CDN, probe từ đa vùng (curl từ nhiều region) và kiểm tra edge inconsistency.
  • Full-page cache/Varnish: ensure BAN API được gọi và edge trả nội dung mới.
  • DB replication lag: force write to master, read from replica; simulate lag và verify job waits or reads from master.
  • SSRF and input validation: feed malicious URLs and confirm blocked.
  • Load tests: burst publish (batch import) to validate rate limiting and queue behavior.

Giám sát sau staging

  • Kiểm tra logs, DLQ, Search Console URL Inspection sample, và health dashboard trước phép bật production.

Tương thích và yêu cầu phiên bản

Yêu cầu tối thiểu đề xuất để plugin hoạt động ổn định: PHP 7.4+ và WordPress 5.8+. Khuyến nghị môi trường sản xuất: PHP 8.0+ và WP latest minor để tận dụng performance và security fixes.

Cách kiểm tra compatibility

  • PHP: kiểm tra PHPVERSION trong runtime hoặc từ WP admin Tools -> Site Health. Trong mã plugin: if (versioncompare(PHPVERSION, ‘7.4’, ‘<‘)) { deactivatewithadminnotice(); }.
  • WP: use getbloginfo(‘version’) hoặc defined(‘WPHOME’) để so sánh phiên bản khi kích hoạt.

Required PHP extensions (bắt buộc hoặc rất khuyến nghị)

  • curl, json, openssl, mbstring, xml: bắt buộc cho HTTP requests, JWT/OAuth, và XML sitemap.
  • zip: khuyến nghị cho packaging/export.
  • intl: khuyến nghị nếu xử lý locale/format ngày.
  • redis/memcached: tùy chọn nếu dùng queue/caching.

Fallbacks và polyfills

  • Cung cấp kiểm tra function_exists trước khi gọi extension; nếu thiếu, tắt chức năng tương ứng và log cảnh báo.
  • Khi cần polyfill, sử dụng composer packages (ví dụ symfony/polyfill-mbstring) và implement graceful degradation: ví dụ nếu curl absent, fallback sang wpremotepost (kèm kiểm tra transport).

Xử lý lỗi và giám sát vận hành

Mục tiêu: đảm bảo gửi tín hiệu tới Google ổn định, có phục hồi tự động, và cảnh báo kịp thời khi có sự cố.

  • Retry policy: exponential backoff với max_retries = 5 (base = 30s, multiplier = 2, jitter 0–30s). Sau 5 lần thất bại, chuyển job sang DLQ.
  • Dead-letter queue (DLQ): lưu job thất bại cùng metadata (URL, reason, attempts, last_response). TTL DLQ = 30 ngày, chế độ purge theo retention policy. Admin có giao diện để xem, requeue hoặc xóa từng entry.
  • Admin notification: gửi cảnh báo qua email + webhook/Slack khi DLQ size > 10 hoặc khi failure rate > 5% trong window 15 phút. Gửi daily summary với số job thành công/thất bại và top lỗi.
  • Health endpoint: /wp-json/myplugin/v1/health trả JSON {status, queuelength, dlqlength, lastsendtimestamp}. Bảo vệ bằng capability hoặc token.
  • Metrics to export: Prometheus metrics names ví dụ mypluginsendtotal, mypluginsendfailurestotal, mypluginqueuelength, myplugindlqlength, mypluginsendlatencyseconds_bucket. Expose /metrics hoặc pushgateway.
  • SLAs và alert thresholds: aim 95% success within 10 minutes of publish; alert if queue_length > 500 or avg send latency > 5s for 5m.

Quy trình cập nhật bảo mật và migration scripts

Phần này mô tả quy trình phát hành bản vá bảo mật, cách đặt version semantic, viết và chạy migration scripts cho thay đổi DB, ghi chú breaking changes và hướng dẫn cập nhật cho admin.

  1. Semantic versioning và release
  • Dùng SemVer: MAJOR.MINOR.PATCH. Tăng PATCH cho vá bảo mật/bugfix, MINOR cho tính năng tương thích ngược, MAJOR cho thay đổi không tương thích.
  • Ghi CHANGELOG.md theo chuẩn Keep a Changelog; tag release trên Git.
  1. Quy trình release patch bảo mật
  • Tạo branch hotfix; chạy CI tests; nếu pass, build package, ký mã nếu áp dụng, tạo release private cho security triage trước khi public.
  • Notify admins qua email/Slack và ghi release notes ngắn gọn gồm: CVE id nếu có, mức ảnh hưởng, required action.
  1. Migration scripts cho DB changes
  • Migrations versioned theo plugin version, lưu trong folder migrations với tên 01addcolumnpostsmeta.sql hoặc PHP class Migration20260716AddIndex.
  • Migrations phải idempotent và dùng transaction khi DB hỗ trợ; kiểm tra tồn tại cột/index trước khi ALTER.
  • Cung cấp rollback script nếu có thể; nếu rollback khó, document irreversible change rõ ràng.
  1. Kịch bản chạy migration
  • Khi activate/update: hook activation check current migration version, enqueue migration job to run in background worker (không chạy long blocking trong web request).
  • Trước chạy: auto backup DB snapshot và export sitemap; verify backup success.
  • Chạy migration trên staging và canary trước production.
  1. Chú ý breaking changes và hướng dẫn cập nhật
  • Mỗi MAJOR release gồm file UPGRADE.md nêu: các thay đổi breaking, bước migration thủ công nếu cần, sample WP-CLI commands để apply migrations, và checklist rollback.
  • Trong admin UI hiển thị banner nếu migration required, kèm button “Run migration” with capability check and pre-migration backup.
  1. Kiểm tra sau migration và vận hành
  • Test health endpoint, run smoke tests, validate sitemap và một số URL mẫu trả 200.
  • Nếu migration fail, auto-restore từ backup và alert admin.
  1. Bảo mật và audit
  • Không commit migration chứa credential. Migration logs không ghi secrets. Lưu audit trail: who ran migration, timestamp, result.

Hướng dẫn cài đặt và deploy

Mô tả ngắn: hướng dẫn đóng gói plugin, lưu credential an toàn, các bước chạy khi kích hoạt và checklist kiểm tra trước khi đưa lên production.

  1. Packaging
  • Đặt mã vào thư mục plugin chuẩn: wp-content/plugins/your-plugin/.
  • Loại trừ file credential và .env khỏi repo; thêm vào .gitignore.
  • Tạo file ZIP phân phối từ thư mục chứa mã không có secrets.
  1. Lưu credential và quyền file
  • Đặt file service account JSON ngoài document root, ví dụ /var/www/keys/your-sa.json.
  • Quyền file:
sudo chown www-data:www-data /var/www/keys/your-sa.json
sudo chmod 600 /var/www/keys/your-sa.json
  • Trong wp-config.php, tham chiếu đường dẫn, không copy nội dung vào DB:
define('MYPLUGIN_GOOGLE_CREDENTIALS', '/var/www/keys/your-sa.json');
  1. Steps on activate
  • Activation hook tạo custom tables và index nếu chưa có; dùng dbDelta cho an toàn.
  • Kiểm tra credential: gọi API test nhẹ chỉ đọc quyền; nếu lỗi, hủy kích hoạt tự động và log chi tiết (không ghi secret).
  • Tạo initial sitemap shards và log entry “activated”.
  1. Checklist pre-production
  • Credentials: nằm ngoài webroot, perms 600, chủ sở hữu process web.
  • Environment: WP_ENV==’production’ hoặc domain whitelist bật auto-send.
  • Cache/CDN: xác nhận purge API có thể gọi và test purge.
  • Queue: Redis/SQS endpoint reachable; worker test chạy.
  • Rate limits: config throttle mặc định (e.g., 5 req/min).
  • Backup: DB snapshot trước migration; smoke tests cho 5 sample URLs trả 200 và sitemap chứa chúng.
  • Logging: health metrics và alert email bật.

Mọi thao tác thay đổi credential phải được audit và ghi vào CHANGELOG release.

Hướng dẫn vận hành cho admin

Khi vào phần quản trị, bạn ưu tiên URL bằng cách gắn flag ưu tiên trên từng bài hoặc bật rule trong mục Filters (ví dụ post_type = landing, tag = campaign). Ưu tiên chỉ nên dành cho URL cần index ngay, không cho toàn site.

Đọc log: mở bảng Log, lọc theo status = failed / pending / sent; xem cột responsecode và responsebody rút gọn. Không bao giờ tìm trong log chuỗi credential. Nếu nhiều lỗi 429/403, giảm throttle hoặc tạm dừng auto-send.

Test URL: dùng công cụ Test URL trong UI. Quy trình test: 1) kiểm tra HTTP HEAD trả 200; 2) kiểm tra không có x-robots-tag: noindex; 3) xác nhận canonical trỏ đúng; 4) kiểm tra sitemap shard chứa URL. Nếu có CDN, dùng nút Purge + re-test.

Interpret Search Console: so sánh time-to-index trước/sau gửi; dùng URL Inspection để xem cách Googlebot thấy trang; kiểm tra Coverage báo lỗi noindex, redirect, hoặc crawl denied.

When to pause auto-send: 1) spike lỗi API (liên tục 429/403 trong 10 phút); 2) cache/CDN chưa purge dẫn đến nhiều 4xx khi kiểm tra; 3) phát hiện credential leak; 4) khi deploy lớn hoặc staging bị bật auto-send. Khi pause, plugin giữ queue và gửi thông báo admin.

Metrics và cách đo hiệu quả (KPI + collection)

Mục tiêu đo: chứng minh plugin giảm time-to-index và tăng tỷ lệ URL được index nhanh. Các KPI chính bạn nên thu:

  1. Time-to-index (median, p90)
  • đo từ timestamp publish → first indexed timestamp trong Search Console. Tính trung bình, p50, p90 theo cohort (post_type, priority).
  1. Percent indexed within X hours
  • ví dụ % indexed trong 1h, 6h, 24h, 7d. So sánh requested vs indexed.
  1. Requested vs indexed
  • số URL gửi (API/ping) hàng ngày so với số URL Google báo indexed.
  1. Crawl frequency và crawl success
  • crawl attempts per URL/week, 4xx/5xx rate, last crawl timestamp.

Data collection và nguồn

  • Search Console API: dùng URL Inspection API (sample) và Index Coverage + Search Analytics để lấy indexed state, last crawled, và performance. Lưu responses có timestamp.
  • Logs plugin: lưu events sent, response codes, retries, and DLQ.

Mẫu dashboard (gợi ý)

  • time-to-index histogram; line chart % indexed within 24h over time; stacked bar requested vs indexed; heatmap crawl frequency by URL group. Xuất metrics ra Prometheus/Influx hoặc Google Data Studio kết nối BigQuery chứa logs và Search Console export để báo cáo.

Case notes: chính sách Google và khi nên ngưng tự động

Mình tóm ý chính cho bạn về rủi ro chính sách Google và các tình huống cần tắt chức năng gửi tự động. Google không cho phép spam request hoặc thao tác gây lạm dụng API; gửi nhiều request không cần thiết hoặc gửi URL không public có thể dẫn tới throttling, lỗi 4xx/5xx, hoặc xem xét hành vi bất thường của tài khoản. Luôn tuân thủ quota của Google Cloud và đọc quota trong Console trước khi bật tính năng tự động.

Khi nên ngưng chức năng tự động:

  • phát hiện lỗi 429/403 liên tiếp;
  • backlog queue tăng mạnh hoặc retry storm;
  • thay đổi môi trường (deploy, CDN purge, DNS) có nguy cơ gây race;
  • phát hiện credential leak hoặc endpoint công khai;
  • site chạy trên staging hoặc có URL không-public;
  • khi legal/compliance review yêu cầu audit mọi request.

Best practice: mặc định tắt auto-send, bật conservative mode, giới hạn RPM và daily quota, áp token-bucket global, lưu logs không chứa secret, có circuit breaker tự động và thông báo admin khi ngưỡng lỗi vượt mức.

Kết luận

Mình đã trình bày thiết kế và luồng triển khai để bạn có plugin WordPress giúp rút ngắn thời gian Google index và đo đạc bằng chứng vận hành. Sau khi áp dụng, bạn sẽ có hệ thống sitemap-first an toàn, queue có retry và KPI để đánh giá thực tế hiệu quả.

Lưu ý thực tế quan trọng nhất: luôn giữ credential ngoài webroot với quyền chặt, bật dry-run và canary khi triển khai API để tránh gửi hàng loạt ngoài ý muốn.

Cách tạo 1 plugin SEO cho wp bằng AI

Xây plugin SEO-AI cho WordPress an toàn và vận hành

Mình sẽ hướng dẫn bạn thiết kế và triển khai plugin SEO-AI trên server WordPress để sinh title, meta description, JSON-LD và gợi ý internal links bằng API AI qua REST với X-API-KEY. Ví dụ minh họa trong bài giả định gọi AI từ server (PHP), lưu khóa an toàn và dùng JSON Schema để validate đầu ra.

Mục tiêu dự án

Mục tiêu của plugin SEO-AI mình trình bày là giúp bạn, biên tập viên và quản trị viên sinh tiêu đề, meta description, schema.org JSON-LD và gợi ý internal linking bằng AI, bảo vệ khóa API và đảm bảo vận hành. Plugin cung cấp giao diện kiểm duyệt, cơ chế retry, backoff, throttle, cache và xử lý bất đồng bộ khi cần. Người dùng mục tiêu là nhà phát triển, SEO editor và chủ site. Tuyên bố sứ mệnh: cung cấp đề xuất SEO chất lượng, có thể kiểm soát và vận hành an toàn, để mọi quyết định thiết kế luôn ưu tiên bảo mật, khả năng khôi phục và tối ưu chi phí với giám sát vận hành.

Phạm vi tính năng mẫu

Mục tiêu: cung cấp tập tính năng SEO-AI mẫu để bạn nhanh triển khai và kiểm soát phạm vi hành vi. Dưới đây là các tính năng minh họa, cùng input/output và giới hạn rõ ràng.

  • Sinh meta title và meta description
  • Hành vi: dựa trên post title, excerpt, snippet content và template để gợi ý nhiều biến thể. Trả về text đã được sanitize, kèm confidence và token usage nếu provider trả.
  • Input: post_id, title, excerpt, optional custom prompt, constraints (max_length, tone).
  • Output: array of {title, description, length, source_hash}.
  • Không làm: tự động public lên front-end trừ khi admin bật auto-apply; không sinh long-form content.
  • Sinh JSON-LD schema (Article, Breadcrumb)
  • Hành vi: tạo block JSON-LD tối thiểu hợp lệ dựa trên post metadata.
  • Input: post data, site config.
  • Output: object JSON-LD ready-to-insert.
  • Không làm: không tự publish schema nếu invalid; không generate complex schema types tự động.
  • Gợi ý internal linking
  • Hành vi: phân tích post content, suggest internal targets với anchor text và score.
  • Input: post content, taxonomy, search index snippet.
  • Output: list of {post_id, url, anchor_suggestion, relevance_score}.
  • Không làm: không tự-add link vào content; chỉ gợi ý cho biên tập viên.
  • Audit SEO on-demand
  • Hành vi: chạy checklist (meta presence, length, headings, images alt, schema), trả report và actionable tips.
  • Input: post_id.
  • Output: structured report with severity and recommendations.
  • Không làm: không thay đổi file hệ thống hay can thiệp analytics tự động.
  • Batch processing / background jobs
  • Hành vi: queue hàng loạt post để generate, throttle theo rate limit, cache kết quả.
  • Input: list post_ids, batch options.
  • Output: job status, per-post results saved vào post_meta.
  • Không làm: không chạy toàn bộ site đồng thời nếu vượt quota; sẽ enqueue và throttle theo cấu hình.

Yêu cầu môi trường và tương thích

Plugin nhắm tới WordPress 6.x và PHP 8.x. Mình khuyến nghị môi trường tối thiểu sau để hoạt động ổn định:

  • WordPress: 6.0+ (REST API, capability hooks, Action Scheduler tương thích).
  • PHP: 8.0+; hỗ trợ cú pháp hiện đại, typed properties và performance.
  • PHP extensions cần thiết: curl (HTTP client), sodium (libsodium) để mã hóa an toàn; bổ sung: json, mbstring, hash, openssl (nếu sodium không có).
  • Filesystem: quyền ghi cho thư mục uploads/ để lưu tạm file streaming, quyền ghi cho wp-content/plugins/your-plugin nếu cần lưu config cục bộ trong dev; quyền ghi cho wp_options và post_meta (cơ sở dữ liệu). Tuyệt đối không lưu API key plaintext vào thư mục có thể public.
  • Webserver: HTTPS bắt buộc cho outbound/inbound; hỗ trợ kết nối outbound đến provider API.

Ghi chú tương thích:

  • Nếu PHP <8: loại bỏ kiểu dữ liệu bắt buộc trong hàm, tránh named arguments, hoặc cung cấp phiên bản code backport; nếu sodium không có, dùng thư viện libsodium-compat hoặc openssl với cảnh báo bảo mật.
  • Nếu WP <6: kiểm tra API REST và Action Scheduler availability; nếu thiếu, dùng admin-ajax hoặc tích hợp Action Scheduler như dependency, và giảm một số hook block-editor hiện đại.
  • Trên hosting chia sẻ, ưu tiên chế độ background jobs thay vì gọi đồng bộ trong request để tránh timeout PHP-FPM.

Kiến trúc tổng quan

Mục tiêu: mô-đun hoá plugin để rõ trách nhiệm, dễ kiểm thử và an toàn khi gọi API AI từ server. Sau đây là sơ đồ module và trách nhiệm từng phần, đủ để bạn triển khai hoặc để AI hiểu cấu trúc.

Sơ đồ module (textual)

  • UI (Admin / Meta box)
  • Trách nhiệm: giao diện nhập prompt, nút generate, hiển thị gợi ý, hiển thị trạng thái job và lỗi.
  • Tương tác: gọi REST endpoint, hiển thị nonce-protected responses, cho phép preview và apply.
  • API layer (REST endpoints)
  • Trách nhiệm: tiếp nhận request từ UI, kiểm tra nonce và capability, throttle sơ bộ, enqueue task hoặc thực thi đồng bộ.
  • Routes chính: /seoai/v1/generate-meta, /seoai/v1/health, /seoai/v1/settings/test-ping.
  • Service layer (AiClient wrapper)
  • Trách nhiệm: đóng gói cuộc gọi tới provider (REST + X-API-KEY), retry/backoff, honor Retry-After, circuit breaker, streaming support.
  • Abstraction: request(prompt, opts), stream(request, handler), ping() – giữ provider-agnostic interface để dễ đổi SDK.
  • Storage layer
  • Trách nhiệm: lưu cấu hình, cache, kết quả.
  • Thành phần: encrypted wp_options cho config, post_meta cho suggestions nhẹ, transients cho cache ngắn hạn, custom table cho payload lớn/đếm usage.
  • Background worker (Action Scheduler / cron)
  • Trách nhiệm: xử lý jobs bất đồng bộ, batch generation, retry theo schedule, garbage collect locks.
  • Tích hợp: enqueue từ REST endpoint khi hosting hạn chế, notify via admin notices khi job xong.
  • Logger / Telemetry
  • Trách nhiệm: ghi event non-sensitive (status, latency, error codes), không bao giờ log API keys hoặc raw prompts chứa PII.
  • Lưu trữ: rotate logs, optional external metrics hook.

Luồng minh họa ngắn: UI -> REST -> permission check -> cache lookup -> AiClient call (sync or enqueue) -> validate JSON -> sanitize -> acquire per-post lock -> save to post_meta/transient -> respond. Mỗi module có boundary rõ ràng để kiểm soát bảo mật, throttling và retry.

Luồng dữ liệu tổng thể

Tóm tắt: khi biên tập viên bấm “Generate”, dữ liệu đi qua một chuỗi kiểm soát an toàn trước khi lưu vào post_meta. Dưới đây là luồng từng bước và điểm kiểm soát bảo mật mà bạn cần triển khai.

1) Client (editor UI)

  • Hành động: người dùng bấm nút, JS gửi AJAX/REST.
  • Kiểm soát: gửi WP nonce, hạn chế số lần click client-side (debounce), giới hạn kích thước payload trên client.

2) WP REST endpoint

  • Hành động: nhận request /seoai/v1/generate-meta.
  • Kiểm soát: permission_callback check current_user_can(‘edit_post’, $post_id); verify nonce; rate-limit per-user.

3) Input sanitize

  • Hành động: lấy post title, excerpt, content snippet, custom prompt.
  • Kiểm soát: wp_strip_all_tags / sanitize_text_field; enforce max length; reject URLs hoặc filter theo whitelist; strip control chars.

4) Prompt builder

  • Hành động: kết hợp template system + user content vào envelope JSON.
  • Kiểm soát: place user content only in user slot; prepend immutable system instructions (output only JSON schema); truncate long context; compute content hash for cache key.

5) Cache check

  • Hành động: lookup by key = hash(post_id + content_hash + template_version + provider_version).
  • Kiểm soát: return cached result if valid; limit cache TTL; record cache hit/miss metric.

6) AiClient (server-side)

  • Hành động: perform HTTP request to provider with X-API-KEY header.
  • Kiểm soát: read API key from ENV/secure store; set connect + request timeouts; concurrency semaphore; retry/backoff honoring Retry-After; do not log Authorization header.

7) Provider

  • Hành động: remote model returns response (stream or buffered).
  • Kiểm soát: prefer streaming when possible; cap response bytes; abort on oversized response.

8) Validate JSON

  • Hành động: json_decode + validate against JSON Schema.
  • Kiểm soát: strict schema validator; reject or sanitize if parsing fails; attempt safe extraction only as fallback.

9) Sanitize outputs

  • Hành động: sanitize title/description strings.
  • Kiểm soát: escape on render, strip HTML if necessary, enforce length limits.

10) Save post_meta

  • Hành động: acquire per-post lock (transient or DB GET_LOCK), update_post_meta atomically, release lock.
  • Kiểm soát: avoid race by single-writer pattern; audit log minimal event (no secrets).

11) Return to client

  • Hành động: respond with status, suggestions, cached flag.
  • Kiểm soát: mask internal errors, present user-friendly messages, enqueue background retry if needed.

Ở mỗi bước, log chỉ metadata, theo dõi latency/p95/p99, và alert khi 429/timeout/JSON errors tăng vọt. Nếu host yếu, default to async queue to avoid blocking PHP-FPM.

Quyết định triển khai: server-side AI calls

Mình khuyến nghị gọi AI từ server (PHP) làm mặc định. Lý do chính: bảo mật API key (key không bao giờ xuất ra client), dễ tập trung logging và đo lường, và thuận tiện để áp đặt throttle, cache, retry và circuit breaker ở một chỗ. Server-side cũng cho bạn khả năng mã hóa key, kiểm soát payload (truncate, sanitize) trước khi gửi, và dễ tích hợp vào cron/worker để xử lý bất đồng bộ.

Nhược điểm cần cân nhắc: gọi từ server tiêu tốn tài nguyên CPU/memory và có thể gia tăng độ trễ so với trải nghiệm client-native. Trên hosting yếu, các cuộc gọi đồng bộ có thể chiếm PHP-FPM worker và gây 502/504. Ngoài ra, chi phí API có thể tăng nếu không cache tốt hoặc retry quá nhiều.

Nếu bạn muốn cho client gọi trực tiếp, hãy cẩn trọng: không gửi provider API key ra trình duyệt. Các phương án an toàn thực tế:

  • Phát hành token ngắn hạn do server cấp, có scope và TTL giới hạn, chỉ dùng cho một request cụ thể.
  • Dùng server-side proxy: client gọi proxy của bạn, proxy xác thực, kiểm tra quota, bổ sung header X-API-KEY và forward. Proxy cho phép throttle, audit và redaction trước khi gọi provider.
    Trong mọi phương án, giữ nguyên tắc: không log secrets, áp nonce/capability, và bắt buộc rate-limit.

Thiết kế Settings schema

Mục tiêu: cung cấp một schema cấu hình máy đọc được cho trang Settings của plugin, giúp cả con người và AI hiểu rõ các trường, kiểu, giá trị mặc định và phạm vi an toàn. Mình đưa ra JSON-like schema mẫu kèm mô tả ngắn cho từng trường; bạn có thể dùng schema này để render form, validate input và lưu cấu hình.

Mẫu JSON-like schema (máy đọc được)
{
“provider”: {
“selection”: { “type”: “string”, “enum”: [“custom”,”openai”,”anthropic”,”other”], “default”: “custom”, “description”: “Chọn nhà cung cấp hoặc custom endpoint” },
“endpoint”: { “type”: “string”, “format”: “uri”, “default”: “”, “description”: “Base URL HTTPS của provider. Bắt buộc nếu selection = custom” }
},
“auth”: {
“storage_mode”: { “type”: “string”, “enum”: [“env”,”wp-config”,”encrypted-wp_options”], “default”: “encrypted-wp_options”, “description”: “Nơi lưu API key; ưu tiên env/wp-config” },
“test_ping_on_save”: { “type”: “boolean”, “default”: true, “description”: “Thử gọi 1 request nhỏ khi save key” }
},
“timeouts”: {
“connect_timeout_sec”: { “type”: “integer”, “default”: 3 },
“request_timeout_sec”: { “type”: “integer”, “default”: 10, “description”: “Timeout cho foreground admin requests” },
“background_timeout_sec”: { “type”: “integer”, “default”: 30, “description”: “Timeout cho cron/background” }
},
“retry_backoff”: {
“max_retries”: { “type”: “integer”, “default”: 2 },
“initial_backoff_ms”: { “type”: “integer”, “default”: 500 },
“max_backoff_ms”: { “type”: “integer”, “default”: 4000 },
“jitter”: { “type”: “number”, “default”: 0.25 }
},
“limits”: {
“concurrency_limit”: { “type”: “integer”, “default”: 5, “description”: “Số request outbound song song tối đa” },
“throttle_per_minute”: { “type”: “integer”, “default”: 60, “description”: “Số request tối đa mỗi phút cho site” },
“max_tokens”: { “type”: “integer”, “default”: 512 },
“response_max_bytes”: { “type”: “integer”, “default”: 200000, “description”: “Giới hạn bytes cho response trước khi truncate/abort” },
“max_payload_bytes”: { “type”: “integer”, “default”: 200000 }
},
“cache”: {
“cache_ttl_sec”: { “type”: “integer”, “default”: 86400 }
},
“logging”: {
“log_level”: { “type”: “string”, “enum”: [“error”,”warn”,”info”,”debug”], “default”: “warn”, “description”: “Không log secrets ở levels thấp hơn debug” },
“log_redaction”: { “type”: “boolean”, “default”: true }
},
“admin”: {
“admin_contact_email”: { “type”: “string”, “format”: “email”, “default”: “” },
“notify_on_errors”: { “type”: “boolean”, “default”: true }
},
“meta”: {
“schema_version”: { “type”: “integer”, “default”: 1 }
}
}

Ghi chú ngắn: các giá trị default là điểm khởi đầu an toàn. Khi plugin cài, mình khuyến nghị probe host/provider để điều chỉnh timeout, concurrency và response_max_bytes theo tài nguyên thực tế. Nếu bạn muốn, mình có thể cung cấp validator JSON Schema hoàn chỉnh để dùng server-side.

Mẫu JSON cấu hình (machine-readable)

Dưới đây là object mẫu settings để hệ thống máy và AI hiểu cấu hình plugin. Bạn có thể dùng trực tiếp khi cài đặt hoặc để AI đọc. Mình để các trường bắt buộc và giá trị mặc định an toàn; thay giá trị key_encrypted bằng dữ liệu đã mã hóa thực tế.

{
“provider”: {
“name”: “custom”,
“url”: “https://api.example-ai.com/v1/generate”
},
“auth”: {
“mode”: “encrypted-wp_options”,
“key_encrypted”: “BASE64_ENCRYPTED_KEY_STRING”
},
“timeouts”: {
“connect”: 3,
“request”: 10
},
“limits”: {
“concurrency”: 5,
“per_minute”: 60
},
“cache_ttl”: 86400,
“max_tokens”: 512,
“response_max_bytes”: 200000,
“schema_version”: 1
}

Ghi chú ngắn: connect và request là giây; auth.mode có thể là env, wp-config, hoặc encrypted-wp_options; key_encrypted lưu ciphertext base64. response_max_bytes là giới hạn bytes cho response trước khi truncate; cache_ttl là giây. Mình khuyến nghị plugin validate schema này trước khi kích hoạt để tránh gửi dữ liệu nhạy cảm ra ngoài.

Chi tiết AiClient abstraction

Mục tiêu: một lớp trừu tượng duy nhất để gọi provider AI, dễ mock trong test, dễ thay bằng SDK chính thức của nhà cung cấp, và có cơ chế retry/backoff/jitter cùng circuit breaker.

Giao diện (interface) cơ bản mà mình gợi ý:

  • request(string|array prompt, array opts) : array
  • Gọi đồng bộ, trả về decoded JSON hoặc ném exception.
  • stream(array request, callable handler) : void
  • Mở kết nối streaming; gọi handler($chunk) cho mỗi chunk; handler trả boolean để tiếp tục hay dừng.
  • ping() : bool
  • Kiểm tra endpoint + auth nhanh để validate key/config.
  • parse_error(mixed response, int http_code) : Exception|null
  • Chuẩn hóa lỗi provider thành dạng Exception nội bộ, nhận diện 429 và Retry-After.

Thực thi REST cơ bản

  • Header auth: luôn gửi X-API-KEY: trong header. Nếu phải dùng SDK của provider, thay toàn bộ nội dung HTTP client bằng SDK call, nhưng vẫn giữ interface request/stream/ping/parse_error.
  • Body: wp_json_encode hoặc json_encode; Content-Type: application/json.
  • Timeout: tách connect timeout và request timeout.

Retry, honor Retry-After, backoff with jitter

  • Nguyên tắc:
  • Chỉ retry trên transient errors: network timeout, 5xx, 429.
  • Nếu response có header Retry-After, dùng giá trị đó trước khi retry.
  • Chính sách đề xuất:
  • max_attempts = opts[‘retries’] ?? 2
  • base_backoff_ms = opts[‘backoff_ms’] ?? 500
  • on attempt n: wait = Retry-After if present else base_backoff_ms * 2^(n-1)
  • jitter: random_between(0, wait * 0.25) added or subtracted
  • tổng timeout tổng không vượt total_budget (configurable)
  • Không retry trên 4xx trừ 429.

Circuit breaker

  • Trạng thái: CLOSED, OPEN, HALF_OPEN.
  • Threshholds đề xuất:
  • failure_threshold = 5 failures within sliding_window_seconds = 60
  • open_timeout = 120 seconds
  • half_open_attempts = 1
  • Khi open: mọi request trả nhanh lỗi 503 hoặc enqueue job background.
  • Khi half open: cho 1 request thử lại; nếu thành công, đóng lại; nếu fail, mở lại và tăng backoff.

Hooking vào plugin

  • AiClient nên emit metric events: request_start, request_end, response_time_ms, http_code, error_type. Dùng để điều chỉnh giới hạn runtime.
  • Khi đổi sang SDK, implement same interface bằng adapter: SDKClient implements AiClientInterface và map SDK calls vào request/stream/ping/parse_error.

Gợi ý triển khai ngắn

  • Dùng cURL hoặc Guzzle để có thể access curl_getinfo/CURLINFO_PRIMARY_IP khi cần SSRF check, và CURLOPT_WRITEFUNCTION cho streaming/capping response.
  • Không log X-API-KEY; log request_id từ provider nếu có, status và latency.

Thiết kế prompt và schema đầu vào cho AI

Mục tiêu: gửi cho model một envelope rõ ràng, có cấu trúc để giảm lỗi parse, tránh prompt injection và bắt model trả đúng JSON. Mình thiết kế envelope gồm 4 phần chính: task (mục tiêu ngắn), post (context cho bài), constraints (giới hạn), site (metadata). Gửi system role để đặt luật, gửi user content chỉ ở phần post. Yêu cầu model trả pure JSON và validate theo JSON Schema trước khi dùng.

Mẫu envelope JSON gửi cho model (ví dụ)
{
“task”: “generate_meta_and_schema”,
“post”: {
“id”: 123,
“title”: “Hướng dẫn trồng cà chua tại nhà”,
“excerpt”: “Kỹ thuật trồng, chăm sóc và phòng bệnh căn bản”,
“content_snippet”: “Chọn giống tốt; đất tơi xốp; tưới nước hợp lý…”
},
“constraints”: {
“max_title_length”: 60,
“max_desc_length”: 160,
“language”: “vi”,
“tone”: “informative”
},
“site”: {
“name”: “Vườn Nhà”,
“url”: “https://vuon.example”
}
}

Vì sao đóng gói như vậy

  • Tách task ra ngoài giúp model không bị lẫn với content và giảm prompt injection.
  • post chỉ chứa context đã sanitize; không gửi raw HTML hay secrets.
  • constraints đảm bảo output ngắn, có thể map thành max_tokens của provider.
  • site cung cấp thông tin để model tạo JSON-LD hợp lệ.

Cách dùng system role / assistant role

  • Nếu provider hỗ trợ roles, set system role = “You are an SEO assistant. Follow rules: return ONLY valid JSON matching schema; do not add explanation; do not hallucinate dates.” User role chứa envelope.post. Assistant role không cần khi request là single-turn.

Yêu cầu trả pure JSON

  • Trong system role nhắc rõ “Output must be pure JSON, nothing else.” Không tin tưởng tuyệt đối nhưng đây là ràng buộc đầu tiên. Luôn validate response bằng JSON Schema trước khi lưu.

JSON Schema mẫu cho input (envelope)
{
“$schema”: “http://json-schema.org/draft-07/schema#”,
“type”: “object”,
“required”: [“task”,”post”,”constraints”,”site”],
“properties”: {
“task”: { “type”: “string” },
“post”: {
“type”: “object”,
“required”: [“id”,”title”],
“properties”: {
“id”: { “type”: “integer” },
“title”: { “type”: “string”, “maxLength”: 300 },
“excerpt”: { “type”: “string” },
“content_snippet”: { “type”: “string” }
}
},
“constraints”: {
“type”: “object”,
“properties”: {
“max_title_length”: { “type”: “integer” },
“max_desc_length”: { “type”: “integer” },
“language”: { “type”: “string” },
“tone”: { “type”: “string” }
}
},
“site”: {
“type”: “object”,
“properties”: {
“name”: { “type”: “string” },
“url”: { “type”: “string”, “format”: “uri” }
}
}
}
}

JSON Schema mẫu cho output AI (bắt buộc)

Mục tiêu: định nghĩa rõ cấu trúc JSON mà AI phải trả để plugin có thể validate, sanitize và lưu an toàn. Dưới đây là (1) JSON Schema machine-readable để kiểm tra đầu ra, (2) giải thích các keys bắt buộc/tuỳ chọn và luật kiểm tra (validation rules), và (3) một ví dụ hoàn chỉnh điền sẵn.

1) JSON Schema (machine-readable)
{
“$schema”: “http://json-schema.org/draft-07/schema#”,
“type”: “object”,
“required”: [“title”,”description”],
“properties”: {
“title”: { “type”: “string”, “maxLength”: 120 },
“description”: { “type”: “string”, “maxLength”: 320 },
“json_ld”: {
“type”: [“object”,”null”]
},
“internal_links”: {
“type”: “array”,
“items”: {
“type”: “object”,
“required”: [“post_id”,”anchor”],
“properties”: {
“post_id”: { “type”: “integer”, “minimum”: 1 },
“anchor”: { “type”: “string”, “maxLength”: 100 },
“relevance”: { “type”: “number”, “minimum”: 0, “maximum”: 1 }
},
“additionalProperties”: false
},
“maxItems”: 10
},
“warnings”: {
“type”: “array”,
“items”: { “type”: “string”, “maxLength”: 240 },
“maxItems”: 10
}
},
“additionalProperties”: false
}

2) Giải thích keys và validation rules (plugin sẽ áp dụng)

  • title (required) – string, maxLength 120. Plugin sẽ trim, collapse whitespace, và escape khi hiển thị.
  • description (required) – string, maxLength 320. Plugin sẽ strip tags, replace control-chars, và escape khi in vào meta tag (esc_attr).
  • json_ld (optional) – object hoặc null. Nếu có, plugin sẽ:
  • validate that @context and @type exist where applicable,
  • ensure no script tags or executable content,
  • re-encode bằng json_encode và kiểm tra cú pháp. Nếu không hợp lệ, nó bị loại bỏ và ghi warning.
  • internal_links (optional) – array of objects. Mỗi object phải có:
  • post_id: integer > 0. Plugin sẽ kiểm tra tồn tại post bằng get_post(post_id) before accepting.
  • anchor: string, maxLength 100.
  • relevance: float between 0 và 1. Nếu thiếu, plugin gán null hoặc 0.
  • Array giới hạn tối đa 10 mục để tránh payload lớn.
  • warnings (optional) – danh sách string ngắn (maxLength 240) để AI báo vấn đề (ví dụ “content too short”).

3) Ví dụ hoàn chỉnh (điền sẵn)
{
“title”: “Hướng dẫn trồng cà chua tại nhà”,
“description”: “Các bước cơ bản trồng và chăm sóc cà chua để đạt năng suất cao. Bao gồm chọn giống, đất, tưới, và phòng bệnh.”,
“json_ld”: {
“@context”: “https://schema.org”,
“@type”: “Article”,
“headline”: “Hướng dẫn trồng cà chua tại nhà”,
“author”: { “name”: “Vườn Nhà” },
“datePublished”: “2026-01-15”
},
“internal_links”: [
{ “post_id”: 42, “anchor”: “kỹ thuật tưới”, “relevance”: 0.87 },
{ “post_id”: 87, “anchor”: “phòng bệnh cho cà chua”, “relevance”: 0.75 }
],
“warnings”: []
}

Lưu ý vận hành: sau json_decode, plugin bắt buộc chạy validator theo schema trên; nếu validation fail thì trả lỗi rõ ràng và không tự động áp dụng thay đổi trên post. Mình khuyên bạn dùng thư viện JSON Schema validator (PHP) để thực thi kiểm tra này trước mọi thao tác ghi.

Bộ prompt mẫu hoàn chỉnh

Dẫn nhập ngắn: dưới là bộ prompt template gồm message “system” và “user” để gửi cho model. Yêu cầu rõ: model chỉ trả pure JSON theo schema đã định, không trả giải thích hay text phụ. Mình để phần prompt chính bằng tiếng Anh (phù hợp hầu hết API) và kèm chú giải tiếng Việt ngắn.

System (máy):
You are an SEO assistant. Follow these rules exactly:
1) Output ONLY a single JSON object that strictly matches the schema: { “title”: string, “description”: string, “json_ld”: object|null, “internal_links”: array, “warnings”: array }.
2) Do not output any extra text, explanation, or markup.
3) Keep title <= {{max_title_length}} characters and description <= {{max_desc_length}} characters.
4) Tone: {{tone}}. Language: {{language}}.
5) If you cannot produce a field, set it to null or an empty array as appropriate.
6) Do not fabricate dates or facts. Use provided post content only.

User (dữ liệu):
Task: generate_meta_and_schema
Post:
{
“id”: {{post_id}},
“title”: “{{post_title}}”,
“excerpt”: “{{post_excerpt}}”,
“content_snippet”: “{{post_content_snippet}}”
}
Constraints:
{
“max_title_length”: {{max_title_length}},
“max_desc_length”: {{max_desc_length}},
“tone”: “{{tone}}”,
“language”: “{{language}}”,
“site_name”: “{{site_name}}”,
“site_url”: “{{site_url}}”
}

Chú giải ngắn: thay các {{…}} bằng giá trị thực; system buộc model chỉ trả JSON.

Ví dụ điền sẵn (post sample):

  • post_id: 123
  • post_title: Hướng dẫn trồng cà chua tại nhà
  • post_excerpt: Tổng hợp kỹ thuật chọn giống, chăm sóc, và phòng bệnh.
  • post_content_snippet: Chọn giống tốt, chuẩn bị đất giàu dinh dưỡng, tưới hợp lý, bón phân, theo dõi rệp và bệnh nấm.
  • max_title_length: 60
  • max_desc_length: 160
  • tone: informative
  • language: vi
  • site_name: Vườn Nhà
  • site_url: https://vuon.example

Expected AI output JSON (ví dụ):
{
“title”: “Hướng dẫn trồng cà chua tại nhà”,
“description”: “Các bước cơ bản để trồng và chăm sóc cà chua tại nhà: chọn giống, chuẩn bị đất, tưới và phòng bệnh để đạt năng suất.”,
“json_ld”: {
“@context”: “https://schema.org”,
“@type”: “Article”,
“headline”: “Hướng dẫn trồng cà chua tại nhà”,
“author”: { “name”: “Vườn Nhà” }
},
“internal_links”: [
{ “post_id”: 42, “anchor”: “kỹ thuật tưới”, “relevance”: 0.87 }
],
“warnings”: []
}

REST endpoint spec (machine readable)

Dưới đây là định nghĩa machine-readable cho các REST route chính của plugin. Mỗi route nêu method, headers required, body params, permission_callback (mô tả khả năng), response codes và payload mẫu.

1) Route: /wp-json/seoai/v1/generate-meta

  • Method: POST
  • Headers:
  • X-WP-Nonce: string (required)
  • Content-Type: application/json
  • Permission callback: current_user_can(‘edit_post’, post_id) AND wp_verify_nonce(header_nonce)
  • Body (JSON):
  • post_id: integer required
  • max_title_length: integer optional
  • max_desc_length: integer optional
  • tone: string optional
  • language: string optional
  • custom_prompt: string optional
  • Responses:
  • 200 OK
    • payload:
      {
      “status”:”ok”,
      “suggestions”:[
      {
      “title”:”string”,
      “description”:”string”,
      “json_ld”: object|null,
      “model”:”string”,
      “confidence”:0.0
      }
      ],
      “cached”: boolean
      }
  • 400 Bad Request
    • { “status”:”error”, “message”:”Invalid input” }
  • 401 Unauthorized
    • { “status”:”error”, “message”:”Permission denied” }
  • 429 Too Many Requests
    • { “status”:”error”, “message”:”Rate limit”, “retry_after”: integer_seconds }
  • 502 Bad Gateway
    • { “status”:”error”, “message”:”AI provider error” }
  • 503 Service Unavailable
    • { “status”:”error”, “message”:”Service temporarily unavailable” }

2) Route: /wp-json/seoai/v1/status

  • Method: GET
  • Headers: X-WP-Nonce optional for read-only, recommended for admin
  • Permission callback: current_user_can(‘manage_options’)
  • Response 200:
    { “status”:”ok”, “version”:”0.x”, “mode”:”online|degraded|offline” }

3) Route: /wp-json/seoai/v1/health

  • Method: GET
  • Auth: capability manage_options
  • Purpose: quick probe of provider connectivity and cache
  • Response 200:
    { “uptime”: “ISO8601”, “provider_ping_ms”: integer, “cache_ok”: boolean, “last_error”: null|string }

4) Route: /wp-json/seoai/v1/settings/test-ping

  • Method: POST
  • Headers: X-WP-Nonce required
  • Permission callback: current_user_can(‘manage_options’)
  • Body:
    { “provider_endpoint”:”string”, “auth_mode”:”env|encrypted|none” }
  • Responses:
  • 200: { “status”:”ok”, “ping_ms”: integer }
  • 401/400/502 as above

5) Route: /wp-json/seoai/v1/batch-enqueue

  • Method: POST
  • Headers: X-WP-Nonce required
  • Permission callback: current_user_can(‘manage_options’) OR current_user_can(‘edit_posts’) if limited per-user
  • Body:
    {
    “post_ids”:[1,2,3],
    “task”:”generate-meta”,
    “priority”:”low|normal|high”,
    “notify_user_id”: integer|null
    }
  • Responses:
  • 202 Accepted
    • { “status”:”accepted”, “job_id”:”string”, “queued_count”: integer }
  • 400/401/429 as above

Ghi chú bảo mật ngắn

  • Tất cả endpoint phải kiểm tra nonce và capability. Không trả API key. Trong payload lỗi không log nội dung raw. Các response schemas trên là mẫu; implement server-side validation và schema validation trước khi chấp nhận kết quả từ provider.

Luồng dữ liệu chi tiết cho tác vụ sinh meta (step-by-step)

Mục tiêu: mô tả tuần tự nội bộ khi biên tập viên bấm “Generate meta” tới khi kết quả được lưu, kèm các điểm dễ lỗi và hành động fallback.

1) Client gửi request

  • JS gửi POST tới /wp-json/seoai/v1/generate-meta cùng X-WP-Nonce và JSON body { post_id, max_title_length, max_desc_length, tone, custom_prompt }.
  • Nếu client timeout trước khi nhận kết quả, xử lý UX bằng thông báo “Đã đưa vào hàng đợi” nếu backend chuyển sang async.

2) Server permission check

  • REST route permission_callback xác nhận wp_verify_nonce và current_user_can(‘edit_post’, post_id).
  • Nếu fail: trả 401 và dừng.

3) Thu thập context

  • Load post bằng get_post(post_id).
  • Tập hợp context: title, excerpt, content snippet (ví dụ first 500 words), site name, lang, featured image URL.
  • Nếu post không tồn tại: trả 400.

4) Sanitize và truncate theo token/bytes

  • Chuyển tất cả field về plain UTF-8, strip tags, loại control chars.
  • Tính kích thước payload bytes; nếu vượt max_payload_bytes cấu hình hoặc token-estimate vượt limit, thực hiện truncate có logic ưu tiên: excerpt trước, content sau, hoặc tóm tắt server-side.
  • Nếu input quá lớn và admin mode cho phép, chuyển sang background job và trả 202 Accepted.

5) Cache lookup

  • Tạo cache_key = hash(post_id + content_hash + template_version + provider_version).
  • Nếu transient/post_meta cache tồn tại và chưa hết TTL, trả cached result ngay với cached=true.

6) Build prompt envelope

  • Gói prompt: { task, post, constraints, site, examples } theo JSON schema plugin. Đặt system instruction rõ ràng yêu cầu trả pure JSON.

7) AiClient call with timeouts

  • AiClient thực hiện HTTP POST với headers X-API-KEY, connect_timeout và request_timeout từ settings.
  • Nếu provider hỗ trợ streaming, bật streaming handler; nếu không, dùng buffered request với response_max_bytes cap.

8) Streaming vs buffered handling

  • Streaming: ghi từng chunk vào temp file hoặc transient chunk, kiểm soát total bytes, khi đủ hoặc model kết thúc tiến hành parse incremental.
  • Buffered: đọc body, nếu bytes > response_max_bytes, abort connection and flag truncated.

9) Validate response JSON schema

  • json_decode và validate theo output JSON Schema. Nếu invalid, cố gắng extract JSON block; nếu vẫn fail, log warning and fallback.

10) Sanitize text fields

  • Truncate fields to configured max lengths, strip HTML, normalize whitespace, escape for DB.

11) Acquire per-post lock

  • Trước update, acquire lock per-post_id (transient lock or GET_LOCK). Nếu không lấy được lock trong timeout ngắn, enqueue retry or return 409 with message “Busy – try again”.

12) Atomic update_post_meta + audit

  • update_post_meta($post_id, ‘_seoai_meta_suggestions’, $payload) trong single operation.
  • Viết audit entry (user_id, post_id, timestamp, provider_id, cache_key, status) vào custom table hoặc error log with redaction.

13) Release lock

  • Release transient or call RELEASE_LOCK.

14) Respond to client

  • Trả 200 với suggestions array, cached=false and latency metrics. Nếu queued, trả 202 with job_id.

Points where errors occur and fallback actions

  • Permission fail: return 401.
  • Payload too large: truncate; if truncation degrades quality, enqueue background job and inform user.
  • HTTP connect timeout or request timeout: if transient, retry with backoff up to configured attempts; if persistent, enqueue background job and return template fallback (e.g., excerpt-derived description).
  • Provider 429: honor Retry-After header, decrement concurrency, enqueue job, return 429 with retry_after to client.
  • Streaming abort or response exceeds cap: mark as truncated, try summary follow-up request or queue for background summarization.
  • Invalid JSON: attempt safe extraction; if fail, log and return error with safe fallback template.
  • Lock contention: fail fast with 409 or queue request to avoid race conditions.

Ghi chú vận hành: log chỉ metadata và provider request id, không log API keys hoặc raw prompts chứa PII.

Kiểm soát input từ editor và chống XSS/SSRF/prompt injection

Mục tiêu ngắn: mọi input từ biên tập viên phải đi qua lớp validate -> normalize -> truncate -> encode trước khi lưu hoặc gửi cho model. Không bao giờ tin input là “an toàn” ngay cả khi user là editor. Dưới đây là danh sách hàm WP/PHP cần dùng, chiến lược chống prompt injection, và checklist validation per-field bạn có thể áp dụng ngay.

Hàm WP/PHP và chức năng

  • sanitize_text_field($s) – cho các trường ngắn như title, tone, language.
  • wp_strip_all_tags($s) – loại bỏ HTML trước khi gửi vào prompt khi bạn muốn plain text.
  • wp_kses($s, $allowed) – nếu cần cho phép HTML có whitelist, dùng whitelist rõ ràng.
  • wp_http_validate_url($url) – kiểm tra cú pháp URL cơ bản.
  • dns_get_record($host, DNS_A + DNS_AAAA) – lấy bản ghi IP để kiểm tra dải IP.
  • filter_var($ip, FILTER_VALIDATE_IP) – validate IP.
  • json_encode / wp_json_encode – mã hoá payload JSON an toàn khi gửi.
  • curl with CURLOPT_WRITEFUNCTION or Guzzle streaming – để giới hạn/cắt response thay vì buffer toàn bộ.
  • memory_get_usage(), ini_get(‘memory_limit’) – trước khi gửi payload lớn, kiểm tra headroom bộ nhớ.
  • openssl_random_pseudo_bytes / sodium_* – nếu cần mã hoá input sensitivity trước lưu.

Chống SSRF cụ thể

  • Chỉ cho phép schemes https (reject file, gopher, ftp).
  • Resolve host bằng dns_get_record, kiểm tra mỗi IP không rơi vào private ranges (10.0.0.0/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ::1, fc00::/7).
  • Khi kết nối, bind tới IP đã resolve dùng CURLOPT_RESOLVE hoặc connect to numeric IP and set Host header, và kiểm tra curl_getinfo(CURLINFO_PRIMARY_IP) để xác nhận peer IP không private.
  • Không follow redirects tự động; nếu follow thì re-validate mỗi bước.

Chống XSS / lưu trữ an toàn

  • Trước khi lưu text: wp_strip_all_tags hoặc wp_kses với whitelist rất hạn chế.
  • Khi render meta box hoặc front-end: esc_html, esc_attr, hoặc wp_kses_post tùy context.
  • Khi in JSON-LD: echo wp_json_encode($data) inside

Cách build 1 themes wordpress bằng AI

Tạo và triển khai WordPress PHP theme bằng AI

Mình hướng dẫn bạn từng bước để dùng AI sinh, kiểm thử và đóng gói một WordPress PHP theme cổ điển, từ scaffold tới deploy. Hướng dẫn dùng ví dụ NovaPress làm minh họa và kèm mẫu prompt, file mẫu, checklist bảo mật cùng lệnh kiểm tra để bạn chạy ngay.

Tổng quan ngắn về types of WordPress themes

WordPress có hai kiểu theme chính bạn sẽ gặp khi phát triển: PHP theme cổ điển và block theme (full-site editing, FSE). PHP theme cổ điển dựa trên template files PHP – index.php, header.php, single.php v.v. – và tận dụng vòng đời request của WordPress cùng các hook và template hierarchy để render. Block theme dùng block system và theme.json, lưu template dưới dạng block markup hoặc trong database; chỉnh sửa template được thực hiện trực tiếp bằng Site Editor trong admin.

Sự khác biệt kỹ thuật chính:

  1. Cấu trúc và lưu trữ – PHP theme: template là file PHP trên filesystem; FSE: template có thể là file HTML block trong /block-templates hoặc post type wp_template trong DB.
  2. Quy trình chỉnh sửa – PHP theme: chỉnh sửa layout thường phải qua code hoặc Customizer; FSE: editor trực quan cho phép thay đổi header, footer, template parts từ admin.
  3. Logic server-side – PHP theme: cho phép viết logic PHP trực tiếp trong template; FSE: khuyến khích đóng gói logic vào dynamic blocks hoặc vào plugin riêng.
  4. Phụ thuộc REST – FSE dựa nhiều vào REST API để load/save templates từ Site Editor; PHP theme ít phụ thuộc REST cho rendering.

Lý do mình chọn mặc định là PHP theme (tương thích WP 5.9+):

  • Tương thích rộng hơn với plugin cũ, child-theme workflow ổn định, dễ kiểm soát logic PHP và giảm rủi ro khi hosting/WAF chặn REST.
  • Dễ scaffold và debug khi bạn cần triển khai nhanh bằng AI.

Nếu bạn muốn chuyển sang block theme, cần thay đổi chỗ sau:

  • Thay template PHP bằng block-templates/*.html và thêm theme.json;
  • Thiết kế block patterns, cung cấp editor-styles và kiểm tra REST endpoints cho Site Editor.

Yêu cầu tiền điều kiện và môi trường

Trước khi bắt tay tạo theme bằng AI, bạn cần chuẩn bị và kiểm tra môi trường phát triển, runtime và công cụ. Mình tóm gọn các yêu cầu thiết yếu và cách kiểm tra.

WordPress / PHP / phiên bản

  • WordPress: mục tiêu tối thiểu WP 5.9+. Kiểm tra: trong admin vào Dashboard > Updates hoặc chạy wp core version.
  • PHP: tối thiểu PHP 7.4, tốt nhất PHP 8.0+. Kiểm tra: php -v hoặc phpinfo().

Node / npm (nếu build assets)

  • Dùng khi có SASS/JS build pipeline. Cài Node.js LTS và npm hoặc pnpm/yarn.
  • Kiểm tra: node -v và npm -v; xác định nếu dự án cần vite/webpack trong package.json.

Composer

  • Dùng để quản lý thư viện PHP. Kiểm tra: composer –version; nếu composer.json có, chạy composer install để xác minh dependency.

Git

  • Quản lý phiên bản bắt buộc. Kiểm tra: git –version; đảm bảo .gitignore loại trừ vendor, node_modules, .env, và không commit secrets.

WP-CLI

  • Tiện ích deploy và test: wp –info. Thực hành: wp theme activate, wp plugin install, wp db export.

Hosting basics

  • Kiểm tra hosting PHP version, extensions (gd/imagick, mbstring, json), memorylimit, postmaxsize, uploadmaxfilesize, displayerrors, open_basedir.
  • Kiểm tra: php -m, php -i | grep memorylimit, php -i | grep uploadmax_filesize.
  • REST API: curl -i https://example.com/wp-json/ để đảm bảo /wp-json/ không bị WAF chặn.

Dev stacks & cách kiểm tra environment

  • Local: LocalWP, Docker/Lando, or Vagrant. Với Docker, kiểm tra container PHP/MySQL chạy đúng phiên bản.
  • CI: đảm bảo runner có PHP và Node đúng phiên bản; composer install và npm ci chạy không lỗi.
  • Quy trình kiểm tra nhanh: chạy php -l trên file PHP, vendor/bin/phpcs –standard=WordPress, vendor/bin/phpstan analyse nếu có, và npm run build để đảm bảo assets compile.

File structure mẫu cho theme PHP cơ bản

Dưới đây là cây thư mục mẫu cho một theme WordPress PHP cổ điển. Mình trình bày từng file/folder với mô tả ngắn để bạn biết vai trò và những gì cần kiểm tra nhanh khi AI sinh mã.

  • my-theme/
    • style.css

| Header comment chuẩn WordPress: Theme Name, Theme URI, Author, Version, Text Domain, License. File này bắt buộc để WP nhận diện theme.

  • index.php

| Fallback template tối thiểu. Chứa Loop cơ bản để hiển thị bài nếu không có template chuyên biệt.

  • functions.php

| Nơi đăng ký theme supports, menus, widget areas, enqueue scripts/styles, và khai báo hook. Đây cũng là chỗ tổ chức sanitize/escape cho theme options.

  • header.php

| Phần đầu của mọi trang. Gọi doctype, languageattributes(), wphead(), bodyclass(), logo/menu. Phải có wphead().

  • footer.php

| Phần cuối trang. Gọi wp_footer() trước </body>. Chứa credit, scripts phụ nếu cần.

  • sidebar.php

| Vùng sidebar nếu theme có. Đăng ký dynamic_sidebar trong functions.php để widget hoạt động.

  • single.php

| Template cho bài đơn. Nên gọi gettemplatepart(‘template-parts/content’, ‘single’).

  • page.php

| Template cho trang tĩnh.

  • archive.php

| Template cho category/tag/author/date archives.

  • search.php

| Kết quả tìm kiếm.

  • 404.php

| Trang lỗi 404.

  • template-parts/

| content.php | content-single.php | header-brand.php | Thư mục chứa các phần tái sử dụng. Dùng gettemplatepart để giữ loop và scope biến đúng.

  • assets/

| css/main.css | js/main.js | images/ | Chứa tài sản front-end. Enqueue bằng wpenqueuestyle/script với version từ filemtime hoặc theme version.

  • languages/

| my-theme.pot | Thư mục chứa file dịch để hỗ trợ i18n.

  • screenshot.png

| 1200×900 minh họa theme trong Appearance > Themes.

  • readme.txt

| Hướng dẫn cài đặt nhanh, yêu cầu PHP/WP, license, changelog cơ bản.

Mỗi file PHP cần tuân thủ escape/sanitize theo context và không in debug ra production. Bạn nên commit tree này rồi dùng AI tạo nội dung file từng bước.

Chi tiết nội dung bắt buộc trong style.css và readme.txt

Phần này nêu chính xác nội dung tối thiểu bạn phải có trong style.css để WordPress nhận diện theme, và mẫu readme.txt tối thiểu để người dùng triển khai. Mình đưa trước mẫu thực thi hoàn chỉnh, sau đó là bản mẫu với biến để bạn thay ngay.

Ví dụ hoàn chỉnh (copy-paste vào style.css đầu file)

/ Theme Name: Minimal AI Theme Theme URI: https://example.com/minimal-ai-theme Author: Bạn Tên Author URI: https://example.com Description: Lightweight WordPress theme scaffolded with AI – PHP classic theme, responsive. Version: 1.0.0 Text Domain: minimal-ai-theme Domain Path: /languages Requires at least: 5.9 Requires PHP: 7.4 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: blog, custom-logo, accessibility-ready, responsive /

Mẫu template với biến để điền

/ Theme Name: [THEME_NAME] Theme URI: [THEME_URI] Author: [AUTHOR_NAME] Author URI: [AUTHOR_URI] Description: [SHORT_DESCRIPTION] Version: [MAJOR.MINOR.PATCH] Text Domain: [TEXT_DOMAIN] Domain Path: /languages Requires at least: [WP_MIN_VERSION] Requires PHP: [PHP_MIN_VERSION] License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: [comma,separated,tags] /

Readme.txt tối thiểu (plain text)

=== [THEMENAME] === Contributors: [author-slug] Tags: [tags] Requires at least: [WPMINVERSION] Tested up to: [WPTESTEDVERSION] Requires PHP: [PHPMIN_VERSION] Stable tag: [Version] License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html

== Description == [One-paragraph description of theme, purpose, and main features.]

== Installation ==

  • Upload theme folder to wp-content/themes/
  • Activate via Appearance > Themes
  • Configure menus and widgets as needed

== Changelog == 1.0.0 – Initial release

== Notes == Include language files in /languages and ensure textdomain matches style.css

Lưu ý bảo mật ngắn: không lưu API keys hoặc secrets trong style.css hay readme.txt. Mình khuyên bạn cập nhật Version mỗi khi sửa và giữ Text Domain nhất quán.

Quy trình 9 bước tổng quan để build theme bằng AI

Dưới đây là khung nhanh để bạn và AI cùng triển khai một theme WordPress PHP chuẩn. Mỗi bước là một nhiệm vụ nhỏ, gửi cho AI file-by-file và review thủ công.

  • Định nghĩa yêu cầu: xác định tên theme, textdomain, target WP/PHP, layout, menus, widget areas, accessibility và license. 
  • Scaffold: tạo cấu trúc thư mục, style.css header, index.php minimal, screenshot. 
  • Header/footer: viết header.php có wphead() và bodyclass(), footer.php có wp_footer(), logo và menu. 
  • Template files: sinh single.php, page.php, archive.php, 404.php và template-parts, dùng Loop chuẩn và gettemplatepart(). 
  • Functions: functions.php đăng ký theme_support, menus, widgets, enqueue assets, và xử lý AJAX/REST an toàn với nonce và capability checks. 
  • Styles/assets: tổ chức assets (css/js/images), build config nếu cần, versioning bằng filemtime hoặc theme version. 
  • Build: chạy build scripts, minify, tạo source maps nếu cần, tránh nhúng secrets. 
  • Test: chạy lint, static analysis, kiểm thử manual trên local, kiểm tra escaping, nonces, wpresetpostdata(). 
  • Package: đóng gói zip, tắt WP_DEBUG, kiểm tra file permissions, cập nhật readme và changelog trước deploy.

Bước 1 – Định nghĩa yêu cầu (brief)

Trước khi gọi AI để sinh theme, bạn cần một brief rất cụ thể. Mục tiêu của brief là giảm nhầm lẫn, bắt buộc các ràng buộc bảo mật và deploy, và phân chia scope file-by-file. Dưới đây là checklist chi tiết để bạn điền trước khi gửi prompt. Sau checklist có một ví dụ đã điền sẵn và một mẫu prompt có chỗ bạn thay giá trị.

  1. Thông tin cơ bản
  2. Theme name:
  3. Text domain (khoá i18n):
  4. Short description (1 câu):
  5. License: (ví dụ GPL-2+)
  6. Mục đích và audience
  7. Loại site: (blog, corporate, landing, shop nhỏ)
  8. Người dùng chính: (editor, marketer, developer)
  9. Requirement highlights: (ví dụ landing pages thường đổi layout)
  10. Layout và cấu trúc
  11. Layout chính: (single column / sidebar right / two-column grid)
  12. Breakpoints responsive: (mobile <=480px, tablet <=768px, desktop)
  13. Header behaviour: (sticky / normal)
  14. Footer sections count: (1 / 2 / 3)
  15. Menus và widget areas
  16. Menu locations: (primary, footer, social)
  17. Widget areas: (sidebar, footer-1, footer-2)
  18. Mega menu needed: yes/no
  19. Templates cần có
  20. Required templates: (home, single, page, archive, search, 404)
  21. Custom templates: (landing.php, template-fullwidth.php)
  22. Features & integrations
  23. Theme supports: (title-tag, post-thumbnails, html5, custom-logo)
  24. Gutenberg support: yes/no
  25. Demo content importer: yes/no
  26. Integrations: (contact form plugin, SEO plugin compatibility)
  27. AJAX/REST endpoints needed: list và purpose
  28. Accessibility (a11y)
  29. WCAG target: (AA)
  30. Required checks: (alt text enforcement, keyboard nav, skip-link)
  31. RTL support: yes/no
  32. i18n / translations
  33. Languages to ship: (vi, en)
  34. Provide POT file: yes/no
  35. Textdomain to use: (same as above)
  36. Target platform & runtime
  37. WordPress target: (>=5.9)
  38. Minimum PHP: (>=7.4)
  39. Required PHP extensions: (gd, mbstring, imagick)
  40. Node/npm required for build: yes/no
  41. Hosting & deployment constraints
  42. Hosting type: (shared/managed/CPanel/SSH with WP-CLI)
  43. WAF or CDN restrictions: (blocks /wp-json/ ?)
  44. File permission policies: (no theme-folder write at runtime)
  45. Max upload size / memory limits to consider
  46. Security & privacy constraints
  47. Disallow storing secrets in repo: yes
  48. Logging policy: no debug output in production
  49. Required capability checks for endpoints: list
  50. Deliverables & format
  51. Deliverable: (zip + README + changelog + POT)
  52. Code style / license: (PSR-12 optional, WPCS, GPL-2+)
  53. Output format from AI: one file per response, no extra commentary

Ví dụ đã điền (sẵn dùng)

  • Theme name: NovaPress
  • Text domain: novapress
  • Short description: Lightweight business theme for small agencies
  • License: GPL-2+
  • Type: corporate site, editors non-technical
  • Layout: header with logo left, nav right, optional right sidebar on pages
  • Breakpoints: 480/768/1200
  • Menus: primary, footer
  • Widgets: sidebar, footer-1, footer-2
  • Templates: home, single, page, archive, search, 404, template-landing.php
  • Supports: title-tag, post-thumbnails, html5, custom-logo
  • Gutenberg: no (classic PHP theme)
  • Languages: vi, en; provide POT
  • WP >=5.9; PHP >=7.4; extensions: gd, mbstring
  • Hosting: managed WP host, WP-CLI available, CDN caches /wp-json/ so no FSE
  • Security: no secrets in repo, all forms require nonces
  • Deliverable: zip, README, POT, code passes phpcs WPCS

Mẫu prompt copy-paste-ready (thay giá trị trong ngoặc)

  • English prompt for AI tools:

“Create a WordPress PHP theme scaffold named [Theme Name] with textdomain ‘[textdomain]’. Target WordPress [WP target] and PHP [PHP min]. Purpose: [short description]. Layout: [layout details]. Required templates: [list]. Theme supports: [list]. Menus: [list]. Widget areas: [list]. Accessibility: target WCAG [level], include skip link and keyboard nav. i18n: generate POT and use textdomain. Hosting notes: [hosting constraints]. Security constraints: no secrets in repo, all state-changing endpoints must use nonces and capability checks. Output: return one file per response, only file contents, and include short comment header in each PHP file describing security checks performed. License: [license].” (Vietnamese summary: dùng prompt trên để tạo scaffold; thay giá trị trong ngoặc bằng thông tin bạn đã điền.)

Hoàn tất bước này trước khi chuyển sang scaffold. Mình sẽ dùng trường bạn điền để tạo prompt file-by-file. Bạn muốn mình chuyển bản NovaPress thành scaffold luôn không?

Bước 2 – Scaffold bằng AI (scaffold files)

Trước khi yêu cầu AI tạo code, chuẩn bị brief nhỏ và quy tắc đầu ra rõ ràng. Mục tiêu của bước này: tạo cấu trúc file cơ bản và vài file khởi tạo an toàn, output dạng file-only để dễ kiểm tra bằng diff và commit.

  • Cách prompt scaffold an toàn
  • Nêu rõ: theme name, textdomain, target WP/PHP, license (ví dụ GPL-2+).
  • Yêu cầu “output only file contents” cho từng file, không giải thích thêm.
  • Yêu cầu mỗi file PHP bắt đầu bằng <?php và có comment header nêu security checks (escaping, nonces, capability).
  • Bắt buộc: không chèn secrets, không hard-code URLs, không in debug.
  • Ví dụ prompt ngắn (copy-paste-ready):

“Create scaffold for a WordPress PHP theme named NovaPress, textdomain ‘novapress’, target WP 5.9+, PHP 7.4+. Produce these files only: style.css, index.php, header.php, footer.php, functions.php, template-parts/content.php, assets/css/main.css, screenshot.png, readme.txt. For each PHP file return only file contents. Each PHP file must include a short comment listing applied security measures (escaping/sanitization/nonces where applicable). License GPL-2+. Do not output any explanation.”

  • Output dạng file-only
  • Yêu cầu AI trả từng file riêng biệt. Sau khi nhận file, lưu vào repo local và commit ngay. Nếu tool AI trả cả thư mục tree, tách thành files trước commit.
  • Không accept responses có mô tả kèm theo. Nếu AI thêm text, phản hồi lại yêu cầu “Return only the file contents.”
  • Kiểm tra sơ bộ sau scaffold
  • Syntax: php -l path/to/file.php
  • Phpcs basic: vendor/bin/phpcs –standard=WordPress path/to/file.php
  • Search patterns: grep -n “password\|secret\|AKIA” .
  • Quick run: wp theme activate your-theme trong môi trường dev; load home, single; check PHP errors in logs.
  • Verify functions.php includes setup hook addaction(‘aftersetuptheme’, …), and enqueue hooked to wpenqueue_scripts.
  • Nếu không dùng AI: WP-CLI scaffold
  • Lệnh hữu dụng cho scaffold cơ bản:

wp scaffold s nova-press –themename=”NovaPress” –activate hoặc wp scaffold theme my-theme –theme_name=”My Theme” –activate

  • Sau scaffold CLI, chỉnh sửa functions.php để thêm security checks, enqueue, textdomain, và commit.

Thực hiện scaffold từng file, kiểm tra ngay bằng lệnh trên, rồi mới chuyển sang bước viết template chi tiết. Bạn muốn mình tạo file scaffold NovaPress theo prompt mẫu không?

Bước 3 – Viết header.php và footer.php đúng chuẩn

Mục tiêu: header.php và footer.php phải chứa các hook core, hỗ trợ override bởi child-theme, in đúng ngôn ngữ, và không để lộ dữ liệu chưa escape. Mình đưa mẫu hoàn chỉnh bạn có thể dán vào theme ngay, rồi dưới đó là những chú ý bảo mật và checklist kiểm thử.

Mẫu header.php (đã điền sẵn) <?php /**

  • Theme header

*/ ?><!doctype html> <html <?php languageattributes(); ?>> <head> <meta charset=”<?php bloginfo( ‘charset’ ); ?>” /> <meta name=”viewport” content=”width=device-width, initial-scale=1″ /> <?php wphead(); ?> </head> <body <?php bodyclass(); ?>> <?php wpbodyopen(); ?> <header class=”site-header” role=”banner”> <div class=”site-branding”> <?php if ( functionexists( ‘thecustomlogo’ ) && hascustomlogo() ) { thecustomlogo(); } else { echo ‘<a href=”‘ . escurl( homeurl( ‘/’ ) ) . ‘” class=”site-title”>’ . eschtml( getbloginfo( ‘name’ ) ) . ‘</a>’; } ?> </div> <nav class=”main-navigation” role=”navigation” aria-label=”Primary Menu”> <?php wpnavmenu( array( ‘themelocation’ => ‘primary’, ‘container’      => false, ‘menuclass’     => ‘menu’, ‘fallbackcb’    => ‘wppage_menu’, ‘depth’          => 2, ) ); ?> </nav> </header>

Mẫu footer.php (đã điền sẵn) <?php /**

  • Theme footer

*/ ?> <footer class=”site-footer” role=”contentinfo”> <div class=”site-info”> <a href=”<?php echo escurl( homeurl( ‘/’ ) ); ?>”><?php echo eschtml( getbloginfo( ‘name’ ) ); ?></a> <span class=”sep”> – </span> <span><?php echo eschtml( datei18n( ‘Y’ ) ); ?></span> </div> </footer> <?php wp_footer(); ?> </body> </html>

Lưu ý bảo mật / escape

  • Luôn escape URL và text với escurl, eschtml hoặc esc_attr theo ngữ cảnh.
  • Không include file bằng đường dẫn tĩnh; dùng gettemplatepart() nếu cần template parts.
  • Không enqueue script/style trong header/footer trực tiếp; dùng wpenqueuescripts hook trong functions.php.
  • Kiểm tra existence của functions trước khi gọi (function_exists).

Test checklist (kiểm tra nội bộ)

  • Mở trang chủ và kiểm tra console lỗi JS; xem source để đảm bảo wphead và wpfooter xuất tài nguyên plugin.
  • Thử active child-theme để xác nhận logo và menu có thể override.
  • Kiểm tra accessibility: header nav có aria-label, keyboard tab order hợp lý.
  • Kiểm tra responsive: menu collapse/visual ở mobile.
  • Kiểm tra security: tìm trong source bất kỳ echo không dùng esc*; search repo cho vardump/print_r.

Bước 4 – Sinh template files (single/page/archive/search/404)

Mục tiêu của bước này là tạo các template file hiển thị: single.php, page.php, archive.php, search.php, 404.php với loop chuẩn, tách phần hiển thị ra template-parts, và đảm bảo state global của WordPress không bị ô nhiễm khi dùng truy vấn phụ. Dẫn dắt ngắn: mỗi template phải tuân thủ lifecycle của WP loop, dùng gettemplatepart để hỗ trợ child-theme override, và luôn reset global state sau WP_Query phụ để tránh phá pagination, widget và sidebar.

  • Nguyên tắc tạo loop chuẩn (single/page/index/archive)
  • Dùng loop mặc định của WP khi file được gọi trong hierarchy:
    • while ( haveposts() ) : thepost(); gettemplatepart( ‘template-parts/content’, getposttype() ); endwhile;
  • Không truy cập trực tiếp $post->posttitle hay $post->postcontent nếu bạn chưa gọi thepost() hay setuppostdata(). Nếu cần đọc biến $post thủ công bên ngoài loop, gọi global $post; setuppostdata( $post ); sau khi xong gọi wpreset_postdata().
  • Cách xử lý WP_Query phụ đúng – mẫu hoàn chỉnh
  • Khi cần hiển thị “related posts” hay “recent posts” trong single.php, dùng WP_Query và luôn reset postdata.

Mẫu hoàn chỉnh (dán vào single.php hoặc template part):

<?php if ( haveposts() ) : while ( haveposts() ) : thepost(); gettemplate_part( ‘template-parts/content’, ‘single’ ); endwhile;

// Related posts – secondary query safe pattern $relatedargs = array( ‘postsperpage’ => 3, ‘postnotin’   => array( gettheID() ), ‘categoryin’   => wp_get_post_categories( get_the_ID() ), ); $related_query = new WP_Query( $related_args ); if ( $related_query->have_posts() ) : ?> <aside class=”related-posts”> <h2><?php echo esc_html( ‘Related posts’, ‘mytheme’ ); ?></h2> <?php while ( $relatedquery->haveposts() ) : $relatedquery->thepost(); gettemplatepart( ‘template-parts/content’, ‘excerpt’ ); endwhile; ?> </aside> <?php endif; wpreset_postdata(); // quan trọng: phục hồi global $post endif; ?>

  • Ghi nhớ: dùng wpresetpostdata(), không phải wpresetquery(), trừ khi bạn đã gọi queryposts() – và tránh queryposts() vì nó thay đổi global $wp_query.
  • Pagination và truy vấn phụ
  • Nếu dùng loop chính, dùng thepostspagination() hoặc postsnavlink().
  • Nếu bạn dùng WPQuery cho một danh sách trang có pagination, phải truyền paged param và build pagination dựa trên $customquery->maxnumpages. Ví dụ:

$paged = getqueryvar( ‘paged’ ) ? absint( getqueryvar( ‘paged’ ) ) : 1; $args = array( ‘paged’ => $paged, ‘postsperpage’ => 10, … ); $custom = new WPQuery( $args ); // render loop echo paginatelinks( array( ‘total’ => $custom->maxnumpages, ‘current’ => $paged ) ); wpresetpostdata();

  • Test pagination: tạo nhiều bài để vượt số bài mỗi trang, click trang 2, 3, kiểm tra URL có paged param và nội dung khớp.
  • gettemplatepart và child-theme override
  • Luôn tách nội dung hiển thị vào template-parts và gọi bằng gettemplatepart( ‘template-parts/content’, ‘single’ ); để child-theme có thể đặt file tương ứng và override dễ dàng.
  • Không include hoặc require bằng đường dẫn cứng. Tránh include gettemplatedirectory() . ‘/parts/header.php’ vì child-theme sẽ không override được.
  • Tương tác với widget và sidebar
  • Widget/sidebar dựa trên global $wpquery và global $post trong nhiều widget. Luôn wpresetpostdata() trước khi gọi dynamicsidebar(); nếu phần hiển thị widget nằm sau custom query phụ, sidebar có thể hiển thị bài sai nếu global chưa được reset.
  • Test widget interactions: đặt widget hiển thị recent posts vào sidebar, rồi load page có secondary query. Nếu widget hiển thị bài của query phụ thay vì global main loop, bạn chưa reset postdata.
  • Các lỗi AI hay mắc và cách phát hiện
  • Quên wpresetpostdata() sau WPQuery phụ – phát hiện: sidebar/recent posts/pagination show wrong posts. Kiểm tra bằng tạm thời thêm global $post; vardump( gettheID() ) trước và sau query.
  • Sử dụng queryposts() – lỗi: làm thay đổi global $wpquery. Phát hiện: pagination hỏng, use grep ‘query_posts’ trong repo.
  • In ra $post->posttitle trực tiếp trong template part mà không gọi thetitle() hay eschtml – phát hiện: grep for “->posttitle” và “echo \$post”.
  • Include file bằng đường dẫn cứng thay vì gettemplatepart – phát hiện: grep include|require combined with gettemplatedirectory().
  • Enqueue script/style trực tiếp trong template file – phát hiện: grep ‘wpenqueuescript’ trong .php ngoài functions.php; script có thể bị enqueue sai thời điểm.
  • Missing thepostspagination on archive templates – phát hiện: pagination link absent; inspect template code.
  • Using global $post without declaring global in function scope – phát hiện: notices or wrong content when strict error reporting enabled. Test with WP_DEBUG true.
  • Cách test nhanh để bạn làm
  • Turn on WP_DEBUG in dev.
  • Manually navigate: home, archive page 2, single with related posts, check sidebar content.
  • Use grep checks:
    • grep -R “wpresetpostdata” -n .
    • grep -R “query_posts” -n .
    • grep -R “->post_title” -n .
    • grep -R “wpenqueuescript” -n template-parts/ || true

Kết luận: khi yêu cầu AI sinh các template, bắt AI xuất pattern loop chuẩn, include mẫu secondary WPQuery với wpresetpostdata(), và gettemplate_part usage. Yêu cầu AI liệt kê mọi chỗ cần reset global state và trả về test cases ngắn để bạn chạy ngay.

Bước 5 – Viết functions.php an toàn

Mục tiêu: functions.php phải đăng ký theme supports, menu, enqueue assets an toàn, xử lý customizer/theme_mod với sanitize, và mẫu handler cho AJAX/REST có nonce và kiểm tra capability. Dưới đây là mẫu cấu trúc functions.php hoàn chỉnh kèm comment giải thích từng hàm để bạn copy-paste và điều chỉnh tên textdomain/tên theme cho đúng.

Mẫu functions.php (file hoàn chỉnh, thay [MYTHEMETEXTDOMAIN] bằng textdomain của bạn):

<?php /**

  • Theme functions and setup

*

  • @package My_Theme

*/

if ( ! defined( ‘MYTHEMETEXTDOMAIN’ ) ) { define( ‘MYTHEMETEXTDOMAIN’, ‘my_theme’ ); }

/**

  • Thiết lập theme supports và đăng ký menu

*/ function mythemesetup() { // Title tag management addthemesupport( ‘title-tag’ );

// Featured images addthemesupport( ‘post-thumbnails’ );

// HTML5 support cho một số form và markup addthemesupport( ‘html5’, array( ‘search-form’, ‘comment-form’, ‘comment-list’, ‘gallery’, ‘caption’ ) );

// Custom logo addthemesupport( ‘custom-logo’ );

// Register navigation menus registernavmenus( array( ‘primary’ => _( ‘Primary Menu’, MYTHEMETEXTDOMAIN ), ) ); } addaction( ‘aftersetuptheme’, ‘mythemesetup’ ); // chạy sớm và an toàn

/**

  • Enqueue styles and scripts an toàn
  • – version dùng filemtime nếu tồn tại để cache-busting trên dev
  • – load script ở footer nếu cần

*/ function mythemeenqueueassets() { $theme = wpget_theme(); $version = $theme->get( ‘Version’ ) ?: ‘1.0.0’;

$cssfile = gettemplatedirectory() . ‘/assets/css/main.css’; $cssuri  = gettemplatedirectoryuri() . ‘/assets/css/main.css’; if ( fileexists( $cssfile ) ) { $version = filemtime( $cssfile ); }

wpenqueuestyle( ‘my-theme-main’, $cssuri, array(), $version ); wpenqueuescript( ‘my-theme-main’, gettemplatedirectoryuri() . ‘/assets/js/main.js’, array( ‘jquery’ ), $version, true );

// Localize nonce for AJAX wplocalizescript( ‘my-theme-main’, ‘MyTheme’, array( ‘ajaxurl’ => adminurl( ‘admin-ajax.php’ ), ‘nonce’    => wpcreatenonce( ‘mythemenonce’ ), ) ); } addaction( ‘wpenqueuescripts’, ‘mythemeenqueueassets’ );

/**

  • Safe wrapper to output custom logo HTML escaped

*/ function mythemethecustomlogosafe() { $html = getcustomlogo(); echo wpkses_post( $html ); // allow only safe HTML from core }

/**

  • Theme mod / customizer handling example
  • – sanitize callback dùng sanitizetextfield

*/ function mythemecustomizeregister( $wpcustomize ) { $wpcustomize->addsetting( ‘mythemeheadertext’, array( ‘default’           => ”, ‘sanitizecallback’ => ‘sanitizetextfield’, ‘capability’        => ‘editthemeoptions’, ) );

$wpcustomize->addcontrol( ‘mythemeheadertext’, array( ‘label’   => ( ‘Header text’, MYTHEMETEXTDOMAIN ), ‘section’ => ‘titletagline’, ‘type’    => ‘text’, ) ); } addaction( ‘customizeregister’, ‘mythemecustomize_register’ );

/**

  • AJAX handler example – chỉ cho người dùng đã đăng nhập có capability ‘edit_posts’
  • – checkajaxreferer để phòng CSRF
  • – sanitize input trước khi dùng

*/ function mythemeajaxsavefavorite() { checkajaxreferer( ‘mythemenonce’, ‘security’ );

if ( ! currentusercan( ‘editposts’ ) ) { wpsendjsonerror( ‘forbidden’, 403 ); }

$postid = isset( $POST[‘postid’] ) ? absint( $POST[‘postid’] ) : 0; if ( ! $postid ) { wpsendjsonerror( ‘invalidpost’, 400 ); }

// Example action – store as user meta safely $userid = getcurrentuserid(); updateusermeta( $userid, ‘favoritepost‘ . $postid, 1 );

wpsendjsonsuccess( array( ‘saved’ => true ) ); } addaction( ‘wpajaxmythemesavefavorite’, ‘mythemeajaxsave_favorite’ ); // no nopriv – requires login

/**

  • REST endpoint example with permission_callback

*/ function mythemeregisterrestroutes() { registerrestroute( ‘my-theme/v1’, ‘/status’, array( ‘methods’  => ‘POST’, ‘callback’ => ‘mythemereststatusupdate’, ‘permissioncallback’ => function ( $request ) { return currentusercan( ‘editposts’ ); // enforce capability }, ) ); } addaction( ‘restapiinit’, ‘mythemeregisterrest_routes’ );

function mythemereststatusupdate( WPRESTRequest $request ) { $data = $request->getparam( ‘value’ ); $value = sanitizetextfield( $data ); // perform safe update… return restensure_response( array( ‘ok’ => true, ‘value’ => $value ) ); }

?>

Prompt mẫu (mẫu prompt 2) để dùng với AI Tiếng Việt tóm ý: prompt này bạn dán vào ChatGPT/Copilot để yêu cầu sinh functions.php an toàn, chỉ trả về nội dung file.

Prompt (English, copy-paste-ready): “Generate a complete functions.php for a WordPress theme named ‘My Theme’ with textdomain ‘my_theme’. Target WordPress 5.9+ and PHP 7.4+. Requirements:

  • Add action aftersetuptheme to register theme supports: title-tag, post-thumbnails, html5 (search-form, comment-form, comment-list, gallery, caption), custom-logo.
  • Register a ‘primary’ nav menu.
  • Enqueue assets assets/css/main.css and assets/js/main.js using filemtime for version fallback to theme version. Localize a nonce for AJAX.
  • Add a customize_register example with sanitize callbacks.
  • Provide a safe AJAX handler using checkajaxreferer and currentusercan.
  • Provide a REST route example with permission_callback checking capabilities and sanitize REST inputs.
  • Use esc*/wpksespost when outputting HTML and sanitizetext_field/absint for inputs.
  • Include inline comments above each function explaining purpose and security reasoning.

Return only the contents of functions.php and nothing else.”

Giải thích ngắn: prompt yêu cầu rõ tên theme, targets, security checks và comment giải thích. Bạn có thể chỉnh textdomain và tên hàm cho phù hợp project.

Bước 6 – Styles và assets

Styles và assets quyết định trải nghiệm người dùng và performance. Mình tóm tắt best practices để bạn tổ chức CSS/SCSS, đặt tên class, xử lý critical CSS, editor styles, font loading và enqueuing an toàn. Nếu bạn dùng build step thì có lợi cho cache-busting và minify; nếu không dùng build thì giữ cấu trúc đơn giản, rõ ràng và tránh inlining secrets.

  • Cấu trúc CSS/SCSS
  • src/scss/
    • base/_reset.scss
    • base/_typography.scss
    • components/_buttons.scss
    • layout/_header.scss
    • layout/_footer.scss
    • pages/_home.scss
    • main.scss  // imports trên, chỉ một entry
  • dist/css/main.css  // production build

Nguyên tắc: một entry point cho production; tách components để dễ reuse.

  • Naming conventions
  • Dùng BEM nhẹ: .c-card, .c-card__title, .u-hidden, .is-active
  • Prefix class riêng theme để tránh va chạm với plugin: .myt- (ví dụ .myt-nav)
  • Utility classes ngắn gọn và dùng prefix u- hoặc util-
  • Critical CSS và editor styles
  • Extract critical CSS cho above-the-fold nhỏ gọn (<10 KB nếu có thể). Load critical inline hoặc via <link rel=”preload”> rồi swap.
  • Editor styles: tạo file editor-style.css phù hợp với main styles để WYSIWYG trong block editor. Enqueue bằng addthemesupport( ‘editor-styles’ ) và addeditorstyle().
  • Font loading, preconnect, prefetch
  • Đặt preload/preconnect trong wphead qua wpenqueue_scripts hook:
    • <link rel=”preconnect” href=”https://fonts.gstatic.com” crossorigin>
    • <link rel=”preload” as=”style” href=”https://fonts.googleapis.com/…” onload=”this.rel=’stylesheet'”>
  • Nếu dùng local fonts, serve woff2 và khai báo font-display: swap để tránh FOIT.
  • Enqueuing an toàn
  • Luôn enqueue trên hook wpenqueuescripts cho front-end và adminenqueuescripts cho admin.
  • Versioning bằng filemtime hoặc theme version để cache-busting.
  • Không in trực tiếp URL hoặc secrets vào JS bundle. Nếu cần dynamic data, dùng wplocalizescript hoặc wpaddinlinescript với dữ liệu đã được escjs/wpjsonencode.

Ví dụ enqueue an toàn (bản hoàn chỉnh để copy):

function mytenqueueassets() { $theme = wpgettheme(); $version = $theme->get(‘Version’) ?: filemtime( gettemplatedirectory() . ‘/dist/css/main.css’ ); wpenqueuestyle( ‘myt-main’, gettemplatedirectoryuri() . ‘/dist/css/main.css’, array(), $version ); wpenqueuescript( ‘myt-main’, gettemplatedirectoryuri() . ‘/dist/js/main.js’, array(), $version, true ); wplocalizescript( ‘myt-main’, ‘Myt’, array( ‘ajaxurl’ => adminurl( ‘admin-ajax.php’ ), ‘nonce’    => wpcreatenonce( ‘mytnonce’ ), ) ); } addaction( ‘wpenqueuescripts’, ‘mytenqueueassets’ );

Phiên bản có biến số để bạn thay tên/textdomain:

function {THEMEPREFIX}enqueueassets() { $theme = wpgettheme(); $version = $theme->get(‘Version’) ?: filemtime( gettemplatedirectory() . ‘/{ASSETPATH}/main.css’ ); wpenqueuestyle( ‘{THEMEPREFIX}-main’, gettemplatedirectoryuri() . ‘/{ASSETPATH}/main.css’, array(), $version ); wpenqueuescript( ‘{THEMEPREFIX}-main’, gettemplatedirectoryuri() . ‘/{ASSETPATH}/main.js’, array(), $version, true ); wplocalizescript( ‘{THEMEPREFIX}-main’, ‘{JSNAMESPACE}’, array( ‘ajaxurl’ => adminurl( ‘admin-ajax.php’ ), ‘nonce’    => wpcreatenonce( ‘{THEMEPREFIX}nonce’ ), ) ); } addaction( ‘wpenqueuescripts’, ‘{THEMEPREFIX}enqueueassets’ );

  • Option build vs no-build
  • Với build: dùng npm + PostCSS, Autoprefixer, CSS minify, sourcemaps; commit only dist for deploy or build on CI. Đặt CI job chạy lint và build.
  • Không build: viết plain CSS, sử dụng conventions và manual versioning; tránh modern syntax không tương thích PHP hosting hoặc old browsers.

Kết luận ngắn: tổ chức SCSS theo modules, dùng BEM và prefix, tải font an toàn với preload/preconnect, extract critical CSS, và enqueuing chỉ qua hook thích hợp với versioning. Nếu bạn muốn, mình có thể tạo pipeline npm minimal và config PostCSS tương thích để build assets cho theme của bạn.

Bước 7 – Build/compile pipeline cơ bản

Bước này biến source (SCSS, JS module, assets) thành bundle production an toàn, tối ưu và có cache-busting. Mình tóm gọn luồng tối thiểu: dev build có sourcemap, production build minify và sinh tên file có hash; tuyệt đối không inlining secrets vào bundle; mọi secret chỉ truyền qua server-side hoặc runtime API calls.

  • Công cụ cơ bản
  • Node/npm hoặc pnpm, bundler nhẹ như Vite hoặc webpack, PostCSS + Autoprefixer, cssnano cho minify.
  • package.json scripts mẫu, chạy trên CI và dev machine.
  • npm scripts ví dụ (copy-paste)
  • package.json scripts:
    • “dev”: “vite”
    • “build”: “vite build”
    • “preview”: “vite preview”
  • Nếu dùng webpack:
    • “dev”: “webpack –mode development –watch –devtool cheap-module-source-map”
    • “build”: “webpack –mode production –env production”
  • Production build – yêu cầu
  • Minify JS/CSS, tree-shaking nếu có, extract CSS thành file riêng.
  • Generate source maps nhưng chỉ lưu ở staging/internal hoặc upload vào private artifact bucket; không public hoá source maps trộn chứa secrets.
  • Cache-busting: filename hashing. Ví dụ output: main.0a1b2c.css và main.0a1b2c.js. Trong theme, enqueue bằng version từ filename hoặc manifest.json do bundler xuất ra.
  • Example build command (Vite):
    • NODE_ENV=production npm run build
    • result in dist/ with assets manifest dist/manifest.json
  • Tránh inlining secrets into bundles
  • Never inject CI secrets or API keys into front-end source at build time. Nếu front-end cần token ngắn, generate token server-side at request time and inject via wplocalizescript or an endpoint that requires auth.
  • In CI, mask secrets in logs and avoid echoing env vars.
  • Cache-busting and enqueue example (functions.php snippet)
  • Use manifest to enqueue:
    • $manifest = jsondecode( filegetcontents( gettemplate_directory() . ‘/dist/manifest.json’ ), true );
    • wpenqueuestyle( ‘theme-main’, gettemplatedirectory_uri() . ‘/dist/’ . $manifest[‘main.css’][‘file’], array(), null );
    • wpenqueuescript( ‘theme-main’, gettemplatedirectory_uri() . ‘/dist/’ . $manifest[‘main.js’][‘file’], array(), null, true );

Kết luận ngắn: dev build = sourcemap, fast; production = minify + hashed filenames + no secrets inlined; chạy via npm run build (Vite) hoặc webpack build command tuỳ tool bạn chọn.

Bước 8 – Kiểm thử (functional + security)

Mục tiêu của bước này là đảm bảo theme hoạt động đúng từ góc nhìn chức năng và an toàn về mặt bảo mật trước khi đóng gói. Bạn cần kết hợp kiểm thử thủ công và automated. Dưới đây là quy trình cụ thể kèm checklist mà bạn có thể chạy ngay.

  • Quy trình test tổng quát – thứ tự thực hiện

1.1. Chuẩn bị

  • Bật môi trường dev với cùng PHP/WP version như production.
  • Kích hoạt WPDEBUG = true ở dev để thấy warnings. Trên production WPDEBUG phải false.
  • Kích hoạt theme trên staging: wp theme activate my-theme

1.2. Automated static checks (first gate)

  • Syntax check tất cả PHP: find . -name ‘*.php’ -exec php -l {} \;
  • PHPCS với WordPress standard: vendor/bin/phpcs –standard=WordPress path/to/theme
  • PHPStan basic: vendor/bin/phpstan analyse -l 0 path/to/theme
  • Secret scan: gitleaks detect . hoặc gitleaks –path=. –report=gitleaks-report.json
  • Grep nhanh các pattern nguy hiểm:
    • grep -R “unserialize(” -n .
    • grep -R “eval(” -n .
    • grep -R “wpajaxnopriv.*addaction” -n .

1.3. Unit tests / Integration (nếu có)

  • Nếu bạn có logic PHP tách được – viết PHPUnit tests. Minimal: test helper functions, sanitization wrappers, REST permission callbacks.
  • Run tests: vendor/bin/phpunit
  • Nếu không có unit tests, bắt buộc có integration checks phía dưới.

1.4. Integration via WP-CLI

  • Test theme activation and basic endpoints:
    • wp theme activate my-theme
    • wp eval ‘vardump(istheme_active(“my-theme”));’
  • Kiểm tra REST endpoints:
    • curl -i https://example.test/wp-json/wp/v2/posts
    • Với cookie auth simulate save: use browser or wp-cli restdorequest in server context to test POST/PUT on /wp/v2/templates or your custom endpoints.
  • Kiểm tra database side effects: wp db query “SELECT COUNT(*) FROM wpposts WHERE posttype=’post'”

1.5. Browser checks – manual acceptance

  • Core pages: home, single post, page, archive, search, 404, author page
  • Components: menu, breadcrumbs, featured image, comments, widgets, custom logo, footer credits
  • Responsive: test breakpoints on Chrome device toolbar or real devices
  • Interactive: JS behaviors, lazyload, form submissions, AJAX actions

1.6. Accessibility quick-checks

  • Run Lighthouse accessibility audit in Chrome DevTools.
  • Check all images have alt attributes.
  • Ensure form inputs have label elements.
  • Keyboard navigation: tab order logical, focus visible.
  • Headings structure: one H1 per content page.
  • Security-focused checks – manual and automated

2.1. Output escaping

  • Search for raw echoes: grep -R “<?php echo ” -n . and verify each uses eschtml/escattr/escurl/wpkses_post as appropriate.
  • Ensure the_content usage is filtered appropriately when rendering untrusted HTML.

2.2. Input sanitization and validation

  • Find usage of $POST, $GET, $_REQUEST and ensure values are sanitized before use:
    • sanitizetextfield, sanitizeemail, escurlraw, absint, wpkses_post
  • Check saving to options or post meta uses proper sanitization.

2.3. Nonces and capability checks

  • For any state-changing handler (forms, admin-post handlers, AJAX, REST) verify:
    • Form includes wpnoncefield(‘action’,’nonce’)
    • Handler calls checkadminreferer or checkajaxreferer
    • Handlers enforce currentusercan where needed

2.4. Database queries

  • Ensure all custom $wpdb queries use $wpdb->prepare or $wpdb->insert/$wpdb->update with formats.

2.5. File uploads and includes

  • Any file upload uses wphandleupload and wpcheckfiletypeandext
  • Any include/require using user-provided values must be whitelisted – never include($_GET[‘file’] . ‘.php’).

2.6. REST and AJAX endpoints

  • For registerrestroute ensure permission_callback checks capability or authentication.
  • From browser while logged in as admin, perform full edit-save cycle against your custom REST endpoints and watch responses and HTTP status codes.
  • Testing multiple loops, pagination, and global state
  • Create test content with many posts to exercise pagination. Verify:
    • Paginated archive pages show correct current page and next/prev links.
    • Custom WPQuery usage in templates calls wpreset_postdata() after query.
    • Sidebar widgets that rely on global $wp_query render correct data after secondary queries.
  • Test loops nesting: if you use getposts or WPQuery inside single.php to show related posts, ensure you call setuppostdata and wpreset_postdata appropriately.
  • Regression checklist to run before every release
  • wp theme activate my-theme
  • Visit home, archive page 2, single post, search with query containing special chars
  • Submit contact form and verify nonce check triggered when nonce missing
  • Upload image and verify MIME checks and that no PHP upload possible
  • Run phpcs and phpstan again
  • Check error logs for PHP notices/warnings
  • Final acceptance gate – deploy checklist (minimally)
  • WP_DEBUG = false
  • No PHPCS errors of severity high
  • No secret scan hits
  • Manual smoke test passed for admin and public flows
  • Backup snapshot created and deployment rollback tested

Kết luận ngắn gọn

  • Kết hợp static analysis, WP-CLI integration, unit tests nếu có, và manual browser/accessibility/security checks. Tập trung ưu tiên vào escaping, sanitization, nonces, capability checks, và đảm bảo multiple-loop/pagination behavior đúng. Nếu bất kỳ test bảo mật nào fail, chặn deploy cho đến khi fix.

Bước 9 – Package và deploy

Mục tiêu của bước này là đóng gói theme an toàn, kiểm tra cấu hình production và triển khai lên server theo quy trình có kiểm soát. Làm từng bước nhỏ, kiểm tra sau mỗi thao tác.

  • Chuẩn bị trước khi đóng gói
  • Đảm bảo WPDEBUG = false trên môi trường đích. Trên dev vẫn để WPDEBUG = true để phát hiện lỗi. 
  • Xóa mọi vardump/echo debug, test credentials, và file .env khỏi thư mục theme. Chạy grep nhanh: grep -R “vardump\|print_r\|DEBUG” -n . 
  • Cập nhật version trong style.css và trong đọc thay đổi. Ghi CHANGELOG.md theo semantic versioning.
  • Tạo file zip để upload qua WP Admin
  • Kiểm tra quyền file: files PHP 644, thư mục 755; uploads writable. Đảm bảo theme folder không writable bởi www-data nếu không cần ghi runtime. 
  • Tạo zip: zip -r my-theme-1.0.0.zip my-theme/ –exclude=”node_modules/” –exclude=”vendor/” 
  • Kiểm tra zip trước khi upload: unzip -l my-theme-1.0.0.zip
  • Upload via WP Admin (khi phù hợp)
  • Appearance > Themes > Add New > Upload Theme. Kích hoạt và chạy smoke test: home, single, customizer, menus. Nếu upload bị giới hạn kích thước, dùng SFTP hoặc deploy bằng Git.
  • SFTP / Git deploy
  • SFTP: upload thư mục my-theme vào wp-content/themes/; thiết lập ownership đúng (chown webuser:www-data nếu cần). Sau upload, chạy wp theme activate my-theme hoặc kích hoạt qua admin. 
  • Git deploy: push lên repo staging; CI build tạo artifact zip; deploy script (rsync/scp) copy tới server. Luôn dùng deploy key với quyền tối thiểu và pipeline có step manual approval cho production.
  • Lưu ý permissions và bảo mật
  • Không cho phép theme folder writable nếu không cần. Chỉ wp-content/uploads và cache folder có thể writable. 
  • Đặt DISALLOWFILEEDIT = true trong wp-config.php trên production để chặn chỉnh code qua admin.
  • Sau deploy
  • Kiểm tra error logs (nginx/apache, PHP-FPM). Kiểm thử nhanh: pagination, widgets, AJAX/REST endpoints, upload file. Ghi log release trong CHANGELOG.md và tag release trong Git: git tag -a v1.0.0 -m “Release v1.0.0” && git push –tags.

Kết thúc: nếu tất cả kiểm tra pass, thông báo cho client và lưu snapshot backup trước khi đóng cửa sổ deploy.

Mẫu prompt 1 – brief tổng quát để tạo scaffold (sẵn dùng)

Mục đích: prompt này để bạn dán vào ChatGPT/Copilot/Codeium nhằm sinh scaffold cho một theme WordPress PHP cổ điển tương thích WordPress 5.9+. Nội dung trả về phải là cây file theme với nội dung từng file, sẵn để bạn copy vào dự án. Dưới đây có 1 ví dụ hoàn chỉnh đã điền thông số, rồi template prompt có chỗ bạn thay [PLACEHOLDER].

Ví dụ đã điền (copy-paste vào AI ngay) Generate a WordPress classic PHP theme scaffold named “BrightStudio” with textdomain “brightstudio”. Target WordPress 5.9+ and PHP 7.4+. Produce the theme folder structure and the full contents of each file below. Required files: style.css (with valid WordPress theme header including Theme Name, Text Domain, Version, License GPL-2+), index.php, functions.php, header.php, footer.php, single.php, page.php, template-parts/content.php, assets/css/main.css, assets/js/main.js, languages/ placeholder, screenshot.png placeholder, readme.txt. In functions.php register theme supports: title-tag, post-thumbnails, html5 (search-form, comment-form, comment-list, gallery, caption), custom-logo; register ‘primary’ menu; enqueue assets using filemtime fallback to theme version. Use gettemplatepart for templates. All PHP outputs must be escaped appropriately. Return only the file tree and full file contents, one file after another, no extra explanation.

Prompt template (thay chỗ trong ngoặc vuông) Generate a WordPress classic PHP theme scaffold named “[THEMENAME]” with textdomain “[TEXTDOMAIN]”. Target WordPress [WPVERSION]+ and PHP [PHPMIN]+. Produce the theme folder structure and the full contents of each file below. Required files: style.css (with valid WordPress theme header including Theme Name, Text Domain, Version, License GPL-2+), index.php, functions.php, header.php, footer.php, single.php, page.php, template-parts/content.php, assets/css/main.css, assets/js/main.js, languages/ placeholder, screenshot.png placeholder, readme.txt. In functions.php register theme supports: title-tag, post-thumbnails, html5 (search-form, comment-form, comment-list, gallery, caption), custom-logo; register ‘primary’ menu; enqueue assets using filemtime fallback to theme version. Use gettemplate_part for templates. All PHP outputs must be escaped appropriately. Return only the file tree and full file contents, one file after another, no extra explanation.

Ghi log thay đổi và README cho người dùng theme

Một README và CHANGELOG rõ ràng giúp bạn và người dùng hiểu nhanh yêu cầu hệ thống, cách cài đặt, lịch sử thay đổi và những điểm cần chú ý khi nâng cấp. Dưới đây mình trình bày cả ví dụ hoàn chỉnh đã điền để bạn copy trực tiếp, rồi mẫu template có chỗ để bạn thay thông số. Mỗi phần gồm nội dung cần có, định dạng khuyến nghị, và lệnh git hữu ích để tạo tag/release.

Ví dụ hoàn chỉnh (copy-paste)

  • Theme name: BrightStudio
  • Textdomain: brightstudio
  • Version hiện tại: 1.2.0

CHANGELOG.md (ví dụ)

# Changelog

All notable changes to this project will be documented in this file.

## [1.2.0] – 2026-06-01

### Added

– Thêm pattern block “Hero” cho trang chủ.

– Hỗ trợ new image sizes và responsive srcset.

### Fixed

– Sửa lỗi XSS trong template-part content-single bằng esc_html.

– Cập nhật enqueue assets để fallback filemtime khi không tìm thấy theme version.

### Security

– Thêm checkajaxreferer cho AJAX endpoint contactformsubmit.

## [1.1.0] – 2025-12-10

### Added

– Hỗ trợ custom logo và menu ‘primary’.

### Changed

– Tối ưu CSS, giảm payload initial.

## [1.0.0] – 2025-08-15

### Initial release

– Scaffold cơ bản: header, footer, templates, assets.

README.md (ví dụ)

# BrightStudio

Version 1.2.0

Textdomain: brightstudio

License: GPL-2+

## Yêu cầu

– WordPress 5.9+

– PHP 7.4+

## Cài đặt

1. Upload ˋbrightstudio.zipˋ qua Appearance > Themes > Add New > Upload Theme, hoặc giải nén vào ˋwp-content/themes/brightstudioˋ.

2. Kích hoạt theme.

3. Vào Appearance > Menus, gán menu cho vị trí “Primary”.

## Nâng cấp

– Trước khi cập nhật, sao lưu database và wp-content.

– Nếu bạn đã chỉnh sửa template qua Site Editor, lưu ý template lưu trong database có thể override file mới.

## Hooks and filters

– ˋbrightstudiobeforecontentˋ – action trước nội dung chính. Usage: addaction(‘brightstudiobefore_content’, function(){});

– ˋbrightstudiomodifytitleˋ – filter để thay đổi title. Usage: addfilter(‘brightstudiomodifytitle’, ‘yourfunc’);

## Debug / Troubleshooting

– Bật WP_DEBUG khi test local. Tắt trước production.

– Nếu thấy lỗi trắng trang, kiểm tra ˋwp-content/debug.logˋ và server error log.

## License & Credits

– License: GPL-2+

– Author: Your Name

Mẫu template CHANGELOG.md (thay chỗ trong ngoặc)

  • Hãy giữ cấu trúc Semantic Versioning: MAJOR.MINOR.PATCH.
  • Ghi ngày ISO YYYY-MM-DD.
  • Nhóm mục theo Added, Changed, Fixed, Security, Removed.

Mẫu CHANGELOG.md (template)

# Changelog

## [{VERSION}] – {YYYY-MM-DD}

### Added

– {Mô tả tính năng mới}

### Changed

– {Mô tả thay đổi không tương thích}

### Fixed

– {Mô tả lỗi đã sửa}

### Security

– {Mô tả bản vá bảo mật}

Mẫu README.md (template)

  • Phần mở: name, version, license, textdomain.
  • Yêu cầu hệ thống: WP, PHP, extensions cần.
  • Cài đặt: bước từng bước upload/activate, WP-CLI alternative.
  • Nâng cấp: chú ý overwrite DB-stored templates, child theme, backup.
  • Hooks/Filters: liệt kê hook public, args, return values.
  • Troubleshooting: logs, common fixes, contact/support.
  • Contributing: coding standard, how to run tests.
  • License: include GPL-2+ statement.

Ví dụ header style.css snippet bạn nên copy vào README

/∗

Theme Name: {THEME_NAME}

Theme URI: {PROJECT_URL}

Author: {AUTHOR_NAME}

Version: {VERSION}

Text Domain: {TEXTDOMAIN}

License: GPL-2+

∗/

Lệnh git / release hữu ích

  • Tag release: git tag -a v1.2.0 -m “Release v1.2.0” && git push origin v1.2.0
  • Tạo release notes trên GitHub: dùng nội dung CHANGELOG.md cho body.
  • Nếu phát hiện secret trong history: rotate key, purge bằng git filter-repo hoặc BFG, và ghi chú trong CHANGELOG Security.

Ghi chú vận hành nhanh

  • Mỗi commit thay đổi behavior hoặc API (hooks, AJAX, REST) phải kèm entry CHANGELOG và tăng version theo semver.
  • Cho patch release (security/fix) luôn kèm tag và nhanh chóng push.
  • Nếu update có thể break child themes hoặc DB templates, thêm mục “Upgrade notes” chi tiết: files affected, manual steps.

Checklist ngắn trước release

  • Run phpcs, phpstan, eslint, build assets.
  • Chạy grep tìm secrets: grep -R “API_KEY\|SECRET\|PASSWORD” .
  • Backup DB, tạo tag, upload zip, cập nhật CHANGELOG và README.

Kết luận ngắn

  • README và CHANGELOG là phần của sản phẩm, không phải ghi chú nội bộ. Viết rõ, cập nhật mỗi release, và luôn kèm hướng dẫn roll-back. Nếu bạn muốn, mình sẽ tạo file README.md và CHANGELOG.md đã điền cho theme cụ thể bạn đang làm. Bạn muốn mình làm cho theme nào?

Mẫu prompt 3 – prompt để sinh template cụ thể (ví dụ single.php)

Mục tiêu: một prompt sẵn dùng để yêu cầu AI sinh file single.php an toàn cho theme PHP chuẩn. Yêu cầu rõ loop chuẩn, dùng gettemplatepart, escape output theo ngữ cảnh, handle comments, và nhắc kiểm tra wpresetpostdata khi có WP_Query phụ. Dưới đây có hai bản: một bản hoàn chỉnh bạn có thể copy-paste vào ChatGPT/Copilot; một bản template với chỗ để bạn thay thông số.

Prompt hoàn chỉnh (copy-paste, tiếng Anh vì tools hiểu tốt hơn): Please generate the complete contents of a WordPress theme file single.php for a classic PHP theme named “BrightStudio” with textdomain “brightstudio”. Requirements:

  • Use getheader() and getfooter().
  • Implement the standard Loop: while ( haveposts() ) : thepost(); then include gettemplatepart(‘template-parts/content’, ‘single’); endwhile;
  • If a secondary WPQuery is needed for related posts example, demonstrate proper use of setuppostdata and wpresetpostdata().
  • Load comments_template() only when appropriate.
  • All echoed output must be escaped according to context: eschtml for plain text, escattr for attributes, escurl for URLs, wpkses_post when allowing limited HTML.
  • Do not enqueue scripts here. Do not include hard-coded credentials or debug output.
  • Add concise inline comments explaining security choices (escaping, nonces if forms appear, capability checks).
  • Return only the PHP file content, no extra explanation or markdown.

Tóm tắt tiếng Việt: prompt trên tạo single.php sẵn dùng cho theme BrightStudio; thay “BrightStudio” và “brightstudio” nếu bạn cần tên khác.

Prompt template với biến để thay:

  • Replace [THEME_NAME] and [TEXTDOMAIN] in the English prompt for your theme.

Hướng dẫn điều chỉnh cho block theme (chỉ nêu chỗ khác)

Trong phần này mình tóm tắt những điểm khác biệt chính khi bạn chuyển từ PHP theme cổ điển sang một block theme / full‑site editing (FSE). Mình chỉ nêu các thay đổi thiết thực bạn cần làm so với quy trình PHP đã mô tả: cấu trúc file, lưu template, theme.json, patterns, editor styles, bảo mật REST và testing. Nếu bạn muốn toàn bộ theme theo FSE, mình sẽ tạo scaffold riêng.

  • File structure khác biệt chính

1.1 Các thư mục và file bắt buộc hoặc phổ biến

  • block-templates/ – chứa template files .html (ví dụ index.html, single.html).
  • block-template-parts/ – chứa template part .html (ví dụ header.html, footer.html).
  • theme.json – cấu hình style và settings cho editor và front-end.
  • patterns/ (tùy chọn) – mẫu block patterns .json hoặc .html.
  • style.css vẫn cần header metadata cho WP theme detection, nhưng template logic di chuyển khỏi .php.

1.2 Vấn đề kế thừa child theme

  • Khi editor lưu template vào database, template file trong theme có thể bị ghi đè hoặc bị shadowed bởi bản lưu trong DB. Ghi chú này quan trọng cho quá trình cập nhật theme.
  • theme.json – chức năng và ví dụ tối thiểu
  • Mục đích: quản lý global styles, settings, block supports, và editor palettes.
  • Luôn khai báo version và cung cấp cả editor và front-end settings.

Ví dụ hoàn chỉnh (copy-paste-ready) theme.json tối thiểu: { “version”: 2, “settings”: { “color”: { “custom”: true, “palette”: [ { “slug”: “primary”, “color”: “#0d6efd”, “name”: “Primary” }, { “slug”: “dark”, “color”: “#111827”, “name”: “Dark” } ] }, “typography”: { “fontSizes”: [ { “slug”: “small”, “size”: “14px” }, { “slug”: “normal”, “size”: “16px” }, { “slug”: “large”, “size”: “24px” } ] } }, “styles”: { “color”: { “background”: “#ffffff”, “text”: “#111827” }, “typography”: { “fontFamily”: “system-ui, sans-serif” } } } Mẫu template để thay: thay giá trị màu, font, và version theo dự án.

  • Viết block template .html an toàn – ví dụ minimal
  • Template files là HTML chứa block markup. Không dùng PHP để in trực tiếp giá trị người dùng trong template file. Nếu cần dynamic logic, viết server-side render block trong plugin.

Ví dụ hoàn chỉnh block-templates/single.html: <!– wp:template-part {“slug”:”header”,”tagName”:”header”} /–> <!– wp:post-title /–> <!– wp:post-content /–> <!– wp:template-part {“slug”:”footer”,”tagName”:”footer”} /–> Lưu ý: không in dữ liệu raw trong file .html; mọi dynamic behavior phải qua blocks hoặc server-side render.

  • Dynamic blocks và server-side rendering
  • Nếu bạn cần logic PHP phức tạp (custom query, conditional display), tạo dynamic block (registerblocktype with render_callback) trong plugin hoặc trong functions.php của theme.
  • Trong render_callback phải:
    • sanitize input attributes,
    • kiểm tra capability nếu block cho phép actions,
    • escape output bằng wpksespost/escattr/eschtml tùy ngữ cảnh.
  • Ví dụ ngắn registerblocktype (một hàm trong PHP):

registerblocktype( ‘mytheme/related-posts’, [ ‘rendercallback’ => ‘mythemerenderrelatedposts’ ] ); function mythemerenderrelatedposts( $attributes ) { $count = isset($attributes[‘count’]) ? absint($attributes[‘count’]) : 3; // custom query, sanitize và prepare $query = new WPQuery( [ ‘postsperpage’ => $count ] ); obstart(); if ( $query->haveposts() ) { echo ‘<div class=”related-posts”>’; while ( $query->haveposts() ) { $query->thepost(); echo ‘<a href=”‘ . escurl( getpermalink() ) . ‘”>’ . eschtml( getthetitle() ) . ‘</a>’; } echo ‘</div>’; } wpresetpostdata(); return obget_clean(); }

  • Ghi chú: luôn gọi wpresetpostdata sau WP_Query để tránh ô nhiễm global $post.
  • Editor styles và preview
  • Sử dụng editor styles để đồng bộ front-end với editor:
    • style.css cho front-end,
    • addthemesupport( ‘editor-styles’ );
    • addeditorstyle( ‘assets/css/editor.css’ );
  • theme.json cũng có thể generate editor styles; kiểm tra output CSS và tối ưu để tránh duplication.
  • REST, authentication và permission cho FSE
  • Site Editor dùng REST endpoints để đọc/ghi templates. Trên môi trường staging/host bạn phải:
    • đảm bảo /wp-json/wp/v2/templates và /wp/v2/template-parts trả 200 cho GET,
    • kiểm tra POST/PUT yêu cầu authentication thành công với cookie + X-WP-Nonce header.
  • Khi tạo custom endpoints hoặc block REST routes, luôn cung cấp permissioncallback kiểm tra capability, ví dụ currentusercan( ‘edittheme_options’ ).
  • Trước khi lựa chọn FSE, thực hiện bài test save template bằng trình duyệt (login admin, mở Site Editor, chỉnh header, bấm Save) để kiểm tra quyền và WAF/Cache không chặn.
  • Patterns và reusable parts
  • Đặt patterns trong patterns/ hoặc register via PHP using registerblockpattern.
  • Khi thiết kế patterns, tránh hard-coded URLs, text hoặc data sensitive.
  • Nếu pattern chứa dynamic parts, cân nhắc hướng dynamic block thay vì raw HTML.
  • Build pipeline khác biệt
  • FSE thường cần build step cho block assets (editor JS, block.json). Khi dùng block.json, mỗi block folder cần block.json, index.js, editor.scss.
  • Không embed server secrets into block JS. Quy tắc: mọi key cần bảo mật không được đưa vào frontend bundles.
  • Testing FSE-specific
  • Manual: login admin -> Appearance > Editor -> mở site editor -> thử edit header/footer -> Save -> kiểm tra response network cho /wp-json/ request có 200.
  • Automated: viết curl script to GET /wp-json/wp/v2/templates and attempt an authenticated PUT using a cookie jar to confirm write capability in staging.
  • Verify that updated templates persist and child-theme behavior is as bạn mong muốn.
  • Checklist ngắn trước deploy (FSE)
  • theme.json validated and minimal duplicate CSS,
  • block-templates and block-template-parts present and consistent,
  • editor can save templates on staging (authenticated POST/PUT success),
  • any server-side render blocks have sanitize/escape and capability checks,
  • no secrets in JS bundles,
  • backups for DB templates if you plan to push theme updates that may be overridden by DB-stored templates.

Kết luận ngắn gọn và mẫu biến

  • Mẫu thay thế nhanh cho theme.json và single template mình đã để hoàn chỉnh ở trên. Thay [PRIMARYCOLOR], [FONTSTACK], [THEME_VERSION] nếu cần. Nếu bạn muốn, mình sẽ chuyển một scaffold PHP theme sang full block theme theo các giá trị của bạn và kèm script kiểm tra REST tự động. Bạn muốn mình làm tiếp phần đó không?

Hướng dẫn tương tác lặp với AI

Đoạn này hướng dẫn chiến thuật thực tế để bạn dùng AI hiệu quả khi xây theme WordPress. Mỗi vòng tương tác là một commit, mỗi prompt trả về một file hoặc một patch, và bạn luôn kiểm tra bằng diff và test nhỏ trước khi tiếp tục.

  • Phân đoạn task nhỏ
  • Chia work thành unit nhỏ: style.css, functions.php, header.php, template-part content, enqueue script, AJAX handler. Mỗi prompt chỉ yêu cầu một file hoặc một hàm.
  • Lợi ích: dễ review, dễ revert, dễ viết tests.
  • Yêu cầu output-only và định dạng rõ
  • Luôn bắt đầu prompt bằng: “Trả về chỉ nội dung file [filename] và nothing else.” Nếu cần, yêu cầu mã PHP bắt đầu bằng <?php và không kèm giải thích.
  • Với file JSON/HTML, yêu cầu đúng MIME và không kèm markdown.
  • Commit từng file, review diff
  • Sau khi AI trả file, lưu vào repo local, chạy linter/quick test, rồi commit với message rõ ràng. Mỗi commit tương ứng 1 feature nhỏ.
  • Dùng git diff để so sánh. Nếu AI sửa file khác, block deploy và yêu cầu sửa patch nhỏ.
  • Craft follow-ups cụ thể
  • Khi thấy bug, trả prompt vừa đủ thông tin: bao gồm đoạn code gốc, dòng lỗi, input test case. Yêu cầu patch theo format unified diff hoặc chỉ trả lại function đã sửa.
  • Yêu cầu AI giải thích ngắn dưới dạng comment trong code thay vì prose dài.
  • Yêu cầu tests và security-check snippets
  • Yêu cầu AI cung cấp:
    • Một hoặc hai unit test PHP (PHPUnit) hoặc WP-CLI test command.
    • Một grep hoặc phpcs one-liner để detect patterns (eg. missing wpnoncefield, raw echo).
    • Một short security checklist comment block vùng code mới.
  • Chạy ngay các test hoặc grep trên máy của bạn.
  • Sử dụng review bằng diff và incremental patches
  • Nếu AI làm sai, yêu cầu “Return a patch in unified diff format” hoặc “Return new content for this file only” để dễ apply.
  • Yêu cầu nhỏ: “Only change lines X-Y” khi muốn hạn chế scope.

Ví dụ follow-up prompts (bản English copy-paste-ready, kèm tóm tắt VN)

  • Prompt A (fix escaping): “Here is functions.php content: [PASTE]. Find instances of unescaped output and return a unified diff that escapes output with eschtml/escattr/esc_url as appropriate. Do not change other logic.”
    • Tóm tắt VN: yêu cầu patch escape output, chỉ trả unified diff.
  • Prompt B (add nonce check): “In this AJAX handler [PASTE], add server-side nonce verification and currentusercan checks. Return only the modified handler function.”
    • Tóm tắt VN: thêm checkajaxreferer và capability.
  • Prompt C (provide test): “Provide one PHPUnit test that asserts mycustomsanitize() returns expected sanitized string for input examples: [list]. Return only the test file.”
    • Tóm tắt VN: tạo unit test.

Kết luận ngắn

  • Luôn nhỏ, luôn commit, luôn diff, luôn test. Yêu cầu output-only và snippet test/security là cách nhanh nhất để biến AI từ generator thành trợ thủ an toàn cho bạn.

Checklist kiểm thử chức năng và bảo mật trước khi deploy

Phần này là danh mục kiểm tra cuối cùng bạn chạy trước khi đóng gói và đưa theme lên production. Mỗi mục gồm kiểm tra cụ thể, chỉ dẫn sửa nếu phát hiện lỗi, và lệnh hoặc snippet nhanh để chạy. Chạy theo thứ tự hoặc tích hợp vào script deploy.

  • Tắt debug trên production
  • Kiểm tra: trong wp-config.php, WPDEBUG phải là false; displayerrors off trên PHP.
  • Lệnh kiểm tra nhanh: grep -n “WP_DEBUG” wp-config.php
  • Nếu sai: thay bằng
    • define(‘WP_DEBUG’, false);
    • iniset(‘displayerrors’, ‘0’);
  • Hành động bổ sung: kiểm tra error log sau deploy; bật WPDEBUGLOG chỉ trên staging nếu cần.
  • File permissions và ownership
  • Kiểm tra: file PHP 644, thư mục 755; uploads writable bởi web user; theme folder không writable trừ khi cần.
  • Lệnh nhanh:
    • find wp-content/themes/my-theme -type f -perm /o+w -print
    • find wp-content/themes/my-theme -type d -perm /o+w -print
  • Nếu phát hiện file writable: chown/chmod
    • sudo chown -R www-data:www-data wp-content/themes/my-theme
    • find wp-content/themes/my-theme -type f -exec chmod 644 {} \;
    • find wp-content/themes/my-theme -type d -exec chmod 755 {} \;
  • Lưu ý: trên managed hosts, dùng panel để set ownership nếu không có SSH.
  • Remove debug info và test strings
  • Kiểm tra: không còn vardump/printr/console.log trong code; không có API keys or test credentials.
  • Lệnh grep:
    • grep -R “vardump\|printr\|console.log\|die(” .
    • grep -R “AKIA\|SECRET\|PASSWORD\|API_KEY” .
  • Nếu tìm thấy: xóa các dòng hoặc thay bằng logging an toàn (error_log) chỉ trên staging; rotate keys nếu bị commit.
  • Validate user input
  • Kiểm tra: mọi đầu vào từ $POST/$GET/$_REQUEST được sanitize trước khi lưu hoặc dùng.
  • Pattern check nhanh:
    • grep -R “\$POST\|\$GET\|\$_REQUEST” -n theme-folder
  • Nếu phát hiện direct usage: áp dụng sanitize_* tương ứng
    • text: sanitizetextfield()
    • email: sanitize_email()
    • url: escurlraw() trước lưu
    • int: absint()/intval()
  • Hướng dẫn thêm: ghi chú trên hàm lưu dữ liệu để reviewers biết đã sanitize.
  • Escape output phù hợp ngữ cảnh
  • Kiểm tra: mọi echo/print dùng eschtml/escattr/escurl/wpkses_post tùy ngữ cảnh.
  • Tìm nhanh raw echo:
    • grep -R “echo \$[a-zA-Z0-9]\+” -n theme-folder | grep -v “esc“
  • Nếu còn raw: sửa theo context
    • HTML body: echo esc_html( $var );
    • Attribute: echo esc_attr( $var );
    • URL: echo esc_url( $url );
    • Allow safe HTML: echo wpksespost( $html );
  • Nonces cho form và AJAX
  • Kiểm tra: mọi form thay đổi trạng thái và AJAX handlers phải có nonce field và verify ở server.
  • Tìm nhanh:
    • grep -R “wpnoncefield\|checkadminreferer\|checkajaxreferer” -n theme-folder
  • Nếu thiếu: thêm wpnoncefield(‘myaction’,’mynonce’) vào form và trong handler:
    • checkadminreferer(‘myaction’,’mynonce’);
    • hoặc checkajaxreferer(‘my_action’,’security’);
  • Kiểm tra bằng tay: gửi request POST giả mạo và xác nhận server trả lỗi khi nonce sai.
  • Capability checks cho hành động nhạy cảm
  • Kiểm tra: trước updateoption, delete post, file write, gọi currentuser_can với capability phù hợp.
  • Lệnh tìm:
    • grep -R “updateoption\|wpinsertpost\|wpdelete_post” -n theme-folder
  • Nếu thiếu check: thêm
    • if ( ! currentusercan(‘manageoptions’) ) { wpdie(‘Permission denied’); }
  • Ghi chú: với REST endpoints, xác định permission_callback kiểm tra capability.
  • Prepare backups trước deploy
  • Kiểm tra: đã có snapshot DB và copy theme release trước khi deploy.
  • Lệnh tóm tắt:
    • wp db export backups/site-$(date +%F).sql
    • tar czf backups/theme-$(date +%F).tar.gz wp-content/themes/my-theme
  • Hành động: lưu backup ra remote storage hoặc S3, giữ ít nhất 2 bản gần nhất.
  • Changelog và versioning
  • Kiểm tra: style.css version cập nhật; CHANGELOG.md hoặc readme.txt có ghi thay đổi.
  • Hành động: trước deploy, tăng version theo semantic versioning; commit changelog; thêm release tag.
  • Lệnh tạo zip cho upload:
    • zip -r my-theme-v1.2.0.zip my-theme/
  • Smoke tests sau deploy
  • Kiểm tra nhanh trên production sau deploy:
    • Load homepage, single post, page, admin appearance > customize.
    • Kiểm tra console lỗi JS, network 404 cho assets.
    • Kiểm tra upload image via media library.
  • Nếu phát hiện lỗi: rollback bằng restoring theme tarball + DB snapshot.

Kết thúc: chạy checklist này tự động trong CI càng nhiều càng tốt; các mục security showstoppers (unserialize on input, public AJAX that mutates state without nonce/capability, exposed secrets) phải block deploy và yêu cầu immediate fix. Nếu muốn, mình có thể cung cấp script Bash/GitHub Action thực thi các lệnh grep và tạo report tự động. Bạn muốn mình viết script đó không?

Compatibility considerations

Phần này tập trung các yêu cầu tương thích và ngưỡng cần kiểm tra trước khi deploy theme. Mỗi mục gồm điều cần làm và cách kiểm tra nhanh.

WordPress version

  • Mục tiêu mặc định: WordPress 5.9+. Nếu bạn cần hỗ trợ thấp hơn, kiểm tra API được dùng (ví dụ theme.json, wpbodyopen, block features).
  • Kiểm tra: trong staging, chạy wp core version; nếu dùng block features, thử mở Site Editor để xác nhận không lỗi.

PHP version

  • Ngưỡng khuyến nghị: tối thiểu PHP 7.4; ưu tiên PHP 8.0+ cho hiệu năng và cú pháp hiện đại.
  • Kiểm tra: search mã nguồn cho syntax mới (typed properties, arrow functions, union types). Nếu thấy, yêu cầu PHP >=8.0 và ghi rõ trong README.

Theme supports và features

  • Luôn đăng ký core supports phù hợp: title-tag, post-thumbnails, html5, custom-logo, align-wide nếu dùng editor blocks.
  • Nếu bạn muốn child-theme friendly: expose filters cho markup chính và dùng gettemplatepart / locate_template để giữ khả năng override.

Child-theme strategy

  • Thiết kế file template để child theme có thể override file bằng cùng path. Tránh require/include đường dẫn tuyệt đối; luôn dùng gettemplatepart / locate_template.
  • Ghi chú: nếu theme dùng templates lưu trong DB (FSE), document cách sync file->db và hậu quả khi parent update.

Hooks & filters

  • Cung cấp filter cho các output quan trọng, ví dụ applyfilters(‘mythemepostmeta’, $html, $postid).
  • Kiểm tra: audit mã để chắc rằng không include/require bypass hệ thống template; prefer hooks over hard-coded logic.

Plugin compatibility

  • Kiểm tra các plugin thường dùng: SEO, caching, page builders. Nếu hỗ trợ page builder như Elementor, confirm templates không ghi đè bằng include cứng.
  • Nếu theme dùng features từ plugin, kiểm tra function_exists trước khi gọi.

REST API và CORS

  • Nếu theme dùng REST endpoints hoặc Site Editor, đảm bảo /wp-json/ không bị WAF/CDN cache. Test cả GET và authenticated POST.
  • Nếu front-end JS gọi REST từ origin khác, cấu hình CORS thích hợp trên server và dùng authentication phù hợp (cookie nonce cho same-origin, token cho 3rd-party).

Database charset

  • Yêu cầu DB utf8mb4 để tránh lỗi emoji/truncate. Trước import demo content, chạy kiểm tra:
    • SELECT @@charactersetdatabase, @@collation_database;
    • Nếu không phải utf8mb4, document migration steps hoặc chuyển đổi trước khi import.

PHP extensions và runtime

  • Kiểm tra presence của các extension: gd hoặc imagick (image manipulation), mbstring (multibyte strings), json, curl.
  • Kiểm tra memory_limit >=128M, recommended 256M nếu import demo hoặc xử lý media lớn.
  • Kiểm tra allowurlfopen / cURL availability nếu theme fetch remote fonts/assets.

Quick acceptance checklist (staging)

  • php -v phù hợp target. 
  • wp core version >=5.9. 
  • composer check-platform-reqs nếu dùng composer. 
  • curl -i https://example.com/wp-json/wp/v2/templates trả HTTP 200. 
  • php -m kiểm tra gd, mbstring, curl, json. 
  • SELECT @@charactersetdatabase trả utf8mb4.

Ghi rõ trong README điều kiện môi trường và các ngưỡng phiên bản để khách hàng và hosting ops biết trước.

Performance considerations (basic)

Hiệu năng là yếu tố quyết định trải nghiệm người dùng và SEO. Dưới đây là những thực hành cơ bản bạn nên áp dụng cho theme PHP WordPress để tối ưu tải trang.

  • Asset loading
    • Enqueue CSS và JS đúng hook: wpenqueuescripts. Thêm phiên bản bằng filemtime hoặc theme version để cache-busting.
    • Tải script ở footer khi có thể. Chỉ load script trên trang cần thiết bằng điều kiện issingular, ispagetemplate hoặc dùng wplocalize_script để truyền data.
  • Critical CSS
    • Trích phần CSS cần thiết cho hiển thị đầu tiên và inline vào head. Phần CSS khác tách ra thành main.css và tải bất đồng bộ bằng preload hoặc media attribute rồi onload để avoid render-blocking.
    • Nếu không có build pipeline, tối thiểu đặt CSS quan trọng trực tiếp trong header nhỏ gọn.
  • Image sizes và srcset
    • Sử dụng srcset và sizes qua wpgetattachmentimagesrcset/wpgetattachment_image để cho trình duyệt chọn kích thước phù hợp.
    • Generate multiple image sizes with addimagesize và gọi thepostthumbnail with appropriate size. Sử dụng responsive images cho background images bằng picture element nếu cần.
  • Caching tips
    • Leverage object cache và transient API cho truy vấn tốn kém. Ví dụ lưu kết quả WPQuery phức tạp bằng settransient with expiration.
    • Sử dụng browser caching headers từ server/CDN và version assets để long-term caching.
  • Lazyloading
    • Bật native loading=”lazy” cho images và iframes; chỉ preload critical images above-the-fold.
    • Với gallery nặng, load low-res placeholder rồi lazyload high-res.
  • Avoid excessive WP_Query in templates
    • Tránh tạo nhiều custom WPQuery trong template parts. Nếu cần, dùng getposts với suppressfilters false và luôn gọi wpreset_postdata sau custom query.
    • Cache results of expensive queries with transients and invalidate on save_post via hook.

Áp dụng các điểm này sẽ giảm TTFB và First Contentful Paint, cải thiện trải nghiệm trên thiết bị di động và băng thông hạn chế.

Accessibility quick-checks

Accessibility là yêu cầu bắt buộc trước khi deploy theme. Dưới đây là kiểm tra nhanh mà bạn có thể chạy thủ công hoặc lồng vào checklist QA.

  • Alt attributes
  • Mọi img output phải có alt. Nếu ảnh trang trí, dùng alt=””.
  • Kiểm tra các thumbnail, logo, background image được in qua CSS thì đảm bảo nội dung tương đương cung cấp bằng text.
  • Heading structure
  • Trang phải có một H1 duy nhất cho tiêu đề chính. Các heading con dùng H2, H3 theo thứ tự logic, không nhảy cấp.
  • Kiểm tra bằng devtools Outline hoặc extension Browser.
  • Form labels
  • Mọi input cần label tương ứng. Nếu không hiển thị label, dùng aria-label hoặc aria-labelledby.
  • Kiểm tra validation messages có liên kết tới input qua aria-describedby.
  • Focus order và keyboard navigation
  • Dùng Tab để duyệt toàn bộ UI; đảm bảo focus không mắc kẹt và thứ tự hợp lý.
  • Kiểm tra visible focus ring, controls reachable bằng keyboard.
  • ARIA basics
  • Chỉ dùng ARIA khi semantic HTML không thể giải quyết.
  • Kiểm tra roles cho dialog, navigation, main landmark, live regions.
  • Tools to use
  • Lighthouse: audit accessibility score, quick fixes.
  • axe DevTools: deep rule checks, cụ thể XPaths lỗi.
  • WAVE extension: kiểm tra contrast và missing labels.

Thực hiện những kiểm tra này trên mẫu nội dung thực tế trước khi deploy.

Licensing và i18n

Bắt đầu bằng việc làm rõ license và khả năng dịch. Mình mô tả các bước thực tế để bạn áp dụng ngay.

  1. Embed GPL-2+ vào style.css và README
  2. style.css header mẫu (bản hoàn chỉnh, thay thông tin tương ứng):

/ Theme Name: My Theme Theme URI: https://example.com/ Author: Your Name Author URI: https://example.com/ Description: Short description. Version: 1.0.0 Text Domain: my-theme Domain Path: /languages License: GNU General Public License v2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html /

  • README: thêm đoạn License: “This theme is licensed under the GNU General Public License v2 or later. See LICENSE.txt.” và đính kèm file LICENSE.txt chứa text GPL-2+.
  • License header trong file PHP
  • Ở đầu mỗi file PHP quan trọng (functions.php, template-parts), chèn block comment ngắn:

<?php /**

  • File: functions.php
  • Theme: My Theme
  • Copyright: Your Name
  • License: GNU General Public License v2 or later

*/

  • Không lưu secrets trong header.
  • Text domain, load và dùng hàm i18n
  • Trong functions.php gọi:

loadthemetextdomain( ‘my-theme’, gettemplatedirectory() . ‘/languages’ );

  • Khi viết chuỗi, dùng luôn các hàm i18n và escaping:

eschtml( ‘Hello world’, ‘my-theme’ ); e( ‘Read more’, ‘my-theme’ ); echo escattr( gettheme_mod( ‘tagline’ ) );

  • Nguyên tắc: /_e/_x để dịch; esc_html/escattr khi xuất ra; wpkses_post nếu cho phép HTML an toàn.
  • Tạo POT và thư mục ngôn ngữ
  • Tạo folder languages/ trong theme. Ví dụ file: languages/my-theme.pot
  • Dùng WP-CLI để sinh POT:

wp i18n make-pot wp-content/themes/my-theme wp-content/themes/my-theme/languages/my-theme.pot

  • Dịch bằng Poedit hoặc công cụ, xuất .po và .mo vào languages/: my-theme-frFR.po và my-theme-frFR.mo
  • Kiểm tra
  • Kiểm thử locale bằng define(‘WPLANG’,’fr_FR’) trên dev hoặc đổi user profile locale, kiểm tra chuỗi hiển thị.

Lưu ý nhỏ: textdomain phải thống nhất khắp repo; filenames .mo/.po đặt theo textdomain-locale.mo để WP load tự động.

Tools, linters và lệnh hữu ích

Dưới đây là bộ công cụ và các lệnh thực tế mình khuyên bạn dùng để scaffold, kiểm tra, lint, phân tích tĩnh, quét secrets và deploy theme. Mình viết kèm mẫu lệnh để bạn copy-paste vào CI hoặc terminal.

AI assistants: ChatGPT, GitHub Copilot, Codeium

  • Tip nhanh: chia task nhỏ, yêu cầu “return only file contents” khi cần file; trong prompt bắt buộc nêu: target WP/PHP version, textdomain, license (GPL-2+), security rules (escape, nonce, $wpdb->prepare). Khi dùng Copilot/Codeium, chấp nhận gợi ý nhưng always review security-related lines (DB, file upload, AJAX/REST).
  • Ví dụ prompt ngắn: “Generate functions.php for theme X. Must: register supports, enqueue assets with filemtime, sanitize theme_mods, include nonce checks. Return only PHP file.”

PHP linters và static analysis

  • PHP_CodeSniffer + WordPress Coding Standards (WPCS)
    • Cài: composer require –dev “squizlabs/php_codesniffer” “wp-coding-standards/wpcs”
    • Chạy: vendor/bin/phpcs –standard=WordPress path/to/theme
  • PHPStan (static analyzer)
    • Cài: composer require –dev phpstan/phpstan
    • Chạy: vendor/bin/phpstan analyse path/to/theme -l 0

WP-aware checks

  • phpcs với standard WordPress-Extra/Num, hoặc thêm WPCS ruleset. Thêm sniff cho security nếu có.

WP-CLI – thao tác WordPress từ terminal

  • Cài: https://wp-cli.org/
  • Một số lệnh hữu dụng:
    • wp core version
    • wp theme activate my-theme
    • wp plugin install query-monitor –activate
    • wp i18n make-pot wp-content/themes/my-theme wp-content/themes/my-theme/languages/my-theme.pot

Composer / npm

  • Composer để quản lý libs PHP:
    • composer install
    • composer check-platform-reqs
  • npm cho frontend build:
    • npm ci
    • npm run build
    • npm run dev

Security scanning / secrets

  • Gitleaks (scan repo history)
    • cài: brew install gitleaks or download binary
    • chạy: gitleaks detect -s . –report-path=gitleaks-report.json
  • truffleHog (heuristic secrets)
    • chạy: trufflehog filesystem –path .
  • grep / basic scans
    • grep -RIn “AKIA\|PRIVATE_KEY\|password\|secret” .
    • find . -type f -name “*.php” -exec php -l {} \; 2>&1 | grep -v “No syntax errors”

Useful shell snippets

  • Syntax check nhanh cho tất cả PHP:
    • find . -name ‘*.php’ -print0 | xargs -0 -n1 php -l
  • Count occurrences of escaping usage:
    • grep -RInE “eschtml\(|escattr\(|esc_url\(” | wc -l
  • Search for unserialize or eval:
    • grep -RInE “unserialize\(|eval\(|create_function\(” .

CI/CD sample ideas

  • Steps: checkout -> composer install -> phpcs -> phpstan -> npm ci && npm run build -> gitleaks -> package -> manual_approval -> deploy.
  • Always mask secrets in CI logs and avoid injecting secrets into front-end bundles.

Nếu bạn muốn, mình có thể xuất mẫu GitHub Actions workflow sẵn dùng kèm các job ở trên để bạn paste vào repo. Bạn muốn mình làm luôn không?

Kết luận

Sau khi làm theo hướng dẫn, bạn sẽ có một quy trình rõ ràng để dùng AI sinh, kiểm thử và đóng gói một WordPress PHP theme an toàn, có thể deploy được trên môi trường sản xuất. Theme sẽ tương thích rộng, dễ debug và tuân thủ các nguyên tắc bảo mật cơ bản.

Lưu ý thực tế quan trọng: trước mỗi deploy luôn chạy các bước static checks, secret scan và test smoke trên staging với WP_DEBUG=false để tránh đưa lỗi hoặc dữ liệu nhạy cảm lên production.

Phụ lục – QA check

Dưới đây là đánh giá rà soát theo bảng bạn đưa ra. Mình trình bày ngắn gọn từng mục, kèm nhận xét và gợi ý sửa nếu có vi phạm. Lưu ý: mình tránh dùng dấu gạch dài như bạn yêu cầu.

  1. Chủ đề có đúng hay không? Có vượt phạm vi không?
  2. Kết quả: ĐẠT
  3. Nhận xét: Bản phân tích đề cập cách build theme WordPress bằng AI, bao gồm scaffold, kiểm thử bảo mật, và đóng gói. Phạm vi rõ ràng và nằm trong khuôn chủ đề bạn đặt.
  4. Mục trong phạm vi có được phục vụ không? Mục nào bị bỏ?
  5. Kết quả: ĐẠT
  6. Nhận xét: Các phần cơ bản như tổng quan types of themes, tiền điều kiện, cấu trúc thư mục, quy trình 9 bước, mẫu prompt, kiểm tra bảo mật, và hướng dẫn triển khai đều có mặt. Không thấy mục quan trọng bị bỏ.
  7. Yêu cầu và prompt đầy đủ có được đáp ứng không? Có đủ mẫu prompt không?
  8. Kết quả: ĐẠT
  9. Nhận xét: Có ý tưởng rõ ràng cho prompt scaffold, mẫu prompt chi tiết nhiều mẫu (prompt 1, 2, 3, 4), và hướng dẫn tương tác với AI. Tuy vậy cần xem lại các placeholder để tránh để lại [THEME_NAME]-style placeholders ở đầu ra thực tế.
  10. Đích có thể hiện ngầm qua cấu trúc không? Người đọc tự nhận ra được tác vụ?
  11. Kết quả: ĐẠT
  12. Nhận xét: Đích rõ là đọc giả là dev muốn xây dựng và triển khai theme bằng AI, và cấu trúc bài giúp họ làm đúng chu trình từ scaffold tới deploy.
  13. Độ sâu (DEPTH) có nhất quán và phù hợp với mức yêu cầu chưa?
  14. Kết quả: ĐẠT
  15. Nhận xét: Nội dung rất chi tiết, ở mức practitioner/expert. Có nhiều phần cụ thể, hướng dẫn và ví dụ thực hành. Tuy nhiên còn có mặt bằng chi tiết hơn ở một số phần có thể được tối giản khi áp dụng thực tế.
  16. Nếu DEPTH là Expert có phân biệt cách làm thông thường vs đúng không? Có nêu quy tắc KHÔNG áp dụng không?
  17. Kết quả: VI PHẠM
  18. Nhận xét: Nội dung thể hiện nhiều khuyến nghị đúng và thực hành tốt, nhưng không thực sự phân biệt rõ ràng giữa cách làm thông thường và cách làm đúng theo chuẩn, đồng thời chưa nêu rõ ràng các quy tắc KHÔNG áp dụng ở một số trường hợp. Gợi ý sửa: thêm một phần nhỏ “Cách làm đúng” so sánh với “Cách làm phổ biến” kèm danh sách quy tắc hoặc tình huống KHÔNG được áp dụng.
  19. Có phần mẩu cụt, thiếu chi tiết không?
  20. Kết quả: ĐẠT
  21. Nhận xét: Phần lớn có chi tiết đầy đủ; chưa thấy phần quan trọng nào thiếu sót. Nếu có, có thể bổ sung thêm ví dụ konkret cho từng trường hợp.
  22. Có ví dụ/mẫu điền sẵn hoàn chỉnh không, không để trống?
  23. Kết quả: ĐẠT
  24. Nhận xét: Có nhiều mẫu prompt và mẫu cấu trúc file, cũng có ví dụ điền sẵn cho scaffold và prompt. Cần lưu ý khi sao chép để không bỏ placeholder chưa điền.
  25. Có phần lỗi hay lưu ý hay gặp không?
  26. Kết quả: ĐẠT
  27. Nhận xét: Có phần liệt kê lỗi AI hay mắc phải khi sinh theme và cách nhận diện, sửa, cùng checklist kiểm tra. Đọc giả nhận được cảnh báo hữu ích.
  28. Còn dấu [KHÔNG CHẮC] nào lọt vào bài không?
  29. Kết quả: VI PHẠM
  30. Nhận xét: Có nhiều chỗ chứa các placeholder như [THEMENAME], [TEXTDOMAIN], [WPVERSION], [PHP_MIN], v.v. Các placeholder này nếu để lại trong output cuối cùng sẽ gây lúng túng khi ai dùng làm scaffold thực tế. Cần thay bằng giá trị cụ thể hoặc cơ chế điền tự động trước khi xuất.
  31. Bài có em-dash không?
  32. Kết quả: ĐẠT
  33. Nhận xét: Không thấy dùng dấu gạch dài. Toàn bài sử dụng dấu gạch ngắn và ký hiệu chuẩn. Đáp ứng yêu cầu cấm em-dash.
  34. Bà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?
  35. Kết quả: VI PHẠM
  36. Nhận xét: Không có một CTA rõ ràng, thậm chí phần kết có lời mời hoặc kêu gọi hành động, và không có mục FAQ riêng hay heading nhắm tới yếu tố tâm lý. Đề xuất bổ sung một FAQ ngắn và một CTA cho hành động tiếp theo như “Bạn muốn mình làm scaffold cho NovaPress không?” hoặc “Liên hệ để được tư vấn”.
  37. Có mở section bằng bullet ngay lập tức không?
  38. Kết quả: ĐẠT
  39. Nhận xét: Phần mở đầu có tiêu đề và sau đó đưa các mục bằng cấu trúc danh sách hoặc tiêu đề phụ. Các phần có đánh dấu số và bullet lan tỏa hợp lý, không gây lẫn lộn.
  40. Có meta-comment về nguồn dữ liệu hay cách bài này được tạo không?
  41. Kết quả: VI PHẠM
  42. Nhận xét: Không có meta-comment rõ ràng về nguồn dữ liệu hoặc cách bài này được tổng hợp. Cần thêm một chú thích ngắn ở đầu bài hoặc một section “Nguồn/Phương pháp”.
  43. NGƯỜI ĐỌC THỨ CẤP khác có được hướng ngôn ngữ phù hợp không?
  44. Kết quả: ĐẠT
  45. Nhận xét: Đang nhắm tới độc giả là nhà phát triển web hoặc freelancer. Giọng văn, từ ngữ và cấu trúc phù hợp. Không có phần riêng cho người đọc thứ cấp nhưng nội dung vẫn dễ tiếp cận.
  46. Có nhắc lại nguyên văn KHỐNG TRỐNG hoặc ĐÍCH không?
  47. Kết quả: ĐẠT
  48. Nhận xét: Bảng đã có phần “KHÔNG XÁC ĐỊNH” và phần phạm vi rõ ràng, kết hợp ở các mục. Đủ nhắc lại cơ bản cho mục tiêu.

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

Chú ý chung để sửa nhanh:

  • Loại bỏ hết mọi chỗ để [KHÔNG CHẮC] và thay bằng giá trị thật hoặc cơ chế tự điền trước khi xuất bản scaffold thực tế.
  • Thêm một phần ngắn FAQ và một CTA để tăng tính hướng dẫn và kêu gọi hành động.
  • Thêm một meta-comment ngắn ở đầu bài mô tả nguồn dữ liệu/phương pháp tổng hợp để làm rõ nguồn gốc nội dung.
  • Nếu muốn, mình có thể giúp bạn chỉnh sửa cụ thể các phần bị vi phạm và trả lại bản cập nhật theo định dạng file hoặc patch.

Bạn muốn mình bắt đầu sửa chi tiết các phần bị vi phạm và xuất bản một bản cập nhật không có placeholder, kèm một phần FAQ và CTA nhỏ không?

Cách render video nhanh trên app pc tự build bằng AI

Tối ưu tốc độ render video trên PC tự build

Mình sẽ hướng dẫn bạn các bước thực tế để đo đạc, phát hiện cổ chai và tối ưu pipeline render video, kèm kiến trúc stages, lệnh FFmpeg mẫu, kỹ thuật AI giảm tải và quy trình an toàn khi cho AI sinh mã. Ví dụ minh họa dùng Windows và Ubuntu, đồng thời trình bày NVENC, QSV và VAAPI.

Tổng quan ngắn về các yếu tố ảnh hưởng tốc độ render

Mình tóm tắt nhanh các thành phần chính có thể làm cổ chai khi bạn render video trên PC tự build, để bạn biết chỗ nào cần đo trước khi tối ưu.

Render là một pipeline nhiều giai đoạn – decode, tiền xử lý, AI/compute, upload host-to-device, encode, muxing, ghi ra đĩa hoặc network. Mỗi giai đoạn có thể thành cổ chai riêng. Dưới đây là các yếu tố bạn phải kiểm tra sớm và dấu hiệu tương ứng.

1) Codec và mode encode

  • Codec chọn (H.264, H.265, AV1…) quyết định khối lượng tính toán. Encoder phần mềm cho chất lượng/bit tốt hơn ở bitrate thấp nhưng tốn CPU. Encoder phần cứng (NVENC/QSV/VCN) cho throughput cao và latency thấp nhưng trade-off chất lượng/bitrate. Kiểm tra: thời gian encode/frame và chất lượng-per-bit.

2) Encoder phần cứng vs phần mềm

  • Phần cứng giảm tải CPU nhưng phụ thuộc queue nội bộ, số session và VRAM. Nếu GPU bị chia sẻ với AI, đừng quên contention. Kiểm tra: GPU utilization, VRAM usage, lỗi driver “device busy”.

3) I/O và disk

  • Đọc file nguồn và ghi output có thể ngưng pipeline. Disk chậm hoặc ghi synchronous sẽ stall encode. Kiểm tra: I/O throughput (fio, iostat) và latency, disk queue.

4) CPU

  • Tiền xử lý, color conversion, và packet muxing thường chạy trên CPU. Threading kém hoặc contention hướng tới nhiều context switch làm chậm. Kiểm tra: CPU utilization, syscalls hot spots (perf, top).

5) GPU và memory

  • Host-to-device transfers qua PCIe là điểm nghẽn thường thấy. Page-locked (pinned) memory, batch transfer và overlap copy/compute giảm bottleneck. Kiểm tra: PCIe bandwidth, memcpy timings (profilers).

6) Threading và synchronization

  • Thiết kế producer-consumer, số in-flight frames và locking ảnh hưởng throughput. Nếu producer block vì queue đầy hoặc consumer wait, GPU idle. Kiểm tra: latency submit encode, queue lengths, mutex waits.

7) Disk và muxing

  • Muxer blocking, container buffering và frequent header emissions (SPS/PPS) có thể làm stall. Kiểm tra: ffmpeg logs, max_muxing_queue_size warnings.

Tóm lại, trước khi tối ưu, bạn nên instrument pipeline: timestamp các stage (decode, preprocess, H2D copy, encode_submit, bitstream_map, mux), thu GPU/CPU/memory/I/O metrics. Khi biết rõ cổ chai, áp chiến lược phù hợp: tune params, overlap transfer, limit in-flight frames hoặc chuyển sang encoder phần cứng.

Kiến trúc pipeline đề xuất cho app PC tự build

Mình sẽ đề xuất một kiến trúc nhiều lớp rõ ràng, dễ tách thành stages độc lập để bạn có thể song song hoá, đo đạc và tối ưu từng nơi gây cổ chai. Mỗi stage là một unit có queue riêng, metrics riêng và phạm vi quyền truy cập tài nguyên rõ ràng.

1) Stage 0 – Input / Demux

  • Chức năng: đọc file/stream, giải mã container, xuất raw packets hoặc frames.
  • Tách độc lập: tiến trình hoặc thread pool riêng chỉ chịu I/O và demux. Ghi log throughput read, disk latency, packet queue length.
  • Tối ưu: prefetch file chunks, async disk I/O, cache metadata; đẩy hw-decoding nếu khả dụng để tránh host copy sớm.

2) Stage 1 – Decode (SW hoặc HW)

  • Chức năng: decode packet thành surfaces frames. Có thể dùng hwaccel (CUVID/VAAPI/QuickSync) hoặc software decoder.
  • Tách độc lập: decoder worker pool với pool surface được quản lý tập trung.
  • Tối ưu: nếu dùng hw decode, publish surfaces zero-copy vào GPU pipeline; tránh hwdownload unless necessary.

3) Stage 2 – Preproc AI (denoise, SR, interp)

  • Chức năng: chạy model AI trên frames. Có thể chạy trên GPU khác, same GPU, hoặc CPU.
  • Tách độc lập: service/process riêng với API queue; nhận surfaces hoặc paths, trả processed surfaces hoặc metadata.
  • Tối ưu: prefer zero-copy interop (CUDA-OpenGL, VAAPI dmabuf) để tránh host-to-device copies; giới hạn batch size và reuse model context; đo GPU memory và kernel time.

4) Stage 3 – Color convert / Scale / Pixel format match

  • Chức năng: chuyển sang định dạng encoder yêu cầu (NV12, P010).
  • Tách độc lập: hardware scaler nếu có; đặt trước encoder để encoder nhận đúng format.
  • Tối ưu: dùng hwscale, tránh swscale trên CPU nếu GPU rảnh.

5) Stage 4 – Encoder

  • Chức năng: gửi surfaces tới encoder phần cứng hoặc phần mềm, nhận bitstream packets.
  • Tách độc lập: encoder worker pool chịu trách nhiệm submit, retry on busy, và release surface.
  • Tối ưu: implement backpressure – chỉ submit khi surface slot/tracker còn; instrument submit latency và queue depth. Reuse encoder sessions, tune async_depth/surface pool theo kết quả đo.

6) Stage 5 – Muxing và Output I/O

  • Chức năng: nhận packets từ encoder, reorder nếu cần, mux vào container và ghi file/stream.
  • Tách độc lập: async muxer với output buffer; chuyển I/O nặng ra writer thread hoặc separate process để tránh block encode thread.
  • Tối ưu: tăng max_muxing_queue_size hợp lý, write asynchronously, rotate files/chunks.

7) Kiến trúc orchestration và observability

  • Control plane: job queue, scheduler, per-stage metrics (latency, throughput, queue depth, memory usage), centralized logs.
  • Phương pháp song song hoá: pipeline dạng producer-consumer, bounded queues giữa stages, worker pools per stage, và khả năng scale-out bằng multi-process hoặc multi-GPU mapping.
  • Kiểm tra nội bộ: instrument timestamps khi frame đi qua mỗi stage để tạo waterfall timing. Dùng metrics để tìm cổ chai – host-to-device copy, encoder submit latency, or mux blocking.

Kết luận: tách rõ ràng theo stages, dùng zero-copy paths khi có thể, áp backpressure với bounded pools, và đo mọi queue depth cùng latencies. Từ đó bạn có cơ sở để tự điều chỉnh batch size, number of workers, và tài nguyên GPU/CPU cho hiệu suất tối ưu.

Phát hiện phần cứng và kiểm tra khả năng encoder theo hệ điều hành

Đoạn này hướng dẫn nhanh cách xác định GPU/IGPU trên Windows và Ubuntu, cách kiểm tra NVENC / QSV / VCN (hoặc AMF/VAAPI) có sẵn, và các lệnh/ứng dụng bạn chạy để kiểm tra thực tế.

1) Xác định GPU cơ bản

  • Windows:
  • Mở PowerShell và chạy:
    Get-WmiObject Win32_VideoController | Select Name, DriverVersion, AdapterRAM
  • Hoặc mở Device Manager > Display adapters. Với NVIDIA, chạy nvidia-smi trong CMD nếu cài driver.
  • Ubuntu:
  • lspci -nn | grep -i vga
  • lspci -nn | grep -i display
  • nvidia-smi (nếu NVIDIA driver cài)
  • ls /dev/dri để thấy renderD* cho iGPU/VAAPI

2) Kiểm tra encoder có được ffmpeg nhận diện

  • Liệt kê encoder liên quan:
  • ffmpeg -hide_banner -encoders | grep -E “nvenc|qsv|amf|vaapi|v4l2m2m”
  • Nếu bạn thấy h264_nvenc / hevc_nvenc → NVENC; h264_qsv → QSV; h264_amf → AMD AMF (Windows); h264_vaapi → VAAPI (Linux).
  • Nếu không thấy, ffmpeg build hiện tại thiếu hỗ trợ vendor hoặc thiếu runtime libs.

3) Kiểm tra runtime và test encode nhanh

  • NVIDIA (any OS with driver + toolkit):
  • nvidia-smi
  • Test encode:
    ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset fast -b:v 5M out_nvenc.mp4
  • Intel QuickSync (Windows/Ubuntu with oneVPL/libmfx):
  • Ubuntu: vainfo (cần intel-media-driver)
  • Test:
    ffmpeg -init_hw_device qsv=hw -hwaccel qsv -i input.mp4 -c:v h264_qsv out_qsv.mp4
  • AMD (Windows AMF; Linux VAAPI for VCN):
  • Windows test (AMF, nếu driver hỗ trợ):
    ffmpeg -i input.mp4 -c:v h264_amf out_amf.mp4
  • Ubuntu VAAPI test:
    ffmpeg -vaapi_device /dev/dri/renderD128 -hwaccel vaapi -i input.mp4 -c:v h264_vaapi out_vaapi.mp4

4) Kiểm tra quyền và devices (Ubuntu)

  • ls -l /dev/dri/renderD128
  • groups $(whoami) để đảm bảo bạn ở trong nhóm video nếu cần: sudo usermod -aG video $USER

5) Cách đọc kết quả

  • Nếu ffmpeg chạy và tạo file out_*.mp4 với tốc độ nhanh hơn software encode và không báo lỗi, encoder phần cứng hoạt động.
  • Nếu lỗi như “Unknown encoder” → ffmpeg không build với encoder; “device busy” hoặc “cannot open” → kiểm tra driver, device node, quyền, hoặc container passthrough.

Mình gợi ý: trước khi thay tham số, hãy chạy lệnh liệt kê encoders và một test encode ngắn để khẳng định runtime hoạt động trên máy bạn.

Các lựa chọn encoder phần cứng theo nhà sản xuất và đặc điểm chính

Dẫn nhập ngắn: khi bạn muốn tăng tốc render video trên PC tự build, lựa chọn encoder phần cứng là quyết định then chốt. Dưới đây mình tóm tắt ba gia đình encoder phần cứng phổ biến, nêu ưu nhược cơ bản và những chỉ số thực tế bạn cần cân nhắc khi chọn cho pipeline của mình.

1) NVIDIA NVENC (NVIDIA Video Encoder)

  • Đặc điểm chính: là block mã hóa nằm trên GPU NVIDIA; thường được truy cập qua NVENC API hoặc thông qua FFmpeg với h264_nvenc / hevc_nvenc. Hỗ trợ rộng trong các công cụ và driver NVIDIA.
  • Ưu điểm: throughput cao, latency thấp, tích hợp tốt trong môi trường CUDA; dễ scale cho nhiều luồng encode so với CPU; nhiều wrapper/driver và công cụ profiling sẵn.
  • Nhược điểm: quality-per-bitrate thường thấp hơn encoder phần mềm tinh vi ở bitrate rất thấp; giới hạn số session hiệu quả tùy model GPU và driver; cạnh tranh tài nguyên nếu bạn đặt cả AI inference và NVENC trên cùng GPU.
  • Chỉ số cần quan tâm: codec/profiles supported (H.264/H.265/AV1 nếu có), maximum concurrent encode sessions được hỗ trợ, VRAM overhead cho surfaces/bitstream, async_depth/surfaces behavior, driver/SDK version compatibility.

2) Intel QuickSync / OneVPL

  • Đặc điểm chính: encoder media tích hợp trong iGPU của CPU Intel; truy cập qua OneVPL / Media SDK, hoặc FFmpeg h264_qsv / hevc_qsv; trên Linux thường dùng VA-API kết hợp driver intel-media-driver.
  • Ưu điểm: tiết kiệm năng lượng cho hệ thống laptop/desktop có iGPU, latency thấp, tốt cho realtime streaming trên máy cá nhân; ít phụ thuộc vào PCIe copy nếu pipeline tận dụng iGPU đúng cách.
  • Nhược điểm: hiệu năng/quality phụ thuộc thế hệ iGPU; trên desktop có dGPU đôi khi iGPU bị tắt trong BIOS; driver và runtime (oneVPL, libmfx) có thể gây mismatch. Một số tính năng nâng cao (ví dụ lookahead sâu, profile codec mới) bị giới hạn theo đời CPU.
  • Chỉ số cần quan tâm: generation iGPU (xe, UHD, v.v.), driver/oneVPL version, vaapi/libmfx availability, required copies host↔device, power/thermal constraints.

3) AMD VCE/VCN và AMF (AMD Media Framework)

  • Đặc điểm chính: VCE trước đây, VCN là block media mới hơn trên GPU AMD; AMF là SDK vendor (chủ yếu Windows), trên Linux thường dùng VA-API/VDPAU tương thích.
  • Ưu điểm: cung cấp encode phần cứng cho hệ AMD, có thể cạnh tranh tốt về throughput ở một số model; tích hợp vào pipeline VA-API trên Linux.
  • Nhược điểm: mức hỗ trợ trong FFmpeg/tooling và tính ổn định driver có thể khác biệt giữa desktop và server; SDK/driver maturity thay đổi theo model.
  • Chỉ số cần quan tâm: driver support cho AMF/VCN, VA-API integration, firmware/firmware blobs, supported bit depths và profiles.

Các chỉ số chung bạn cần so sánh trước khi quyết định

  • Supported codecs và profiles (H.264/H.265/AV1; 8-bit/10-bit/HDR)
  • Throughput thực tế tại target resolution/frame rate (fps encode) và latency end-to-end
  • Quality-per-bitrate ở bitrate mục tiêu (so sánh bằng VMAF/SSIM/subjective)
  • Maximum concurrent sessions và in-flight frame recommendations
  • VRAM usage per session, async_depth và ảnh hưởng tới memory pressure
  • Driver/SDK maturity và tích hợp với toolchain bạn dùng (FFmpeg, GStreamer, NVidia SDK, Intel OneVPL, AMD AMF)
  • Host-to-device copy overhead và yêu cầu zero-copy paths (VAAPI/DXGI/CUDA interop)
  • Power/thermal constraints và tác động tới rollback/driver stability

Kết luận ngắn: chọn encoder dựa trên mục tiêu chính của bạn. Nếu ưu tiên throughput/latency và đã có GPU hỗ trợ NVENC, đó là lựa chọn thường hợp lý. Nếu dùng iGPU trên laptop hoặc cần tiết kiệm năng lượng, QuickSync là lựa chọn tốt. Nếu hệ của bạn là AMD hoặc bạn cần VA-API stack trên Linux, VCN/AMF là phương án. Luôn kiểm tra driver/SDK tương thích và đo throughput/quality thực tế trên tập dữ liệu đại diện trước khi triển khai.

Hướng dẫn tích hợp FFmpeg với encoder phần cứng + mẫu lệnh cơ bản

Dẫn vào: khi bạn muốn tận dụng encoder phần cứng để tăng tốc render, FFmpeg thường là điểm nối dễ tiếp cận. Ở đây mình trình bày các bước tích hợp chung, ý nghĩa các flags thường dùng và ít nhất một mẫu lệnh thực tế cho từng vendor. Mình giả định bạn đã cài driver/SDK phù hợp; mọi bước liên quan tới mount/device chỉ áp dụng khi chạy trong container hoặc WSL sẽ được ghi chú.

1) Nguyên tắc chung trước khi chạy
1.1. Kiểm tra FFmpeg build: ffmpeg -buildconf và ffmpeg -encoders | grep -E “nvenc|qsv|vaapi|amf” để thấy encoder có được bật hay không.
1.2. Đảm bảo driver/runtime có device node: Linux: ls /dev/dri và nvidia-smi; Windows: Device Manager / nvidia-smi.
1.3. Nếu chạy trong container, dùng NVIDIA Container Toolkit (–gpus) hoặc map /dev/dri phù hợp; không mount toàn bộ host.

2) Ý nghĩa các flag FFmpeg quan trọng

  • -hwaccel : bật giải mã/accel phía HW (cuda, qsv, vaapi).
  • -init_hw_device : khởi tạo thiết bị HW để reuse trong filters/encoders (ví dụ init_hw_device cuda=cu:0 hay vaapi=hw:/dev/dri/renderD128).
  • -filter_hw_device : chỉ định thiết bị để các hw filters dùng.
  • hwupload / hwupload_cuda / hwupload_qsv: filter để upload frame vào bộ nhớ thiết bị tương ứng. Tên filter phụ thuộc hw backend.
  • -surfaces : tuỳ chọn encoder (nvenc/vaapi) để đặt số surfaces/frame buffers mà wrapper alloc; giúp điều chỉnh in-flight frames.
  • -async_depth : tuỳ chọn encoder để chỉ định số frame có thể chờ xử lý bất đồng bộ; tên option có thể khác giữa encoder (đặt sau -c:v … nếu được).
    Lưu ý: một số flag là filter (hwupload), số khác là encoder private options (surfaces, async_depth). Đặt private options sau tên encoder bằng cú pháp -c:v h264_nvenc -surfaces 12 …

3) Mẫu lệnh thực tế — NVIDIA NVENC (Linux/Windows)
Mục tiêu: dùng CUDA hwaccel, upload bằng hwupload_cuda, encode NVENC, async surfaces.

  • Linux/Windows (CUDA-enabled FFmpeg):
    ffmpeg -hwaccel cuda -hwaccel_device 0 -init_hw_device cuda=cu:0 -i input.mp4 \
    -vf “format=nv12,hwupload_cuda” \
    -c:v h264_nvenc -preset p1 -rc vbr_hq -b:v 8M -surfaces 16 -async_depth 4 \
    out_h264_nvenc.mp4
    Ghi chú: dùng hwupload_cuda khi decoder/filters chạy trên CPU; nếu bạn decode với cuvid dùng -hwaccel cuvid kết hợp hwupload_cuda.

4) Mẫu lệnh — Intel QuickSync / QSV (Windows/Ubuntu with oneVPL)
Cách init OneVPL/QSV và dùng qsv encoder:

  • Linux/Windows (ffmpeg built with qsv):
    ffmpeg -init_hw_device qsv=hw -hwaccel qsv -hwaccel_device hw -i input.mp4 \
    -vf “format=nv12,hwupload” \
    -c:v h264_qsv -preset veryfast -global_quality 23 -look_ahead 1 \
    out_h264_qsv.mp4
    Ghi chú: tuỳ driver và ffmpeg build, hwupload có thể là hwupload_qsv; một số tuỳ chọn như look_ahead hoặc global_quality là private options.

5) Mẫu lệnh — VAAPI (Linux, thường dùng cho Intel/AMD VA-API)

  • Linux (VA-API):
    ffmpeg -init_hw_device vaapi=hw:/dev/dri/renderD128 -filter_hw_device hw \
    -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -i input.mp4 \
    -vf ‘format=nv12,hwupload’ -c:v h264_vaapi -profile:v high -b:v 6M -surfaces 4 \
    out_h264_vaapi.mp4
    Ghi chú: trên VAAPI, filter_hw_device giúp các hw filters dùng cùng device; -surfaces giới hạn số surface để điều chỉnh in-flight.

6) Mẫu lệnh — AMD AMF (Windows; FFmpeg hỗ trợ h264_amf)

  • Windows (AMF):
    ffmpeg -i input.mp4 -c:v h264_amf -usage transcoding -quality high -b:v 8M \
    out_h264_amf.mp4
    Ghi chú: AMF option names khác biệt; kiểm tra ffmpeg -h encoder=h264_amf để xem các private options.

4) Lời khuyên tuning nhanh

  • Bắt đầu với surfaces=4..8 và async_depth=2..4, tăng dần tới khi throughput tăng nhưng không gây OOM hoặc driver errors.
  • Nếu thấy CPU-bound màu conversion, dùng hwaccel decode path để giảm copy và dùng hwupload_* phù hợp.
  • Khi dùng AI trên cùng GPU, giảm surfaces/async_depth để tránh VRAM contention.
  • Đọc logs ffmpeg với -loglevel debug để thấy khi encoder trả lỗi BUSY hay device busy; xử lý bằng retry/backoff trong code của bạn.

Kết thúc: các mẫu trên là đầy đủ lệnh có thể copy-paste; trước khi chạy trong môi trường production, kiểm tra ffmpeg -encoders và ffmpeg -h encoder= để biết private options chính xác cho phiên bản của bạn. Nếu bạn muốn, mình có thể tạo script test tự động để sweep surfaces/async_depth và thu metrics.

Tham số encoder quan trọng và trade-off tốc độ chất lượng

Phần này giải thích vì sao các tham số encoder thường thấy — CRF/bitrate, preset, GOP/keyint, B-frames, ref-frames, vbv/rc_buffer, slices/tiles, async_depth/surfaces — ảnh hưởng tới tốc độ render và khi nào thay đổi có thể làm nhanh hơn hoặc gây lỗi. Mình sẽ tập trung vào cơ chế thực thi và dấu hiệu bạn cần quan sát.

1) CRF / bitrate

  • Cơ chế: CRF/CQP đặt chất lượng mục tiêu cho từng khung; bitrate (CBR/VBR) ép encoder xuất mức bit cố định hoặc trong vùng. Ở cùng preset, CRF thấp hơn (chất lượng cao hơn) làm encoder phải mã nhiều chi tiết hơn, tăng time-per-frame. CBR có thể gây encoder nội bộ phải điều chỉnh bitrate liên tục, tạo overhead rate-control.
  • Khi thay đổi nhanh hơn: tăng CRF (giảm chất lượng) thường giảm tính toán và tăng throughput. Chuyển từ VBR_HQ sang CQP có thể giảm CPU nếu driver/SDK phải làm ít vòng điều khiển.
  • Khi gây lỗi: đặt bitrate rất chặt với vbv quá nhỏ có thể dẫn tới encoder không thể tuân thủ window và driver/SDK báo lỗi hoặc giảm chất lượng đột ngột.

2) preset

  • Cơ chế: preset chọn mức độ tìm kiếm motion, subpel precision, lookahead, heuristics RDO. Preset nhanh cắt bớt bước tính toán.
  • Khi thay đổi nhanh hơn: chọn preset “fast” hoặc NVENC preset “llhp/ll” cho low-latency và tốc độ cao.
  • Khi gây lỗi/chậm: một số preset low-latency tạo nhiều submits nhỏ, tăng overhead PCIe và có thể làm queue driver đầy – tổng throughput giảm. Luôn test preset mới với workload thực.

3) GOP / keyint

  • Cơ chế: keyint = khoảng giữa keyframes. Keyframe nhiều hơn tăng bitrate và công việc encode per-second; keyint lớn hơn giảm header frequency và đôi khi tăng compression efficiency.
  • Khi thay đổi nhanh hơn: tăng keyint cho batch/offline để giảm header overhead. Đối với streaming có latency mục tiêu, giảm keyint có thể cần nhưng làm tăng công việc.
  • Khi gây lỗi: nếu muxer không được cấu hình để buffer packets out-of-order do B-frames, tăng B-frames + long GOP có thể làm full mux buffer và báo lỗi.

4) B-frames và ref-frames

  • Cơ chế: B-frames yêu cầu lookahead và reordering. Ref-frames tăng khả năng nén nhưng chiếm bộ nhớ tham chiếu.
  • Khi thay đổi nhanh hơn: giảm B-frames và ref-frames để giảm latency và bộ nhớ, thường tăng throughput.
  • Khi gây lỗi: đặt B/ref quá cao có thể vượt giới hạn phần cứng -> driver trả lỗi hoặc FFmpeg fallback sang phần mềm.

5) vbv_buf_size / rc_buffer

  • Cơ chế: điều khiển window buffering cho rate-control. Buffer nhỏ ép encoder smooth bitrate, có thể tăng compute.
  • Khi thay đổi nhanh hơn: tăng buffer để cho encoder có “đệm” xử lý biến động – giảm stalls. Nhưng buffer quá lớn chiếm RAM/VRAM.
  • Khi gây lỗi: buffer quá nhỏ gây bitrate spikes or encoder warnings; quá lớn gây memory pressure và swap.

6) slices / tiles

  • Cơ chế: chia frame để parallelize encoding trên CPU/GPU. Giảm latency nhờ chia tải, nhưng giảm hiệu suất nén do mất prediction toàn khung.
  • Khi thay đổi nhanh hơn: bật slices cho nhiều CPU core khi software encoder CPU-bound.
  • Khi gây lỗi: một số hardware encoders không hỗ trợ slices/tiles tốt; bật có thể khiến driver từ chối hoặc tạo artifact.

7) async_depth / surfaces (NVENC/driver options)

  • Cơ chế: async_depth và surfaces quy định số frames in-flight từ host tới encoder. Tăng cho phép overlap copy/compute/encode.
  • Khi thay đổi nhanh hơn: tăng tới mức driver/GPU còn VRAM và queue chưa đầy sẽ tăng throughput.
  • Khi gây lỗi: vượt quá khả năng driver dẫn tới BUSY/DEVICE_BUSY return, hoặc OOM nếu VRAM không đủ. Dấu hiệu: encode submit blocking, latency tăng, driver warnings, hoặc process crash.

Kiểm soát rủi ro và phương pháp thử nghiệm

  • Thử từng tham số một, đo throughput và latency. Giữ nhật ký nvidia-smi / ffmpeg -loglevel debug.
  • Nếu thấy throughput giảm sau thay đổi, kiểm tra: VRAM usage, driver BUSY messages, PCIe bandwidth, và muxer queue errors.
  • Nguyên tắc an toàn: start conservative – surfaces/async_depth thấp (2–8), B-frames modest (0–2) khi dùng HW encoder; tăng dần nếu metrics cải thiện. Nếu bạn chạy AI processing trên cùng GPU, ưu tiên giảm async_depth và surfaces để tránh VRAM contention.

Tóm lại, mỗi tham số tác động qua một trong ba kênh chính: tính toán nội bộ encoder, lượng dữ liệu di chuyển host↔device, hoặc bộ nhớ/buffer nội bộ. Bạn phải đo cả ba để biết thay đổi nào thực sự tăng throughput mà không làm giảm ổn định.

Kỹ thuật giảm workload bằng AI trước/sau encode

Mở đầu ngắn: mục tiêu là dùng AI để giảm dữ liệu cần encode hoặc giảm độ phức tạp mà vẫn giữ cảm nhận chất lượng. Mình liệt kê các kỹ thuật chính, khi nên đặt chúng vào pipeline (pre-encode hay post-encode), và lưu ý vận hành để đạt throughput cao.

1) Denoising (tiền xử lý, pre-encode)

  • Khi áp dụng: nguồn noisy (low-light, high-ISO) trước khi nén. Loại bỏ nhiễu giúp encoder đạt cùng chất lượng với bitrate thấp hơn.
  • Cách dùng: chạy một mô-đun denoiser nhẹ trên GPU (FastDVDnet, VRT nhỏ, hoặc các impl PyTorch/ONNX) rồi feed đầu ra NV12 cho encoder phần cứng.
  • Trade-off: thời gian inference thêm vào pipeline; chọn model nhẹ hoặc chạy batch nhiều frame để amortize overhead.

2) Super-resolution (sau hoặc trước tùy mục tiêu)

  • Khi áp dụng: nếu người nhận có thể upscaling bằng AI, bạn có thể encode ở độ phân giải thấp hơn rồi nâng lại phía client. Hoặc upscale đầu vào trước khi encode để giảm artifact ở bitrate thấp.
  • Cách dùng: encode 1080p, deliver, client dùng SR model để dựng 4K. Hoặc pre-upscale + encode nếu muốn giảm blocking trên chi tiết nhỏ.
  • Lưu ý: chia nhiệm vụ giữa server và client theo bandwidth và latency.

3) Frame interpolation / temporal synthesis

  • Khi áp dụng: muốn giảm fps encode để tiết kiệm bitrate. Encode 30 fps và dùng frame interpolation trên client để dựng 60 fps, hoặc ngược lại, giảm số frame cần encode server-side.
  • Rủi ro: motion-heavy scenes có thể tạo ghosting nếu model yếu. Test perceptual quality trước khi deploy.

4) Selective-frame render và content-aware skipping

  • Khi áp dụng: cảnh tĩnh dài, slide, hay overlay chữ. Chỉ encode keyframes hoặc giảm bitrate cho vùng tĩnh; sử dụng perceptual change-detection để skip frame encode.
  • Kỹ thuật: compute cheap motion metric per-frame (SSIM delta, histogram diff) trên CPU/GPU; nếu delta < threshold, duplicate frame metadata thay vì re-encode full frame.

Vận hành thực tế và tối ưu

  • Đặt AI pre-processing vào worker pool riêng, chạy batch để giảm host-to-device overhead; dùng pinned host memory và pipe rawvideo giữa ffmpeg và script Python để giảm copy.
  • Nếu AI và encoder chung GPU, giảm async_depth / surfaces để tránh VRAM contention; có thể đặt AI trên GPU0 và encoder dùng NVENC trên GPU1 nếu có multi-GPU.
  • Pipeline example (complete command): chuyển 1920×1080@30fps frame qua script PyTorch denoise rồi encode NVENC
  • ffmpeg -i input.mp4 -f rawvideo -pix_fmt rgb24 -s 1920×1080 -r 30 – | python denoise_pipe.py –width 1920 –height 1080 | ffmpeg -f rawvideo -pix_fmt rgb24 -s 1920×1080 -r 30 -i – -c:v h264_nvenc -preset llhq out.mp4
  • Giải thích ngắn: denoise_pipe.py đọc rawvideo từ stdin, ghi rawvideo ra stdout; cả hai bước cần đồng bộ format và kích thước.

Kết luận ngắn: chọn kỹ thuật theo dạng nội dung và constraint latency. Luôn đo throughput end-to-end sau khi thêm AI; nếu AI tạo bottleneck, chuyển sang batching, dùng mixed precision hoặc ra khỏi GPU encode để tránh cạnh tranh VRAM.

Thiết kế batch và streaming render để tận dụng đa luồng và GPU

Mình tóm gọn: mục tiêu là duy trì băng thông hẹp nhất giữa CPU và GPU, tránh queue overflow ở tầng encoder phần cứng, và tận dụng overlap giữa copy, compute và encode. Thiết kế thực tế gồm một job queue trung tâm, chia video thành chunk hợp lý, pool worker đa luồng, reuse surface/buffer, và cơ chế backpressure để giới hạn số frame in-flight.

1) Luồng chính và job queue

  • Job queue là hàng FIFO hoặc priority queue chứa các task kiểu: encode-chunk(start_frame, end_frame, preset, output). Queue phải hỗ trợ peek/retry và backpressure signal.
  • Producer (IO/decoder) đẩy chunk sau khi decode/preprocess vào queue; consumer là worker pool.

2) Chunking video – chiến lược chia kích thước

  • Mục tiêu: amortize overhead copy. Heuristics:
  • Low-latency streaming: chunk = 1-5 giây video. Giữ kích thước nhỏ để giảm đầu ra delay.
  • Batch offline: chunk = 5-60 giây, hoặc fixed MB 50-500 MB, tùy RAM/VRAM và disk speed.
  • Kiểm tra: nếu nhiều transfer nhỏ làm throughput giảm, tăng chunk size. Nếu latency mục tiêu bị vượt, giảm chunk size.

3) Worker pool và mapping tới GPU

  • Tạo pool worker cố định, mỗi worker chạy trong process hoặc thread lâu đời để reuse context và tránh cost khởi tạo.
  • Mapping: mỗi worker gắn cố định vào một GPU (CUDA_VISIBLE_DEVICES hoặc device assignment). Tránh nhiều worker thay đổi GPU runtime thường xuyên.
  • Nếu multi-GPU, cân bằng jobs round-robin hoặc theo load metric (GPU mem free, encode throughput).

4) Reuse surfaces và buffer pool

  • Dùng pool của surfaces/surfaces-backed frames: allocate N device surfaces (NV12, aligned) và reuse cho nhiều frames/chunks để tránh frequent alloc/free.
  • Pool size = async_depth * workers_per_gpu + safety_margin. Thử nghiệm để chọn giá trị; khởi điểm an toàn 2-4 surfaces per worker.

5) Overlap copy / compute / encode

  • Pipeline trong mỗi worker:
  1. Read chunk from disk or receive decoded frames into pinned host buffers.
  2. Async host->device copy (cudaMemcpyAsync / cuvid / VA-API hwupload) into a free device surface.
  3. If AI preprocessing required, dispatch inference kernels on separate CUDA stream and write output into device surface or into another surface pool.
  4. Submit encode using encoder API with the device surface handle. Do not block on submit; use non-blocking API or query return status.
  5. Map bitstream async, push to muxer thread that writes to disk/network.
  • Use multiple CUDA streams: one for H2D copies, one for inference, one for encode submissions when supported, to maximize concurrency.

6) Backpressure and limit in-flight frames

  • Implement semaphore or token-bucket per GPU that counts in-flight frames/surfaces. Acquire token before starting H2D; release token after bitstream written and surface returned to pool.
  • Heuristic start values: per-GPU in-flight tokens 4 for high-resolution jobs; 2-4 for workloads that share GPU with AI model. Tune empirically by increasing until throughput plateaus or errors appear.

7) Muxing and IO decoupling

  • Muxing/IO run in separate thread/process to avoid blocking encode submission. Use async file writes or a dedicated writer pool.

8) Failure handling and adaptive throttling

  • If encoder returns BUSY or API warns, apply exponential backoff and reduce in-flight tokens. Collect metrics – submit latency, queue length, GPU mem usage – and auto-adjust chunk size or worker concurrency.

Kết luận: tổ chức pipeline thành stages non-blocking, reuse device surfaces, overlap H2D – compute – encode, và áp cơ chế token-based backpressure. Bắt đầu với conservative in-flight limit và chunk size, đo throughput và điều chỉnh cho hệ của bạn.

Kỹ thuật giảm nghẽn host-to-device và driver queue overflow

Mở đầu ngắn: điểm nghẽn phổ biến nhất khi bạn muốn tối đa throughput encode phần cứng là việc đưa khung từ bộ nhớ hệ thống lên thiết bị (host-to-device copy) và việc push quá nhanh khiến queue ở driver/encoder đầy. Mục tiêu ở đây là giảm overhead copy, overlap copy với compute/encode, và áp backpressure để không làm tràn queue driver. Dưới đây là cơ chế, các dấu hiệu nhận biết, và các bước thực thi cụ thể bạn có thể áp dụng ngay.

1) Cơ chế chính cần tối ưu

  • Host-to-device copy: mỗi khung raw (NV12/YUV) phải qua PCIe/DRAM DMA. Nhiều transfer nhỏ gây overhead syscalls và setup DMA nhiều lần.
  • Synchronous copy và submit: gọi memcpy blocking hoặc submit encode đồng bộ sẽ stall producer thread.
  • Driver-side encoder queue: NVENC/QuickSync/VCN có buffer giới hạn cho surfaces/bitstream. Push vượt quá khả năng sẽ trả BUSY/warn hoặc block.
  • Contention VRAM/PCIe/NUMA: copy nhiều luồng cùng lúc có thể saturate băng thông, làm GPU encoder idling.

2) Kỹ thuật giảm nghẽn – thực thi

  • Dùng pinned/page-locked host memory: đăng ký vùng host bằng cudaHostAlloc / cudaHostRegister (hoặc tương đương) để DMA hiệu quả hơn và cho phép async copy không block.
  • Sử dụng async memcpy và streams: cudaMemcpyAsync trên stream riêng cho H2D, và ensure encode submit dùng stream/handle không block. Overlap copy với inference/compute và encode.
  • Reuse device surfaces / buffer pool: allocate một pool surfaces ở đầu và reuse để tránh alloc/free tốn thời gian. Kích cỡ pool = async_depth * workers_per_gpu + margin.
  • Ưu tiên transfer lớn hơn nhiều transfer nhỏ: gom nhiều frame vào buffer lớn khi latency cho phép.
  • Nếu có hỗ trợ zero-copy import (DXGI share, dmabuf/VAAPI import), dùng đường zero-copy để tránh host round-trip.

3) Backpressure pattern và giới hạn in-flight frames

  • Áp token-bucket hoặc semaphore per-GPU. Mỗi token tương ứng một surface/slot in-flight. Producer phải acquire token trước khi bắt đầu H2D. Release token khi bitstream đã ghi xong và surface returned.
  • Khởi điểm heuristic: bắt đầu với in-flight tokens = 4 cho HD; chỉnh tăng giảm dựa trên đo. Nếu GPU còn AI model chạy chung, giảm tokens.
  • Khi driver trả BUSY/warning, worker phải: sleep ngắn với exponential backoff, giảm token count tạm thời và log lỗi.

Ví dụ pseudo-code (điền sẵn)

  • semaphore = Semaphore(initial=4)
  • producer: semaphore.acquire() ; async_h2d_copy(frame, device_surface) ; submit_encode(device_surface, callback=on_encoded)
  • on_encoded(bitstream, device_surface): push_to_muxer(bitstream) ; return_surface_to_pool(device_surface) ; semaphore.release()

4) Dấu hiệu queue overflow và cách xử lý

  • Dấu hiệu:
  • Encode submit latency tăng đột ngột (thời gian giữa call và return lớn hơn bình thường).
  • API trả BUSY / MFX_WRN_DEVICE_BUSY / NVENC error codes hoặc ffmpeg show “Device or resource busy”.
  • Bitstream output lag – PTS in->out tăng dần.
  • GPU encoder utilization thấp trong khi host-to-device bandwidth cao.
  • GPU memory usage tăng dần tới plateau (driver giữ surfaces).
  • Xử lý:
  • Bắt đầu bằng giảm số in-flight tokens; throttling producer.
  • Chuyển các bước I/O/muxing ra thread riêng để không block release token.
  • Bật profiling (Nsight Systems, nvprof, vainfo) để xác định liệu bottleneck là PCIe hay encoder HW.
  • Nếu bottleneck là PCIe, tăng chunk size và/hoặc giảm số luồng copy song song.
  • Nếu driver warning xuất hiện thường xuyên, cập nhật driver/firmware và kiểm tra SDK release notes.

5) Cách đo và kiểm chứng

  • Instrument thời gian: timestamp trước/after async_h2d_start, after_h2d_complete, before_encode_submit, after_encode_return, after_bitstream_mapped. Phân tích histogram per-stage.
  • Dùng vendor profiler: Nsight Systems cho CUDA/NVENC; vainfo/oneVPL traces cho Intel; radeontop/ROCProfiler cho AMD.
  • Ghi nvidia-smi logs (memory.used, utilization.gpu) và PCIe counters nếu có.

Kết luận thực dụng: tổ chức pipeline thành stages non-blocking, dùng pinned memory + async memcpy + reuse surface pool, và áp token-based backpressure. Bắt đầu conservative với 2–4 in-flight frames, đo latency submit và throughput, rồi điều chỉnh. Nếu thấy API BUSY hoặc bitstream lag, giảm in-flight ngay và debug bằng traces vendor để tìm nguyên nhân chính xác.

Cấu hình môi trường để AI “chạy với GPU” an toàn

Mở đầu ngắn: khi bạn cho AI chạy code cần GPU trên máy cá nhân hoặc server, mục tiêu là cho quyền vừa đủ để tận dụng GPU nhưng không mở toang filesystem, secrets, hay quyền kernel. Dưới đây so sánh ba môi trường phổ biến và hướng dẫn cấu hình an toàn thực thi ngay.

1) Docker + nvidia-container-toolkit (Linux, Windows Docker Desktop)

  • Ưu: dễ cô lập, control mounts, resource limits, runtime tích hợp GPU.
  • Quy tắc an toàn:
  • Không dùng –privileged và không mount /var/run/docker.sock.
  • Dùng runtime Nvidia: –gpus ‘”device=0″‘ để chỉ expose device cần thiết.
  • Chạy non-root user trong container: Dockerfile tạo user và dùng –user.
  • Giới hạn CPU/memory: –cpus=”4″ –memory=”8g” –pids-limit=200.
  • Mount chỉ thư mục cần read-only: -v /data/models:/models:ro -v /data/out:/out:rw.
  • Drop capabilities: –cap-drop=ALL. Thêm seccomp/apparmor: –security-opt seccomp=/path/strict.json –security-opt no-new-privileges:true.
  • Mạng: nếu không cần internet, chạy container trong network nội bộ hoặc chặn egress bằng firewall.
  • Ví dụ lệnh an toàn (điền sẵn):
    docker run –rm \
    –gpus ‘”device=0″‘ \
    –user 1001:1001 \
    –cap-drop=ALL \
    –security-opt no-new-privileges:true \
    –security-opt seccomp=/etc/docker/seccomp-strict.json \
    –read-only \
    –tmpfs /tmp:rw,size=200m \
    –cpus=”4″ –memory=”12g” \
    -v /srv/models:/models:ro \
    -v /srv/input:/input:ro \
    -v /srv/output:/output:rw \
    my-gpu-image:tag

2) WSL2 + CUDA (Windows)

  • Ưu: trực tiếp dùng GPU trên Windows, tiện dev.
  • An toàn: tránh mount toàn bộ C:\ vào WSL; giữ dữ liệu nhạy cảm trên Windows side không mounted; tạo user không phải root trong distro; giới hạn network bằng firewall Windows; tải driver NVIDIA cho WSL theo hướng dẫn chính thức.

3) Máy ảo đám mây với GPU (AWS/GCP/Azure)

  • Ưu: dễ scale và tách hoàn toàn khỏi môi trường dev.
  • An toàn:
  • Sử dụng IAM/service account với quyền tối thiểu để truy cập storage.
  • Không lưu secrets trong image; dùng secret manager và short-lived tokens.
  • Chỉ attach GPU vào VM cần thiết; dùng VPC firewall để hạn chế egress.
  • Dùng disk encryption và instance templates CI-built images.

Lưu ý vận hành chung

  • Tuyệt đối không chạy mã AI sinh ra tự động dưới quyền root hoặc với quyền truy cập toàn bộ filesystem. Bắt buộc code review thủ công, pre-scan tìm secrets và sandboxed dry-run không có network và không có GPU trước khi cho chạy trong môi trường có GPU. Giữ logs và secrets mã hóa, rotate token ngắn hạn.

Quy trình an toàn khi dùng AI sinh mã truy cập phần cứng

Khi bạn cho AI sinh mã có khả năng truy cập GPU, device nodes, hoặc thực thi trên hệ thống, mục tiêu là cho quyền vừa đủ để làm việc, đồng thời ngăn rò rỉ khóa và leo thang quyền. Dưới đây mình trình bày quy trình bắt buộc, các mức kiểm soát và checklist bạn phải hoàn tất trước khi chạy bất kỳ mã AI sinh nào trên phần cứng thật.

Nguyên tắc nền tảng

  • Principle of least privilege: chỉ cấp đúng thiết bị và thư mục cần thiết, theo nguyên tắc read-only nếu có thể.
  • Human-in-the-loop: mọi mã sinh ra phải qua code review thủ công bởi người có thẩm quyền trước khi chạy. Tự động hoá chỉ cho phép bước kiểm tra chuẩn, không cho phép chạy trực tiếp.
  • Sandboxed execution: chạy lần đầu trong sandbox không có mạng, không có GPU, user không phải root, thời gian và tài nguyên giới hạn. Chỉ khi test pass mới cho chạy trong môi trường GPU có kiểm soát.
  • Secrets scanning and handling: không cho phép mã chứa secrets; sử dụng secret manager để cung cấp credentials với quyền tối thiểu và thời hạn ngắn.
  • Network egress control: chặn egress mặc định; nếu mã cần tải model từ internet, whitelist domain cụ thể hoặc pre-download model vào storage an toàn.

Quy trình thực thi an toàn – bước theo thứ tự
1) Yêu cầu AI sinh mã kèm ràng buộc an toàn rõ ràng: không dùng root, không mount toàn bộ filesystem, không chứa hard-coded secrets, chỉ tạo script để chạy local.
2) Tự động scan code đầu ra bằng công cụ tìm secrets (detect-secrets, truffleHog), static analyzer (bandit cho Python, shellcheck cho shell). Fail nếu phát hiện secret hoặc lệnh nguy hiểm.
3) Manual code review bởi người duyệt: kiểm tra lệnh shell, curl|sh, scp, docker.sock access, systemctl, dd, mknod, mount, chown -R /. Yêu cầu reviewer chấm nhận hoặc từ chối trước khi bước tiếp.
4) Dry-run trong sandbox: chạy script trong container read-only, no-network, no-GPU, non-root, với timeouts và logs. Kiểm tra hành vi file I/O, network attempts, process spawn.
5) Nếu dry-run sạch, deploy vào container GPU giới hạn: non-root user, –gpus device explicitly, –cap-drop=ALL, read-only rootfs, tmpfs cho /tmp, seccomp profile và no-new-privileges. Giới hạn CPU/memory và pids. Mở network egress theo whitelist nếu cần.
6) Giám sát runtime: bật audit logging, file access watch, egress monitor, and automatic kill on policy violations. Rotate any created credentials and revoke after use.
7) Post-run review và rollback plan: lưu logs, snapshot outputs, nếu phát hiện bất thường thì rollback và điều tra.

Checklist bắt buộc trước khi chạy mã AI sinh ra

  • Reviewer đã ký duyệt mã? Có / Không.
  • Secret-scan result: pass / fail.
  • Dry-run in sandbox: pass / fail.
  • Container run flags sẵn sàng: non-root, –gpus cụ thể, –cap-drop=ALL, read-only mounts, seccomp, resource limits.
  • Network egress policy: whitelist domains listed.
  • Secrets provided via secret manager or ephemeral volume, không qua ENV plain text.
  • Monitoring và alerting active: auditd/host logging, network egress alerts.
  • Rollback / kill switch tested.

Mẫu ngắn chính sách mounts và quyền

  • Mount chỉ /data/input:ro, /models:ro, /data/output:rw. Không mount /, /home, /root, /etc. Không mount /var/run/docker.sock. Sử dụng user không phải root.

Nếu bạn muốn, mình sẽ gửi mẫu Docker run command an toàn và mẫu checklist dạng máy đọc (CSV) để tích hợp vào CI trước khi cho phép chạy mã AI sinh mã trên GPU.

Mẫu prompt an toàn để yêu cầu AI sinh mã hoặc script (FFmpeg/Docker/SDK)

Dẫn vào ngắn: mẫu prompt này giúp bạn yêu cầu AI tạo script FFmpeg, Docker run hoặc cài SDK nhưng theo chuẩn an toàn: không root, không secrets, chỉ output mã chạy, và kèm bước kiểm tra thủ công bắt buộc trước khi chạy trên máy thật.

Mẫu prompt (bản hoàn chỉnh, copy-paste vào AI):
“Write a single self-contained bash script that configures and runs a GPU-accelerated FFmpeg encode job on Linux (Ubuntu). Requirements:
1) Do not use root or sudo. All filesystem paths must be under /home/runner/projects/encode and created with non-root user permissions.
2) Do not include or reference any secrets, API keys, passwords, or private tokens.
3) Only output the bash script code block. Do not output commentary, filenames outside /home/runner/projects/encode, or instructions to run as root.
4) Use NVIDIA NVENC via FFmpeg. Assume ffmpeg binary with nvenc is already installed at /usr/local/bin/ffmpeg.
5) The script must:

  • create folders /home/runner/projects/encode/{input,output,models} with mode 0755,
  • validate GPU availability by running /usr/local/bin/ffmpeg -hide_banner -hwaccels and exit non-zero if nvenc unavailable,
  • run one safe encode: /usr/local/bin/ffmpeg -y -hwaccel cuda -i /home/runner/projects/encode/input/sample.mp4 -c:v h264_nvenc -preset p4 -rc vbr_hq -cq 23 -b:v 4M -max_muxing_queue_size 1024 /home/runner/projects/encode/output/out.mp4
  • write logs to /home/runner/projects/encode/output/run.log
    6) Add runtime sanity checks: verify output file exists and size > 1MB.
    7) Include explicit comment block at top listing reviewer checklist items (manual review required) and do not perform any network access or downloads.”

Bản có biến số (nếu bạn dùng): replace /home/runner/projects/encode with your chosen project dir, and /usr/local/bin/ffmpeg with absolute ffmpeg path.

Các bước review thủ công bạn bắt buộc làm trước khi chạy
1) Scan script bằng detect-secrets/truffleHog and shellcheck. Fail nếu phát hiện secrets or shellcheck errors.
2) Read top comment checklist and verify no network calls, no docker.sock, no mounts outside project dir.
3) Run script in sandbox container with no network and no GPU (to validate no destructive ops).
4) If sandbox pass, run on target with resource limits, non-root user, and monitor logs/audit for unexpected file access or outbound connections.

Hướng dẫn cài driver và SDK cần thiết

Phần này hướng dẫn ngắn gọn các driver/SDK bạn cần cài cho ba nhà sản xuất chính và cách kiểm tra hoạt động sau cài trên Windows và Ubuntu. Mình giả định bạn là dev tự build app và có quyền cài driver trên máy.

NVIDIA

  • Cài driver GPU từ trang chính thức: https://www.nvidia.com/Download/index.aspx
  • Nếu cần CUDA (AI hoặc zero-copy): https://developer.nvidia.com/cuda-downloads
  • NVENC / Video Codec SDK (để tích hợp encoder phần cứng): https://developer.nvidia.com/nvidia-video-codec-sdk
    Kiểm tra:
  • Windows: chạy nvidia-smi từ Command Prompt; kiểm tra Device Manager.
  • Ubuntu: nvidia-smi; nvcc –version (nếu cài CUDA).
  • Kiểm tra NVENC có hoạt động: ffmpeg -hide_banner -encoders | grep nvenc hoặc thử encode mẫu:
    /usr/local/bin/ffmpeg -hwaccel cuda -i input.mp4 -c:v h264_nvenc -preset p4 out.mp4

Intel (oneVPL / libmfx / media-driver)

  • Cài oneVPL / Media SDK hoặc Intel Media Driver (tùy gen CPU): https://www.intel.com/content/www/us/en/developer/tools/oneapi/onevpl.html và https://github.com/intel/media-driver
  • Trên Linux, cần libva + intel-media-driver: https://github.com/intel/media-driver
    Kiểm tra:
  • Ubuntu: vainfo (cần libva-utils) để xem backend; ls /dev/dri/renderD*; ffmpeg -hwaccel qsv -hide_banner -encoders | grep qsv
  • Windows: kiểm tra Device Manager cho Intel iGPU và cài driver OEM/Intel; dùng ffmpeg h264_qsv test:
    ffmpeg -init_hw_device qsv=hw -filter_hw_device hw -hwaccel qsv -i input.mp4 -c:v h264_qsv out.mp4

AMD (amdgpu / AMF / VA-API)

  • Cài driver chính thức: https://www.amd.com/en/support
  • AMF (Windows) và VA-API (Linux) cho acceleration: https://gpuopen.com/amf/ và https://www.freedesktop.org/wiki/Software/vaapi/
    Kiểm tra:
  • Ubuntu: lsmod | grep amdgpu; vainfo; ffmpeg -hide_banner -encoders | grep vaapi
  • Windows: kiểm tra Radeon Software and try ffmpeg h264_amf (Windows) hoặc vaapi paths on Linux:
    ffmpeg -hwaccel vaapi -vaapi_device /dev/dri/renderD128 -i input.mp4 -c:v h264_vaapi out.mp4

Bước kiểm tra sau cài (tổng quát)
1) Xác nhận driver kernel/module loaded: Linux sử dụng lsmod và dmesg | grep -i .
2) Xác nhận runtime libs tồn tại: ldconfig -p | grep .
3) Dùng ffmpeg -encoders để thấy encoder vendor xuất hiện.
4) Thực hiện 1 encode mẫu nhẹ và kiểm tra output, thời gian, và log lỗi. Nếu chạy trong container, đảm bảo device passthrough (–gpus hoặc mount /dev/dri) và vendor runtime (nvidia-container-toolkit, intel libs) đã cài.

Nếu gặp lỗi, thu nvidia-smi / vainfo / dmesg logs và đối chiếu với tài liệu vendor trước khi tinh chỉnh. Mình có thể gửi lệnh kiểm tra chi tiết hơn cho hệ của bạn nếu bạn cung cấp OS và model GPU.

Ví dụ cấu hình thực tiễn 1 – Windows dùng NVENC + AI denoiser

Mô tả ngắn: pipeline tách làm hai tiến trình – tiến trình AI denoiser (chạy trên GPU hoặc CPU tùy cấu hình) nhận raw frames qua pipe, xuất frames đã làm sạch vào pipe khác; FFmpeg đọc lại pipe và encode bằng NVENC. Cách tách giúp bạn kiểm soát VRAM và dễ review mã AI trước khi cho chạy với quyền truy cập phần cứng.

Yêu cầu trước khi bắt đầu
1) Windows 10/11, driver NVIDIA mới nhất (kiểm tra bằng chạy nvidia-smi).
2) FFmpeg build có hỗ trợ h264_nvenc (ffmpeg -encoders | findstr nvenc).
3) Python/ONNX/Torch-based denoiser script sẵn sàng (ví dụ denoise.py dùng ONNXRuntime hoặc PyTorch). Dùng model đã được bạn duyệt.

Cài driver nhanh

  • Tải driver từ https://www.nvidia.com/Download.
  • Kiểm tra: mở cmd, chạy nvidia-smi. Nếu hiển thị GPU và driver thì ok.

Mẫu pipeline đầy đủ (bản hoàn chỉnh, bạn có thể copy chạy sau khi review)
1) Tạo named pipes trên Windows (PowerShell):

  • New-Item -ItemType NamedPipe -Path \.\pipe\denoise_in
  • New-Item -ItemType NamedPipe -Path \.\pipe\denoise_out
    2) Chạy denoiser (ví dụ Python) trong cửa sổ PowerShell 1:
  • python denoise.py –input \.\pipe\denoise_in –output \.\pipe\denoise_out –model C:\models\denoise.onnx –device cuda –precision fp16
    3) Chạy FFmpeg (PowerShell 2) để decode nguồn và pipe vào denoiser, sau đó encode NVENC:
  • ffmpeg -y -hwaccel cuda -hwaccel_output_format cuda -i “C:\input\source.mp4” -f rawvideo -pix_fmt yuv420p -vf “format=nv12,scale=3840:2160” -vcodec rawvideo – | ffmpeg -f rawvideo -pix_fmt yuv420p -s 3840x2160 -i \\.\pipe\denoise_out
    -c:v h264_nvenc -preset p4 -rc vbr_hq -b:v 12M -maxrate 14M -g 120 -bf 2 -surfaces 20 -async_depth 4 `
    -profile:v high -level 4.2 -pix_fmt nv12 “C:\output\encoded_nvenc.mp4”

Giải thích các tham số quan trọng

  • -hwaccel cuda và -hwaccel_output_format cuda: tận dụng decoder/CPU->GPU upload nhanh.
  • -surfaces 20: tăng số surface để NVENC có thể có frames in-flight. Bắt đầu từ 8-20, test trên hệ bạn.
  • -async_depth 4: giới hạn số frame async in-flight giữa FFmpeg và NVENC; giảm nếu thấy driver errors hoặc tăng latency.
  • Denoiser chạy riêng: bạn kiểm soát batch size và precision (–precision fp16) để giảm VRAM.

Resource limits và vận hành an toàn

  • Nếu denoiser và NVENC cùng dùng GPU, monitor VRAM bằng NVIDIA Task Manager hoặc nvidia-smi –loop=1. Đặt denoiser batch nhỏ nếu VRAM >80% used.
  • Tránh chạy cả hai tiến trình ở chế độ Administrator nếu không cần. Chạy dưới user bình thường.
  • Giới hạn CPU cho FFmpeg/denoiser bằng Windows Job Objects hoặc công cụ Process Lasso nếu cần.
  • Thực hiện trial run nhỏ (10s clip) trước khi render toàn bộ.

Checklist an toàn trước khi chạy mã AI sinh tự động
1) Review thủ công file denoise.py và mọi script – tìm lệnh upload, network call, hay read các file trong C:\Users.
2) Không gắn kết toàn bộ C:\ vào container/pipe. Chỉ cho phép folder models và input/output cần thiết.
3) Kiểm tra model nguồn, checksum và quyền sở hữu. Không tải model từ URL chưa được xác thực khi chạy tự động.
4) Chạy denoiser đầu tiên ở chế độ CPU hoặc sandbox để quan sát hành vi.
5) Giám sát realtime: nvidia-smi, Event Viewer, FFmpeg logs với -loglevel verbose. Nếu driver reset hoặc OOM xuất hiện, dừng tiến trình ngay.

Các biến thể (điền sẵn ngay sau bản hoàn chỉnh)

  • Nếu bạn có GPU riêng cho AI (GPU1) và GPU cho NVENC (GPU0), set denoiser –device cuda:1 và ffmpeg –gpu 0 (hoặc chọn -gpu index tùy build).
  • Nếu muốn output HEVC: thay -c:v h264_nvenc bằng -c:v hevc_nvenc và điều chỉnh -b:v.
  • Nếu pipeline dùng stdin/stdout pipes (không named pipes), thay \.\pipe… bằng – | và stdout/ stdin tương ứng.

Nếu bạn muốn, mình có thể cung cấp một script PowerShell tự động tạo named pipes, khởi denoiser mẫu (dùng ONNXRuntime), và chạy ffmpeg với logging cấu hình sẵn để bạn test an toàn. Bạn muốn mình viết file đó không?

Ví dụ cấu hình thực tiễn 2 – Ubuntu dùng Docker + GPU share

Mở đầu ngắn: mục tiêu là chạy pipeline render/AI inference trên Ubuntu bằng Docker, cho phép nhiều job chia sẻ GPU một cách an toàn và có thể điều chỉnh concurrency theo thực tế. Dưới đây là cấu trúc image, lệnh chạy container an toàn, cách mount model weights read-only, một script orchestration đơn giản cho batch jobs, và cách đo + điều chỉnh concurrency.

Mô tả Docker image
1) Cơ bản: dùng base image chính thức của NVIDIA để có CUDA và thư viện runtime. Ví dụ Dockerfile nhỏ:

  • FROM nvidia/cuda:12.1-runtime-ubuntu22.04
  • RUN apt-get update && apt-get install -y ffmpeg python3 python3-pip
  • COPY requirements.txt .
  • RUN pip3 install -r requirements.txt
  • USER 1000:1000
  • WORKDIR /work
  • ENTRYPOINT [“python3”, “worker.py”]
    2) Ghi chú: image chứa script worker.py thực hiện: lấy một chunk video hoặc frames từ mounted input, (tuỳ chọn) gọi model inference bằng PyTorch/ONNXRuntime, rồi gọi ffmpeg để encode NVENC. Image chạy với non-root user 1000.

Lệnh chạy container an toàn (ví dụ cho một worker trên GPU 0)
docker run –rm \
–gpus ‘”device=0″‘ \
–cap-drop=ALL \
–security-opt no-new-privileges:true \
–security-opt seccomp=unconfined \
–read-only \
–tmpfs /tmp:rw,size=256m \
–tmpfs /var/log:rw,size=64m \
-v /srv/videos/input:/work/input:ro \
-v /srv/models:/models:ro \
-v /srv/videos/output:/work/output:rw \
–memory=6g \
–cpus=2 \
–user 1000:1000 \
myregistry.example.com/render-worker:latest

Giải thích ngắn:

  • –gpus ‘”device=0″‘ chỉ expose đúng GPU cần thiết.
  • –cap-drop=ALL và no-new-privileges giảm surface attack.
  • Mounts cho input và /models ở chế độ :ro để ngăn container sửa file model hoặc dữ liệu nguồn.
  • –read-only rootfs và tmpfs cho /tmp/nghĩa là chỉ vùng cần ghi được phép.

Script orchestration batch jobs – worker pool (bash)

  • Tệp: run_batch.sh

!/usr/bin/env bash

INPUT_DIR=/srv/videos/to_process
MAX_CONCURRENCY=3
SEMAPHORE=/tmp/worker.sem
mkfifo $SEMAPHORE

init tokens

for i in $(seq 1 $MAX_CONCURRENCY); do echo > $SEMAPHORE & done
for f in “$INPUT_DIR”/*.{mp4,mkv}; do
read -r token < $SEMAPHORE ( docker run –rm –gpus ‘”device=0″‘ … \ -v “$f”:/work/input/file:ro \ myregistry.example.com/render-worker:latest /work/input/file echo > $SEMAPHORE
) &
done
wait
rm $SEMAPHORE

  • Khái niệm: semaphore file tokens giới hạn số container chạy đồng thời. Thay MAX_CONCURRENCY phù hợp.

Đo và điều chỉnh concurrency
1) Thu thập metrics: chạy nvidia-smi –query-gpu=timestamp,index,memory.used,utilization.gpu –format=csv -l 2 > gpu.log
2) Ghi logs CPU/mem và ffmpeg timing trong container vào /var/log (tmpfs) và stream ra host nếu cần.
3) Phân tích: nếu utilization.gpu thấp nhưng memory.used cao, giảm số job hoặc giảm model VRAM; nếu utilization cao và throughput không tăng khi tăng concurrency, khả năng bão hòa PCIe hoặc memory bandwidth – giảm concurrency hoặc tăng chunk size.
4) Quy tắc bắt đầu: cho consumer GPU chia sẻ với AI model, thử MAX_CONCURRENCY bắt đầu 1, rồi 2, sau đó tăng từng bước, quan sát gpu.log và latency per job. Chấp nhận cấu hình tối ưu khi tổng throughput tăng nhưng latency chấp nhận.

Kết luận ngắn: cấu hình an toàn = expose đúng GPU, read-only mounts cho models, non-root user, cap-drop, run orchestration bằng semaphore để kiểm soát concurrency, và luôn giám sát nvidia-smi để điều chỉnh theo thực tế.

Mẫu script test để đo in-flight frames, queue depth và băng thông H2D

Mục tiêu: một script dễ chạy trên Ubuntu có CUDA/NVIDIA và ffmpeg, tự động tăng async_depth/surfaces và số worker song song, thu nvidia-smi logs, đo thời gian FFmpeg trên mỗi job, chạy bài test băng thông PCIe, và xuất CSV để bạn phân tích ngưỡng. Bạn review mã trước khi chạy, không chạy auto-elevated.

Ý tưởng chung
1) Lặp qua các cấu hình: async_depth ∈ {2,4,8,16}, surfaces ∈ {4,8,16}, parallel_jobs ∈ {1,2,4}.
2) Với mỗi cấu hình, khởi chạy N worker ffmpeg (mỗi worker encode một file chunk) và đồng thời chạy đo nvidia-smi logging và bandwidth test.
3) Ghi lại: start/end timestamps, per-job encode time, GPU memory used, GPU util%, pci H2D throughput (từ CUDA bandwidthTest hoặc bằng đọc /sys nếu có).
4) Sau mỗi run, tính fps tổng, trung bình latency, max memory, và nếu thấy: a) encode_submit latency tăng > 2x hoặc fps giảm khi tăng in-flight => giảm async_depth; b) GPU util low but PCIe rx high => optimize copy (bigger chunks, pinned memory).

Ví dụ script bash (sẵn chạy trên Ubuntu)

!/usr/bin/env bash

set -euo pipefail
OUT=./test_results.csv
echo “async_depth,surfaces,parallel_jobs,timestamp,total_fps,avg_job_sec,gpu_mem_mb,gpu_util_pct,pcie_mb_s” > $OUT
INPUT_SAMPLE=/srv/videos/sample_30s.mp4
for async in 2 4 8 16; do
for surf in 4 8 16; do
for pj in 1 2 4; do
ts=$(date -Iseconds)
# start nvidia-smi logging in background
nvidia-smi –query-gpu=timestamp,memory.used,utilization.gpu –format=csv -l 1 > nvsmi.log & NVSMPID=$!
# start bandwidth test in background (assumes cuda-samples bandwidthTest built)
./bandwidthTest –memory=pinned –mode=peer2peer > bw.log & BWPID=$!
# launch parallel ffmpeg jobs and time them
start=$(date +%s.%N)
for i in $(seq 1 $pj); do
ffmpeg -y -hwaccel cuda -hwaccel_device 0 -i “$INPUT_SAMPLE” \
-c:v h264_nvenc -preset p1 -async_depth $async -surfaces $surf \
-gpu 0 /dev/null > job${i}.log 2>&1 &
done
wait
end=$(date +%s.%N)
kill $NVSMPID $BWPID || true
elapsed=$(echo “$end – $start” | bc)
avg_job=$(echo “$elapsed / $pj” | bc -l)
# parse nvsmi latest
gpu_mem=$(tail -n1 nvsmi.log | awk -F, ‘{print $2}’ | tr -d ‘ ‘)
gpu_util=$(tail -n1 nvsmi.log | awk -F, ‘{print $3}’ | tr -d ‘ %’)
pcie=$(tail -n1 bw.log | awk ‘/Device to Host/ {print $NF}’ || echo 0)
total_fps=$(echo “scale=2; 30 * $pj * 1 / ($elapsed/30)” | bc -l || echo 0)
echo “$async,$surf,$pj,$ts,$total_fps,$avg_job,$gpu_mem,$gpu_util,$pcie” >> $OUT
done
done
done

Các metric cần thu và cách dùng

  • total_fps: throughput tổng. Nếu tăng async/surfaces tăng fps => good. Nếu fps giảm => queue overflow or contention.
  • avg_job_sec / encode_submit latency: nếu encode call time tăng rõ rệt khi async tăng => driver-side queue/block.
  • gpu_mem_mb: tăng dần chỉ ra surfaces allocation; nếu vượt VRAM margin thì OOM nguy cơ.
  • gpu_util_pct: thấp + pci H2D cao => transfer-bound. Cao + pci low => compute-bound.
  • pcie_mb_s: nếu gần link capacity, giảm parallel jobs hoặc tăng chunk size.

Quy tắc ví dụ

  • Nếu gpu_util < 40% và pcie_mb_s > 70% link => giảm parallel_jobs hoặc tăng chunk size.
  • Nếu avg_job_sec tăng > 25% khi async từ 8→16 => giảm async_depth.
  • Nếu gpu_mem_peak > total_vram – 1GB => giảm surfaces hoặc parallel_jobs.

Bạn có muốn mình chuyển script này thành phiên bản Python có plotting và backoff logic tự động?

Danh sách kiểm tra gỡ lỗi nhanh khi render chậm hoặc encoder không được nhận diện

Dưới đây là checklist nhanh để bạn chạy qua khi gặp render chậm hoặc FFmpeg/ứng dụng không thấy encoder phần cứng. Mình viết theo thứ tự ưu tiên kiểm tra, kèm lệnh mẫu để chạy trên Linux và ghi chú Windows khi khác.

1) Kiểm tra driver và device

  • Linux: nvidia-smi; lspci -nn | grep -i nvidia; ls /dev/nvidia*; ls -l /dev/dri
  • Windows: chạy nvidia-smi.exe trong CMD; kiểm tra Device Manager
  • Nếu nvidia-smi lỗi, cập nhật driver hoặc purge + cài lại (DDU trên Windows).

2) Kiểm tra build và thư viện FFmpeg

  • ffmpeg -buildconf và ffmpeg -encoders | grep -E “nvenc|qsv|amf|vaapi”
  • ldd $(which ffmpeg) | grep -E “nvidia|mfx|amf”
  • Nếu thiếu encoder, dùng ffmpeg build hỗ trợ vendor SDK hoặc cài bản có sẵn.

3) Kiểm tra quyền truy cập thiết bị

  • Linux: đảm bảo user nằm trong nhóm video hoặc render: groups $USER; sudo usermod -aG video $USER; kiểm tra quyền /dev/dri/renderD128
  • Container: đảm bảo chạy với –gpus hoặc nvidia-container-toolkit; tránh mount /var/run/docker.sock

4) Logs và kernel messages

  • Linux: dmesg | tail -n 50; journalctl -e | grep -i nvidia; xem audit/AppArmor deny
  • FFmpeg: chạy với -loglevel debug và đọc lỗi đầy đủ

5) Kiểm tra VAAPI / QSV / AMF

  • VAAPI: vainfo
  • QSV: ffmpeg -init_hw_device qsv=hw -hwaccel qsv -i input -c:v h264_qsv out.mp4 và đọc lỗi
  • AMF/VCN: kiểm tra driver Radeon và ffmpeg -encoders

6) Kiểm tra I/O và bottlenecks hệ thống

  • iostat -x 1, top/htop để thấy CPU bound hoặc I/O bound
  • Nếu PCIe copy là bottleneck, đo bằng CUDA bandwidthTest

7) Kiểm tra container runtime flags

  • Docker mẫu an toàn: docker run –rm –gpus ‘”device=0″‘ –cap-drop=ALL –security-opt no-new-privileges:true -v /models:/models:ro myimage
  • Nếu container không thấy /dev/nvidia*, cài nvidia-container-toolkit và chạy lại.

8) Quick fixes to try

  • Tăng max_muxing_queue_size trong FFmpeg khi lỗi “Too many packets buffered”
  • Giảm async_depth / surfaces nếu thấy driver queue overflow
  • Thử encode đơn giản: ffmpeg -hwaccel cuda -i in.mp4 -c:v h264_nvenc -preset p1 out.mp4 để xác định vấn đề cơ bản

Nếu bạn muốn, mình có thể gửi script kiểm tra tự động cho Ubuntu với logging để bạn chạy và thu evidence chi tiết.

Lưu ý bảo mật cụ thể và các lỗi thường gặp khi cấp quyền cho AI chạy mã

Mình tóm gọn những rủi ro hay gặp, cách phòng ngừa thực tế và bước khôi phục nếu có rò rỉ hay gây hại. Đây là checklist bạn áp dụng trước khi cho AI sinh mã chạy trên máy có quyền truy cập phần cứng.

Những lỗi phổ biến

  • Mount quá rộng: mount toàn bộ / hoặc thư mục home vào container cho phép đọc/ghi mọi khóa và config.
  • Expose docker.sock: ai có truy cập docker.sock có thể tạo container với quyền root trên host.
  • Đưa secrets qua ENV hoặc file chung: biến môi trường và file cấu hình dễ bị in log hoặc đọc bởi process khác.
  • Chạy container/process dưới root hoặc dùng –privileged: phá vỡ rào bảo mật, cho phép thay đổi device nodes.
  • Share agent sockets (ssh-agent) hoặc credential helpers: dẫn tới credential forwarding và exfiltration.
  • Cho truy cập trực tiếp /dev hoặc mount thiết bị mà không kiểm soát: AI có thể thao tác thiết bị, đọc bộ nhớ, hoặc ghi persistent backdoor.

Phòng ngừa bắt buộc
1) Principle of least privilege: chỉ cấp đúng device cần thiết. Trên Docker dùng –gpus ‘”device=0″‘, –cap-drop=ALL, –security-opt no-new-privileges:true, read-only rootfs, tmpfs /tmp.
2) Không mount /, /home, ~/.ssh, ~/.aws; chỉ mount thư mục chứa input hoặc đặt model weights trong volume chỉ-read.
3) Secrets qua secret manager, không qua ENV. Nếu phải truyền tạm, xóa sau khi dùng.
4) Bắt buộc non-root user trong image và CI pipeline; disable privilege escalation.
5) Sandboxing: chạy mã AI lần đầu trong môi trường no-network và read-only, bật seccomp/AppArmor profile.
6) Static scan + human review: auto-scan mã tìm patterns nguy hiểm (curl|sh, scp, rm -rf, openssh keys) và phải có review con người trước khi chạy với quyền cao.
7) Giới hạn egress: whitelist outbound endpoints, chặn exfiltration.

Khi phát hiện rò rỉ/thiệt hại
1) Isolate ngay: dừng container/process, isolate host khỏi mạng, revoke network tokens.
2) Revoke và rotate secrets: hủy mọi AWS/GCP/Azure keys, rotate SSH keys, reset service account tokens.
3) Kiểm tra logs và thu evidence: syslog, docker logs, nvidia-smi timeline, netflow; chụp memory dump nếu cần.
4) Quét toàn hệ: tìm file bị đọc/ copied, search for exfil scripts, check cron and authorized_keys for persistence.
5) Khôi phục: restore từ backup sạch nếu có thay đổi hệ thống quan trọng; rebuild host images and containers from trusted CI.
6) Báo cáo và post-mortem: khóa bị lộ, lượng data, hành động mitigations, lessons learned, và cập nhật policy.

Ví dụ cấu hình Docker an toàn (mẫu, bạn sửa path cho phù hợp):
docker run –rm –gpus ‘”device=0″‘ –cap-drop=ALL –security-opt no-new-privileges:true –read-only –tmpfs /tmp:rw,size=200m -v /data/input:/data:ro -v /data/out:/out:rw –user 1001:1001 myimage:tag

Luôn nhớ: không bao giờ tự động chạy mã AI sinh ra với quyền cao nếu chưa qua review và sandbox. Nếu bạn muốn, mình gửi checklist script tự động phát hiện patterns nguy hiểm trước khi chạy.

Kịch bản so sánh quyết định dùng encoder phần cứng hay phần mềm

Mở đầu ngắn: khi bạn tối ưu pipeline render video, câu hỏi “dùng encoder phần cứng hay phần mềm” thực tế là cân trade-off giữa latency/throughput và chất lượng-per-bit. Dưới đây là một “bảng quyết định dạng lời nói” để bạn hỏi và trả lời nhanh, kèm những tình huống AI preprocessing có thể đổi chiều quyết định.

1) Mục tiêu chính của bạn là gì?

  • Nếu ưu tiên latency rất thấp hoặc throughput cao (nhiều luồng, realtime streaming, preview nhanh), chọn encoder phần cứng.
  • Nếu ưu tiên chất lượng-per-bit tối đa cho bản master hoặc archive, chọn encoder phần mềm.

2) Điều kiện hệ thống và chi phí

  • Nếu CPU không đủ cho software encoder ở bitrate/quality mục tiêu hoặc bạn muốn tiết kiệm power/cost, chọn phần cứng.
  • Nếu bạn có cụm CPU mạnh hoặc chấp nhận thời gian encode lâu cho chất lượng, chọn phần mềm.

3) Yêu cầu codec và tính năng nâng cao

  • Nếu cần AV1 ở chất lượng hàng đầu hiện nay, phần mềm (libaom/rav1e) có thể cho quality tốt hơn; phần cứng AV1 còn mới và chưa phổ biến.
  • Nếu cần 10-bit HDR, kiểm tra liệu encoder phần cứng trên GPU của bạn hỗ trợ profile đó; nếu không, phần mềm là lựa chọn an toàn.

4) Khi AI preprocessing có thể thay đổi quyết định

  • AI giảm workload: nếu bạn áp dụng denoising hoặc super-resolution trước encode để loại bỏ nhiễu và tăng tái tạo chi tiết, bạn có thể hạ bitrate đầu vào mà vẫn giữ chất lượng. Kết quả: phần cứng sẽ đủ tốt hơn – chọn hardware để tăng throughput.
  • AI tạo chi tiết khó nén: nếu AI thêm nhiều texture tần số cao (ví dụ GAN-based enhancement) khiến bitstream khó nén hiệu quả, phần mềm encoder với RDO sâu có thể nén tốt hơn – chọn software.
  • Quyền truy cập GPU cho AI: nếu AI chạy trên cùng GPU với encoder phần cứng gây contention VRAM/PCIe, tổng throughput có thể giảm. Trong trường hợp này cân nhắc chạy AI trên CPU/server khác hoặc đổi sang software encoder nếu CPU đủ mạnh.

5) Lời nói kết cho bạn

  • Rule of thumb nói cho bạn: realtime và multi-stream chọn hardware; chất lượng tối đa cho bản cuối chọn software. Trước khi quyết, chạy thử hai cấu hình với một đoạn mẫu đặc trưng của dự án bạn – include AI preprocessing nếu bạn dự định dùng – và so sánh throughput, latency, và quality metric (VMAF/visual check). Kết quả thử nghiệm thực tế là quyết định cuối cùng.

Kế hoạch test và tuning thực nghiệm (trình tự hành động)

Mục tiêu: một checklist step-by-step để bạn thực hiện đo và tối ưu pipeline render bằng encoder phần cứng. Mỗi bước gồm lệnh mẫu, chỉ số cần thu và quyết định tiếp theo. Trước khi bắt đầu, chuẩn bị: build ffmpeg có hwaccel tương ứng, vendor driver + SDK, script ghi log, và thư mục output metrics.

1) Thiết lập baseline single-job

  • Mục: xác định throughput/latency cơ sở trên 1 job.
  • Hành động:
  • Windows/Ubuntu: chạy ffmpeg 1 job, ví dụ NVENC:
    ffmpeg -hwaccel cuda -hwaccel_device 0 -i input.mp4 -c:v h264_nvenc -preset p1 -rc vbr_hq -gpu 0 -surfaces 8 -async_depth 2 out.mp4
  • Thu: fps trung bình, thời gian tổng, peak GPU memory (nvidia-smi –query-gpu=memory.used,utilization.gpu –format=csv -l 1), CPU load, PCIe copy pattern (Nsight Systems trace).
  • Lưu metrics vào CSV: timestamp, job_id, async_depth, surfaces, fps, mem_used, gpu_util.

2) Tăng async_depth / surface count (one variable at a time)

  • Mục: tìm sweet-spot in-flight frames.
  • Hành động:
  • Thử async_depth = 2,4,8,12; tương ứng điều chỉnh -surfaces.
  • Lệnh mẫu như trên, thay async_depth và -surfaces.
  • Đo: fps delta, encode_submit latency (instrument trong app hoặc dùng ffmpeg -report), driver return codes.
  • Quyết định: nếu fps tăng và memory ok, giữ; nếu latency submit tăng hoặc driver trả busy, quay lại giá trị trước.

3) Thử concurrency (parallel jobs)

  • Mục: đo contention khi chạy nhiều job lên cùng GPU.
  • Hành động:
  • Chạy 1, 2, 4 parallel ffmpeg processes. Ví dụ bằng GNU parallel hoặc simple shell loop.
  • Ghi tổng fps và fps per-job.
  • Đo: tổng throughput vs number jobs, GPU memory, PCIe bandwidth.
  • Nếu tổng throughput không tăng hoặc giảm, bạn đã chạm bão hòa; giảm concurrency hoặc giảm async_depth per-job.

4) Kiểm tra NUMA và affinity (Linux)

  • Mục: phát hiện NUMA penalty trên host-to-device copy.
  • Hành động:
  • Dùng numactl để bind producer thread vào CPU node gần GPU:
    numactl –cpunodebind=1 –membind=1 ./run_encode.sh
  • So sánh throughput vs non-bound.
  • Nếu cải thiện đáng kể, áp đặt CPU/memory affinity cho production.

5) Điều chỉnh chunk size và batching

  • Mục: tìm chunk size amortize copy overhead.
  • Hành động:
  • So sánh chunk ~50 MB, 200 MB, 1 GB bằng cách chia video thành đoạn tương ứng trước khi encode.
  • Đo H2D per-transfer latency và overall fps.
  • Quyết định: chọn chunk nhỏ nhất đạt >95% throughput so với lớn nhất.

6) Kiểm tra I/O và muxing decoupling

  • Mục: đảm bảo disk I/O không block.
  • Hành động:
  • Tách muxing sang process riêng hoặc ghi vào tmpfs: –tmpfs /tmp và kiểm tra fps.
  • Nếu ghi nhanh ở tmpfs but chậm khi viết đĩa, cần nâng cấp I/O hoặc làm async upload.

7) Lưu trữ, so sánh và rollback

  • Mục: có thể so sánh các run reproducible.
  • Hành động:
  • Mỗi run lưu JSON/CSV: config (async_depth, surfaces, preset, chunk_size, num_jobs), metrics, logs, trace file.
  • Dùng script plot để so sánh fps, latency, mem_used.
  • Nếu thay đổi gây lỗi hoặc driver reset, rollback config và ghi incident report.

8) Checklist cuối trước production

  • Verify driver/SDK versions, ffmpeg build, reproduce best config 3 lần, kiểm tra stability 10 phút, đảm bảo không auto-run code không review, và lưu cấu hình vào repo cùng với metrics.

Thực hiện theo chu trình: baseline -> tune one variable -> concurrency -> affinity -> chunking -> persist metrics. Nếu muốn, mình có thể cung cấp script test tự động (bash/python) theo checklist trên. Bạn có muốn mình sinh script đó cho Ubuntu với CUDA/NVENC không?

Tài nguyên tham khảo hữu ích và công cụ profiling

Đây là bộ công cụ và tài nguyên mình khuyên bạn dùng để phát hiện cổ chai hiệu năng và cấu hình encoder phần cứng. Mình kèm lệnh mẫu và nơi tra cứu chính thức để bạn thay thế theo phiên bản.

  • Nsight Systems (NVIDIA) | profiling hệ thống, trace copy/encode overlap
  • Lệnh mở nhanh: nsys profile –output=trace ./your_binary
  • Docs: https://developer.nvidia.com/nsight-systems
  • Nsight Compute (NVIDIA) | phân tích kernel CUDA nếu bạn có AI trên GPU
  • ncu –target-processes all ./your_ai_app
  • Docs: https://developer.nvidia.com/nsight-compute
  • nvidia-smi | giám sát VRAM, GPU util, power, driver
  • nvidia-smi –query-gpu=index,name,memory.used,utilization.gpu –format=csv -l 1
  • VA-API tools (Intel/AMD) | kiểm tra VA support trên Linux
  • vainfo (Intel) và vaapinfo (các bản VA-API)
  • Install: sudo apt install vainfo libva-utils
  • ffmpeg build tips | cần build với hwaccel/encoder tương ứng
  • Kiểm tra: ffmpeg -buildconf và ffmpeg -encoders | grep -E “nvenc|qsv|amf|vaapi”
  • Hướng dẫn build: https://trac.ffmpeg.org/wiki/Hardware/VAAPI và https://trac.ffmpeg.org/wiki/HWAccelIntro
  • Script sẵn có và mẫu test
  • Bandwidth test: CUDA samples bandwidthTest; NVENC samples in Video Codec SDK
  • Mẫu: script thu nvidia-smi logs + ffmpeg -report để so sánh runs
  • Docs vendor để thay thế theo phiên bản
  • NVIDIA Video Codec SDK: https://developer.nvidia.com/nvidia-video-codec-sdk
  • Intel oneVPL / Media SDK: https://www.intel.com/content/www/us/en/developer/tools/media-sdk/overview.html
  • AMD AMF / VCN guides: https://gpuopen.com

Dùng các công cụ trên kết hợp traces và script tự động để xác định in-flight frames, PCIe bottleneck và driver/SDK mismatch trên hệ của bạn. Mình có thể gửi mẫu script thu thập metrics nếu bạn cần.

bạn cần biết

Trước khi áp chiến lược tối ưu hóa render bằng phần cứng và AI, bạn cần trả lời những câu sau để chọn phương án phù hợp.

  • VRAM và phân bổ
  • GPU của bạn có tổng VRAM bao nhiêu? (nvidia-smi / Windows Task Manager)
  • AI model khi tải chiếm bao nhiêu VRAM ổn định? (chạy model và đo peak)
  • Dự phòng VRAM an toàn là bao nhiêu cho encoder và driver? (đặt margin)
  • PCIe và I/O
  • Mainboard đang dùng PCIe gen mấy và có bao nhiêu lane cho GPU? (lspci / Device Manager)
  • HDD/SSD có đủ throughput để feed/flush chunk lớn không? (fio hoặc disk benchmark)
  • Có NVMe chia lane với GPU không? (kiểm tra sơ đồ lanes của bo mạch)
  • Multi-GPU và mapping
  • Bạn có nhiều GPU hay chỉ 1? Muốn phân job theo GPU hay share 1 GPU?
  • Nếu multi-GPU, có NVLink / peer-to-peer? Có NUMA ảnh hưởng không?
  • Workload AI và chia tài nguyên
  • AI preprocessing (denoise/SR) chạy trên cùng GPU hay CPU/remote?
  • Mỗi frame/model inference latency và batch size là bao nhiêu? Có thể chạy batched không?
  • Mục tiêu latency và throughput
  • Mục tiêu latency từng frame là bao nhiêu ms?
  • Mục tiêu throughput (FPS hay total time cho X phút video) là bao nhiêu?
  • Hệ điều hành và runtime
  • Bạn chạy Windows hay Ubuntu? FFmpeg/driver/SDK đã cài bản nào?
  • Container/WSL/VM có dùng GPU passthrough không?
  • Giới hạn an toàn khi cho AI sinh mã
  • Bạn có chính sách mã hóa secrets, review thủ công, sandbox execution không?
  • Ai chịu trách nhiệm phê duyệt và rollback khi có lỗi?

Trả lời cụ thể từng câu trên, ghi chỉ số đo được, rồi từ đó chọn: hardware encode + batching, hoặc software encode + heavy AI, hoặc hybrid.

Kết luận

Mình hy vọng sau khi áp dụng các bước đo đạc, tách stages, và tuning bạn sẽ thấy throughput encode tăng rõ rệt và pipeline ổn định hơn. Một lưu ý quan trọng: luôn đo trước và sau mỗi thay đổi, và bắt buộc review thủ công mọi mã AI sinh tự động trước khi cho chạy trên phần cứng thật.