Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

Kiểm thực thể thương hiệu bằng JSON-LD và llms.txt: script Python khoảng 60 dòng

Lần gần nhất mình rà một website dịch vụ nhỏ, cùng một doanh nghiệp mà trang chủ có ba phiên bản: JSON-LD ghi số hotline cũ, footer ghi số mới, còn llms.txt vẫn dùng tên trước khi đổi thương hiệu. Người xem lướt qua khôn

Lần gần nhất mình rà một website dịch vụ nhỏ, cùng một doanh nghiệp mà trang chủ có ba phiên bản: JSON-LD ghi số hotline cũ, footer ghi số mới, còn llms.txt vẫn dùng tên trước khi đổi thương hiệu. Người xem lướt qua không để ý, nhưng máy đọc từng dòng thì thấy ba câu trả lời khác nhau cho câu hỏi "doanh nghiệp này là ai".

Bài này là cách mình kiểm nhanh chuyện đó trước khi làm bất cứ việc SEO AI nào khác: một script đọc JSON-LD trên trang chủ, đọc llms.txt, rồi so từng trường với một bộ thông tin gốc do chủ doanh nghiệp duyệt. Thay example.com bằng domain của bạn.

Bước 1: viết bộ thông tin gốc thành file

Mọi so sánh cần một mốc. Đừng lấy mốc từ website, vì website có thể đang sai. Viết tay một file entity.json và nhờ chủ doanh nghiệp xác nhận từng dòng:

{
  "name": "Example Studio",
  "url": "https://example.com/",
  "telephone": "+84 912 345 678",
  "sameAs": [
    "https://www.facebook.com/examplestudio",
    "https://www.linkedin.com/company/examplestudio"
  ]
}

Bước 2: lấy JSON-LD từ HTML ban đầu

Nhiều bot AI chỉ đọc HTML trả về lần đầu, không chạy JavaScript. Vì vậy script dùng requests thuần, không dùng trình duyệt headless. Nếu schema của bạn chỉ xuất hiện sau khi JS chạy, script sẽ báo không thấy, và đó chính là lỗi cần sửa.

import json, re, requests

UA = {"User-Agent": "entity-check/1.0"}

def jsonld_blocks(url):
    html = requests.get(url, headers=UA, timeout=15).text
    blocks = re.findall(
        r'<script[^>]+application/ld\+json[^>]*>(.*?)</script>', html, re.S)
    out = []
    for b in blocks:
        try:
            data = json.loads(b)
        except json.JSONDecodeError:
            print("JSON-LD lỗi cú pháp:", b[:80])
            continue
        items = data.get("@graph", [data]) if isinstance(data, dict) else data
        out.extend(i for i in items if isinstance(i, dict))
    return out

def find_org(items):
    for i in items:
        t = i.get("@type")
        types = t if isinstance(t, list) else [t]
        if {"Organization", "LocalBusiness", "ProfessionalService"} & set(types):
            return i
    return None

Bước 3: đọc llms.txt

llms.txt là một đề xuất của cộng đồng (llmstxt.org), chưa phải chuẩn chính thức, và Google không nhắc tới nó trong hướng dẫn về tính năng AI trên Search. Mình vẫn kiểm vì nếu đã đặt file thì nó không được nói khác website. Theo đề xuất, dòng # đầu tiên là tên dự án, dòng > ngay sau là phần tóm tắt.

def read_llms(base):
    r = requests.get(base.rstrip("/") + "/llms.txt", headers=UA, timeout=15)
    if r.status_code != 200:
        return None
    lines = r.text.splitlines()
    name = next((l[2:].strip() for l in lines if l.startswith("# ")), None)
    summary = next((l[2:].strip() for l in lines if l.startswith("> ")), "")
    phones = re.findall(r"\+?\d[\d .]{8,}\d", r.text)
    return {"name": name, "summary": summary, "phones": phones}

Bước 4: so với bộ thông tin gốc

def digits(s):
    return re.sub(r"\D", "", s or "").removeprefix("84").lstrip("0")

def check(base, truth):
    org = find_org(jsonld_blocks(base))
    if not org:
        print("[LỖI] Không thấy Organization trong HTML ban đầu")
        return
    if org.get("name") != truth["name"]:
        print("[LỆCH] name:", org.get("name"), "≠", truth["name"])
    if org.get("url", "").rstrip("/") != truth["url"].rstrip("/"):
        print("[LỆCH] url:", org.get("url"))
    if digits(org.get("telephone")) != digits(truth["telephone"]):
        print("[LỆCH] telephone:", org.get("telephone"))
    missing = set(truth["sameAs"]) - set(org.get("sameAs", []))
    for m in missing:
        print("[THIẾU] sameAs:", m)
    for s in org.get("sameAs", []):
        code = requests.head(s, headers=UA, timeout=15, allow_redirects=True).status_code
        if code >= 400:
            print("[CHẾT] sameAs", code, s)

    llms = read_llms(base)
    if llms is None:
        print("[INFO] Không có llms.txt, bỏ qua")
    else:
        if llms["name"] != truth["name"]:
            print("[LỆCH] llms.txt name:", llms["name"])
        if llms["phones"] and digits(truth["telephone"]) not in map(digits, llms["phones"]):
            print("[LỆCH] llms.txt có số điện thoại khác:", llms["phones"])

if __name__ == "__main__":
    truth = json.load(open("entity.json", encoding="utf-8"))
    check(truth["url"], truth)

Chạy python check_entity.py. Không in gì ngoài dòng INFO là tốt.

Ba lỗi mình gặp nhiều nhất khi chạy script này

  1. Schema chỉ có sau khi JS chạy. Plugin hoặc tag manager chèn JSON-LD phía client. Google vẫn render được, nhưng nhiều crawler AI thì không. Cách sửa là xuất JSON-LD ngay từ server.
  2. sameAs trỏ vào trang đã chết. Fanpage cũ bị gỡ, hồ sơ danh bạ hết hạn. Một link chết trong sameAs không phá gì lớn, nhưng nó là một nguồn không còn xác nhận được bạn.
  3. Số điện thoại viết ba kiểu. 0912..., +84 912..., 84912... trên ba trang khác nhau. Con người hiểu là một, còn so khớp chuỗi thì không. Chọn một định dạng và dùng ở mọi nơi.

Script này không làm được gì

Nó chỉ kiểm phần bạn tự kiểm soát trên website. Nó không cho biết AI đang nói gì về bạn, và không chứng minh sửa xong thì AI sẽ nhắc tên. Phần đó cần hỏi lại một bộ câu hỏi mẫu trên từng nền tảng theo định kỳ và ghi lại câu trả lời.

Nếu muốn xem việc kiểm thực thể này nằm ở đâu trong cả quy trình (thống nhất thông tin, viết đoạn trả lời dễ trích, mở đường cho bot AI, đo lượt vào từ AI trong GA4), mình có mô tả 6 phần việc ở trang dịch vụ SEO AI của Mạnh Digital.

Đọc thêm trong loạt bài này:

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.