# Kiểm tra sơ đồ trang web WordPress bằng script: bắt URL chuyển hướng, noindex và canonical sai

> Một script kiểm sơ đồ trang web (sitemap, tệp XML liệt kê các URL mà trang muốn Google biết) làm năm kiểm tra với từng URL trong tệp. Nó gọi URL mà không đi

URL: https://mona.software/kiem-tra-so-do-trang-web-wordpress-bang-script/
Ngày đăng: 2026-11-02T10:02:29+07:00
Cập nhật: 2026-11-02T10:02:36+07:00
Tác giả: Lê Minh Nhựt

Một script kiểm sơ đồ trang web (sitemap, tệp XML liệt kê các URL mà trang muốn Google biết) làm năm kiểm tra với từng URL trong tệp. Nó gọi URL mà không đi theo chuyển hướng để đọc đúng mã HTTP (cần 200), tìm `noindex` trong thẻ meta lẫn header `X-Robots-Tag`, so thẻ canonical với chính URL đó, hỏi `robots.txt` xem Googlebot có được vào không, và đếm URL bị liệt kê hai lần. MONA làm đúng các bước này trên mona.software: sitemap có 956 URL ngày 27/10/2026, trong đó 12 URL trả mã 301 bị gỡ khỏi sơ đồ; lần chạy lại ngày 29/10 trên 944 URL còn lại cho 944 mã 200 và 0 lỗi ở bốn kiểm tra còn lại. Bẫy nằm ở bước đọc `robots.txt`: Python gửi User-Agent mặc định, máy chủ trả 403, và `urllib.robotparser` hiểu 403 là cấm toàn bộ.

Mọi đoạn mã và kết quả bên dưới chạy trên Python 3.9.6 (macOS) với thư viện chuẩn, ngày 29/10/2026. Site WordPress đã có sitemap do plugin SEO (tối ưu công cụ tìm kiếm) tạo; script chỉ kiểm thứ plugin đó xuất ra, bằng cách gọi từ bên ngoài giống loạt bài về [WordPress REST API](https://mona.software/wordpress-rest-api-la-gi/). Sitemap của mona.software là một sitemap index do plugin MONA SEO tạo, gồm 6 tệp con: bài viết, trang, giải pháp và ba nhóm phân loại. Một lần chạy mất gần nửa giờ vì script gọi từng URL một.

## Sitemap cần chứa đúng những URL trả 200, cho phép lập chỉ mục và tự trỏ canonical

Google yêu cầu URL (địa chỉ một trang) trong sitemap phải là URL đầy đủ, mỗi tệp tối đa 50MB (chưa nén) hoặc 50.000 URL, và khuyên liệt kê những URL bạn muốn thấy trong kết quả tìm kiếm ([Search Central, Build and submit a sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap), đọc ngày 29/10/2026). Trang đó không nói gì về URL chuyển hướng hay URL có noindex. Kết luận cho hai loại này là suy luận của MONA từ trang trợ giúp Search Console: URL chuyển hướng không được lập chỉ mục chính nó, URL có noindex bị Google bỏ qua, nên để chúng trong sitemap chỉ thêm nhiễu cho báo cáo. Một sitemap lẫn các URL này làm tổng số URL gửi đi và số URL được lập chỉ mục lệch nhau, và người đọc báo cáo phải tự loại từng URL xem lệch do đâu.

![Mục Sitemap best practices của Google: giới hạn 50MB hoặc 50.000 URL và URL tuyệt đối](https://mona.software/wp-content/uploads/2026/11/kiem-tra-so-do-trang-web-wordpress-bang-script-google-sitemap-mona.webp)

Mục Sitemap best practices: giới hạn kích thước và yêu cầu URL tuyệt đối. Nguồn developers.google.com, chụp ngày 29/10/2026.

Năm kiểm tra của script ứng với năm kiểu lỗi, mỗi kiểu để lại một dấu trong báo cáo Lập chỉ mục trang (Page indexing) của Search Console:

| Kiểm tra | Script đọc gì | Dấu hiệu trong Search Console |
| --- | --- | --- |
| Mã HTTP khác 200 | Dòng trạng thái, không theo chuyển hướng | Trang chuyển hướng không được lập chỉ mục chính nó; trang 404 bị bỏ |
| noindex | Thẻ meta robots và header X-Robots-Tag | Google không lập chỉ mục trang có noindex |
| Canonical khác chính URL | Thẻ link rel=canonical trong HTML | Alternate page with proper canonical tag, hoặc Google tự chọn canonical khác |
| Bị robots.txt chặn | Quy tắc Disallow áp cho Googlebot | URL bị robots.txt chặn |
| URL lặp trong sitemap | So số URL liệt kê với số URL khác nhau | Không có trạng thái riêng, chỉ lãng phí lượt gọi |

Cột bên phải lấy từ phần định nghĩa trạng thái của [trang trợ giúp báo cáo Lập chỉ mục trang](https://support.google.com/webmasters/answer/7440203). Trạng thái Crawled – currently not indexed không có trong bảng vì không kiểm tra nào của script giải thích được nó: Google chỉ nói trang đã được thu thập nhưng chưa lập chỉ mục, có thể được lập chỉ mục về sau.

![Trang trợ giúp Search Console định nghĩa trạng thái Crawled - currently not indexed](https://mona.software/wp-content/uploads/2026/11/kiem-tra-so-do-trang-web-wordpress-bang-script-gsc-mona.webp)

Định nghĩa của Crawled – currently not indexed và Alternate page with proper canonical tag. Nguồn support.google.com, chụp ngày 29/10/2026.

## Python đọc robots.txt thành cấm toàn bộ khi máy chủ trả 403

`robots.txt` là tệp ở gốc site khai báo thư mục nào crawler (chương trình thu thập trang) được hay không được vào. Script phải đọc nó đúng trước khi kết luận URL nào bị chặn. User-Agent là chuỗi tự khai tên chương trình gọi, nằm trong header của mỗi yêu cầu HTTP. Thư viện `urllib.robotparser` của Python sai ở một chỗ: nó tự tải tệp bằng User-Agent mặc định `Python-urllib/3.x`. Máy chủ của mona.software trả 403 cho User-Agent đó, còn curl với User-Agent mặc định của curl nhận 200:

```
$ curl -s -o /dev/null -w "curl default UA: %{http_code}\n" https://mona.software/robots.txt
curl default UA: 200
$ curl -s -o /dev/null -w "Python-urllib header via curl: %{http_code}\n" -A "Python-urllib/3.9" https://mona.software/robots.txt
Python-urllib header via curl: 403
```

Lệnh đầu để curl dùng User-Agent riêng của nó, lệnh sau giả chuỗi của urllib bằng tùy chọn `-A`. Cùng một URL, chỉ đổi User-Agent là đổi kết quả, vậy 403 nằm ở tầng chặn bot (phản hồi mang header `server: cloudflare`); quy tắc nào cụ thể gây ra thì MONA chưa kiểm. Đoạn mã dưới tải `robots.txt` hai cách: cách một gọi `read()` như tài liệu hướng dẫn, cách hai tự kiểm mã trạng thái, gửi User-Agent nhận diện được rồi đưa các dòng chữ vào `parse()`:

```
import urllib.request
import urllib.robotparser

ROBOTS = "https://mona.software/robots.txt"
PAGE = "https://mona.software/gioi-thieu/"

# 1. Naive: RobotFileParser.read() sends the default "Python-urllib/3.x" User-Agent.
rp = urllib.robotparser.RobotFileParser(ROBOTS)
rp.read()
print("read():  disallow_all =", rp.disallow_all, "| can_fetch(Googlebot) =", rp.can_fetch("Googlebot", PAGE))
# 2. Check the status yourself, send a real User-Agent, and feed the lines to parse().
req = urllib.request.Request(ROBOTS, headers={"User-Agent": "MONA-sitemap-audit/1.0 (+https://mona.software/)"})
with urllib.request.urlopen(req) as resp:
    status, text = resp.status, resp.read().decode("utf-8")
rp2 = urllib.robotparser.RobotFileParser()
rp2.parse(text.splitlines())
print("parse(): HTTP", status, "| disallow_all =", rp2.disallow_all,
      "| can_fetch(Googlebot) =", rp2.can_fetch("Googlebot", PAGE),
      "| /wp-admin/ =", rp2.can_fetch("Googlebot", "https://mona.software/wp-admin/"))
```

Kết quả ngày 29/10/2026:

```
$ python3 robots_trap.py
read():  disallow_all = True | can_fetch(Googlebot) = False
parse(): HTTP 200 | disallow_all = False | can_fetch(Googlebot) = True | /wp-admin/ = False
```

Cách một báo `disallow_all = True`: mọi URL của site đều bị coi là cấm, kể cả trang chủ. Lý do nằm trong mã nguồn CPython 3.9.6 (xem bằng `inspect.getsource`): khi `read()` gặp lỗi 401 hoặc 403 thì đặt cấm toàn bộ, gặp lỗi 4xx khác thì đặt cho phép toàn bộ. [Tài liệu Python](https://docs.python.org/3/library/urllib.robotparser.html) chỉ mô tả `read()` là hàm tải tệp và đưa vào bộ phân tích, không nói gì về hành vi này (đọc ngày 29/10/2026); MONA chưa kiểm các phiên bản Python khác 3.9.6.

Hành vi đó trái với cách các bên đặt ra luật đọc `robots.txt`. Bảng sau so ba nguồn: RFC (Request for Comments) 9309 (tài liệu chuẩn của giao thức robots.txt do IETF, tổ chức chuẩn Internet, ban hành), tài liệu của Google và mã nguồn thư viện Python:

| Phản hồi khi tải robots.txt | RFC 9309 | Google | urllib.robotparser 3.9.6 |
| --- | --- | --- | --- |
| 4xx (có 403) | Mục 2.3.1.3: crawler có thể truy cập mọi tài nguyên | Coi như không có tệp, không hạn chế (trừ 429) | 401 và 403: cấm toàn bộ; 4xx khác: cho phép toàn bộ |
| 5xx | Mục 2.3.1.4: bắt buộc coi là cấm toàn bộ | Dừng thu thập 12 giờ đầu, rồi dùng bản lưu gần nhất | `can_fetch` trả False cho mọi URL |

Cột cuối chạy thật: MONA dựng máy chủ giả trả lần lượt 200, 404, 403 và 500 cho `robots.txt` rồi gọi `can_fetch("Googlebot", "/page/")`, kết quả `True`, `True`, `False`, `False` (tệp `robots_status_lab.py` trong kho ghi chú của MONA). Hai cột còn lại đọc từ [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309.html) và từ [trang robots.txt của Google](https://developers.google.com/search/docs/crawling-indexing/robots/robots_txt).

![RFC 9309 mục 2.3.1.3 và 2.3.1.4: mã 4xx là không khả dụng, mã 5xx là cấm toàn bộ](https://mona.software/wp-content/uploads/2026/11/kiem-tra-so-do-trang-web-wordpress-bang-script-rfc9309-mona.webp)

RFC 9309, mục 2.3.1.3 và 2.3.1.4: 4xx là không khả dụng, 5xx là bắt buộc cấm toàn bộ. Nguồn rfc-editor.org, chụp ngày 29/10/2026.

Hậu quả thực tế: một script kiểm tra tự viết, hoặc một công cụ quét link dựng trên `RobotFileParser.read()`, sẽ báo “0 URL được phép” trên site đứng sau lớp chặn bot, và người kiểm có thể đi sửa một `robots.txt` vốn không hỏng. Googlebot không dính lỗi này, vì nhật ký 19 đến 29/10 không có mã 403 nào gửi cho Googlebot thật (số liệu ở phần nhật ký server). Cách phòng là script dừng ngay khi `robots.txt` không trả 200 để người chạy xem lại, thay vì đoán.

![Bảng Google xử lý mã HTTP của robots.txt: lỗi 4xx coi như không có tệp, 5xx dừng thu thập](https://mona.software/wp-content/uploads/2026/11/kiem-tra-so-do-trang-web-wordpress-bang-script-google-robots-mona.webp)

Bảng xử lý mã HTTP của robots.txt trong tài liệu Google. Nguồn developers.google.com, chụp ngày 29/10/2026.

![Màn hình thống kê trong phần mềm MONA làm cho The IELTS Workshop](https://mona.software/wp-content/themes/monatheme/home-v2/assets/img/cs/ielts-thongke.webp)

Phần mềm cho việc này

Quy trình đang chạy mà phần mềm có sẵn không làm được?

MONA khảo sát nghiệp vụ trước dòng code đầu tiên.

[Viết phần mềm theo yêu cầu](https://mona.software/viet-phan-mem-theo-yeu-cau/)[Xem case study](https://mona.software/#case-study)

## Script 117 dòng gọi từng URL mà không theo chuyển hướng

Script nhận địa chỉ sitemap, đọc `robots.txt` cùng host, đi qua sitemap index (tệp liệt kê các tệp sitemap con) để gom URL, rồi gọi từng URL một. Nếu `robots.txt` hoặc một tệp sitemap không trả 200, script dừng ngay và nói rõ mã nhận được:

```
#!/usr/bin/env python3
"""Audit every URL in a sitemap: status, redirect, noindex, canonical, robots.txt."""
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import urllib.robotparser
from collections import Counter
from html.parser import HTMLParser
from xml.etree import ElementTree as ET

UA = "MONA-sitemap-audit/1.0 (+https://mona.software/)"
DELAY = 0.3  # seconds between requests
NS = "{http://www.sitemaps.org/schemas/sitemap/0.9}"

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None  # a 301 comes back as an error we can read

opener = urllib.request.build_opener(NoRedirect)

def fetch(url):
    """Return (status, headers, body_text). Never follows a redirect."""
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    try:
        resp = opener.open(req, timeout=20)
    except urllib.error.HTTPError as err:
        resp = err
    body = resp.read(600000).decode("utf-8", "replace")
    return resp.status, resp.headers, body

class HeadInfo(HTMLParser):
    def __init__(self):
        super().__init__()
        self.robots, self.canonical = [], None

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "meta" and (a.get("name") or "").lower() in ("robots", "googlebot"):
            self.robots.append((a.get("content") or "").lower())
        if tag == "link" and "canonical" in (a.get("rel") or "").lower().split():
            self.canonical = a.get("href")

def sitemap_urls(url, seen_files):
    """Walk a sitemap index and return every <loc> of its URL sets."""
    status, _, body = fetch(url)
    if status != 200:
        sys.exit(f"sitemap {url} answered {status}")
    seen_files.append(url)
    root = ET.fromstring(body.encode("utf-8"))
    locs = [e.text.strip() for e in root.iter(NS + "loc")]
    if root.tag == NS + "sitemapindex":
        urls = []
        for child in locs:
            time.sleep(DELAY)
            urls += sitemap_urls(child, seen_files)
        return urls
    return locs

def main(sitemap, limit=None):
    base = "{0.scheme}://{0.netloc}".format(urllib.parse.urlparse(sitemap))
    status, _, text = fetch(base + "/robots.txt")
    print(f"robots.txt: HTTP {status}")
    if status != 200:
        sys.exit("robots.txt not readable: stop and fix the fetch before judging any URL")
    robots = urllib.robotparser.RobotFileParser()
    robots.parse(text.splitlines())

    files = []
    urls = sitemap_urls(sitemap, files)
    unique = list(dict.fromkeys(urls))
    print(f"sitemap files: {len(files)}; URLs listed: {len(urls)}; unique: {len(unique)}")
    if limit:
        unique = unique[:limit]

    codes, problems = Counter(), {k: [] for k in
        ("redirect", "not_200", "noindex", "canonical_differs", "no_canonical", "blocked_by_robots")}
    for i, url in enumerate(unique, 1):
        time.sleep(DELAY)
        status, headers, body = fetch(url)
        codes[status] += 1
        if not robots.can_fetch("Googlebot", url):
            problems["blocked_by_robots"].append(url)
        if 300 <= status < 400:
            problems["redirect"].append(f"{url} -> {status} {headers.get('Location')}")
            continue
        if status != 200:
            problems["not_200"].append(f"{url} -> {status}")
            continue
        info = HeadInfo()
        info.feed(body)
        flags = " ".join(info.robots + [headers.get("X-Robots-Tag", "").lower()])
        if "noindex" in flags:
            problems["noindex"].append(url)
        if info.canonical is None:
            problems["no_canonical"].append(url)
        elif info.canonical != url:
            problems["canonical_differs"].append(f"{url} -> {info.canonical}")
        if i % 100 == 0:
            print(f"  ... {i}/{len(unique)}", file=sys.stderr)

    print("status codes:", dict(sorted(codes.items())))
    for name, items in problems.items():
        print(f"{name}: {len(items)}")
        for line in items[:10]:
            print("   ", line)

if __name__ == "__main__":
    main(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else None)
```

Ba quyết định trong mã đáng giải thích. Lớp `NoRedirect` chặn việc tự theo chuyển hướng: nếu để urllib theo, mọi URL trả 301 sẽ hiện thành mã 200 của trang đích và lỗi biến mất. Mỗi lần gọi là GET đầy đủ chứ không phải HEAD, vì thẻ meta robots và thẻ canonical nằm trong thân HTML; HEAD chỉ đủ cho mã trạng thái và header. Giữa hai lần gọi có `DELAY = 0.3` giây và các lần gọi chạy lần lượt, không song song: 944 URL mất từ 12:03 đến 12:31 (khoảng 28 phút, tức 0,55 URL mỗi giây, tính cả thời gian máy chủ trả lời).

Chạy bằng một dòng, kết quả đưa vào tệp để so với lần sau:

```
python3 sitemap_audit.py https://mona.software/sitemap.xml > run-live.txt
```

Script có hai điểm yếu đã biết. Script đọc HTML thô và không thấy thẻ canonical hay noindex do JavaScript chèn vào sau khi tải; site WordPress dùng giao diện tách rời (headless) thì phải kiểm bằng trình duyệt thật, như trong [nghiên cứu chuyển WordPress sang headless giữ link](https://mona.software/nghien-cuu/chuyen-website-wordpress-sang-headless-giu-link-seo/). Điểm yếu thứ hai: script so canonical bằng so sánh chuỗi chính xác, nên khác mỗi dấu gạch chéo cuối cũng bị báo; kết quả đó đúng với cách đặt ra của script, còn có nên coi là lỗi hay không thì tùy plugin.

## Kết quả trên mona.software và trên site thử có đủ năm lỗi

Kết quả chạy ngày 29/10/2026 trên sitemap thật, nguyên văn:

```
$ python3 sitemap_audit.py https://mona.software/sitemap.xml
robots.txt: HTTP 200
sitemap files: 7; URLs listed: 944; unique: 944
status codes: {200: 944}
redirect: 0
not_200: 0
noindex: 0
canonical_differs: 0
no_canonical: 0
blocked_by_robots: 0
```

Sitemap có 7 tệp (một tệp index và sáu tệp con), 944 URL, không lặp. Cả 944 URL trả 200, không có noindex, canonical nào cũng trỏ về đúng URL, và không URL nào bị robots.txt chặn. Con số này là kết quả sau sửa: ngày 27/10 sitemap còn 956 URL, 12 bài viết trả 301 vì đã có quy tắc chuyển hướng nhưng vẫn nằm trong sơ đồ. MONA đặt cờ loại khỏi sitemap của plugin MONA SEO cho 12 bài đó; các bài vẫn xuất bản, chỉ không còn trong sơ đồ.

Một lần chạy sạch không chứng minh script bắt được lỗi, vì vậy MONA dựng thêm một site thử cục bộ (máy chủ Python 43 dòng ở `127.0.0.1:5181`) với mỗi kiểu lỗi một trang. Máy chủ chạy bằng `http.server` của thư viện chuẩn, khởi động bằng `python3 fixture_server.py`. Robots.txt của site thử có dòng `Disallow: /admin/`, và `HOST` là `http://127.0.0.1:5181`. Phần khai báo các trang lỗi:

```
ROUTES = {
    "/ok/": (200, {}, '<link rel="canonical" href="%s/ok/">' % HOST),
    "/ok-two/": (200, {}, '<link rel="canonical" href="%s/ok-two/">' % HOST),
    "/draft/": (200, {}, '<meta name="robots" content="noindex,follow"><link rel="canonical" href="%s/draft/">' % HOST),
    "/header-noindex/": (200, {"X-Robots-Tag": "noindex"}, '<link rel="canonical" href="%s/header-noindex/">' % HOST),
    "/copy/": (200, {}, '<link rel="canonical" href="%s/ok/">' % HOST),
    "/old-post/": (301, {"Location": "/ok/"}, ""),
    "/gone/": (404, {}, ""),
    "/admin/report/": (200, {}, '<link rel="canonical" href="%s/admin/report/">' % HOST),
}
```

Script chạy trên site thử cho kết quả:

```
$ python3 sitemap_audit.py http://127.0.0.1:5181/sitemap.xml
robots.txt: HTTP 200
sitemap files: 1; URLs listed: 9; unique: 8
status codes: {200: 6, 301: 1, 404: 1}
redirect: 1
    http://127.0.0.1:5181/old-post/ -> 301 /ok/
not_200: 1
    http://127.0.0.1:5181/gone/ -> 404
noindex: 2
    http://127.0.0.1:5181/draft/
    http://127.0.0.1:5181/header-noindex/
canonical_differs: 1
    http://127.0.0.1:5181/copy/ -> http://127.0.0.1:5181/ok/
no_canonical: 0
blocked_by_robots: 1
    http://127.0.0.1:5181/admin/report/
```

Cả năm kiểu lỗi đều lộ ra: `/old-post/` trả 301, `/gone/` trả 404, hai trang noindex (một qua thẻ meta, một qua header), `/copy/` khai canonical của trang khác, `/admin/report/` nằm trong vùng bị chặn. Trang `/ok-two/` khai canonical đúng chính nó nên không bị báo, cho thấy script không báo oan trang hợp lệ. Dòng đầu cho thấy 9 URL được liệt kê nhưng chỉ 8 URL khác nhau, tức `/ok/` bị lặp.

Hai tệp ngoài sitemap cũng cần kiểm bằng tay. Ngày 27/10, `/favicon.ico` và `/apple-touch-icon.png` trả 404; sau khi chép hai tệp vào thư mục gốc của site, vòng lặp này cho kết quả:

```
$ for p in /favicon.ico /apple-touch-icon.png /robots.txt /sitemap.xml; do printf "%s " $p; curl -s -o /dev/null -w "%{http_code}\n" -A "MONA-sitemap-audit/1.0 (+https://mona.software/)" https://mona.software$p; done
/favicon.ico 200
/apple-touch-icon.png 200
/robots.txt 200
/sitemap.xml 200
```

## Nhật ký server cho biết Googlebot nhận được gì, nếu lọc đúng IP

Phần này chỉ làm được khi có quyền đọc nhật ký truy cập của server. Script chạy với User-Agent của MONA, trong khi bộ lọc bot có thể đối xử Googlebot khác. Phản hồi 403 ở phần trước chứng minh server phân biệt User-Agent, nên 200 mà script nhận không bảo đảm Googlebot cũng nhận 200. Nguồn trả lời là nhật ký truy cập của server. Với nhật ký nginx định dạng combined (trạng thái ở cột 9, đường dẫn ở cột 7), ba lệnh sau đếm yêu cầu của Googlebot trong 11 ngày, từ 19/10 đến 29/10/2026:

```
$ grep -c Googlebot access.log
5166
$ grep Googlebot access.log | awk '{print $9}' | sort | uniq -c | sort -rn
3419 200
 924 404
 420 301
 301 304
  72 403
  25 302
   5 401
$ grep Googlebot access.log | awk '{print $7}' | grep -E '^/(robots.txt|sitemap.xml|favicon.ico)$' | sort | uniq -c
   6 /favicon.ico
  25 /robots.txt
  20 /sitemap.xml
```

Các số này chưa phải của Googlebot thật: bất kỳ bot nào cũng đặt được chuỗi “Googlebot” vào User-Agent. Google hướng dẫn xác minh bằng cách tra ngược DNS của IP (địa chỉ mạng của máy gửi yêu cầu), hoặc đối chiếu với danh sách dải IP Google công bố (Search Central, mục xác minh Googlebot; MONA tải tệp `https://developers.google.com/static/crawling/ipranges/common-crawlers.json` ngày 29/10/2026, tệp tạo lúc 28/10). Cột đầu của nhật ký là IP của mạng phân phối nội dung (CDN) Cloudflare vì site đứng sau nó; MONA đã cấu hình ghi thêm IP gốc từ header `CF-Connecting-IP` thành trường `cf_ip` ở cuối dòng, và script dưới đọc trường đó:

```
import ipaddress
import json
import re
import sys
from collections import Counter

ranges = json.load(open(sys.argv[1]))["prefixes"]
nets = [ipaddress.ip_network(p.get("ipv4Prefix") or p.get("ipv6Prefix")) for p in ranges]
count = Counter()
for line in open(sys.argv[2]):
    m = re.search(r'cf_ip="([^"]*)"', line)
    status = line.split()[8]
    if not m:
        count[("no cf_ip", status)] += 1
        continue
    ip = ipaddress.ip_address(m.group(1))
    real = any(ip in n for n in nets if n.version == ip.version)
    count[("in Google ranges" if real else "NOT in Google ranges", status)] += 1
for (who, status), n in sorted(count.items()):
    print(f"{who:22} {status}  {n}")
```

Kết quả trên 5.166 dòng:

```
$ python3 verify_googlebot.py common-crawlers.json access.log
NOT in Google ranges   200  623
NOT in Google ranges   301  48
NOT in Google ranges   401  5
NOT in Google ranges   403  72
NOT in Google ranges   404  680
in Google ranges       200  2796
in Google ranges       301  372
in Google ranges       302  25
in Google ranges       304  301
in Google ranges       404  244
```

Chỉ 3.738 dòng (72%) đến từ dải IP của Google; 1.428 dòng (28%) mang tên Googlebot nhưng IP nằm ngoài dải. Trong 3.738 dòng của Googlebot thật, 2.796 nhận mã 200, 372 nhận 301, 301 nhận 304, 244 nhận 404 và 25 nhận 302; không có mã 5xx và không có 403. Cả 72 mã 403 và 5 mã 401 nằm ở nhóm ngoài dải IP Google. Số 4.155 yêu cầu Googlebot (17 đến 27/10) trong ghi chép nội bộ của MONA ngày 27/10 không nói đã lọc theo IP; nếu chưa lọc, số đó gồm cả nhóm giả mạo.

Với Googlebot thật, `/favicon.ico` nhận 404 sáu lần trong cửa sổ 11 ngày đó (MONA chép tệp ngày 27/10), và `/sitemap.xml` nhận 200 chín lần (trong 20 yêu cầu mang tên Googlebot, 9 yêu cầu đến từ IP Google). Đây là lý do vòng lặp curl ở phần trước kiểm cả hai tệp ngoài sitemap.

Nhật ký cho biết có 372 lần Googlebot thật nhận 301, nhưng chưa cho biết URL đó nằm trong sitemap hay đến từ liên kết cũ. Cách tách: lấy cột 7 của các dòng 301 rồi đối chiếu với danh sách URL của script; ở lần chạy ngày 29/10 không URL nào trong sitemap trả 301, tức mọi lượt 301 còn lại đến từ URL ngoài sitemap. Phép đối chiếu theo từng URL này MONA chưa chạy. Trước khi gỡ 12 bài ngày 27/10, các bài đó là nguồn 301 nằm trong sơ đồ.

## Những điều kiểm tra này không thấy

Script xác nhận sơ đồ sạch và nhật ký xác nhận Googlebot thật nhận gì; hai thứ đó chưa nói được vì sao Google chọn không lập chỉ mục một trang. Script nhìn từ phía người gọi, nhật ký nhìn từ phía Googlebot, còn quyết định lập chỉ mục chỉ hiện ra trong Search Console. Với site có hàng trăm URL, ba nguồn này đối chiếu được với nhau. Bảng sau ghi từng giới hạn và bằng chứng đi kèm:

| Điều chưa biết | Số liệu hoặc bằng chứng | Cách biết |
| --- | --- | --- |
| Vì sao trang ở trạng thái Crawled – currently not indexed | 417 trang ngày 27/10/2026. Google không nêu lý do cho từng trang. | Xuất danh sách URL mẫu từ Search Console rồi chạy script trên chính danh sách đó |
| Trang lưu trữ thẻ (tag) mỏng có phải nguyên nhân không | 346 URL thẻ trong sitemap, 279 URL được Googlebot gọi trong 11 ngày; chưa có bằng chứng nối với 417 trang trên | Đối chiếu danh sách mẫu với loại URL |
| Nguồn của 1.832 URL báo bị robots.txt chặn | robots.txt có dòng Disallow cho các tham số `utm_`, `fbclid`, `gclid` (đọc ngày 29/10); không URL sitemap nào bị chặn | Danh sách URL mẫu từ Search Console |
| Canonical, noindex do JavaScript chèn | Script đọc HTML thô, không thấy | Kiểm bằng trình duyệt thật hoặc công cụ Kiểm tra URL |

## Hỏi đáp

### Sitemap có được chứa URL đang chuyển hướng không?

Không. Trang trợ giúp Search Console nói URL chuyển hướng không được lập chỉ mục chính nó, còn Google khuyên liệt kê URL bạn muốn xuất hiện trong kết quả tìm kiếm. Ghi URL đích vào sitemap, bỏ URL cũ; trên mona.software, 12 URL kiểu này đã được gỡ ngày 27/10/2026. Sau khi gỡ, chạy lại script để xác nhận: lần chạy ngày 29/10 cho 0 URL chuyển hướng trong 944 URL.

### Dùng HEAD thay GET để nhanh hơn được không?

Được nếu chỉ cần mã trạng thái và header `X-Robots-Tag`. HEAD không trả thân HTML, do đó không đọc được thẻ meta robots và thẻ canonical; vì vậy script trong bài dùng GET. Script chỉ đọc tối đa 600.000 byte thân trang cho mỗi URL (`resp.read(600000)`); với 944 URL, thẻ canonical đều nằm trong giới hạn đó. MONA chưa đo chênh lệch tốc độ giữa HEAD và GET.

### Gọi bao nhiêu URL mỗi giây thì lịch sự với site?

Google và RFC 9309 không nêu con số cho trường hợp tự kiểm tra site của mình. MONA chạy một yêu cầu mỗi lần với nghỉ 0,3 giây, tức 0,55 URL mỗi giây trên mona.software, và máy chủ không báo lỗi nào. Hai lần đo bằng curl ngày 29/10 cho thấy một trang trả lời sau 1,13 giây và một trang khác sau 0,50 giây, nên thời gian tổng chủ yếu là thời gian máy chủ trả lời. Với site chia sẻ hosting yếu, hạ xuống một URL mỗi 2 giây rồi đo thời gian phản hồi.

### Có lấy danh sách URL từ WordPress REST API thay cho sitemap được không?

REST API trả danh sách bài đã xuất bản (xem [WordPress REST API là gì](https://mona.software/wordpress-rest-api-la-gi/) và [các lỗi khi gọi REST API](https://mona.software/loi-thuong-gap-wordpress-rest-api/)), còn sitemap là danh sách plugin SEO khai báo cho Google. Hai danh sách khác nhau chính là chỗ lộ bài thiếu hoặc thừa trong sơ đồ. Plugin SEO có tùy chọn loại bài khỏi sitemap, đúng cờ MONA đặt cho 12 bài ngày 27/10, nên một bài có trong REST API mà vắng trong sitemap chưa chắc là lỗi. MONA chưa chạy phép so này trong bài.

### Search Console báo Crawled – currently not indexed thì script giúp được gì?

Script không giải thích được trạng thái này; nó chỉ loại trừ năm lỗi cơ học (chuyển hướng, noindex, canonical lệch, bị chặn, lặp). Nếu cả năm đều sạch, trang thuộc nhóm Google đã thu thập và chưa lập chỉ mục. Google cho biết không cần gửi lại URL để thu thập. Chất lượng và độ trùng lặp của nội dung là chỗ cần xem; với [site WordPress](https://mona.software/wordpress-la-gi/), trang lưu trữ thẻ và danh mục do [plugin](https://mona.software/plugin-wordpress-la-gi/) sinh ra là nhóm MONA nghi trước, nhưng chưa chứng minh.

Doanh nghiệp có site WordPress vài trăm URL trở lên muốn rà sơ đồ trang web cùng nhật ký server, có thể xem [dịch vụ IT của MONA](https://mona.software/dich-vu-it/) hoặc gọi hotline 1900 636 648 để MONA chạy bộ kiểm tra này trên site đó.

## Câu hỏi thường gặp

### Sitemap có được chứa URL đang chuyển hướng không?

Không. Trang trợ giúp Search Console nói URL chuyển hướng không được lập chỉ mục chính nó, còn Google khuyên liệt kê URL bạn muốn xuất hiện trong kết quả tìm kiếm. Ghi URL đích vào sitemap, bỏ URL cũ; trên mona.software, 12 URL kiểu này đã được gỡ ngày 27/10/2026. Sau khi gỡ, chạy lại script để xác nhận: lần chạy ngày 29/10 cho 0 URL chuyển hướng trong 944 URL.

### Dùng HEAD thay GET để nhanh hơn được không?

Được nếu chỉ cần mã trạng thái và header X-Robots-Tag. HEAD không trả thân HTML, do đó không đọc được thẻ meta robots và thẻ canonical; vì vậy script trong bài dùng GET. Script chỉ đọc tối đa 600.000 byte thân trang cho mỗi URL (resp.read(600000)); với 944 URL, thẻ canonical đều nằm trong giới hạn đó. MONA chưa đo chênh lệch tốc độ giữa HEAD và GET.

### Gọi bao nhiêu URL mỗi giây thì lịch sự với site?

Google và RFC 9309 không nêu con số cho trường hợp tự kiểm tra site của mình. MONA chạy một yêu cầu mỗi lần với nghỉ 0,3 giây, tức 0,55 URL mỗi giây trên mona.software, và máy chủ không báo lỗi nào. Hai lần đo bằng curl ngày 29/10 cho thấy một trang trả lời sau 1,13 giây và một trang khác sau 0,50 giây, nên thời gian tổng chủ yếu là thời gian máy chủ trả lời. Với site chia sẻ hosting yếu, hạ xuống một URL mỗi 2 giây rồi đo thời gian phản hồi.

### Có lấy danh sách URL từ WordPress REST API thay cho sitemap được không?

REST API trả danh sách bài đã xuất bản (xem WordPress REST API là gì và các lỗi khi gọi REST API), còn sitemap là danh sách plugin SEO khai báo cho Google. Hai danh sách khác nhau chính là chỗ lộ bài thiếu hoặc thừa trong sơ đồ. Plugin SEO có tùy chọn loại bài khỏi sitemap, đúng cờ MONA đặt cho 12 bài ngày 27/10, nên một bài có trong REST API mà vắng trong sitemap chưa chắc là lỗi. MONA chưa chạy phép so này trong bài.

### Search Console báo Crawled - currently not indexed thì script giúp được gì?

Script không giải thích được trạng thái này; nó chỉ loại trừ năm lỗi cơ học (chuyển hướng, noindex, canonical lệch, bị chặn, lặp). Nếu cả năm đều sạch, trang thuộc nhóm Google đã thu thập và chưa lập chỉ mục. Google cho biết không cần gửi lại URL để thu thập. Chất lượng và độ trùng lặp của nội dung là chỗ cần xem; với site WordPress, trang lưu trữ thẻ và danh mục do plugin sinh ra là nhóm MONA nghi trước, nhưng chưa chứng minh. Doanh nghiệp có site WordPress vài trăm URL trở lên muốn rà sơ đồ trang web cùng nhật ký server, có thể xem dịch vụ IT của MONA hoặc gọi hotline 1900 636 648 để MONA chạy bộ kiểm tra này trên site đó.
