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.

Xây pipeline JTBD bằng AI và JSON-ready

Mình sẽ hướng dẫn bạn cách xây pipeline nghiên cứu JTBD dùng AI: chuẩn hoá JSON input, prompt extraction thành job/outcome, clustering, kiểm soát chất lượng và mapping thành epic trong backlog. Ví dụ minh họa áp dụng cho phỏng vấn audio và survey free-text.

Mục tiêu nghiên cứu JTBD bằng AI

Mục tiêu ngắn gọn: giúp bạn (product researcher / PM) tạo một pipeline AI có thể chuyển dữ liệu nghe/nhìn/text thô thành bộ Jobs, Desired Outcomes, và Actionable Insights đủ tin cậy để ưu tiên roadmap hoặc sinh hypotheses cho thí nghiệm. Dưới đây là mục tiêu cụ thể, phạm vi, người thực hiện, ràng buộc và tiêu chí chấp nhận cùng trade-off cần cân bằng.

  • Đầu ra mong đợi (deliverables)
  • Danh sách canonical Jobs (chuẩn hoá, id duy nhất).
  • Desired Outcomes liên kết với mỗi Job, kèm metric đề xuất (baseline + target).
  • Actionable Insights: hypotheses + suggested experiments (A/B, prototype, concierge).
  • Metadata provenance cho mỗi item: sourcerefs, supportcount, confidence_score.
  • Báo cáo tóm tắt để đưa vào backlog: priority_score và mapping job→epic.
  • Phạm vi nghiên cứu
  • Input: transcripts cuộc phỏng vấn, survey free-text, nhật ký sử dụng; không bao gồm thu thập người tham gia hoặc chi tiết kỹ thuật tích hợp API.
  • Output: JSON-ready artifacts để tự động hoá bước phân tích và tạo draft epics.
  • Người thực hiện chính
  • Product researcher hoặc PM chịu trách nhiệm thiết kế study, kiểm duyệt schema, và review final outputs.
  • AI cung cấp extraction, clustering, và synthesis; con người chịu review quyết định chiến lược.
  • Ràng buộc thời gian và nguồn lực
  • MVP: chạy trên 100–300 transcripts trong 2–4 tuần với 1 researcher + engineering support.
  • Human review budget: dự trù 10–25% của items auto-extracted để kiểm duyệt.
  • Tiêu chí chấp nhận (acceptance)
  • Precision việc trích job >= 0.75 trên sample audited.
  • Mỗi job auto-published phải có ≥ 3 distinct supporting utterances hoặc ≥2 nguồn độc lập.
  • Evidence attached cho mọi job; nếu confidence < 0.7 thì review bắt buộc.
  • Trade-offs cần nêu rõ
  • Precision vs recall: ưu tiên precision khi tự động push vào backlog; ưu tiên recall cho giai đoạn khám phá.
  • Automation vs human-review: tự động hoá các mapping có confidence cao và support_count đủ; giữ người review cho novel, high-impact hoặc low-confidence items.

Mục tiêu này nhằm đảm bảo AI tăng tốc phân tích JTBD nhưng không thay thế kiểm duyệt con người khi rủi ro quyết định sản phẩm cao.

Tổng quan quy trình end-to-end (bước theo bước)

Mục tiêu: chuyển dữ liệu âm thanh và text thô thành Jobs, Desired Outcomes và insight có thể hành động, kèm metadata provenance và checkpoints chất lượng ở mỗi bước.

  1. Audio capture
  • Input: file âm thanh, meta session
  • Output: file audio, sample rate chuẩn, nguồn/consent tag
  • Checkpoint: audio_length, SNR, mic channel, presence of participant consent
  1. ASR (speech-to-text)
  • Input: raw audio
  • Output: verbatim transcript + token-level confidence + speaker diarization if có
  • Checkpoint: avg ASR confidence, WER estimate on sample, speaker labels hợp lý
  1. Transcript cleaning & normalization
  • Input: raw transcript
  • Output: cleaned transcript giữ nguyên câu trích dẫn, alt_transcripts, timestamps
  • Checkpoint: không loại bỏ negation/temporal cues; percent of short/truncated utterances
  1. Preprocessing (tokenization, sentence segmentation, NER)
  • Input: cleaned transcript
  • Output: tokenized sentences, entity spans, temporal tags
  • Checkpoint: preservation of named entities, negation tokens, sentence-boundary sanity
  1. JSON mapping (field mapping & provenance)
  • Input: processed sentences + metadata
  • Output: per-utterance JSON with fields: participantid, timestamp, questionid, utteranceraw, sourcespan, asrconfidence, participantprofile
  • Checkpoint: schema validation pass, percent missing critical fields < threshold
  1. Chunking & summarization (for model context limits)
  • Input: JSON utterances
  • Output: semantically segmented chunks with overlap and extractive summaries, plus raw excerpts attached
  • Checkpoint: chunk overlap >= 20%, check that evidence spans not split across chunks
  1. LLM extraction (map chunk -> JTBD components)
  • Input: chunks + prompt + schema
  • Output: extracted fields: jobtext, trigger, struggle, desiredoutcomes, evidencespans, extractionconfidence
  • Checkpoint: JSON validity, evidence presence, model confidence threshold
  1. Clustering & deduplication
  • Input: extracted jobs with embeddings
  • Output: clustered canonical jobs, support_count, representative quotes
  • Checkpoint: cluster cohesion metrics, duplicate rate, manual review candidates
  1. Synthesis & hypothesis generation
  • Input: clustered jobs + outcomes
  • Output: opportunity hypotheses with suggested experiments, prioritized list, estimated impact, confidence
  • Checkpoint: each hypothesis has >=1 supporting quote per source type or marked low-confidence
  1. Validation & human review
  • Input: hypotheses + provenance
  • Output: adjudicated hypotheses, corrected labels, final confidence
  • Checkpoint: sample audit rate, reviewer overturn rate, inter-annotator agreement
  1. Backlog integration
  • Input: validated hypotheses with metrics and scope
  • Output: draft epics/tickets with acceptance criteria, instrumentation tasks, priority tag
  • Checkpoint: mapping to analytics metrics exist, instrumentation required flagged, review_required true for low-confidence or high-impact items

Vận hành: log provenance at mọi bước, compute uncertainty score per item, sample audits (5%+) và alerts nếu missing-field rate vượt ngưỡng.

Kiến trúc JSON input chuẩn cho phỏng vấn / khảo sát / nhật ký người dùng

Mục tiêu ngắn: cung cấp schema đầu vào chuẩn để feed LLM/AI cho nghiên cứu JTBD, bảo toàn provenance, profile người tham gia, nội dung nguyên gốc, các phiên bản transcript thay thế, và trường kiểm tra chất lượng. Dưới đây là danh sách trường bắt buộc và tùy chọn kèm kiểu dữ liệu, định dạng mẫu, và ví dụ giá trị.

Lưu ý: mọi trường provenance cần giữ liên kết tới bản nguyên (startts,endts,speakerid,sourcetype,asr_confidence).

  1. session_id | string | required
  • Mô tả: id duy nhất cho một buổi (file audio / entry).
  • Ví dụ: “S-20260717-001”
  1. participant_id | string | required
  • Mô tả: id người tham gia (anonymized).
  • Ví dụ: “P-045”
  1. participant_profile | object | required
  • fields:
  • role | string | required | ex: “developer”
  • personatags | array[string] | optional | ex: [“poweruser”,”mobile”]
  • usage_frequency | string | optional enum {daily,weekly,monthly,rare} | ex: “weekly”
  • tech_savviness | string | optional enum {low,medium,high} | ex: “high”
  1. language | string | required (ISO 639-1)
  • Ví dụ: “vi”
  1. sourcetype | string | required enum {“audio”,”surveytext”,”diary”,”support_chat”}
  • Ví dụ: “audio”
  1. utterances | array[object] | required
  • mỗi phần tử là một đoạn thoại hoặc một câu trả lời. Object fields:
  • utterance_id | string | required | ex: “U-000123”
  • start_ts | string | required ISO8601 | ex: “2026-07-01T10:12:34Z”
  • end_ts | string | required ISO8601 | ex: “2026-07-01T10:12:58Z”
  • speaker_id | string | required | ex: “P-045” or “INT-01”
  • speaker_role | string | required enum {“participant”,”interviewer”,”system”}
  • question_id | string | optional | ex: “Q-05”
  • question_text | string | optional | ex: “Kể lần gần đây nhất…”
  • utterance_raw | string | required (verbatim) | ex: “Ừm, lúc đang đi mình khó tìm tài liệu…”
  • alt_transcripts | array[object] | optional (ASR n-best)
  • text | string, confidence | number 0-1
  • asr_confidence | number 0-1 | optional (avg for utterance) | ex: 0.78
  • language | string | optional (per-utterance override)
  • ingestion_checks | object | optional
  • minlengthok | boolean, confidenceok | boolean, langdetect_confidence | number
  1. annotation_tags | array[string] | optional
  • Ví dụ: [“possible_trigger”,”emotion:frustration”]
  1. mapped_fields | object | optional (preliminary human/auto mapping)
  • job_id | string | optional
  • job_text | string | optional
  • segmentrole | string enum {“trigger”,”struggle”,”attempt”,”desiredoutcome”,”context”,”solution_hint”} | optional
  • mappingstate | string enum {“MAPPED”,”INFERRED”,”MISSING”,”UNCERTAIN”} | required if mappedfields present
  • mapping_confidence | number 0-1
  1. provenance (top-level) | object | required
  • recording_id | string | optional
  • transcriptsource | string enum {“ASR”,”humantranscriber”,”hybrid”} | required
  • transcriptgeneratedat | string ISO8601 | optional
  • ingestion_version | string | optional
  1. session_metadata | object | optional
  • study_id | string | optional
  • recruiter_notes | string | optional
  • consent_flag | boolean | required
  1. ingestion_id | string | required unique per record
  • ex: “ing-20260717-xx”
  1. ingestioncheckssummary | object | optional
  • schemavalid | boolean, missingfields | array[string], warnings | array[string]
  1. languagelocale | string | optional (e.g., “viVN”)
  1. confidence_overall | number 0-1 | optional (aggregated)

Ví dụ JSON mẫu ngắn (minified readability):

{ “sessionid”:”S-20260717-001″, “participantid”:”P-045″, “participantprofile”:{“role”:”developer”,”personatags”:[“mobile”,”onthego”],”usagefrequency”:”weekly”,”techsavviness”:”high”}, “language”:”vi”, “sourcetype”:”audio”, “provenance”:{“recordingid”:”rec-0001″,”transcriptsource”:”ASR”,”transcriptgeneratedat”:”2026-07-17T10:00:00Z”,”ingestionversion”:”v1″}, “utterances”:[ {“utteranceid”:”U-000123″,”startts”:”2026-07-01T10:12:34Z”,”endts”:”2026-07-01T10:12:58Z”,”speakerid”:”P-045″,”speakerrole”:”participant”,”questionid”:”Q-05″,”questiontext”:”Kể lần gần đây nhất…”,”utteranceraw”:”Ừm, lúc đang đi mình rất khó tìm tài liệu kỹ thuật trên điện thoại.”,”alttranscripts”:[{“text”:”Lúc đang đi mình khó tìm tài liệu kỹ thuật trên điện thoại”,”confidence”:0.70}],”asrconfidence”:0.78,”ingestionchecks”:{“minlengthok”:true,”confidenceok”:false,”langdetectconfidence”:0.99},”annotationtags”:[“possibletrigger”]} ], “ingestionid”:”ing-20260717-001″, “ingestioncheckssummary”:{“schemavalid”:true,”missingfields”:[],”warnings”:[“utterance asrconfidence < 0.85 – review recommended”]} }

Gợi ý vận hành ngắn:

  • Luôn đính kèm utteranceraw và alttranscripts để AI có nguồn chứng cứ.
  • Giữ mapping_state và provenance để audit.
  • Nếu field bắt buộc bị thiếu, reject record và queue cho human review.

Mình khuyến nghị bạn dùng schema trên làm JSON Schema validator trước khi feed mô hình để giảm lỗi mapping/tóm tắt/chunking.

Ví dụ JSON mẫu: interview long-form (phỏng vấn sâu)

{ “sessionid”: “S-20260717-001”, “participantid”: “P-045”, “participantprofile”: {“role”:”developer”,”personatags”:[“mobile”,”onthego”],”usagefrequency”:”weekly”}, “language”:”vi”, “sourcetype”:”audio”, “provenance”: {“recordingid”:”rec-0001″,”transcriptsource”:”ASR”,”transcriptgeneratedat”:”2026-07-17T10:00:00Z”}, “utterances”: [ { “utteranceid”:”U-000123″, “startts”:”2026-07-01T10:12:34Z”, “endts”:”2026-07-01T10:12:58Z”, “speakerrole”:”participant”, “questionid”:”Q-05″, “questiontext”:”Kể lần gần đây nhất bạn cần tìm tài liệu kỹ thuật trên điện thoại.”, “utteranceraw”:”Lúc đang đi tôi rất khó tìm tài liệu kỹ thuật trên điện thoại, hay mất mạng.”, “alttranscripts”:[{“text”:”Lúc đang đi tôi rất khó tìm tài liệu kỹ thuật trên điện thoại”,”confidence”:0.70}], “asrconfidence”:0.78, “annotationtags”:[“possibletrigger”] } ], “ingestionchecks”:{“schemavalid”:true,”warnings”:[“asrconfidence<0.85″]}, “ingestion_id”:”ing-20260717-001″ }

Giải thích ngắn: interview long-form cần session-level provenance, participantprofile để group theo persona, utteranceraw verbatim để LLM có bằng chứng. alttranscripts và asrconfidence giữ ambiguity; questionid/questiontext giúp phân biệt prompt dẫn dắt. ingestion_checks cho pipeline dừng nếu bất thường.


Ví dụ JSON mẫu: short survey free-text

{ “responseid”:”R-20260718-210″, “participantid”:”P-210″, “participantprofile”:{“role”:”manager”,”usagefrequency”:”monthly”}, “language”:”vi”, “sourcetype”:”surveytext”, “questionid”:”S-Q1″, “questiontext”:”Nêu một điều làm bạn thấy phiền khi dùng sản phẩm.”, “answerraw”:”Khó đặt lịch họp, giao diện rườm rà.”, “answerlength”:33, “timestamp”:”2026-07-18T21:10:00Z”, “ingestionchecks”:{“minlengthok”:true,”languageconfidence”:0.99} }

Giải thích ngắn: survey free-text thường ngắn, nên lưu answerraw nguyên văn và questiontext để ngăn LLM biến prompt thành participant voice. answerlength và ingestionchecks giúp lọc câu quá ngắn hoặc spam.


Ví dụ JSON mẫu: in-app event note (nhật ký sự kiện)

{ “eventid”:”E-20260719-77″, “userid”:”U-501″, “sessionid”:”S-501-07″, “timestamp”:”2026-07-19T08:45:22Z”, “sourcetype”:”inappevent”, “eventtype”:”checkoutfailed”, “eventprops”:{“errorcode”:”PAY-402″,”network”:”cellular”,”cartvalue”:49.99}, “userfeedbackraw”:”Thanh toán lỗi khi mạng yếu”, “language”:”vi”, “linkedutterance_refs”:[“U-000987”] }

Giải thích ngắn: event note phải gồm eventtype + eventprops để nối hành vi với lời phàn nàn. userfeedbackraw là nguồn text ngắn làm bằng chứng cho JTBD. linkedutterancerefs liên kết đến transcript nếu có, giúp provenance.


Ghi chú xử lý trường đặc biệt (tóm tắt)

  • utteranceraw / answerraw / userfeedbackraw: giữ nguyên verbatim, không tự động tóm tắt trước khi đưa cho LLM; nếu cần summary, lưu song song (raw + summary).
  • alttranscripts & asrconfidence: khi confidence thấp, feed cả alt_transcripts vào LLM với instruction “consider ambiguity”.
  • questionid/questiontext: bắt buộc cho mọi phản hồi có source interview/survey để phân biệt interviewer prompt vs participant statement.
  • participant_profile: dùng để phân cụm jobs theo persona; nếu missing nhiều, pipeline phải cảnh báo.
  • provenance fields (recordingid, sessionid, linked refs): bắt buộc cho audit và để LLM trích dẫn bằng chứng khi sinh job/outcome.

Prompt để dán vào LLM (mục tiêu: chuyển raw text thành thành phần JTBD)

  • System instruction (ngắn, đặt ở role system):

“You are an extraction assistant. From a raw user utterance, extract JTBD components precisely and only when supported by text. If evidence is insufficient, refuse with INSUFFICIENT_EVIDENCE. Output valid JSON exactly as specified.”

  • Model settings (recommended):

temperature: 0.0 topp: 0.9 maxtokens: 512 deterministic / sampling: deterministic (no sampling, temp 0)

  • Prompt body (paste as user content). Giữ nguyên format JSON mẫu cuối cùng:
  1. Nhiệm vụ: cho một đoạn raw_text (verbatim), trả về JSON chứa các trường:

jobid (string unique), jobtext (string) – mô tả job in user-centric goal, không là solution, circumstance (string) – bối cảnh rút ra từ text, trigger (string) – tác nhân tức thời kích hoạt job, struggle (string) – khó khăn cụ thể user mô tả, desiredoutcomes (array of objects) – mỗi object {id, text, metricsuggestion (optional)}, evidencespans (array of objects) – mỗi object {startidx, endidx, quote, sourceid}.

  1. Quy tắc quan trọng:
  • Không suy diễn: nếu không có câu chữ hỗ trợ trực tiếp cho job/circumstance/trigger/struggle, trả JSON: {“refusal”:”INSUFFICIENT_EVIDENCE”} exactly.
  • Nếu có bằng chứng nhưng không đủ để điền một trường, để giá trị đó là null.
  • jobtext phải là mục tiêu người dùng, không là tính năng hay giải pháp. Nếu rawtext nêu giải pháp, map sang underlying goal.
  • evidencespans must cite verbatim substrings from rawtext and provide start/end character indices relative to raw_text.
  • jobid & outcome ids: use short stable ids like “job001″, “out_001”.
  1. Output must be valid JSON only, nothing else.
  • Few-shot examples (include 2 examples). Mỗi example gồm input_raw và expected JSON output.

Example 1 – brief Input_raw: “Tôi hay quên mật khẩu khi đăng nhập trên điện thoại, mất quá nhiều thời gian để reset.”

Expected JSON: { “jobid”:”job001″, “jobtext”:”Đăng nhập nhanh vào ứng dụng trên điện thoại”, “circumstance”:”khi đăng nhập trên thiết bị di động”, “trigger”:”quên mật khẩu lúc cần vào ứng dụng”, “struggle”:”phải reset mật khẩu tốn thời gian”, “desiredoutcomes”:[ {“id”:”out001″,”text”:”Hoàn tất đăng nhập trong dưới 30 giây”,”metricsuggestion”:”timetologinseconds”} ], “evidencespans”:[ {“startidx”:0,”endidx”:56,”quote”:”Tôi hay quên mật khẩu khi đăng nhập trên điện thoại, mất quá nhiều thời gian để reset.”} ] }

Example 2 – contains solution mention (must map to underlying job) Input_raw: “Thường thì mình bật chế độ ‘auto-fill’ trên trình duyệt, nhưng vẫn muốn có nút ‘one-click checkout’.”

Expected JSON: { “jobid”:”job002″, “jobtext”:”Hoàn tất giao dịch mua nhanh chóng với ít thao tác”, “circumstance”:”khi mua sắm qua trình duyệt trên điện thoại hoặc desktop”, “trigger”:”cần mua nhanh và không muốn nhập nhiều thông tin”, “struggle”:”quá nhiều bước để hoàn tất mua hàng”, “desiredoutcomes”:[ {“id”:”out002″,”text”:”Giảm số bước để hoàn thành mua hàng xuống tối đa 1-2 thao tác”,”metricsuggestion”:”checkoutsteps”} ], “evidencespans”:[ {“startidx”:24,”endidx”:67,”quote”:”vẫn muốn có nút ‘one-click checkout'”} ] }

  • Template for single-run extraction (to paste with raw text inserted):

{ “rawtext”:”<PASTE THE VERBATIM UTTERANCE HERE>”, “sourceid”:”<SESSIONORRESPONSE_ID>” }

  • Instruction to model to produce final JSON (copy exactly):

“Extract JTBD from the provided rawtext. Follow the rules above. Output a single JSON object with the fields: jobid, jobtext, circumstance, trigger, struggle, desiredoutcomes (array), evidencespans (array). If no sufficient evidence, output exactly: {\”refusal\”:\”INSUFFICIENTEVIDENCE\”}.”

  • Notes vận hành:
  • Khi xử lý nhiều utterances, call model per utterance for highest fidelity.
  • Validate output JSON schema programmatically. If invalid, retry once with same settings.
  • Keep rawtext and evidencespans in provenance store for audits.

Prompt mẫu: tổng hợp và phân loại jobs theo tần suất và mức đau | 360 | prompt để LLM aggregate outputs từ many utterances (list of JSON utterances) thành ranked jobs list với supportcount, painscore, personasaffected; yêu cầu cite top-3 evidence quotes và cung cấp confidencescore cùng justification ngắn; kèm cách parse và validation

Mục tiêu ngắn: cho một tập JSON utterances (mỗi utterance là một quote với provenance), prompt này yêu cầu LLM nhóm và xếp hạng các Jobs To Be Done theo tần suất và mức đau, trả về danh sách job có supportcount, painscore (0-10), personasaffected, top-3 evidence quotes, confidencescore (0-1) và justification ngắn. Kết quả phải có cấu trúc JSON hợp lệ để parse tự động.

System instruction (dùng làm role system): You are an evidence-grounded aggregator. Use only the provided utterances as evidence. Do not invent data. When grouping, prefer conservative merges. If insufficient evidence for a job, exclude it.

User prompt (chèn nguyên list): You will receive “utterances”, an array of objects with fields:

  • utteranceid, participantid, persona, timestamp, rawtext, sourcetype, asr_confidence (0-1).

Task:

  1. Extract candidate atomic jobs phrased as user goals (not solutions).
  2. Cluster similar candidates into canonical jobs.
  3. For each canonical job output: jobid, jobtext, supportcount (distinct participantids), personasaffected (list), painscore (0-10) estimated from text intensity and frequency, top3evidence (array of verbatim quotes with source refs), confidence_score (0-1) with one-sentence justification.
  4. Rank output list by priorityscore, where priorityscore = normalize(supportcount) * 0.6 + normalize(painscore) * 0.4.

Rules:

  • Evidence requirement: a job needs at least 2 distinct participants to be listed automatically. Otherwise mark as “candidatelowsupport” and include only if confidence_score >= 0.85.
  • pain_score scale guidance: 0 no pain, 1-3 minor, 4-6 moderate, 7-8 severe, 9-10 critical. Base on explicit negative language, repeated mentions, and mentions of consequences.
  • For each top evidence include participant_id and timestamp.

Output format: produce valid JSON array jobs with fields:

  • job_id (string)
  • job_text (string)
  • support_count (int)
  • personas_affected (array of strings)
  • pain_score (int 0-10)
  • top3evidence (array of {quote, participant_id, timestamp})
  • confidence_score (float 0-1)
  • justification (string, <=28 words)
  • priority_score (float 0-1)

Parse and validation (to run after model response):

  • Validate JSON schema; reject if any required field missing.
  • supportcount must equal count of distinct participantids referenced in top evidence and in aggregated utterances for that job. If mismatch >1, flag for human review.
  • confidencescore must correlate with number of supporting participants and phrase intensity. If confidencescore > 0.9 but supportcount < 2, set confidencescore := 0.6 automatically and flag.
  • Ensure top3evidence quotes are verbatim substrings of some utterance.raw_text; otherwise reject job.
  • Compute priority_score independently and compare to model value; if difference >0.05, prefer computed value and log discrepancy.

Ví dụ mẫu đầu vào nhỏ và mẫu output mong muốn phải được đặt trong unit test riêng trước chạy trên toàn corpus.

Prompt mẫu: sinh hypothesis opportunity space từ tập jobs & outcomes

Mục tiêu: từ một bộ canonical jobs và desired outcomes (với evidence), tạo danh sách hypothesis/opportunity statements dùng cho roadmap. Mỗi hypothesis là một câu khả thi, có bằng chứng, gợi ý experiment và ước lượng metric tác động; phân loại priority (low/med/high) kèm lý do ngắn. Dưới đây là mẫu prompt bạn dùng trực tiếp với LLM (system + user) và schema JSON đầu ra để parse tự động.

System instruction (role system): Bạn là một product strategist evidence-driven. Chỉ dùng thông tin trong input. Không thêm giả định ngoài bằng chứng. Mỗi hypothesis phải cite supportingjobs bằng jobid từ input. Trả về duy nhất JSON hợp lệ theo schema yêu cầu. Nếu bằng chứng không đủ, trả hypothesisstate: “insufficientevidence”.

User prompt (đưa kèm dữ liệu jobs & outcomes): Dưới đây là “jobslist” và “outcomeslist”. jobslist: mảng object {jobid, jobtext, personas[], supportcount, evidence: [{participantid, quote}]}. outcomeslist: mảng object {outcomeid, outcometext, metric_name, direction}. Nhiệm vụ của bạn:

  1. Tạo tối đa 12 hypothesis có chất lượng cao.
  2. Mỗi hypothesis phải gồm các trường dưới JSON schema: hypothesisid, statement, supportingjobs[] (jobid), expecteduservalue (1-2 câu), suggestedexperiments[] (mỗi experiment gồm type: “A/B”, “concierge”, “prototype”, description, minimalsampleorduration), estimatedimpactmetric (metricname, baselineoptional, expecteddeltaabsoluteorrelative), priority (“low”|”med”|”high”) và reasonforpriority (<=20 từ), confidencescore (0-1), hypothesisstate (“ready”|”insufficientevidence”).
  3. Ưu tiên hypothesis có supportcount >= 3 hoặc supported by >=2 distinct personas. Nếu không, đặt hypothesisstate = “insufficient_evidence”.
  4. Không nêu giải pháp kỹ thuật cụ thể trong statement; nếu input chứa solution-language, convert to underlying job/need.
  5. Giới hạn mỗi statement <= 28 từ; reasonforpriority <= 20 từ.
  6. For estimatedimpactmetric, nếu baseline không có trong input, set baselineoptional = null and mark confidencescore lower.
  7. Sắp xếp output theo priority then confidence_score.

Output JSON schema (ví dụ mẫu một item): [ { “hypothesisid”: “h1”, “statement”: “Người dùng cần hoàn tất đặt hàng trên mobile trong <60s khi đang di chuyển”, “supportingjobs”: [“job12″,”job47″], “expecteduservalue”: “Giảm friction khi mua trong tình huống di chuyển, tăng conversion”, “suggestedexperiments”: [ {“type”:”A/B”,”description”:”One-page checkout vs baseline”,”sampleorduration”:”10k users / 4 weeks”} ], “estimatedimpactmetric”: {“metricname”:”conversionrate”,”baseline”:0.042,”expecteddeltarelative”:0.10}, “priority”:”high”, “reasonforpriority”:”tần suất cao, ảnh hưởng doanh thu”, “confidencescore”:0.78, “hypothesis_state”:”ready” } ]

Validation rules sau model:

  • Mỗi supportingjobs[] item phải tồn tại trong input jobslist; nếu không, reject hypothesis.
  • Nếu supportcount tổng < 3 và personascovered <2 then hypothesisstate = “insufficientevidence”.
  • confidencescore phải tương ứng số evidence: nếu >=0.8 thì supportcount >=3 hoặc multi-source; nếu mismatch, giảm confidence_score tự động và thêm note.
  • estimatedimpactmetric.expecteddelta phải là số hoặc null; nếu null, set confidencescore -=0.15.

Bạn cung cấp chỉ jobslist và outcomeslist; model chỉ sử dụng chúng để sinh hypotheses.

Quy trình thu thập dữ liệu JSON-ready (checklist)

Mục tiêu: thu dữ liệu phỏng vấn/nhật ký có thể nạp thẳng vào pipeline JSON để AI xử lý, đồng thời giữ provenance và giảm bias. Dưới đây là checklist cho bạn — phần dành cho phỏng vấn viên và phần dành cho engineer lưu trữ/ingest.

A. Trước cuộc phỏng vấn (chuẩn bị)

  • Xin phép và ghi consent rõ ràng: purpose, lưu trữ, chia sẻ nội bộ, thời hạn lưu. Ghi consent_id trong metadata.
  • Chuẩn câu hỏi theo dạng non-leading. Mỗi câu phải có questionid và questiontext.
  • Chuẩn persona screen và capture participantid, recruitmenttags.
  • Chuẩn bị thiết bị: test mic, bật speaker diarization nếu có, bật timestamping.

B. Khi thu âm / phỏng vấn (phỏng vấn viên)

  • Bắt đầu bằng lời nhắc consent lồng vào ghi âm; lưu transcript consent trong metadata.
  • Đọc aloud question_id trước mỗi block câu hỏi để ASR/ingest map dễ.
  • Khuyến khích trả lời kiểu kể chuyện, ask-for-specifics: WHEN, WHERE, WHO, CONSEQUENCE.
  • Tránh dẫn dắt: không gợi ý giải pháp; nếu người tham gia nêu giải pháp, tag là solution_mention.
  • Nếu phỏng vấn qua call, bật speaker diarization; ghi rõ speaker labels (interviewer, participant).
  • Ghi chú thủ công minimal: noiseevents, interruptions, emotionalcues (frustrated, excited).

C. Sau thu âm — immediate steps (phỏng vấn viên + engineer)

  • Lưu raw audio file ngay: filename pattern {study}{session}{participant}_{timestamp}.wav
  • Push audio vào secure storage với encryption at rest; log uploader_id and timestamp.
  • Generate ASR transcript but keep raw audio and verbatim transcript. Save ASR confidence per segment.
  • Save metadata file JSON kèm: {participantid, sessionid, questionids, consentid, language, locale, device, interviewer_id}.

D. Ingest / JSON mapping rules (engineer)

  • For each utterance produce JSON record with required fields:
  • respondentid, sessionid, utteranceid, questionid, questiontext, utteranceraw, speakerrole, startts, endts, language, asrconfidence, alttranscripts[], ingestionchecks.
  • Attach provenance: sourcespan {file, start, end}, revieweroverride flag, mapping_state ∈ {MAPPED, INFERRED, UNCERTAIN, MISSING}.
  • If any required field missing, set ingestion_checks.issue and route record to manual queue.

E. Security, access and retention (engineer)

  • Enforce RBAC, audit logs for reads/exports, encrypt in transit and at rest, anonymize PII only after provenance preserved (store mapping table separately).
  • Retention policy: record consent_expiry and auto-archive or delete accordingly.

F. Rules to avoid interviewer-bias and prompt-check

  • Use neutral wording templates; require interviewer to confirm question phrasing pre-session.
  • Do not ask “Would you like a button for X?” Instead ask “Describe last time you tried to X.”
  • Preflight prompt-check: before exporting to AI, run short automated check on JSON to flag records where interviewer_text appears in participant fields or where >40% of participant quotes echo interviewer wording — mark for audit.
  • Tag any record where participant gives solution language; add field solution_mention=true.

G. Quick operational gates (auto checks)

  • Reject auto-ingest if utteranceraw length < 20 chars and utterancetype == “answer”.
  • Flag for human review if asrconfidence < 0.80 or mappingstate == “INFERRED” with high impact tag.
  • Sample 5% sessions weekly for provenance audit and interviewer compliance.

Thực hiện đúng checklist này giúp bạn đưa dữ liệu sạch, traceable, và ít bias vào bước tiếp theo: chuyển thành JSON input cho AI.

Hướng dẫn chunking & tóm tắt an toàn trước khi nạp model

Mục tiêu: bảo toàn ngữ cảnh quan trọng khi chia và tóm tắt transcript trước khi nạp LLM, giảm rủi ro mất trigger, conflation job-with-solution, và hallucination.

Nguyên tắc chung

  • Luôn giữ nguyên văn bản nguyên gốc. Mỗi chunk hoặc summary phải kèm provenance tới đoạn raw (file, startts, endts, speaker_id).
  • Ưu tiên extractive summary thay vì abstractive. Lưu cả summary và raw passage trong JSON input để mô hình có thể tham khảo nguyên văn khi cần chứng cứ.
  • Đính kèm alt_transcripts: nếu ASR có nhiều đề xuất, include top-N alternatives với confidence để model giải quyết ambiguity.

Chunking: kích thước và chiến lược

  • Utterance-level first: nếu bạn có speaker turns rõ ràng, chunk theo speaker turn trước. Một turn hoàn chỉnh giữ ranh giới ngữ nghĩa tốt nhất.
  • Window size theo context:
  • Với context lớn hơn 8k token: chunks 800–1400 token là vùng an toàn cho cuộc phỏng vấn dài.
  • Với context 4k: chunks 500–900 token.
  • Với context nhỏ (<2k): prefer chunk theo utterance/paragraph, không cắt mid-sentence.
  • Overlap: tối thiểu 20–30% token overlap giữa chunks. Overlap bảo đảm trigger nằm trong ít nhất một chunk đầy đủ, giảm split-cue lỗi.
  • Semantic segmentation ưu tiên over fixed token windows: phân đoạn theo đoạn hội thoại, thay vì cắt cứng theo n token. Dùng heuristics: break at speaker turn boundary, sentence boundary, or semantic boundary (topic change detected bằng embedding distance).

Tóm tắt an toàn

  • Chọn extractive summarization làm mặc định: trích các câu nguyên văn chứa trigger, struggle, và desired outcome.
  • Nếu bắt buộc abstractive summary, giới hạn paraphrase mức thấp và luôn giữ quoted utterance verbatim trong evidence.
  • Yêu cầu summary có field evidence_spans: list các span offsets trích nguyên văn. Nếu summary không thể trích span thì gắn flag “UNCERTAIN”.

Metadata và fields nên kèm theo mỗi chunk

  • sourceid, chunkid, startts, endts, speakerid, language, asrconfidence, alttranscripts[], rawtext, summaryextractive, ingestionchecks {minlengthok, confidenceok}, uncertaintyscore.
  • Nếu chunk do preprocessor infer labels (ví dụ segmentrole), set labelsource = “AUTO” hoặc “HUMAN” và label_confidence.

Khuyến cáo về prompt và mô hình

  • Khi nạp từng chunk vào model để extract JTBD components: sử dụng temperature thấp (0–0.2) cho extraction để giảm hallucination. Yêu cầu output phải cite evidence_spans.
  • Với aggregation stage (cluster + synthesize), cho phép temperature cao hơn nhẹ để thêm ý tưởng nhưng bắt buộc model phải list supporting chunk_ids cho mỗi job.

Kiểm tra trước khi gửi

  • Schema validation: required fields present.
  • Length checks: raw_text length >= 20 chars trừ micro-utterances.
  • ASR confidence: if asrconfidence < 0.85 then attach alttranscripts and mark for human review at extraction step.
  • Overlap audit: ensure any token sequence containing question words or negations appears in at least one chunk fully.

Tóm tắt vận hành

  • Preserve raw text and provenance. Chunk theo utterance/semantic boundary within 800–1400 token window, overlap 20–30%. Use extractive summaries and attach alt_transcripts. Enforce evidence spans and low-temperature deterministic extraction. Những quy tắc này giảm mất ngữ cảnh và bảo toàn trigger, struggle, và desired outcome khi nạp model.

Các lỗi tiền xử lý phổ biến và cách phát hiện tự động

Tóm tắt nhanh: tiền xử lý là nơi thường làm hỏng tín hiệu JTBD nhất. Những bước gây lỗi điển hình gồm truncation, stopword-removal, stemming/lemmatization, entity anonymization và sentence-split. Mỗi lỗi có cơ chế riêng và có thể phát hiện tự động bằng loạt kiểm tra schema, ngưỡng độ dài, đánh giá drift embedding, và so sánh n-gram. Dưới đây là danh sách lỗi, cơ chế gây hại, và các kiểm tra tự động bạn nên triển khai với ngưỡng khởi điểm.

  1. Truncation (cắt/bỏ văn bản)
  • Cơ chế gây lỗi: cắt giữa chừng làm mất trigger hoặc giải thích hoàn cảnh; mô tả trở nên vô nghĩa.
  • Phát hiện tự động:
  • Schema check: đảm bảo utterance_raw tồn tại và minLength >= 20 ký tự. Nếu < 20, flag để review.
  • Token window check: nếu chunk chứa câu bị cắt (kết thúc không phải dấu chấm hoặc có ellipsis) => flag.
  • Evidence loss test: so sánh n-gram overlap giữa raw transcript và mapped field; nếu overlap < 0.25 → cảnh báo.
  1. Stopword-removal vô tội vạ
  • Cơ chế: loại bỏ từ loại quan trọng như “không”, “khi”, “vì” làm mất polarity và temporal cues.
  • Phát hiện tự động:
  • Stopword audit: đếm tần suất các stopwords quan trọng trước/sau preprocessing; nếu giảm > 50% ở các từ negation/time → flag.
  • Polarity check: chạy sentiment/negation detector trên raw và processed; nếu polarity đổi khác đáng kể → flag.
  1. Stemming / lemmatization quá mức
  • Cơ chế: làm mất sắc thái động từ và cụm động tác, làm mờ ranh giới giữa hành động và trạng thái.
  • Phát hiện tự động:
  • Lemma drift test: so sánh danh sách lemmas trước/sau; nếu nhiều lemma bị rút về cùng token (lossy merge) > threshold (ví dụ > 10% token diversity reduction) → cảnh báo.
  • NER recall test: chạy NER trên raw vs processed; nếu recall giảm > 15% → flag.
  1. Entity anonymization / normalization quá mức
  • Cơ chế: thay tên riêng bằng generic token làm mất context (ví dụ nền tảng, đối tác).
  • Phát hiện tự động:
  • Entity retention metric: tỉ lệ entities được bảo toàn (entitypreservedrate). Nếu < 0.75 → review.
  • Category-level vs token-level check: nếu anonymize tất cả entities thành same tag mà số lượng distinct entity giảm > 60% → cảnh báo.
  1. Sentence-split / chunking sai
  • Cơ chế: tách câu không theo chủ đề làm trigger và struggle nằm rời rạc trên các chunk khác nhau.
  • Phát hiện tự động:
  • Boundary-split test: xác định gold control phrases (ví dụ trigger keywords) bị chia qua chunk boundary; nếu > 5% trường hợp → điều chỉnh overlap.
  • Overlap adequacy: nếu chunkoverlap < 20% và transcriptlength > 1500 tokens → flag.

Các kiểm tra tự động chung (automation-ready)

  • Schema conformance: required fields present (utteranceraw, participantid, timestamp, language, source_type).
  • Length thresholds: utteranceraw length < 20 -> flag; utterancetokens < 5 -> flag.
  • ASR confidence: confidence_transcript < 0.85 -> queue for human review.
  • Embedding drift: compute cosine(sim(rawembedding, processedembedding)). If < 0.85 -> flag potential lossy normalization.
  • n-gram overlap: Jaccard(rawngrams, processedngrams) < 0.25 -> flag.
  • Field auto-fill rate: if > 10% of critical fields auto-filled with defaults (e.g., “UNKNOWN”) -> pipeline stop.

Ngưỡng khởi điểm và cảnh báo (gợi ý)

  • utterance_raw length < 20 chars -> warning
  • asr_confidence < 0.85 -> review required
  • chunk_overlap < 20% khi transcript > 1500 tokens -> increase overlap
  • embedding cosine < 0.85 (raw vs processed) -> investigate normalization
  • n-gram Jaccard < 0.25 -> possible truncation/summary loss
  • NER recall drop > 15% -> problematic stemming/normalization

Cách vận hành: chạy các kiểm tra trên mỗi bản ghi khi ingest; gán ingestionchecks object vào JSON với flags và uncertaintyscore. Dùng sampling audits (2–5%) cho các bản flagged để cải thiện heuristics. Với những ngưỡng phát hiện, hãy treat as early-warning và routing tức thì tới human-in-the-loop để tránh lệch JTBD downstream.

Cơ chế mapping vào JSON: rules, provenance và trạng thái inferred

Mục tiêu: khi chuyển văn bản thô thành schema JSON để đưa vào LLM hoặc lưu kết quả JTBD, bạn phải bảo toàn nguyên văn evidence, gắn provenance rõ ràng, và đánh dấu rõ trạng thái mỗi trường để tránh model tự điền thông tin không có bằng chứng.

Nguyên tắc chính

  • Giữ nguyên verbatim: luôn lưu utteranceraw nguyên gốc. Mọi trường tổng hợp phải refer lại đoạn verbatim via sourcespan.
  • Không auto-fill job khi bằng chứng không đủ: nếu processor không tìm thấy evidence rõ ràng trong transcript thì để mapping_state = “MISSING” hoặc “UNCERTAIN” chứ không tự sinh job.
  • Truy vết nguồn tới token/span: mỗi giá trị mapped kèm sourcerefs[] gồm {sourceid, speaker, startoffset, endoffset, excerpt}.
  • Ghi confidence per-field: numeric confidence 0..1 cho từng field; dùng để lọc tự động downstream.
  • Lưu audit log per-field: mỗi lần field được tạo hoặc sửa phải append một audit entry {actor, action, timestamp, reason, oldvalue, newvalue}.

Trạng thái mapping (mapping_state)

  • MAPPED: giá trị trực tiếp trích từ văn bản (utterance_raw) hoặc do human annotator chỉ định.
  • INFERRED: giá trị do processor/model suy diễn nhưng có evidence liên kết; bắt buộc kèm confidence >= threshold và source_refs.
  • UNCERTAIN: model đề xuất nhưng evidence yếu; bắt buộc review người nếu trường này quan trọng.
  • MISSING: không tìm thấy thông tin trong nguồn; không tự tạo.
  • BLOCKED: field bị block do privacy/safety rule.

Cấu trúc provenance tối thiểu (ví dụ) { “jobtext”: “hoàn tất đặt hàng nhanh khi đang di chuyển”, “mappingstate”: “INFERRED”, “confidence”: 0.72, “sourcerefs”: [ {“sourceid”:”sess-123″,”speaker”:”P-45″,”start”:134,”end”:178,”excerpt”:”Tôi thường mua nhanh trên điện thoại khi đi xe buýt…”} ], “audit”: [ {“actor”:”automapperv2″,”action”:”create”,”timestamp”:”2026-07-17T10:01Z”,”reason”:”modelextract”,”oldvalue”:null,”new_value”:”…”} ] }

Policy sống còn

  • Không auto-publish job->backlog nếu mapping_state != MAPPED và confidence < 0.9.
  • Với mappingstate == INFERRED: yêu cầu ít nhất 2 sourcerefs độc lập hoặc human-approve trước khi dùng làm epic.
  • Mọi record có mapping_state INFERRED/UNCERTAIN phải được đánh dấu trong dashboard QA để sampling review.

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

  • Thiết kế validator tự động: schema check, provenance present, confidence thresholds, audit log non-empty. Nếu bất kỳ rule vi phạm, route record vào queue human-review.

Kiểm tra chất lượng output AI: checklist và tiêu chí precision/recall thực dụng

Mục tiêu ngắn: trước khi chấp nhận output AI để đưa vào backlog hoặc báo cáo JTBD, bạn cần chạy một bộ kiểm tra tự động và một số kiểm tra ngẫu nhiên thủ công để bảo đảm output có bằng chứng, không bịa, và đủ đầy trường dữ liệu để gắn thành epic/experiment. Dưới đây là checklist nhanh, các chỉ số đo lường thực dụng và cách tính khoảng tin cậy bằng bootstrap.

  1. Checklist tự động trước khi chấp nhận output (mỗi mục là yêu cầu pass/fail)
  • JSON validity
  • Lưu ý: parseable JSON và tuân schema (bắt buộc các trường: jobid, title, description, desiredoutcomes[], source_refs[]).
  • Evidence presence
  • Mỗi job phải có >= 1 sourcerefs; ưu tiên >= 2 distinct respondentids cho auto-accept.
  • Mỗi sourceref phải bao gồm excerpt text + [sourceid, startoffset, endoffset].
  • Field completeness
  • Kiểm tra non-empty cho: title, description, desired_outcomes[]. Nếu missing critical field -> reject.
  • Embedding similarity vs source
  • Tính cosine(embedding(jobtext), embedding(concatenatedsource_excerpts)). Nếu similarity < 0.65 -> flag for review; nếu >= 0.8 và có multi-source support -> OK.
  • Token-level confidence / model certainty
  • Nếu model cung cấp token logprob or label confidence, compute avg token confidence over job_text; nếu < 0.80 -> require review.
  • Nếu không có probs, use ensemble agreement proxy (multiple runs with temp=0 and temp=0.2; low agreement -> review).
  • Duplicate / double-count detection
  • Semantically similar jobs within batch: cluster by embedding; suppress exact duplicates, surface variants with support counts.
  • Solution vs job guard
  • If title/description contains solution-trigger words (button, widget, API, implement), tag as “solution_mention” and require reframe to underlying job.
  • Privacy / PII check
  • Source excerpts must be anonymized per policy; if PII present without consent -> block.
  • Sanity checks (business rules)
  • If desired_outcome proposes metric not instrumented and impact high -> block until instrumentation task created.
  1. Các chỉ số nên track hàng ngày / tuần
  • extractionprecision = TPextractedjobs / (TPextractedjobs + FPextracted_jobs)
  • TP: AI job that human reviewer marks correct vs source.
  • extractionrecall = TPextractedjobs / (TPextractedjobs + FNjobs)
  • FN: gold job present in transcripts but AI missed.
  • evidencecoverage = #jobs with >=1 high-quality excerpt / totaljobs
  • high-quality = excerpt length >= 20 chars AND sourceasrconfidence >= threshold.
  • hallucinationrate = #jobs flagged as no-support-by-source / totaljobs
  • detector: substring + semantic match + NLI heuristics.
  • average_confidence = mean per-job confidence score (0-1).
  • reviewoverturnrate = fraction of auto-accepted jobs later reverted by humans.
  1. Ngưỡng thực dụng gợi ý
  • Auto-accept only if:
  • evidencecoverage per job >= 2 distinct respondents OR evidencecoverage ==1 but avgconfidence >= 0.9 and embeddingsimilarity >= 0.8.
  • extraction_precision (rolling 1k items) >= 0.85 for auto-publish.
  • Semi-auto (draft): evidence 1 respondent AND avg_confidence 0.8-0.9 -> create draft epic for reviewer.
  • Always require human review for novel jobs (embedding distance to corpus < 0.75) or high-impact changes.
  1. Cách tính khoảng tin cậy (95% CI) bằng bootstrap
  • Dữ liệu: N job outputs sampled with labels TP/FP (human-reviewed).
  • Bootstrap procedure:
  • Repeat B times (B = 1000): resample N items with replacement, compute metric (precision or recall) on resample.
  • Obtain empirical distribution of metric; 2.5th and 97.5th percentiles => 95% CI.
  • Báo cáo: precision ± CI; nếu CI dưới target threshold thì safe to automate; nếu upper/lower vượt target, điều chỉnh ngưỡng hoặc tăng sample size.
  1. Quy trình hành động khi phát hiện vấn đề
  • Nếu hallucinationrate weekly > 3% hoặc reviewoverturn_rate > 5% -> pause auto-publish, tăng human review, run root-cause (ASR vs preproc vs prompt).
  • Nếu embeddingsimilarity drift downward hoặc avgconfidence giảm > 10% so với baseline -> trigger recalibration: re-label 200 samples, fine-tune prompt/schema.

Kết luận ngắn: dùng checklist để chặn lỗi cơ bản, track extractionprecision/extractionrecall/evidencecoverage/hallucinationrate liên tục, và dùng bootstrap CI cho quyết định tự động hóa. Luôn bắt nguồn output từ provenance rõ ràng; bất kỳ job nào thiếu bằng chứng hoặc low-confidence đều phải qua con người trước khi thành epic.

Cách đánh giá fidelity giữa raw transcript và extracted components

Mục tiêu: xác định xem một thành phần do AI trích xuất (job, trigger, outcome, quote) có thật sự được hỗ trợ bởi transcript nguyên gốc hay là paraphrase/hallucination. Mình trình bày ba phép kiểm chính, quy tắc kết hợp để phát hiện hallucination/paraphrase, ngưỡng gợi ý, và cách calibrate per-domain bằng labeled set.

  1. Ba phép kiểm cơ bản
  • Exact substring match
  • Kiểm tra xem đoạn quote hoặc cụm từ trích xuất có xuất hiện nguyên văn trong rawtranscript hay alttranscripts. Nếu có, coi là evidence trực tiếp.
  • SBERT embedding similarity
  • Mã hóa câu trích xuất và từng câu trong transcript bằng SBERT hoặc sentence transformer tương đương. Lấy max cosine similarity giữa câu trích xuất và mọi câu nguồn để đánh giá paraphrase. Phù hợp khi ngôn ngữ thay đổi cách diễn đạt.
  • NLI entailment test
  • Dùng mô hình NLI (premise = đoạn transcript hoặc tập câu liên quan, hypothesis = câu trích xuất). Nếu entailment probability cao, output được coi là được suy luận từ source.
  1. Quy tắc kết hợp để phân loại
  • Supported (có bằng chứng trực tiếp)
  • exactsubstring == true OR (sbertsim >= tsimstrong AND nlientailprob >= tnlistrong)
  • Paraphrase-supported
  • exactsubstring == false AND sbertsim ∈ [tsimweak, tsimstrong) AND nlientailprob >= tnlimedium
  • Ghi nhãn là paraphrase, require evidence_span reference
  • Hallucination (no support)
  • exactsubstring == false AND sbertsim < tsimweak AND nlientailprob < tnlimedium
  • Hoặc nli predicts contradiction
  • Ambiguous
  • Conflicting signals (sbert high but nli neutral) → mark UNCERTAIN, route human review
  1. Ngưỡng gợi ý (khởi điểm)
  • tsimstrong = 0.82, tsimweak = 0.68
  • tnlistrong = 0.85, tnlimedium = 0.60
  • exact_substring check là boolean không có ngưỡng
  1. Cách calibrate per-domain với labeled set
  • Chuẩn bị gold set: sample 300–1000 paired examples (extracted component, supporting transcript span) được annotator đánh nhãn: supported / paraphrase / hallucination.
  • Sweep thresholds: grid search trên tsim and tnli, tính precision/recall/F1 cho từng nhãn trên validation fold.
  • Chọn thresholds tối ưu theo mục tiêu:
  • Nếu muốn giảm false positive hallucination (không chặn output đúng) ưu tiên precision: chọn ngưỡng cao hơn.
  • Nếu muốn catch mọi hallucination, ưu tiên recall: hạ ngưỡng paraphrase.
  • Kiểm tra robustness: bootstrap CI cho precision/recall; nếu CI rộng, tăng sample size hoặc thu thêm edge cases.
  • Lưu cấu hình ngưỡng theo domain/lang code, log provenance: sourcespan, sbertscore, nliscore, exactmatch, reviewer_override.
  1. Triaging và operational rules
  • Auto-accept only if Supported.
  • Paraphrase-supported → auto-draft with citation spans.
  • Hallucination/UNCERTAIN → require human adjudication.
  • Periodically sample adjudicated cases để retrain NLI/embedding thresholds.

Kết luận: kết hợp substring, SBERT và NLI cho phép phân biệt được paraphrase và hallucination. Calibrate ngưỡng trên gold set của domain để cân bằng precision/recall phù hợp với rủi ro sản phẩm.

Quy trình thử nghiệm phân bổ nguyên nhân lỗi (root-cause analysis)

Mục tiêu: đo tỉ lệ phần trăm lỗi JTBD qui ra từ từng bước pipeline: capture (ghi âm), transcription, preproc/mapping (normalization, chunking, tóm tắt, map vào JSON), và AI extraction/model. Kết quả báo cáo là phân bố % lỗi kèm khoảng tin cậy bootstrap và hướng sửa ưu tiên.

  1. Chuẩn bị dataset và gold standard
  • Chọn N transcripts = 300–1000, phân phối theo length, ngôn ngữ, channel. Mỗi transcript cần raw audio, verbatim transcript (human nếu có), timestamps, speaker labels.
  • Tạo gold standard: 2–3 product researcher annotate và adjudicate thành các thành phần JTBD (jobs, triggers, circumstances, desired outcomes) với evidence spans và canonical job IDs. Đây là ground truth để so sánh.
  1. Thiết kế pipeline biến thể (controlled variants)
  • Baseline Auto: auto-ASR -> default preproc (standard normalization, summarization, chunking) -> auto-mapping JSON -> model extract.
  • Variants để tách nguyên nhân:
  • V_TxHuman: human-vetted transcript thay cho auto-ASR, giữ preproc/model auto.
  • VPreprocConservative: auto-ASR, conservative preproc (no abstractive summary, larger overlap, NOFILL defaults), auto-mapping.
  • V_MappingHuman: auto-ASR + preproc auto, nhưng human maps transcript -> JSON fields.
  • V_ModelDifferent: giữ inputs, thay đổi model or prompt settings to measure model hallucination.
  • Thêm Human-only ceiling: human transcription + human mapping + human extraction to estimate achievable ceiling and annotator variance.
  1. Chạy thử và thu output
  • Chạy mỗi transcript qua tất cả biến thể. Lưu provenance: which source spans were passed to model, mapping decisions, confidence scores.
  1. Định nghĩa metrics và luật phân bổ lỗi
  • Component-level matches: cho mỗi gold item (job/trigger/outcome) xác định:
  • Matchedinoutput? bằng kết hợp exact match, fuzzy string match và embedding similarity threshold (tune trên nhỏ sample).
  • Khi mismatch xảy ra, gán nguyên nhân bằng hàng bước kiểm tra tuần tự:
  • Nếu gold evidence không xuất hiện trong auto-transcript (so sánh verbatim hoặc alt_transcripts), gán lỗi cho transcription.
  • Nếu evidence có trong auto-transcript nhưng không có trong JSON field được gửi cho model (provenance kiểm tra bằng span mapping), gán lỗi cho preproc/mapping.
  • Nếu evidence có trong JSON fields nhưng model output vẫn sai hoặc thêm nội dung không có evidence, gán lỗi cho model/extraction (hallucination).
  • Nếu gold item không có evidence trong raw audio hoặc annotator không đạt IAA, mark as gold-issue and exclude from attribution or send to adjudication.
  • Ghi chú: dùng earliest-fail attribution. Nếu nhiều nguyên nhân cùng tồn tại, tag multi-cause and apportion fractional blame (ví dụ 50/50) hoặc send to manual adjudication.
  1. Thống kê và bootstrap CI
  • Trên N transcripts, tính số lỗi attributed to each component and compute fraction pcomponent = errorscomponent / total_errors.
  • Bootstrap procedure: resample transcripts with replacement 10,000 lần, recompute pcomponent each lần, lấy 95% CI percentile for pcomponent.
  • Report: point estimate + 95% CI cho từng component; cũng report absolute counts and per-transcript error rate.
  1. Kiểm tra so sánh giữa biến thể
  • So sánh Baseline vs VTxHuman để ước lượng ảnh hưởng transcription: giảm errorrate tương đối là attribution to transcription.
  • So sánh Baseline vs V_PreprocConservative để ước lượng ảnh hưởng preproc/mapping.
  • So sánh Baseline vs V_ModelDifferent để ước lượng model-related errors.
  • Dùng paired statistical test cho tỉ lệ (McNemar cho binary at transcript-level) hoặc bootstrap difference-in-difference để lấy CI.
  1. Thực thi adjudication cho edge cases
  • Lấy 5–10% lỗi phân bổ tự động ngẫu nhiên, cho panel review để kiểm tra attribution accuracy; đo attribution agreement. Nếu agreement thấp, refine attribution rules and re-run.
  1. Kết luận hành động và threshold thực dụng
  • Nếu preproc/mapping chiếm >30–40% lỗi với CI chắc, ưu tiên conservative preproc, provenance và human-in-the-loop mapping cho trường hợp nguy cơ cao.
  • Nếu transcription lỗi chiếm nhiều, đầu tư ASR cải thiện hoặc human-vetted transcripts cho cohort quan trọng.
  • Nếu model-extraction lỗi chiếm nhiều, chỉnh prompt, ensemble, hoặc thêm grounding/evidence-mandatory rules.

Ghi rõ provenance trong báo cáo và đính kèm sample lỗi tiêu biểu để đội kỹ thuật và nghiên cứu hành động.

Cách hiệu chỉnh ngưỡng tiền xử lý và prompt bằng experiment

Mục tiêu ngắn: thiết lập một protocol thực nghiệm để sweep và calibrate các tham số tiền xử lý và prompt (ASR confidence, min utterance length, label_conf thresholds, chunking, prompt temperature, v.v.) sao cho hệ thống extraction JTBD đạt yêu cầu precision/recall và trustworthiness trong từng cohort/ngôn ngữ; đồng thời cung cấp cơ chế dynamic thresholds và rules adaptive khi drift.

  1. Chuẩn bị dataset hiệu chỉnh
  • Tập calibration: chọn 300–1,000 transcripts representative theo chiều dài, ngôn ngữ, channel, persona. Ít nhất 200 cho bước khởi tạo; tăng tới 1,000 nếu đa dạng cao.
  • Gold standard: mỗi transcript phải có annotation bởi 2 researcher cho các trường JTBD (job, trigger, circumstance, desired_outcome) kèm evidence spans; adjudicator quyết định final labels. Lưu provenance: raw audio, verbatim transcript, timestamps.
  • Chia dữ liệu: calibration (60%), validation (20%), test holdout (20%) stratified theo cohort.
  1. Xác định tham số cần sweep
  • ASR confidence threshold t_asr (ví dụ 0.70, 0.80, 0.85, 0.90).
  • Min utterance length L_min (chars hoặc tokens): ví dụ 10, 20, 40.
  • Label acceptance threshold label_conf (0.5, 0.6, 0.7, 0.8) cho auto-labels/auto-mapping.
  • Chunk size C (tokens) và overlap O (tokens or %): ví dụ C = 500, 1000, 1500; O = 0, 10%, 25%.
  • Prompt/model params: temperature (0.0, 0.2, 0.5), maxoutputtokens, ensemble runs n.
  • Post-processing dedupe thresholds: embedding cosine t_merge (0.7, 0.8, 0.88).
  1. Thiết kế experiment grid và runs
  • Tạo grid of configurations. Nếu combinatorial quá lớn, dùng factorial design hoặc Latin hypercube sampling để cover không gian.
  • Với mỗi config chạy pipeline trên calibration set, lưu outputs cùng provenance và per-field confidences.
  • Nếu dùng stochastic prompts, chạy n >= 3 seeds per config để đo variance.
  1. Đo lường metric và attribution
  • Metrics per config:
  • Extraction precision/recall/F1 cho job/trigger/outcome so với gold.
  • Evidence coverage: percent extracted items with >=1 supporting quote.
  • Hallucination rate: percent generated items without supporting span in raw transcript (detect bằng substring + semantic similarity).
  • JSON validity and parsing error rate.
  • Operational metrics: percent records auto-accepted by rules; human-review workload.
  • Đo CI: bootstrap transcripts 1,000 samples để trả 95% CI cho mỗi metric.
  1. Quy trình chọn ngưỡng theo mục tiêu sản phẩm
  • Xác định objective function: ví dụ maximize precision subject to human-review capacity constraint H (reviews/day). Có thể dùng cost function: Cost = cFP * FPcount + creview * reviewcount.
  • Từ kết quả grid search, chọn config tối ưu theo objective. Nếu ưu tiên precision cho auto-publish, pick config có precision >= target (ví dụ 0.85) với recall chấp nhận được.
  • Lưu config per-cohort/ngôn ngữ nếu performance khác biệt.
  1. Cross-validation và holdout test
  • Sau chọn config trên calibration, validate trên validation set; cuối cùng báo performance trên test holdout để ước lượng generalization.
  • Nếu per-cohort performance lệch, maintain thresholds khác nhau cho từng cohort.
  1. Dynamic thresholds per-language / per-cohort
  • For each cohort maintain rolling windows metrics (weekly):
  • mean ASRconfidence, extractionprecision, hallucinationrate, supportcount distribution.
  • Adaptive rules:
  • If mean ASRconfidence for cohort < asrthresholdbycohort -> raise human-review fraction or raise t_asr.
  • If hallucinationrate rises > delta (e.g., +50% over baseline) -> automatically tighten labelconf and lower temperature, and queue recent items for review.
  • If evidencesupportcount median < min_support -> disable auto-accept for that cohort.
  • Store thresholds in config with versioning and meta: {cohortid, thresholds, effectivefrom, reason}.
  1. Continuous monitoring and recalibration
  • Monitor daily/weekly dashboards: precision, recall (sampling), hallucination_rate, auto-accept reversal rate (human undo).
  • If auto-accept reversal rate > R_threshold (e.g., 5%) over rolling N items, trigger automatic recalibration: re-run the grid locally on recent 500 items and propose new thresholds.
  • Use active learning: sample borderline items (score near threshold) for human labeling to improve training data and recalibration.
  1. Practical heuristics and initial starting points
  • ASR: auto-process if asrconfidenceavg >= 0.90; hybrid-review zone 0.80–0.90; require human transcript < 0.80.
  • Lmin: narrative answers min 20 chars; for shorter micro-feedback allow 8–12 chars but require contextwindow.
  • label_conf: accept auto-labels >= 0.75 for low-risk categories; require review 0.6–0.75.
  • Chunking: prefer semantic segmentation, but common defaults: C = 1000 tokens, O = 20% for long transcripts.
  • Prompt: extraction tasks run deterministic temp=0.0; synthesis tasks temp=0.3–0.5 with ensemble n=5 and accept items appearing in >=2 runs.
  1. Reporting and governance
  • For each recalibration produce audit report: dataset used, grid tried, selected thresholds, expected impact on review load, test holdout metrics and CI.
  • Keep human-readable changelog and require product-research sign-off for threshold changes that affect auto-publishing or instrumentation.
  1. Notes on sample sizes and power
  • To estimate precision within ±0.05 with 95% CI for events around 0.8, need ~250–400 labeled items. Tailor labeling budget accordingly.
  • For cohort-specific thresholds, ensure each cohort has at least 200 annotated items over time for stable estimates.

Kết luận ngắn

  • Hiệu chỉnh ngưỡng là một vòng lặp experiment → select → validate → monitor. Thiết kế calibration dataset đa dạng, sweep có kiểm soát, chọn ngưỡng theo objective sản phẩm và human-review capacity, rồi duy trì dynamic thresholds per-cohort với giám sát drift và active learning. Mỗi thay đổi phải được versioned và auditable để tránh bake-in lỗi hệ thống.

Pha trộn AI và con người: quy trình review & feedback loop

Mục tiêu: thiết kế luồng làm việc rõ ràng để AI thực hiện phần lớn công việc lặp, trong khi con người giữ quyền đánh giá các trường hợp rủi ro, mới lạ, hoặc có tác động cao. Mô hình trust tiers L0-L3 và quy tắc triage dưới đây cho phép tự động hoá có kiểm soát và vòng feedback phục hồi chất lượng qua active learning.

  1. Trust tiers (L0-L3)
  • L0 auto-accept: output có confidence cao, evidence >= 3 respondents, trùng với canonical job, không novel, impact low. Hệ thống tự tạo draft ticket low-risk.
  • L1 draft-only: confidence vừa phải hoặc evidence 2 respondents hoặc variant close to canonical. Tạo artifact draft, cần review nhanh 1 người trong vòng SLA ngắn.
  • L2 require-human: novel OR impact high OR any contradiction OR supportcount ==1 OR modelconfidence < threshold. Human reviewer bắt buộc phê duyệt trước khi publish.
  • L3 block: nội dung nhạy cảm, regulatory, hoặc phát hiện hallucination nặng. Block pipeline, gửi cho senior reviewer.
  1. Quy tắc triage tự động
  • Nếu novel == true AND impact_level >= high => route L2 mandatory human.
  • Nếu solutionphrased == true => tag SOLUTIONMENTION, route L1 với yêu cầu rewrite thành job.
  • Nếu provenancesupport < minsupport => route L2.
  • Nếu any contradiction detected => route L3.
  1. Sampling audit rates và cơ chế kiểm soát
  • Baseline: sample random 5% của items auto-accepted (L0) mỗi tuần để audit thủ công.
  • Adaptive sampling: tăng audit rate theo công thức auditrate = baserate * (1 + driftindex), trong đó driftindex dựa trên recent reversalrate và hallucinationrate.
  • Target thresholds: nếu reversalrate > 5% trong 1 tuần, gấp đôi auditrate; nếu >10% thì tạm dừng auto-accept và chuyển các items tương tự sang L2.
  1. Active learning cho retraining
  • Ghi lại mọi item bị chỉnh sửa hoặc bị bác bởi reviewer. Tập hợp thành dataset huấn luyện có nhãn.
  • Chọn các mẫu informative: disagreements, borderline-confidence, novel clusters, và gửi vào retraining pipeline định kỳ.
  • Triển khai retrain cadence: nhỏ mỗi 2 tuần cho prompt/heuristics, lớn mỗi tháng cho fine-tune model nếu có.
  1. SLA và vai trò reviewer
  • SLA cho review theo tier:
  • L1: review <= 48 giờ, turnaround trung bình target 24 giờ.
  • L2: review <= 24 giờ, việc escalate tới senior reviewer trong 4 giờ nếu cần.
  • L3: immediate within 4 giờ.
  • Reviewer workload dashboard: hiển thị queue size, avg time per review, backlog age, reversalrate, %novelinqueue. Mục KPI: avg time <= SLA, reversalrate <= 5%, reviewer_utilization 60-80%.
  1. Metrics dashboard và báo động
  • Theo dõi: autoacceptrate, auditpassrate, reversalrate, hallucinationrate, avgsupportcount, novelrate, reviewerthroughput.
  • Alert rules: reversalrate > 5% -> notify ops; novelrate spike > baseline + 3 sigma -> pause auto-publish for that cohort.
  1. Audit trail và provenance
  • Mỗi quyết định kèm provenance: sourcerefs, evidencespans, modelconfidence, tier, reviewerid, change_log. Dữ liệu này dùng để audit và cho active learning.

Kết luận tóm tắt: hệ thống vận hành theo L0-L3, audit sampling adaptively bảo đảm an toàn, active learning đóng vòng feedback để nâng chất lượng model, và SLA + dashboard giữ reviewer workload trong tầm kiểm soát.

Cách build meta-classifier chống hallucination (SBERT+NLI+meta)

Mục tiêu ngắn: thiết kế detector tự động đánh dấu output AI là grounded hay hallucinated bằng cách kết hợp kiểm tra trực tiếp vào nguồn, so sánh ngữ nghĩa, kiểm tra entailment, và dấu hiệu xác suất từ mô hình. Sau đó huấn luyện meta-classifier (logistic hoặc XGBoost) trên validation set, hiệu chỉnh threshold, và phân tầng cảnh báo người review theo severity.

  1. Inputs và features chính (per candidate item)
  • substring_match (binary): có quote nguyên văn trong source không.
  • sbert_sim (float): max cosine giữa embedding candidate và mọi sentence trong source (dùng SBERT hoặc text-embedding-3).
  • nli_entailment (float): entailment score từ NLI model giữa source concatenated và candidate statement (ví dụ P(entailment)).
  • token_logprob (float): trung bình logprob token của candidate do LLM trả về, nếu có.
  • evidence_count (int): số đoạn trích độc lập hỗ trợ candidate (distinct respondent/session).
  • src_confidence (float): ASR/transcript confidence trung bình của các evidence spans.
  • novelty_score (float): cosine similarity tới nearest canonical job trong corpus; thấp nghĩa mới lạ.
  • length_ratio (float): tỉ lệ token candidate so với evidence quote length.
  1. Nhãn và dữ liệu huấn luyện
  • Tạo validation set annotated: N ≈ 1,000 candidate items, mỗi item do 2 reviewer domain label là TRUE_GROUNDED / HALLUCINATED; adjudicate tranh chấp.
  • Ghi kèm severity labels: minor (thêm chi tiết không đúng), major (thay đổi ý nghĩa), critical (dẫn đến quyết định sản phẩm sai).
  1. Huấn luyện meta-classifier
  • Model: bắt đầu với logistic regression để dễ giải thích; nếu feature interactions quan trọng thì xài XGBoost.
  • Loss: binary cross-entropy; thêm class-weighting nếu dữ liệu imbalance.
  • Cross-validation: k-fold (k=5) và bootstrap để ước lượng CI cho metric.
  • Metrics: AUC, precision@k, recall@k, F1; chú ý precision ở operating point.
  1. Calibration threshold và stratification theo severity
  • Chọn operating threshold theo mục tiêu sản phẩm. Ví dụ:
  • Auto-pass (low risk): P(hallucination) < 0.05
  • Flag for quick review (medium): 0.05 <= P < 0.25
  • Block + senior review (high): P >= 0.25
  • Calibrate bằng PR curve: fix precision target cho auto-pass (ví dụ precision >= 0.95) và chọn threshold với recall tối đa. Dùng bootstrap để lấy 95% CI.
  1. Triaging và workflow vận hành
  • Nếu classifier output low-risk: allow downstream processing but attach provenance and confidence.
  • Medium: enqueue lightweight human check (1-2 phút) — cung cấp evidence spans và model signals.
  • High: block publishing; assign to senior reviewer with full transcript and model rationale.
  • Ghi log reversal: mọi case bị reviewer chỉnh sửa được trả vào training set cho lần retrain tiếp theo.
  1. Giám sát và tái huấn luyện
  • Monitor drift metrics: distribution của sbertsim, tokenlogprob, noveltyrate, falsepositive_rate trên audit sample.
  • Retrain định kỳ hoặc khi audit false positive vượt ngưỡng.

Ghi chú triển khai ngắn

  • Dùng SBERT cho semantic sim, một NLI model được fine-tuned cho entailment, và lấy token_logprob từ LLM nếu API hỗ trợ. Nếu không có logprob, dùng ensemble-agreement proxy.
  • Bắt đầu với logistic để hiểu feature importance; chuyển sang XGBoost khi cần performance. Luôn kèm provenance và UI cho reviewer để giảm tải.

Kiểm soát solution-bias: prompt + post-filters

Mục tiêu ngắn: giảm việc LLM trả về “giải pháp” thay vì “job” bằng hai lớp: (1) prompt rõ ràng và few-shot examples chuyển solution -> underlying job; (2) post-filters tự động phát hiện từ/biểu thức giải pháp và ép LLM rephrase thành goal. Dưới đây là tập mẫu prompt, vài ví dụ few-shot, blacklist, patterns, và luật chuyển hoá.

  1. Hướng dẫn prompt (core)
  • System role: “You are a JTBD extractor. Always extract the user goal or desired outcome, not concrete implementations. If text names a solution, label it as SOLUTIONMENTION and convert to the underlying goal. Provide evidence span(s). If evidence is missing, return INSUFFICIENTEVIDENCE.”
  • Temperature: 0.0 for extraction; max_tokens đủ cho JSON.
  • Output: valid JSON with fields {type: “JOB”|”SOLUTIONMENTION”|”INSUFFICIENTEVIDENCE”, text: “…”, underlying_goal: “…”, evidence: […]}
  1. Few-shot examples (copy-paste into prompt)

Example 1:

  • Input utterance: “Tôi muốn một nút one-click để đăng ký nhanh.”
  • Desired output JSON:

{ “type”: “SOLUTIONMENTION”, “text”: “nút one-click để đăng ký”, “underlyinggoal”: “giảm thời gian hoàn tất việc đăng ký khi đang vội”, “evidence”: [“Tôi muốn một nút one-click để đăng ký nhanh.”] }

Example 2:

  • Input utterance: “Chúng tôi dùng Zapier để sync form vào spreadsheet.”
  • Desired output JSON:

{ “type”: “SOLUTIONMENTION”, “text”: “dùng Zapier để sync form”, “underlyinggoal”: “tự động lưu trữ dữ liệu form để dễ phân tích và truy xuất”, “evidence”: [“Chúng tôi dùng Zapier để sync form vào spreadsheet.”] }

Example 3:

  • Input utterance: “Muốn xem báo cáo mỗi sáng trên email.”
  • Desired output JSON:

{ “type”: “JOB”, “text”: “xem báo cáo mỗi sáng”, “underlying_goal”: “nắm được tình trạng quan trọng vào đầu ngày để hành động kịp thời”, “evidence”: [“Muốn xem báo cáo mỗi sáng trên email.”] }

  1. Blacklist giải pháp và detection rules
  • Blacklist token sample: [“button”,”one-click”,”Zapier”,”plugin”,”API”,”integration”,”script”,”automate”,”cron”,”dashboard widget”,”shortcut”,”plugin”,”connector”,”CSV export”,”webhook”]
  • Pattern heuristics:
  • regex solution_verb: \b(use|install|connect|integrate|click|press|automate|sync|export|import|setup)\b
  • regex tool_names: \b(Zapier|IFTTT|Slack|Stripe|Google Drive|Sheets|API|CRM)\b
  • UI elements: \b(button|modal|dropdown|checkbox|form|widget|banner)\b
  • If any blacklist token or pattern matched within candidate jobtext, mark as SOLUTIONMENTION.
  1. Transformation rules (deterministic)
  • If SOLUTION_MENTION:
  • Step A: extract nouns/phrases that indicate action or tool as solution_text.
  • Step B: ask LLM to answer prompt: “Given this solution: ‘<solutiontext>’ and the surrounding utterance, rewrite as underlying user goal starting with an action verb and including circumstance.” Provide one-line underlyinggoal.
  • Require evidence quote(s) in output.
  • If no explicit goal can be inferred from context, set type = INSUFFICIENT_EVIDENCE and include reason.
  1. Post-filter workflow
  • Run LLM extraction with few-shot prompt.
  • Apply blacklist/pattern detector to LLM jobtext. If detector flags SOLUTIONMENTION but LLM returned type = JOB, override to SOLUTIONMENTION and call rephrase prompt to generate underlyinggoal.
  • Validate resulting underlying_goal: ensure it does not contain tool/UI tokens. If it still contains solution tokens, escalate to human-review queue.
  1. Short checklist cho bạn trước khi deploy
  • Include few-shot examples that cover common local patterns and domain tools.
  • Maintain and evolve blacklist from real data; log overrides to update patterns.
  • Always store provenance: source utterance, start/end, and model responses.
  • Auto-escalate ambiguous cases to human reviewer; do not let pipeline auto-accept SOLUTION_MENTION -> JOB without evidence.

Ứng dụng ngay: dán prompt + three examples vào system message; chạy extractor; bật post-filter blacklist; rephrase SOLUTIONMENTION bằng prompt mẫu và lưu underlyinggoal vào JSON.

Tổng hợp và phân cụm jobs: embedding + clustering + ngưỡng merge/split

Mục tiêu: từ tập các đoạn trích job/outcome/trigger đã được extractor tạo ra, sinh clusters hợp lý để đưa vào taxonomy JTBD, với ngưỡng tự động hợp nhất an toàn và quy tắc tách clusters khi quá đa dạng.

  1. Chuẩn bị embedding
  • Input cho embedding: canonical jobtext (concise job phrasing), aggregated desiredoutcomes (concatenate), trigger_text. Giữ provenance: list evidence quotes.
  • Model embedding khởi điểm: model sentence-level mạnh (ví dụ OpenAI text-embedding-3-large hoặc SBERT đa ngôn ngữ). Chuẩn hóa: lowercasing optional, giữ dấu câu, no aggressive normalization that removes domain tokens.
  • Tạo 3 vectors cho mỗi instance: embjob, emboutcome, emb_trigger. Lưu cùng ID instance.
  1. ANN index và nearest neighbors
  • Chỉ mục ANN: dùng FAISS hoặc similar; index IVF-PQ cho scale lớn. Kiểm tra recall với recall@k trên tập validation.
  • Thực thi tìm k-nearest neighbors k=50 để xây đồ thị similarity nhanh.
  1. Hai bước clustering (scalable then hierarchical)
  • Step A – coarse clustering: chạy community detection or HDBSCAN on ANN graph to get initial candidate clusters. HDBSCAN helps label noise and outliers.
  • Step B – merge clusters bằng hierarchical agglomerative clustering (HAC) dùng linkage = average and distance = 1 – S_text (định nghĩa bên dưới). HAC cho phép cắt cây theo ngưỡng có thể giải thích.
  • Lý do: two-stage giúp scale và tránh merge toàn cục sai.
  1. Scoring function cho pairwise similarity
  • Định nghĩa score S(A,B) = w1 cos(emb_jobA, emb_jobB) + w2 cos(emboutcomeA, emboutcomeB) + w3 cos(emb_triggerA, emb_triggerB) + w4 personamatchscore.
  • personamatchscore: 1 nếu persona tag identical, 0.5 nếu compatible, 0 if incompatible. Persona compatibility rulebook cần chuẩn trước.
  • Gợi ý trọng số khởi điểm: w1=0.40, w2=0.25, w3=0.20, w4=0.15. Những giá trị này phải calibrate.
  1. Ngưỡng merge / review / no-merge
  • Dùng S_composite cho HAC cut:
  • auto-merge nếu S >= 0.88 và clustersupport >= minsupport (min_support: 5 instances cho corpus trung bình; adjust theo scale).
  • review-band nếu 0.75 <= S < 0.88: ghi flag review_required = true, xuất diff highlights giữa items để reviewer xem nhanh.
  • no-merge nếu S < 0.75: keep separate.
  • Lưu ý: ngưỡng là điểm khởi, phải calibrate theo dữ liệu bằng protocol dưới.
  1. Quy tắc split cluster
  • Tính intra-cluster pairwise distances: Dmean, Dmax = mean và max(1 – cos) trong cluster.
  • Tính outcome entropy Houtcome: phân phối các desiredoutcome canonical IDs trong cluster, entropy cao biểu thị cluster đa modal.
  • Split rules:
  • nếu Dmax > 0.35 OR Dmean > 0.18, mark cluster cho splitting candidate.
  • nếu Houtcome > threshold (ví dụ normalized entropy > 0.7) và có >=2 modes each with support >= minsupport, thì split theo outcome-modes.
  • nếu cluster chứa >1 high-confidence persona groups with incompatible constraints, split by persona.
  1. Calibration protocol bằng annotated pair set
  • Tạo annotated dataset: sample N_pairs = 1,000-2,000 pairwise comparisons, humans label same-job? yes/no and reason (variant vs different).
  • Dùng pairs để:
  • calibrate weights w1..w4 via logistic regression mapping features to label;
  • sweep thresholds for auto-merge/review/no-merge by precision/recall trade-off; pick auto-merge threshold achieving precision_target (ví dụ 0.90) on holdout.
  • evaluate split thresholds by constructing synthetic heterogeneous clusters and measuring split recall.
  • Bootstrap CI: compute 95% CI for precision at chosen threshold.
  1. Post-processing and human-in-loop
  • For each auto-merged cluster, generate:
  • medoid instance, canonical phrasing candidate (LLM summarize with temp=0.0), support_count, persona distribution, top 3 evidence quotes.
  • For review-band clusters, UI shows side-by-side examples, S score, and suggested merge reason; reviewer can accept/override merge or split.
  • Logging: persist similarity matrices and provenance to allow undo and audits.
  1. Metrics to monitor in production
  • automergeprecision, reviewacceptrate, post-merge splitrate (reversions), clusterpurity (against gold), and novelty_rate (fraction new canonical jobs).
  • Retrain calibration monthly or on model/version change.

Tóm tắt: pipeline thực thi embedding đa-vector, ANN neighbors, two-stage clustering, composite scoring S với weights có thể học từ annotated pairs; ngưỡng khởi điểm auto-merge >=0.88, review 0.75-0.88, no-merge <0.75; split khi intra-cluster variance hoặc outcome entropy cao; toàn bộ kịch bản phải được calibrate bằng bộ pair annotated và kèm human-in-loop cho review.

Mục tiêu ngắn: chuẩn hoá các nguồn tần suất rồi tính điểm đau (pain) tổng hợp từ ba thành phần chính, sau đó kết hợp thành priority_score để ưu tiên jobs trong backlog.

  1. Chuẩn hoá frequency từ mixed sources
  • Nguồn và cách chuẩn:
  • Survey (ordinal): map trực tiếp sang tỷ lệ/month. Ví dụ: “daily”=20, “weekly”=4, “monthly”=1, “rarely”=0.25. Sau đó chuẩn hoá sang 0..1 bằng f_norm = min(1, value/20).
  • Self-reported (free text): rule-based hoặc classifier extract tokens time-unit; chuyển sang giá trị tương tự và chuẩn hoá như trên.
  • Inferred from logs: dùng event rate per user per 30 days r. Map f_norm = min(1, r / 20) để tương thích với survey scale.
  • Aggregation across nguồn:
  • Tính weighted average: frequency = (wsurvey * fsurvey + wself * fself + wlogs * flogs) / (wsurvey + wself + w_logs).
  • Khuyến nghị trọng số mặc định: wsurvey=1, wself=0.8, wlogs=1.2. Nếu logs là nguồn tin cậy hơn, tăng wlogs.
  • Thresholds đơn giản để gắn nhãn tần suất:
  • low: frequency < 0.15
  • medium: 0.15 <= frequency < 0.4
  • high: frequency >= 0.4
  1. Thành phần pain score (composite)
  • Cấu trúc: painscore = α * frustrationsignal + β effort_cost + γ failure_rate
  • Thành phần đo:
  • frustration_signal (0..1): từ sentiment/specific keywords hoặc survey Likert về mức khó chịu. Map Likert 1..5 -> (0..1): (likert-1)/4.
  • effort_cost (0..1): estimated time/steps to complete task normalized. Ví dụ time in seconds t -> effort = min(1, t/600) nếu >10 phút là 1.
  • failure_rate (0..1): tỷ lệ thất bại hoặc lỗi từ logs; nếu không có logs, estimate từ self-report.
  • Trọng số khuyến nghị: α=0.45, β=0.35, γ=0.20. Lý do: frustration thường phản ánh pain chủ quan mạnh nhất.
  • Ví dụ: frustration 0.8, effort 0.6, failure 0.3 => pain = 0.450.8 + 0.350.6 + 0.2*0.3 = 0.36+0.21+0.06 = 0.63
  1. Từ frequency + pain -> priority_score
  • Công thức gợi ý: priorityscore = normalize( wF * frequency + wP * painscore )
  • khởi điểm: wF = 0.4, wP = 0.6 để ưu tiên pain hơn tần suất
  • normalize về 0..1: divide by (wF + wP) vì both in 0..1 so result already in 0..1
  • Tiering (ngưỡng thực dụng):
  • critical: priority_score >= 0.75
  • high: 0.55 <= priority_score < 0.75
  • medium: 0.35 <= priority_score < 0.55
  • low: < 0.35
  • Kiểm soát provenance và confidence:
  • Kèm mỗi giá trị frequency, pain và priority một trường confidence (0..1) tính từ nguồn (logs higher confidence) và agreement giữa nguồn. Nếu confidence < 0.6, gắn tag “review_required” trước khi auto-prioritize.
  • Ghi chú vận hành:
  • Calibrate trọng số và thresholds trên gold set 200-500 items, dùng bootstrap CI.
  • Nếu muốn ưu tầm ảnh hưởng kinh doanh hơn, tăng wP hoặc nếu muốn ưu tần suất, tăng wF.

Mục tiêu phần này: cung cấp template tiêu chuẩn để chuyển một job/insight JTBD thành một epic trong backlog, kèm cách tính success metrics, recommended experiments, assumptions, effort estimate, confidence, và policy rõ ràng để hệ thống có thể auto-create hoặc require review.

Template epic (trường bắt buộc)

  • epic_title (string): tiêu đề ngắn gọn, noun-phrase, tránh nêu giải pháp.
  • hypothesis_statement (string): câu “Nếu chúng ta… thì người dùng sẽ…” theo format hypothesis (cause → effect → metric).
  • successmetrics (array): mỗi mục gồm {metricname, metrictype (time/percent/count), leadingor_lagging (leading|lagging), baseline, target, rationale}.
  • recommendedexperiments (array ordered): mỗi experiment gồm {phase (discovery|MVP|scale), name, briefgoal, method (qual/quant/A-B/concierge), successcriteria (metric + threshold), estimatedduration}.
  • assumptions (array of strings): giả thuyết cần kiểm chứng trước khi commit dev work.
  • estimatedeffort (object): {discoverydays, mvpstorypoints, scalestorypoints, owner_squad}.
  • confidence (float 0..1): tổng hợp provenance + modelconfidence + humanvotes.
  • provenance (array): list of evidence entries {sourcetype (interview/survey/log), sourceid, excerpt, startts, endts, respondent_id}.
  • tags (array): personas, channel, platform, priority_tags.
  • riskflags (array): privacy | legal | regulatory | technicalunknown.
  • createdby (string) và createdat (ISO8601).
  • reviewrequired (boolean) và reviewreasons (array) nếu true.
  • notesforreviewer (string): hướng dẫn review ngắn.

Ví dụ ngắn (nội dung minh họa)

  • epic_title: “Hoàn tất đặt hàng nhanh khi di chuyển”
  • hypothesisstatement: “Nếu giảm timetocompletecheckout xuống <=45s cho người dùng mobile onthego thì conversionrate sẽ tăng 10% trong cohort mobilehigh_frequency.”
  • successmetrics: [{metricname: “timetocompletecheckout”, type:”time”, leadingor_lagging:”leading”, baseline:120, target:45}]
  • recommendedexperiments: [{phase:”discovery”, name:”shadow checkout interview”, method:”concierge”, successcriteria:”identify 3 viable flows”} …]
  • assumptions: [“Người dùng có ít nhất một phương thức thanh toán lưu sẵn”]
  • estimatedeffort: {discoverydays:10, mvpstorypoints:20, scalestorypoints:60, owner_squad:”checkout”}
  • confidence: 0.62
  • provenance: [{sourcetype:”interview”, sourceid:”int-20260601-12″, excerpt:”Tôi đang đi xe bus và muốn mua gấp, không muốn nhập nhiều thông tin.”}]
  • review_required: true
  • review_reasons: [“confidence < 0.7”, “metric not found in analytics registry”]

Policy: khi auto-create epic vs require review

  • Auto-create allowed only khi ALL điều kiện sau đúng:
  1. confidence >= 0.85
  2. provenance chứa >= 3 distinct respondent_ids OR >=2 sources with one quantitative support
  3. successmetrics.map(metricname) exists in analytics_registry AND baseline is populated
  4. risk_flags is empty
  5. description does not contain obvious solution phrasing (detected by solution-word filter)
  • Nếu bất kỳ điều kiện trên không thỏa, hệ thống sẽ:
  • nếu confidence in [0.7,0.85) và provenance đủ (>=2 distinct respondents) thì auto-create draft epic with reviewrequired = true and assign to productresearcher.
  • nếu confidence <0.7 OR provenance <2 distinct respondents OR any risk_flags present thì block auto-create; create candidate ticket in research queue for human triage.

Quy trình review ngắn

  • Reviewer check 3 yếu tố: (1) evidence traceability (provenance excerpts), (2) metric validity (baseline + instrumentation), (3) assumption testability. Reviewer có thể chỉnh hypothesis_statement, adjust target, hoặc move to discovery task.

Gợi ý vận hành

  • Gắn mỗi epic auto-created một auditlog entry kèm promptversion và model_version.
  • Hàng tuần sample 5% auto-created epics để audit. Nếu false-positive rate >5% trong audit, giảm threshold auto-create confidence 0.05 và tăng review sampling.

Mẹo thực tế

  • Luôn để field reviewrequired mặc định true cho jobs novel (nearestneighbor_similarity to corpus < 0.80).
  • Khi auto-generate recommended_experiments, ưu tiên discovery experiments trước khi commit dev scope.

Tạo template báo cáo JTBD (cho stakeholders)

Mở đầu ngắn: mục tiêu báo cáo này là cung cấp một template sẵn để bạn dán vào tài liệu cho stakeholders. Mỗi trường có định nghĩa ngắn, ví dụ thực tế và snippet text-ready để paste thẳng vào doc.

  1. executive_summary (30–60 từ)
  • Mục: tóm tắt 2–3 câu: vấn đề chính, cơ hội lớn, đề xuất tiếp theo.
  • Snippet paste-ready:
  • “Executive summary: Người dùng gặp khó khăn khi [ngữ cảnh]. Chúng ta thấy cơ hội cải thiện [desired outcome] cho [persona] với ước tính tác động là [metric uplift]. Khuyến nghị: thực hiện 2 thí nghiệm discovery trước khi phát triển.”
  1. topjobsranked (with evidence and scores)
  • Mục: danh sách top 3–7 jobs, mỗi job gồm title, supportcount, painscore, top evidence quotes, confidence_score.
  • Snippet format (paste-ready for each job):
  • “1) Job: [job title]

Support: [N] respondents | Pain: [1–5] Confidence: [0.0–1.0] Evidence: “[verbatim quote]” — source: [interview_id/timestamp]”

  1. opportunity_hypotheses
  • Mục: từ mỗi top job sinh 1–2 hypotheses hành động, gồm assumption and expected outcome.
  • Snippet example:
  • “Hypothesis: Nếu chúng ta [solution-neutral intervention], then [measurable outcome] for [persona]. Key assumption: [assumption].”
  1. recommended_experiments
  • Mục: đề xuất prioritized experiments (discovery → prototype → A/B), với success_criteria và duration estimate.
  • Snippet example:
  • “- Experiment: Concierge test for quick-checkout

Goal: validate reduction in timetocheckout Success: reduce median time from Xs to Ys in pilot Duration: 2 weeks”

  1. roadmapmapping (job -> epic -> successmetric)
  • Mục: mapping rõ ràng từ job tới epic đề xuất và metric đo thành công, kèm baseline + target.
  • Snippet example:
  • “Job: Reduce time to complete checkout

Epic: One-tap mobile checkout MVP Success metric: timetocomplete_checkout (baseline: 120s → target: 45s)”

  1. confidence & caveats
  • Mục: tổng hợp confidence levels, nguồn rủi ro và caveats (bias, sample limitations).
  • Snippet example:
  • “Confidence: 0.62 — Evidence: 5 interviews, 2 survey responses. Caveats: sample skewed to power users; ASR confidence low in 2 sessions.”
  1. appendix with provenance and raw quotes
  • Mục: liệt kê tất cả source_refs, verbatim quotes, raw JSON paths để audit.
  • Snippet format:
  • “Appendix: sources
  • int2026060112 | 00:03:21–00:03:42 | ‘Tôi đang đi xe bus…’
  • survey20260605Q1 | respondent_045 | ‘Mất nhiều thời gian khi…'”

Gợi ý trình bày cho stakeholder doc

  • Đặt executive_summary lên trang đầu
  • Bôi đậm job titles và success metrics
  • Đính kèm appendix cuối để reviewer mở evidence
  • Nếu confidence < 0.7, thêm banner “Requires review” ở đầu báo cáo

Checklist nhanh trước khi gửi

  • Mỗi job có ≥1 verbatim quote
  • Mỗi suggested epic có baseline + target hoặc note “instrumentation required”
  • Mỗi low-confidence item đánh flag review_required

Kết thúc: dùng các snippet ở trên để paste trực tiếp vào stakeholder doc; chỉnh thông tin trong [] cho khớp dữ liệu của bạn.

Cách gắn insights JTBD vào backlog

Mục tiêu: biến mỗi job được xác thực thành một epic có acceptance criteria đo được, sẵn sàng cho discovery hoặc development. Mình trình bày quy tắc mapping, cách đặt tên, cách viết acceptance criteria từ desiredoutcomes, kế hoạch instrumentation, cách xếp ưu tiên bằng priorityscore, và ví dụ phá vỡ thành sprint-level tasks.

  1. Quy tắc chuyển job → epic
  • Điều kiện khởi tạo epic: job có supportcount >= threshold và confidence >= thresholdconfidence, hoặc được human-approved.
  • Nếu job là variant của canonical job đã có, ghi vào existing epic như sub-ticket; không tạo epic mới tự động.
  1. Naming convention epic
  • Mẫu: [JOB] — [Persona] — [Context]
  • Ví dụ: “Hoàn tất đơn hàng nhanh — Mobile Buyer — On the go”
  • Giữ ngắn, dùng goal-oriented phrasing, tránh nêu giải pháp trong tiêu đề.
  1. Acceptance criteria từ desired_outcomes
  • Mỗi desiredoutcome phải map thành một success metric cụ thể: metricname, baseline, target, direction, time_window.
  • Acceptance criteria phải là câu kiểm tra được, ví dụ:
  • “Giảm median timetocomplete_checkout từ 120s xuống <=45s trong 28 ngày sau release.”
  • “Tăng tỷ lệ checkout_success trong mobile session từ 72% lên >=78% trong 30 ngày.”
  • Nếu desired_outcome chưa có metric, add instrumentation task trước khi epic tiến vào development.
  1. Kế hoạch instrumentation
  • Step A: xác định metric_exist? query analytics registry.
  • Nếu metric tồn tại, link query và baseline_value.
  • Nếu không, tạo task “Create metric: <metric_name>” với acceptance: metric visible in analytics and dashboarded.
  • Luôn attach analytics owner và query snippet vào epic.
  • Nếu metric dựa trên proxy (ví dụ cảm xúc), ghi rõ proxy metric và plan thu thập (surveys, short in-app prompt).
  1. Triage priority bằng priority_score
  • Tính priorityscore = w1*frequencynorm + w2pain_score + w3impactestimate + w4*strategicfit.
  • Gợi ý trọng số: w1 0.35, w2 0.30, w3 0.25, w4 0.10. Calibrate theo tổ chức.
  • Tạo các bucket: P0 (blocker/high), P1 (high), P2 (medium), P3 (low).
  • Trong backlog grooming, sắp theo priorityscore; kèm confidence và supportcount để quyết review.
  1. Sprint-level breakdown example (epic: One-tap mobile checkout)
  • Sprint 0 (Discovery, 2 wks)
  • Task 1: Run 5 concierge tests to validate friction points.
  • Task 2: Instrument timetocomplete_checkout event and dashboard baseline.
  • Deliverable: hypothesis statement, sample size calc.
  • Sprint 1 (MVP, 2 wks)
  • Task 3: Implement tokenized payment flow behind feature flag.
  • Task 4: Launch 5% canary, monitor metrics.
  • Deliverable: telemetry showing change vs baseline.
  • Sprint 2 (Iterate, 2 wks)
  • Task 5: A/B test UI microcopy and prefill logic.
  • Task 6: Evaluate results, decide scale or kill.
  • Deliverable: decision and next steps.

Kiểm tra trước khi chuyển epic vào development

  • Có evidence quotes kèm source_refs.
  • Metric linked and baseline provided OR instrumentation task exists.
  • Priorityscore và reviewowner ghi rõ.
  • Review_required flag cleared nếu confidence cao.

Kết luận ngắn: luôn bắt đầu bằng job rõ ràng, derive acceptance criteria từ desired_outcomes, đảm bảo metric được instrumented, rồi chia nhỏ thành discovery → MVP → iterate. Mỗi epic cần provenance và confidence để tránh xây tính năng dựa trên giả thiết yếu.

Mình sẽ trình bày thiết kế nghiệm thu cho micro-experiments và 2-arm A/B test để kiểm chứng hypothesis JTBD do AI sinh ra so với hypothesis do con người sinh ra. Nội dung ngắn, thực dụng, đủ để bạn triển khai ngay.

  1. Định nghĩa KPI (và cách chọn)
  • Chọn 1 primary KPI liên quan trực tiếp tới job. Ví dụ: completionrate, conversionrate, timetocomplete, retention_7d.
  • Chỉ chọn metric có thể đo được và có baseline rõ ràng trong analytics.
  • Liệt kê 1–3 secondary KPIs (safety/UX/behaviour) để phát hiện tác dụng phụ.
  1. Xác định MDE (minimum detectable effect) và sample size
  • MDE là thay đổi tối thiểu bạn muốn phát hiện với ý nghĩa hành động. Chọn MDE dựa trên chi phí triển khai và kỳ vọng kinh doanh.
  • Với binary KPI (conversion), dùng công thức xấp xỉ:
  • zalpha = z(1 – alpha/2), zbeta = z(power)
  • nperarm ≈ [ (zalpha*sqrt(2*p*(1-p)) + zbetasqrt(p1(1-p1)+p2*(1-p2)))^2 ] / (p2 – p1)^2
  • đơn giản hơn với p1 = baseline p, p2 = p*(1+relative_MDE)
  • Với continuous KPI (time), dùng công thức t-test: n = 2(z_alpha+z_beta)^2 sigma^2 / delta^2, với sigma là độ lệch chuẩn ước tính, delta MDE tuyệt đối.
  • Thực hành: tính bằng công cụ sample-size calculator; nếu traffic thấp, tăng MDE chấp nhận được hoặc dùng proxy metric.
  1. Testing window và power considerations
  • Chạy test đủ lâu để thu số mẫu tính theo sample size, và vượt qua chu kỳ sản phẩm/seasonality. Thường tối thiểu 2–4 tuần để ổn định behavior.
  • Đảm bảo mỗi user chỉ xuất hiện trong một điều kiện (independent units) hoặc dùng cluster-randomization nếu random user không khả thi.
  1. Randomization plan
  • Đơn vị ngẫu nhiên hóa: user_id (ưu tiên), session, hoặc account. Ghi rõ seed, hashing method, và lưu cohort assignment để reproducibility.
  • Stratify theo các biến quan trọng (region, device, newvsexisting) nếu sample imbalance có thể ảnh hưởng KPI.
  • Kiểm tra balance pre-launch: các covariates chính phải phân bố tương đồng.
  1. Guardrails và stop rules
  • Predefine safety metrics and abort thresholds (ví dụ tăng error_rate, giảm retention hơn X%).
  • Không peek-and-stop: pre-register stopping rules. Nếu dùng sequential testing, áp dụng correction (alpha spending) hoặc dùng Bayesian monitoring.
  1. Example: 2-arm A/B — AI-derived vs human-derived hypothesis
  • Setup:
  • Primary KPI: micro_conversion (binary).
  • Baseline p = 0.05 (giả định để minh họa), desired relative MDE = 20% → p2 = 0.06.
  • Alpha = 0.05, power = 0.8 → compute nperarm ≈ dùng calculator → ~? (thực tế bạn tính theo công thức hoặc tool).
  • Randomize users 1:1, stratify by country.
  • Testing window 4 weeks, collect events, ensure independent exposure.
  • Guardrails: abort if negative lift > 10% with p < 0.01 on safety metric.
  1. Pre-registration và analysis plan (phải có trước khi chạy)
  • Record: hypothesis statement, primary KPI, baseline, MDE, alpha, power, nperarm, randomization seed/stratification, exclusion rules, primary analysis method (two-proportion z-test or logistic regression), covariate adjustments, handling of missing data, and stopping rules.
  • Analysis:
  • Primary: intention-to-treat (ITT) two-sided test; report point estimate, 95% CI, p-value.
  • Secondary: per-protocol analysis, subgroup analysis (pre-specified), multiplicity correction for multiple comparisons.
  • Robustness: bootstrap CI, Bayesian posterior intervals optional.
  • Reporting: include effect size, N per arm, fidelity metrics (exposure rate), and evidence provenance linking back to JTBD sources.

Kết luận ngắn

  • Thiết kế nghiệm thu phải link trực tiếp job → KPI → MDE → sample size → randomization → guardrails → pre-registration → analysis. Tránh chạy thử nửa vời: nếu traffic không đủ, tăng MDE hoặc làm pilot qualitative trước. Nếu bạn muốn, mình có thể gửi file checklist pre-registration và mẫu compute-sheet để bạn nhập baseline và MDE, thì sẽ cho ra sample size chính xác.

Các rủi ro và bias đặc thù khi dùng AI cho JTBD và cách giảm thiểu

Mình liệt kê các rủi ro chính khi dùng AI trong nghiên cứu Jobs To Be Done (JTBD), giải thích cơ chế gây lỗi, rồi nêu biện pháp giảm thiểu ở hai lớp: technical và operational. Cuối phần là gợi ý cho monitoring alerts và remediation playbook.

  1. Hallucination (mô hình tạo thông tin không có bằng chứng)
  • Cơ chế: model sinh ra job hoặc desired outcome không dựa trên trích đoạn transcript.
  • Technical mitigations: yêu cầu evidence spans trong output; bắt buộc trích dẫn start/end token của transcript; dùng ensemble + NLI/SBERT để kiểm tra grounding; cho model chế độ deterministic (temperature thấp) ở bước extraction.
  • Operational mitigations: mọi job mới phải có >= 2 respondent độc lập để auto-accept; jobs mới chỉ vào backlog dạng draft nếu chỉ có 1 nguồn.
  • Alert/remediation: alert khi tỉ lệ outputs không có evidence > 2% trong batch; remediation: quarantine bộ job đó, tăng sampling human review cho batch.
  1. Solution-bias (AI gán giải pháp thay vì job)
  • Cơ chế: training data và prompt dẫn model ưu tiên biểu đạt giải pháp.
  • Technical: thêm few-shot examples chuyển solution -> underlying job; blacklist các từ chỉ giải pháp trong field jobtitle và ép model trả “SOLUTIONMENTION” nếu phát hiện; post-filter để chuyển giải pháp thành job bằng template “Why does this solution matter?”.
  • Operational: huấn luyện interviewer hỏi theo job-first phrasing; reviewer phải rewrite bất kỳ job chứa solution.
  • Alert: tần suất job có token solution_words > threshold (ví dụ 30%) → trigger retrain prompt and sampling.
  1. Sample bias (mẫu không đại diện)
  • Cơ chế: dữ liệu phỏng vấn quá tập trung vào một persona hoặc kênh.
  • Technical: attach participant_profile metadata và compute representativeness score; khi synthesize, weight theo population strata.
  • Operational: thực hiện stratified sampling khi thu phỏng vấn; yêu cầu minimum respondents per segment trước khi generalize.
  • Alert: nếu top-segment chiếm > 60% của batch, flag and pause auto-aggregation.
  • Remediation: bổ sung target recruiting hoặc downweight overrepresented group.
  1. Language drift và cultural misinterpretation
  • Cơ chế: model yếu ở ngôn ngữ hay dialect, dẫn tới mất nuance hoặc mistranslation.
  • Technical: dùng locale-specific models or translation with back-translation checks; preserve original verbatim and provide bilingual evidence to model.
  • Operational: require bilingual reviewer for low-resource languages; add idiom lexicons into preprocessing.
  • Alert: high mismatch between translated and original embedding similarity > threshold → route to human review.
  1. Privacy / leak risk
  • Cơ chế: transcript chứa PII, model lưu prompt history hoặc output chứa sensitive data.
  • Technical: anonymize at ingestion with reversible tokenization that preserves role/context; strip PII by rule + model-assisted detection; ensure model provider contract prohibits data retention.
  • Operational: only store provenance IDs, not full PII in outputs; enforce data access controls and audit logs.
  • Alert: PII detected in output → immediate revoke publish and run secure scrub pipeline.
  1. Over-reliance on model (automation bias)
  • Cơ chế: team tin output AI mà bỏ qua review, dẫn tới roadmap sai.
  • Technical: surface confidence, provenance, and source_counts with every insight; automatic gating rules based on confidence.
  • Operational: define trust tiers – auto-accept, draft-with-review, require-review; mandatory human sign-off for high-impact epics.
  • Alert: if auto-accepted epics reversed by humans > X% in rolling window → tighten gates and retrain prompts.

Monitoring alerts and remediation playbook – ngắn gọn

  • Metrics to monitor: hallucinationrate, solutionwordrate, representativenessindex, languagemismatchrate, PIIleakevents, autoacceptreversal_rate.
  • Alert thresholds: set conservative defaults (e.g., hallucinationrate > 2%, autoacceptreversalrate > 5%, top-segment > 60%).
  • Remediation steps:
  1. Quarantine affected outputs and tag provenance.
  2. Run targeted human audit sample to measure scope.
  3. Patch at nguồn: adjust prompt, increase chunk overlap, update anonymization, or re-balance sampling.
  4. Re-process affected dataset after fix and publish audit log.
  5. If severe (PII leak or regulatory risk), trigger incident response and notify stakeholders.

Tóm lại, kết hợp ràng buộc kỹ thuật (evidence-first, ensemble, locale-aware) với quy trình vận hành (gates, sampling review, recruitment balance) là cách thực dụng để giảm bias và hạn chế rủi ro khi dùng AI cho JTBD.

Kết luận

Sau khi áp dụng pipeline này, bạn sẽ có quy trình JSON-ready để chuyển dữ liệu phỏng vấn và survey thành Jobs, Desired Outcomes và hypotheses có provenance rõ ràng, đủ để ưu tiên roadmap hoặc khởi tạo experiments. Hệ thống cũng cho phép cân bằng tự động hóa và kiểm duyệt con người để giảm rủi ro khi đưa insight vào backlog.

Lưu ý thực tế quan trọng: luôn giữ provenance verbatim và threshold human-review nghiêm ngặt cho các mục low-support hoặc low-confidence để tránh auto-publish insight không có bằng chứng.

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?