🔎 Getting a GitHub Pages site into Google · พาเว็บ GitHub Pages เข้า Google · 让 GitHub Pages 网站进到 Google 里

Search Console, set up for a github.io address. It can only be verified one way. Write the robots.txt and sitemap GitHub Pages does not give you. Check them as they are actually served. Read your own pages the way a crawler will. Four challenges, all runnable. · ตั้งค่า Search Console สำหรับที่อยู่แบบ github.io ซึ่งยืนยันได้ทางเดียวเท่านั้น เขียน robots.txt กับ sitemap ที่ GitHub Pages ไม่ได้ให้มา ตรวจมันในแบบที่ถูกเสิร์ฟจริง และอ่านหน้าเว็บของคุณเองแบบที่ตัวเก็บข้อมูลจะอ่าน พร้อมโจทย์สี่ข้อที่รันได้จริงทั้งหมด · 为 github.io 地址设置 Search Console,它只有一种验证方式行得通。写出 GitHub Pages 没给你的 robots.txt 和 sitemap,按它们真正被送出的样子检查,并用爬虫的眼光读一遍你自己的页面。四道题,全都能跑。

🇬🇧 English

You put a site on GitHub Pages. Nobody comes. That is normal, and it is not a mystery.

Google has to find the site, decide to read it, and decide to keep it. Search Console is where you watch all three.

This page sets it up for a github.io site, which has one restriction the guides skip.

🇹🇭 ไทย

คุณเอาเว็บขึ้น GitHub Pages แล้วไม่มีใครเข้ามาเลย นั่นเป็นเรื่องปกติ และไม่ใช่เรื่องลึกลับ

Google ต้องหาเว็บให้เจอ ตัดสินใจว่าจะอ่าน และตัดสินใจว่าจะเก็บไว้ Search Console คือที่ที่คุณเฝ้าดูทั้งสามอย่างนั้น

หน้านี้ตั้งค่าให้เว็บแบบ github.io ซึ่งมีข้อจำกัดหนึ่งอย่างที่คู่มือทั่วไปมักข้ามไป

🇨🇳 中文

你把网站放上了 GitHub Pages,然后没人来。这很正常,也不神秘。

Google 得先找到这个站、决定去读它、再决定留着它。Search Console 就是你看这三件事的地方。

这一页针对 github.io 网站来设置,它有一个限制是别的指南会跳过的。

GitHub Pages gives youGitHub Pages ให้อะไรGitHub Pages 给你什么You have to addคุณต้องเพิ่มเอง你得自己加
Hostingโฮสติ้ง托管Pages: free, on httpsPages: ฟรี และเป็น httpsPages:免费,走 httpsyou: nothingคุณ: ไม่ต้องทำอะไร你:什么都不用
robots.txtrobots.txtrobots.txtPages: nothing, it is a 404Pages: ไม่มี ขึ้น 404Pages:没有,是 404you: write oneคุณ: เขียนเอง你:自己写一个
sitemap.xmlsitemap.xmlsitemap.xmlPages: nothingPages: ไม่มีPages:没有you: generate itคุณ: สร้างขึ้นมา你:自己生成
Told Google it existsบอก Google ว่ามีเว็บนี้告诉过 Google 它存在Pages: noPages: ไม่Pages:没有you: Search Consoleคุณ: ใช้ Search Console你:用 Search Console
A NEW SITE IS NOT SECRETLY BROKEN. IT IS JUST NEW. Google finds pages by following links. A brand new github.io address has none pointing at it. There is nothing to follow. Everything on this page is about giving it a way in. Expect days, not minutes, even after you do it right. Google หาหน้าเว็บด้วยการเดินตามลิงก์ ที่อยู่ github.io ที่เพิ่งสร้างใหม่ยังไม่มีลิงก์ไหนชี้มาเลย จึงไม่มีอะไรให้เดินตาม ทุกอย่างในหน้านี้คือการเปิดทางให้มันเข้ามา ให้คาดหวังเป็นวัน ไม่ใช่เป็นนาที ต่อให้คุณทำถูกหมดแล้วก็ตาม Google 靠跟着链接找页面。一个刚建好的 github.io 地址没有任何链接指向它,所以没有东西可跟。这一页讲的都是给它开一条路。就算你全做对了,也要按天算,不是按分钟算。

🌱 Tutorial 1 — A site Google can read · บทที่ 1 — เว็บที่ Google อ่านได้ · 教程 1 —— 一个 Google 读得懂的网站

🇬🇧 English

First the site itself, then a look at your own pages the way a search engine sees them.

🇹🇭 ไทย

เริ่มจากตัวเว็บก่อน แล้วค่อยดูหน้าเว็บของคุณเอง ในแบบที่เสิร์ชเอนจินมองเห็น

🇨🇳 中文

先是网站本身,然后用搜索引擎的眼光看一遍你自己的页面。

1 The repository, and what Pages does not give you · รีโพ และสิ่งที่ Pages ไม่ได้ให้มา · 仓库,以及 Pages 没给你的东西

🇬🇧 English

A repository named yourname.github.io becomes a website at that address. That is the whole setup.

What it does not come with is anything that helps a search engine. Both of these are 404 on a fresh site.

🇹🇭 ไทย

รีโพที่ชื่อ yourname.github.io จะกลายเป็นเว็บไซต์ที่อยู่นั้น การตั้งค่ามีแค่นั้น

สิ่งที่มันไม่ได้แถมมาคืออะไรก็ตามที่ช่วยเสิร์ชเอนจิน ทั้งสองอย่างนี้ขึ้น 404 บนเว็บที่เพิ่งสร้าง

🇨🇳 中文

一个叫 yourname.github.io 的仓库,就会变成那个地址上的网站。设置就这么多。

它没附带任何帮搜索引擎的东西。在一个全新的站上,这两个都是 404。

💻 MAKE IT · at the terminal, once

# The name matters. It must be exactly yourname.github.io
gh repo create yourname.github.io --public --clone
cd yourname.github.io

echo '<!doctype html><html lang="en"><head><title>Hello</title>'\
     '</head><body><h1>Hello</h1></body></html>' > index.html
git add index.html && git commit -m 'first page' && git push

# Settings -> Pages -> Source: main. Then wait a minute.

🖥️ WHAT A FRESH ONE LOOKS LIKE · measured on a real Pages site

https://octocat.github.io/            200
https://octocat.github.io/robots.txt  404   <- nothing there
https://octocat.github.io/sitemap.xml 404   <- nothing there

X-Robots-Tag: not set                       <- good, nothing blocks you
CHECK NOTHING IS BLOCKING YOU BEFORE BLAMING GOOGLE One header hides a whole site from search: X-Robots-Tag: noindex. Nothing on the page shows it. GitHub Pages does not set it, which the measurement above confirms. If you later move to another host, check again. Step 4 does it for you. เฮดเดอร์เดียวคือ X-Robots-Tag: noindex ซ่อนทั้งเว็บจากการค้นหาได้ และไม่มีอะไรบนหน้าเว็บที่แสดงให้เห็น GitHub Pages ไม่ได้ตั้งค่านั้น ซึ่งการวัดข้างบนยืนยันแล้ว ถ้าวันหลังย้ายไปโฮสต์อื่น ให้ตรวจใหม่ ขั้นที่ 4 ตรวจให้คุณเอง 一个响应头 X-Robots-Tag: noindex 就能把整个站从搜索里藏起来,而页面上什么都看不出来。GitHub Pages 不设它,上面的实测确认了这点。以后换别的主机,要再查一次。第 4 步会替你查。
YOUR TURN
  1. Make the repository. Push one page. Open the address.
  2. Now open /robots.txt on your own site. A 404.
  3. Open /sitemap.xml. Also a 404.
  4. Those two are steps 3 and 4.
  1. สร้างรีโพ พุชหนึ่งหน้า แล้วเปิดที่อยู่นั้น
  2. ทีนี้เปิด /robots.txt บนเว็บของคุณ จะได้ 404
  3. เปิด /sitemap.xml ก็ได้ 404 เหมือนกัน
  4. สองอันนั้นคือขั้นที่ 3 กับ 4
  1. 建好仓库,推一个页面,打开那个地址。
  2. 现在打开你自己站上的 /robots.txt,404。
  3. 再打开 /sitemap.xml,也是 404。
  4. 这两个就是第 3 步和第 4 步。

2 Read your own pages first · อ่านหน้าเว็บของตัวเองก่อน · 先读一遍你自己的页面

🇬🇧 English

Before asking Google to look, look yourself. A search engine reads about five things from a page.

If you have no site yet, the first file makes four pages to practise on, two of them deliberately wrong.

🇹🇭 ไทย

ก่อนจะขอให้ Google มาดู ลองดูเองก่อน เสิร์ชเอนจินอ่านจากหน้าเว็บอยู่ราวห้าอย่างเท่านั้น

ถ้ายังไม่มีเว็บ ไฟล์แรกจะสร้างสี่หน้าให้ลองเล่น โดยตั้งใจทำให้ผิดไว้สองหน้า

🇨🇳 中文

在请 Google 来看之前,自己先看。搜索引擎从一个页面上读的东西大约就五样。

如果你还没有站,第一个文件会做四个页面给你练手,其中两个是故意写错的。

📄 NEW FILE · make_sample.py

"""Build a small site to test the scripts against, the shape a student's is.

Four pages, one of them deliberately missing its description and sharing a
title with another, so the checker in step 5 has something to find.
"""
import sys
from pathlib import Path

SITE = Path(__file__).parent / "site"
sys.stdout.reconfigure(encoding="utf-8")

PAGE = """<!doctype html>
<html lang="{lang}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
{description}
</head>
<body>
<h1>{heading}</h1>
<p>{body}</p>
</body>
</html>
"""

DESC = '<meta name="description" content="{0}">'

PAGES = {
    "index.html": dict(
        lang="en", title="Nari's notebook",
        description=DESC.format("Notes on learning Python in Chiang Mai, "
                                "with small programs you can run."),
        heading="Nari's notebook",
        # The front page links to one of the three. The other is an orphan,
        # which is what the last challenge goes looking for.
        body='Everything I have worked out so far. '
             '<a href="python.html">Learning Python</a>.'),
    "python.html": dict(
        lang="en", title="Learning Python",
        description=DESC.format("How I started with Python, and the first "
                                "five things that confused me."),
        heading="Learning Python", body="Start at the prompt, not in a file."),
    "khao-soi.html": dict(
        lang="en", title="Learning Python",
        description="",
        heading="Khao soi", body="The best bowl is on Nimmanhaemin."),
    "drafts/notes.html": dict(
        lang="en", title="Rough notes",
        description=DESC.format("Half-finished notes, not meant for anyone."),
        heading="Rough notes", body="Do not read this yet."),
}


def main():
    for name, fields in PAGES.items():
        target = SITE / name
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(PAGE.format(**fields), encoding="utf-8")
        print(f"  wrote {name}")
    print(f"{len(PAGES)} pages in {SITE}/")


if __name__ == "__main__":
    main()

📄 NEW FILE · check1.py

"""check1.py — read your own pages the way a search engine will."""
import html
import re
import sys
from collections import Counter
from pathlib import Path

FOLDER = Path("site")

# Google shows roughly this much. Longer is not an error; it is just cut off
# in the results, and the part it cuts is the part you wrote last.
TITLE_ROOM = 60
DESCRIPTION_ROOM = 155

sys.stdout.reconfigure(encoding="utf-8")


def first(pattern, text):
    """The first capture of a pattern, unescaped, or None."""
    found = re.search(pattern, text, re.I | re.S)
    return html.unescape(found.group(1)).strip() if found else None


def read(path):
    """Pull out the handful of things a search engine actually reads."""
    text = path.read_text(encoding="utf-8")
    return {
        "title": first(r"<title>(.*?)</title>", text),
        "description": first(r'<meta\s+name="description"\s+content="(.*?)"', text),
        "lang": first(r"<html[^>]*\blang=\"([^\"]+)\"", text),
        "h1": first(r"<h1[^>]*>(.*?)</h1>", text),
        "noindex": bool(re.search(r'name="robots"[^>]*noindex', text, re.I)),
    }


def main():
    pages = {path: read(path) for path in sorted(FOLDER.rglob("*.html"))}

    # Two pages with the same title look like one page to a search engine,
    # and it picks which to show. That choice is not usually yours.
    titles = Counter(page["title"] for page in pages.values() if page["title"])

    problems = 0
    for path, page in pages.items():
        found = []
        if not page["title"]:
            found.append("no title")
        elif titles[page["title"]] > 1:
            found.append(f"title shared with {titles[page['title']] - 1} other")
        elif len(page["title"]) > TITLE_ROOM:
            found.append(f"title is {len(page['title'])} characters, "
                         f"{TITLE_ROOM} get shown")

        if not page["description"]:
            found.append("no description, so Google writes one for you")
        elif len(page["description"]) > DESCRIPTION_ROOM:
            found.append(f"description is {len(page['description'])} characters")

        if not page["lang"]:
            found.append("no lang on <html>")
        if not page["h1"]:
            found.append("no <h1>")

        name = path.relative_to(FOLDER).as_posix()
        if found:
            problems = problems + len(found)
            print(f"{name}")
            for trouble in found:
                print(f"   - {trouble}")
        else:
            print(f"{name}  ok")

    print()
    print(f"{len(pages)} pages, {problems} things to fix")


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
TITLE_ROOM = 60Roughly what Google shows. Longer is cut, and it cuts the end.ประมาณที่ Google แสดง ถ้ายาวกว่าจะถูกตัด และตัดท้าย大致是 Google 显示的量。更长会被截掉,截的是后面。
titles = Counter(page["title"]Two pages with one title look like one page. Google picks which.สองหน้าที่ใช้ชื่อเดียวกันดูเหมือนหน้าเดียว Google เป็นคนเลือกว่าจะโชว์อันไหน两个页面同一个标题,看起来就是一个页面。Google 替你选。
"no description, so Google writes one for you"It will take a sentence off the page. Usually not the one you want.มันจะหยิบประโยคจากหน้านั้นมาใช้ ซึ่งมักไม่ใช่ประโยคที่คุณอยากได้它会从页面上抓一句。通常不是你想要的那句。

🖥️ WHAT IT PRINTED · on the practice site

drafts/notes.html  ok
index.html  ok
khao-soi.html
   - title shared with 1 other
   - no description, so Google writes one for you
python.html
   - title shared with 1 other

4 pages, 3 things to fix
YOUR TURN
  1. Run both files. Read the three complaints.
  2. Fix the title on khao-soi.html and run it again.
  3. Point FOLDER at your own site.
  4. Fix whatever it finds before going on.
  1. รันทั้งสองไฟล์ แล้วอ่านคำบ่นทั้งสามข้อ
  2. แก้ชื่อของ khao-soi.html แล้วรันใหม่
  3. ชี้ FOLDER ไปที่เว็บของคุณเอง
  4. แก้ทุกอย่างที่มันเจอก่อนไปต่อ
  1. 两个文件都跑一遍,读那三条抱怨。
  2. 把 khao-soi.html 的标题改掉,再跑一次。
  3. 把 FOLDER 指向你自己的站。
  4. 在继续之前,把它找到的都修掉。

🗺️ Tutorial 2 — Tell it where everything is · บทที่ 2 — บอกมันว่าอะไรอยู่ตรงไหน · 教程 2 —— 告诉它东西都在哪

🇬🇧 English

Two small files do most of the work: a list of your pages, and a note saying where that list lives.

🇹🇭 ไทย

ไฟล์เล็ก ๆ สองไฟล์ทำงานเกือบทั้งหมด คือรายชื่อหน้าเว็บของคุณ กับบันทึกที่บอกว่ารายชื่อนั้นอยู่ที่ไหน

🇨🇳 中文

两个小文件干了大部分的活:一份你的页面清单,和一张写着清单在哪的纸条。

3 A sitemap, and a robots.txt that points at it · sitemap กับ robots.txt ที่ชี้ไปหามัน · 一份 sitemap,和一个指向它的 robots.txt

🇬🇧 English

A sitemap is a list of the pages you want found. Not every page: the ones you are asking for.

Note what the script leaves out, and what it does with index.html.

🇹🇭 ไทย

sitemap คือรายชื่อหน้าที่คุณอยากให้ถูกค้นเจอ ไม่ใช่ทุกหน้า แต่คือหน้าที่คุณกำลังขอ

สังเกตว่าสคริปต์ตัดอะไรออก และทำอะไรกับ index.html

🇨🇳 中文

sitemap 是一份你希望被找到的页面清单。不是所有页面,是你要求的那些。

注意脚本排除了什么,以及它怎么处理 index.html。

📄 NEW FILE · sitemap1.py

"""sitemap1.py — write the list of pages you want Google to know about."""
import sys
import time
from pathlib import Path

# Your site's address. The trailing slash matters; every URL is built on it.
SITE = "https://nari.github.io/"
FOLDER = Path("site")

# Folders whose pages should not be in the sitemap. A sitemap is a list of
# pages you are asking to have indexed, so drafts do not belong in it.
SKIP = {"drafts"}

sys.stdout.reconfigure(encoding="utf-8")


def pages():
    """Every .html file in the folder, as a web address, shortest first."""
    found = []
    for path in sorted(FOLDER.rglob("*.html")):
        parts = path.relative_to(FOLDER).parts
        if SKIP & set(parts[:-1]):
            continue

        # index.html is the folder itself. Listing both is one page twice.
        where = "/".join(parts[:-1] + ((), (parts[-1],))[parts[-1] != "index.html"])
        found.append((SITE + where, path))
    return found


def main():
    lines = ['<?xml version="1.0" encoding="UTF-8"?>',
             '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
    for url, path in pages():
        # The date the file changed. Google reads it as a hint, not a promise.
        when = time.strftime("%Y-%m-%d", time.gmtime(path.stat().st_mtime))
        lines.append(f"  <url><loc>{url}</loc><lastmod>{when}</lastmod></url>")
        print(f"  {url}")
    lines.append("</urlset>")

    target = FOLDER / "sitemap.xml"
    target.write_text("\n".join(lines) + "\n", encoding="utf-8")

    # robots.txt is how a search engine finds the sitemap without being told.
    robots = FOLDER / "robots.txt"
    robots.write_text("User-agent: *\nAllow: /\n\n"
                      f"Sitemap: {SITE}sitemap.xml\n", encoding="utf-8")

    print(f"{len(lines) - 3} pages -> {target}")
    print(f"and {robots}")


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
SKIP = {"drafts"}A sitemap is a request. Do not request the pages you are hiding.sitemap คือคำขอ อย่าขอหน้าที่คุณกำลังซ่อนอยู่sitemap 是一个请求。别去请求你正在藏的页面。
parts[-1] != "index.html"A folder and its index.html are one page. List it once.โฟลเดอร์กับ index.html ของมันคือหน้าเดียวกัน ให้ใส่แค่ครั้งเดียว一个文件夹和它的 index.html 是同一个页面。只列一次。
Sitemap: {SITE}sitemap.xmlThis line is how a crawler finds the list without being told.บรรทัดนี้คือวิธีที่ตัวเก็บข้อมูลหาเจอรายชื่อโดยไม่ต้องมีใครบอก这一行是爬虫在没人告诉它的情况下找到清单的办法。

🖥️ WHAT IT PRINTED · three pages, one skipped

  https://nari.github.io/
  https://nari.github.io/khao-soi.html
  https://nari.github.io/python.html
3 pages -> site\sitemap.xml
and site\robots.txt
WHETHER A FOLDER URL WORKS DEPENDS ON THE HOST This script turns about/index.html into /about/. GitHub Pages serves that, which microsoft.github.io/monaco-editor/ confirms. Other hosts do not. This site is S3 behind CloudFront. There /io2026/ is a 404 and /io2026/index.html is a 200. Challenge 1 checks yours rather than trusting either of us. สคริปต์นี้เปลี่ยน about/index.html ให้เป็น /about/ GitHub Pages ให้บริการแบบนั้นได้ ซึ่ง microsoft.github.io/monaco-editor/ ยืนยันแล้ว แต่โฮสต์อื่นทำไม่ได้ บนเว็บนี้ซึ่งเป็น S3 หลัง CloudFront /io2026/ ได้ 404 ส่วน /io2026/index.html ได้ 200 โจทย์ข้อ 1 จะตรวจของคุณเอง แทนที่จะเชื่อเราสองคน 这个脚本把 about/index.html 变成 /about/。GitHub Pages 支持这样,microsoft.github.io/monaco-editor/ 可以证明。别的主机不一定。在这个站上(S3 加 CloudFront),/io2026/ 是 404,而 /io2026/index.html 是 200。第 1 题会去查你自己的,而不是听我们任何一方的。
YOUR TURN
  1. Run it. Open site/sitemap.xml and read it.
  2. Notice drafts/notes.html is not in there.
  3. Add your own site's address to SITE.
  4. Commit both files and push.
  1. รันดู แล้วเปิด site/sitemap.xml อ่าน
  2. สังเกตว่า drafts/notes.html ไม่อยู่ในนั้น
  3. ใส่ที่อยู่เว็บของคุณเองลงใน SITE
  4. คอมมิตทั้งสองไฟล์แล้วพุช
  1. 运行,打开 site/sitemap.xml 读一读。
  2. 注意 drafts/notes.html 不在里面。
  3. 把你自己站的地址填进 SITE。
  4. 两个文件都提交并推上去。

4 Check it as it is actually served · ตรวจมันในแบบที่ถูกเสิร์ฟจริง · 按它真正被送出的样子检查

🇬🇧 English

Everything so far read files on your disk. Only the served version counts.

Run it on your site, and on a site that has none of this, and the difference is the whole point.

🇹🇭 ไทย

ที่ผ่านมาทั้งหมดคืออ่านไฟล์บนดิสก์ แต่เวอร์ชันที่ถูกเสิร์ฟออกไปเท่านั้นที่นับ

รันกับเว็บของคุณ แล้วรันกับเว็บที่ไม่มีอะไรพวกนี้เลย ความต่างนั่นแหละคือประเด็นทั้งหมด

🇨🇳 中文

到目前为止都是在读硬盘上的文件。只有被送出去的那个版本算数。

在你的站上跑一次,再在一个什么都没有的站上跑一次,那个差别就是全部的重点。

📄 NEW FILE · live1.py

"""live1.py — check the site the way Google will, over the network.

Everything up to here read files on your disk. This reads the site as it
is actually served, which is the only version that counts.
"""
import re
import sys
import urllib.error
import urllib.request

SITE = sys.argv[1] if len(sys.argv) > 1 else "https://nari.github.io/"
if not SITE.endswith("/"):
    SITE = SITE + "/"

sys.stdout.reconfigure(encoding="utf-8")


def fetch(url):
    """Get a page, and the headers, without pretending to be a browser."""
    request = urllib.request.Request(url, headers={"User-Agent": "a lesson"})
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return response.status, dict(response.headers), response.read()
    except urllib.error.HTTPError as refused:
        return refused.code, dict(refused.headers), b""
    except urllib.error.URLError as trouble:
        print("cannot reach", url, "-", trouble.reason)
        raise SystemExit(1)


def main():
    print("checking", SITE)

    status, headers, body = fetch(SITE)
    print(f"  the front page          {status}")

    # This header can hide a whole site from search, and nothing on the
    # page shows it. It is the first thing to rule out.
    blocked = headers.get("X-Robots-Tag", "")
    print(f"  X-Robots-Tag            {blocked or 'not set, good'}")

    status, _, robots = fetch(SITE + "robots.txt")
    print(f"  robots.txt              {status}")
    sitemap_line = ""
    if status == 200:
        text = robots.decode("utf-8", errors="replace")
        if re.search(r"^\s*Disallow:\s*/\s*$", text, re.M | re.I):
            print("     Disallow: / is in there. That hides everything.")
        found = re.search(r"^\s*Sitemap:\s*(\S+)", text, re.M | re.I)
        sitemap_line = found.group(1) if found else ""
        print(f"     names a sitemap      {sitemap_line or 'no'}")

    target = sitemap_line or SITE + "sitemap.xml"
    status, _, xml = fetch(target)
    print(f"  sitemap.xml             {status}")
    if status != 200:
        return

    urls = re.findall(r"<loc>\s*([^<\s]+)\s*</loc>",
                      xml.decode("utf-8", errors="replace"))
    print(f"     lists                {len(urls)} pages")

    # A sitemap full of dead links is worse than no sitemap. Check a few.
    bad = []
    for url in urls[:12]:
        code, _, _ = fetch(url)
        if code != 200:
            bad.append((code, url))
    print(f"     checked              {min(len(urls), 12)}, "
          f"{len(bad)} not reachable")
    for code, url in bad:
        print(f"       {code}  {url}")


if __name__ == "__main__":
    main()

🖥️ A SITE WITH ALL OF IT · run against a real one

checking https://krueng.ai/
  the front page          200
  X-Robots-Tag            not set, good
  robots.txt              200
     names a sitemap      https://krueng.ai/sitemap.xml
  sitemap.xml             200
     lists                113 pages
     checked              12, 0 not reachable

🖥️ AND A BARE GITHUB PAGES SITE · the same script

checking https://octocat.github.io/
  the front page          200
  X-Robots-Tag            not set, good
  robots.txt              404
  sitemap.xml             404
LineWhat it does · ทำอะไร · 做什么
re.search(r"^\s*Disallow:\s*/\s*$"One line in robots.txt can hide everything. Look for it by name.บรรทัดเดียวใน robots.txt ซ่อนได้ทุกอย่าง ให้ตามหามันตรง ๆrobots.txt 里一行就能把一切藏起来。直接按名字去找它。
for url in urls[:12]:A sitemap of dead links teaches Google to stop reading it.sitemap ที่เต็มไปด้วยลิงก์ตายสอนให้ Google เลิกอ่านมัน一份全是死链的 sitemap,会教会 Google 别再读它。
YOUR TURN
  1. Run it on your own site. Everything should be 200.
  2. Run it on https://octocat.github.io/ for contrast.
  3. Add Disallow: / to your robots.txt and rerun.
  4. Take it out again. Now you know what that looks like.
  1. รันกับเว็บของคุณเอง ทุกอย่างควรได้ 200
  2. รันกับ https://octocat.github.io/ เพื่อเทียบ
  3. ใส่ Disallow: / ลงใน robots.txt แล้วรันใหม่
  4. แล้วเอาออก ทีนี้คุณรู้แล้วว่าหน้าตามันเป็นยังไง
  1. 在你自己的站上跑,应该全是 200。
  2. 再在 https://octocat.github.io/ 上跑一次做对比。
  3. 往 robots.txt 里加一行 Disallow: /,再跑。
  4. 然后删掉。现在你知道那长什么样了。

🔎 Tutorial 3 — Search Console itself · บทที่ 3 — ตัว Search Console เอง · 教程 3 —— Search Console 本身

🇬🇧 English

Two jobs here, and they are both done in a browser. Prove the site is yours, then hand over the sitemap.

The first one has a catch specific to github.io.

🇹🇭 ไทย

ตรงนี้มีสองงาน และทั้งคู่ทำในเบราว์เซอร์ คือพิสูจน์ว่าเว็บเป็นของคุณ แล้วส่ง sitemap ให้

งานแรกมีข้อติดที่เป็นเฉพาะของ github.io

🇨🇳 中文

这里有两件事,都在浏览器里做:证明这个站是你的,然后把 sitemap 交上去。

第一件对 github.io 来说有个坎。

5 Proving the site is yours · พิสูจน์ว่าเว็บเป็นของคุณ · 证明这个站是你的

🇬🇧 English

Go to search.google.com/search-console and add a property. You are offered two kinds, and for a github.io address only one of them can work.

🇹🇭 ไทย

ไปที่ search.google.com/search-console แล้วเพิ่ม property มันจะให้เลือกสองแบบ และสำหรับที่อยู่ github.io มีแบบเดียวเท่านั้นที่ใช้ได้

🇨🇳 中文

去 search.google.com/search-console 添加一个资源。它给你两种选择,而对 github.io 地址来说只有一种行得通。

Domain propertyแบบ Domain网域资源URL prefix propertyแบบ URL prefix网址前缀资源
What it coversครอบคลุมอะไร覆盖什么Domain: every subdomain, http and httpsDomain: ทุกซับโดเมน ทั้ง http และ https网域:所有子域名,http 和 httpsURL prefix: exactly that addressURL prefix: เฉพาะที่อยู่นั้นเป๊ะ ๆ网址前缀:就那一个地址
How you prove itพิสูจน์ยังไง怎么证明Domain: a DNS record, only thatDomain: ต้องใช้ DNS record เท่านั้น网域:只能用 DNS 记录URL prefix: a file, a meta tag, and othersURL prefix: ใช้ไฟล์ แท็ก meta หรือวิธีอื่น网址前缀:文件、meta 标签,还有别的
On yourname.github.ioบน yourname.github.io在 yourname.github.io 上Domain: impossible, you own no DNS thereDomain: ทำไม่ได้ คุณไม่ได้เป็นเจ้าของ DNS ตรงนั้น网域:做不到,那里的 DNS 不归你URL prefix: this is the oneURL prefix: ใช้อันนี้แหละ网址前缀:就用这个

💻 THE FILE METHOD · the one that always works on Pages

# Search Console gives you a file named something like:
#   google1a2b3c4d5e6f7890.html
# Put it at the top of the repository, not in a folder.

git add google1a2b3c4d5e6f7890.html
git commit -m 'search console'
git push

# Wait for the build, then check it yourself before clicking Verify:
curl -I https://yourname.github.io/google1a2b3c4d5e6f7890.html
VERIFY THE FILE IS SERVED BEFORE YOU CLICK VERIFY Two things eat that file. A push that has not finished building yet. And Jekyll, which GitHub Pages runs by default. It skips files and folders whose names start with an underscore. The file Google gives you does not start with one. It survives. Check with curl anyway. A failed verification is slower to undo than a ten second check. มีสองอย่างที่กินไฟล์นั้นไป อย่างแรกคือการพุชที่ยังบิลด์ไม่เสร็จ อย่างที่สองคือ Jekyll ซึ่ง GitHub Pages รันให้โดยค่าเริ่มต้น และมันจะข้ามไฟล์กับโฟลเดอร์ที่ชื่อขึ้นต้นด้วยขีดล่าง ไฟล์ที่ Google ให้มาไม่ได้ขึ้นต้นด้วยขีดล่าง จึงรอด แต่ก็ควรเช็กด้วย curl อยู่ดี การยืนยันที่ล้มเหลวแก้ยากกว่าการเช็กสิบวินาที 有两样东西会吃掉那个文件:一是还没构建完的推送,二是 GitHub Pages 默认会跑的 Jekyll,它会跳过名字以下划线开头的文件和文件夹。Google 给你的文件不是以下划线开头的,所以它没事。但还是用 curl 查一下。验证失败比十秒钟的检查麻烦多了。
YOUR TURN
  1. Open Search Console and choose URL prefix.
  2. Type your full address, with https and the trailing slash.
  3. Download the HTML file, commit it, push it.
  4. curl -I it. Only then click Verify.
  1. เปิด Search Console แล้วเลือกแบบ URL prefix
  2. พิมพ์ที่อยู่เต็มของคุณ พร้อม https และเครื่องหมายทับท้าย
  3. ดาวน์โหลดไฟล์ HTML คอมมิต แล้วพุช
  4. curl -I ดูก่อน แล้วค่อยกด Verify
  1. 打开 Search Console,选网址前缀。
  2. 输入你的完整地址,带 https 和结尾的斜杠。
  3. 下载那个 HTML 文件,提交,推上去。
  4. 先 curl -I 一下。然后再点验证。

6 Handing over the sitemap, and waiting · ส่ง sitemap ให้ แล้วรอ · 把 sitemap 交上去,然后等

🇬🇧 English

Sitemaps, paste sitemap.xml, submit. It should say Success within a minute or two.

Then nothing happens for days, and that is the part people get wrong rather than the setup.

🇹🇭 ไทย

ไปที่ Sitemaps วาง sitemap.xml แล้วกดส่ง มันควรขึ้นว่า Success ภายในหนึ่งสองนาที

จากนั้นจะไม่มีอะไรเกิดขึ้นเป็นวัน ๆ และนั่นคือส่วนที่คนเข้าใจผิด มากกว่าเรื่องการตั้งค่า

🇨🇳 中文

进 Sitemaps,粘贴 sitemap.xml,提交。一两分钟内应该显示 Success。

然后好几天什么都不会发生,而人们弄错的通常是这一点,不是设置。

What it saysมันบอกว่า它说什么What it meansแปลว่าอะไร什么意思
SuccessSuccessSuccessread the file, found the URLs. Not indexed yetอ่านไฟล์แล้ว เจอ URL แล้ว แต่ยังไม่ได้จัดทำดัชนี读到了文件,找到了网址。还没被收录
Couldn't fetchCouldn't fetch无法获取wrong address, or not built yet. Check with curlที่อยู่ผิด หรือยังบิลด์ไม่เสร็จ ให้เช็กด้วย curl地址错了,或者还没构建完。用 curl 查
Discovered, not indexedDiscovered, not indexed已发现,尚未编入索引it knows. It has not decided you are worth itมันรู้แล้ว แต่ยังไม่ตัดสินใจว่าคุณคุ้มค่าพอ它知道了。但还没决定你值不值
Crawled, not indexedCrawled, not indexed已抓取,尚未编入索引it read the page and passed. Usually thin or duplicateมันอ่านหน้านั้นแล้วผ่าน มักเพราะเนื้อหาบางหรือซ้ำ它读了这一页然后跳过。通常是内容太薄或重复
GOOGLE'S PING URL IS GONE. DO NOT COPY IT FROM OLD GUIDES. Guides still tell you to fetch google.com/ping?sitemap=... after every change. That address now answers 404, measured while writing this. Google removed it and reads sitemaps on its own schedule. Bing and Yandex kept a way in, and that is challenge 4. คู่มือหลายที่ยังบอกให้เรียก google.com/ping?sitemap=... ทุกครั้งที่มีการเปลี่ยนแปลง ตอนนี้ที่อยู่นั้นตอบ 404 ซึ่งวัดตอนเขียนหน้านี้ Google เอามันออกไปแล้ว และอ่าน sitemap ตามตารางของตัวเอง ส่วน Bing กับ Yandex ยังเปิดทางไว้ นั่นคือโจทย์ข้อ 4 很多指南还告诉你每次改动后去请求 google.com/ping?sitemap=...。那个地址现在返回 404,写这一页时实测过。Google 把它撤了,改成按自己的节奏读 sitemap。Bing 和 Yandex 还留了一条路,那是第 4 题。
YOUR TURN
  1. Submit the sitemap. Check it says Success.
  2. Use URL Inspection on your front page. Request indexing, once.
  3. Close the tab. Come back in three days, not three hours.
  4. Then read Pages, and see which state your pages are in.
  1. ส่ง sitemap แล้วเช็กว่าขึ้น Success
  2. ใช้ URL Inspection กับหน้าแรก แล้วขอให้จัดทำดัชนี หนึ่งครั้ง
  3. ปิดแท็บไปเลย กลับมาอีกทีในสามวัน ไม่ใช่สามชั่วโมง
  4. แล้วค่อยอ่านหน้า Pages ดูว่าหน้าเว็บของคุณอยู่ในสถานะไหน
  1. 提交 sitemap,确认它显示 Success。
  2. 对首页用一次网址检查,请求编入索引,只一次。
  3. 关掉标签页。三天后再来,不是三小时。
  4. 然后看 Pages 那一栏,看你的页面处在哪个状态。

🏆 Tutorial 4 — Four challenges · บทที่ 4 — โจทย์สี่ข้อ · 教程 4 —— 四道题

🇬🇧 English

The first two find problems Google would find for you, slowly. The last two save you from doing this by hand.

🇹🇭 ไทย

สองข้อแรกหาปัญหาที่ Google จะหาเจอให้คุณเองแบบช้า ๆ ส่วนสองข้อหลังช่วยไม่ให้คุณต้องทำมือ

🇨🇳 中文

前两道找的是 Google 迟早会替你找到的问题。后两道省掉你手工做这些事。

7 Try first. The answers are folded away. · ลองเองก่อน เฉลยพับซ่อนไว้ · 先自己试。答案折起来放着。

🇬🇧 English

Every one of these was run before it was written down.

🇹🇭 ไทย

ทุกข้อในนี้ถูกรันจริงก่อนจะถูกเขียนลงมา

🇨🇳 中文

这里每一道都是先跑过才写下来的。

1Never submit a page that is not there · อย่าส่งหน้าที่ไม่มีอยู่จริง · 别提交一个并不存在的页面

🇬🇧 English

sitemap1.py lists every file it finds. A file on your disk is not the same as a page on your site.

Check each address over the network before you put it in the list.

🇹🇭 ไทย

sitemap1.py ใส่ทุกไฟล์ที่มันเจอ แต่ไฟล์บนดิสก์ไม่ใช่สิ่งเดียวกับหน้าบนเว็บ

ตรวจแต่ละที่อยู่ผ่านเน็ตก่อนจะเอาใส่ลงในรายชื่อ

🇨🇳 中文

sitemap1.py 把找到的文件全列上。硬盘上的文件不等于站上的页面。

在放进清单之前,先联网查一下每个地址。

Show the answerดูเฉลย看答案

📄 verify1.py

"""verify1.py — challenge 1: never submit a page that is not there.

sitemap1.py lists every file it finds. A file on your disk is not the same
as a page on your site: the name can differ by a capital letter, the push
can have failed, the build can have skipped it. Check first, list second.
"""
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

# Point it at your own site and the folder you publish from.
#   python verify1.py https://nari.github.io/ site
SITE = sys.argv[1] if len(sys.argv) > 1 else "https://nari.github.io/"
FOLDER = Path(sys.argv[2] if len(sys.argv) > 2 else "site")
SKIP = {"drafts"}

sys.stdout.reconfigure(encoding="utf-8")


def addresses():
    """The same list sitemap1.py would write."""
    found = []
    for path in sorted(FOLDER.rglob("*.html")):
        parts = path.relative_to(FOLDER).parts
        if SKIP & set(parts[:-1]):
            continue
        tail = "/".join(parts[:-1] + ((), (parts[-1],))[parts[-1] != "index.html"])
        found.append(SITE + tail)
    return found


def answers(url):
    """Does this address really return a page? Follow nothing, forgive nothing."""
    request = urllib.request.Request(url, method="HEAD",
                                     headers={"User-Agent": "a lesson"})
    try:
        with urllib.request.urlopen(request, timeout=20) as response:
            return response.status
    except urllib.error.HTTPError as refused:
        return refused.code
    except urllib.error.URLError:
        return 0


def main():
    good, bad = [], []
    for url in addresses():
        code = answers(url)
        (good if code == 200 else bad).append((code, url))
        print(f"  {code or 'dead':>4}  {url}")
        time.sleep(0.2)          # be polite to your own server

    print()
    print(f"{len(good)} would go in the sitemap, {len(bad)} would not")
    if bad:
        print("Fix these or drop them. A sitemap of 404s teaches Google")
        print("that your sitemap is not worth reading.")


if __name__ == "__main__":
    main()

🖥️ WHAT IT FOUND · 499 pages of a real site

python verify1.py https://krueng.ai/ ../HTML

496 would go in the sitemap, 3 would not

   404  https://krueng.ai/games/mueang-lanna
   404  https://krueng.ai/io2026
   404  https://krueng.ai/it/ai/plans

🇬🇧 English

All three are folders with an index.html inside. This host does not serve those.

That is the careful box from step 3, found by a script instead of by a reader.

🇹🇭 ไทย

ทั้งสามอันเป็นโฟลเดอร์ที่มี index.html อยู่ข้างใน โฮสต์นี้ไม่ได้ให้บริการแบบนั้น

นั่นคือกล่องคำเตือนจากขั้นที่ 3 แต่คราวนี้สคริปต์เป็นคนเจอ ไม่ใช่คนอ่าน

🇨🇳 中文

这三个都是里面放着 index.html 的文件夹。这个主机不提供那种服务。

就是第 3 步那个警告框,只不过这次是脚本发现的,不是读者。

2Find the pages nothing links to · หาหน้าที่ไม่มีอะไรลิงก์ไปหา · 找出没有任何链接指向的页面

🇬🇧 English

Google follows links. A page in your sitemap that nothing points at is one it reaches last and trusts least.

Usually it is a page you forgot, not one you meant to hide.

🇹🇭 ไทย

Google เดินตามลิงก์ หน้าที่อยู่ใน sitemap แต่ไม่มีอะไรชี้ไปหา คือหน้าที่มันไปถึงช้าที่สุดและเชื่อถือน้อยที่สุด

ส่วนใหญ่มันคือหน้าที่คุณลืม ไม่ใช่หน้าที่คุณตั้งใจซ่อน

🇨🇳 中文

Google 跟着链接走。sitemap 里一个没人指向的页面,它最后才到、最不信任。

通常那是你忘了的页面,不是你想藏的。

Show the answerดูเฉลย看答案

📄 orphans1.py

"""orphans1.py — challenge 2: find the pages nothing links to.

Google follows links. A page in your sitemap that no other page points at
is a page it will reach last and trust least, and usually it is a page you
forgot about rather than one you meant to hide.
"""
import re
import sys
from pathlib import Path

FOLDER = Path("site")
sys.stdout.reconfigure(encoding="utf-8")


def linked_from(path):
    """Every local page this one points at, as a path inside the folder."""
    text = path.read_text(encoding="utf-8")
    here = path.parent
    out = set()
    for href in re.findall(r'<a[^>]+href="([^"]+)"', text, re.I):
        # Leave the outside world, anchors and mail links alone.
        if href.startswith(("http://", "https://", "#", "mailto:", "//")):
            continue
        target = (here / href.split("#")[0].split("?")[0]).resolve()
        out.add(target)
    return out


def main():
    pages = sorted(FOLDER.rglob("*.html"))
    pointed_at = set()
    for path in pages:
        pointed_at |= linked_from(path)

    front = (FOLDER / "index.html").resolve()
    orphans = [p for p in pages
               if p.resolve() not in pointed_at and p.resolve() != front]

    print(f"{len(pages)} pages, {len(pointed_at)} link targets found")
    print()
    if not orphans:
        print("nothing is orphaned")
        return
    print("nothing links to these:")
    for path in orphans:
        print("   ", path.relative_to(FOLDER).as_posix())
    print()
    print("Either link to them from a page that is already indexed,")
    print("or take them out of the sitemap. Do not do neither.")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · on the practice site

4 pages, 1 link targets found

nothing links to these:
    drafts/notes.html
    khao-soi.html

Either link to them from a page that is already indexed,
or take them out of the sitemap. Do not do neither.
LineWhat it does · ทำอะไร · 做什么
if href.startswith(("http://", "https://", "#", "mailto:"Only local links count. An outside link is not your structure.นับเฉพาะลิงก์ภายใน ลิงก์ออกข้างนอกไม่ใช่โครงสร้างของคุณ只算站内链接。往外的链接不是你的结构。
p.resolve() != frontThe front page is the way in. Nothing has to link to it.หน้าแรกคือทางเข้า ไม่จำเป็นต้องมีอะไรลิงก์ไปหามัน首页就是入口。不需要任何东西链接到它。

3Rebuild the sitemap on every push · สร้าง sitemap ใหม่ทุกครั้งที่พุช · 每次推送都重建 sitemap

🇬🇧 English

A sitemap you regenerate by hand is a sitemap that is one page out of date.

GitHub can run the script for you and commit the result.

🇹🇭 ไทย

sitemap ที่ต้องสร้างใหม่ด้วยมือ คือ sitemap ที่ล้าหลังอยู่หนึ่งหน้าเสมอ

GitHub รันสคริปต์ให้คุณได้ แล้วคอมมิตผลลัพธ์ให้ด้วย

🇨🇳 中文

靠手工重建的 sitemap,总是慢一个页面。

GitHub 可以替你跑那个脚本,还能把结果提交上去。

Show the answerดูเฉลย看答案

📄 .github/workflows/sitemap.yml

# .github/workflows/sitemap.yml
# Challenge 3: rebuild the sitemap every time you push, so it is never
# one page behind the site.
name: Rebuild the sitemap

on:
  push:
    branches: [main]

# A workflow can read the repository by default. Writing back to it needs
# saying so here, and this one commits the sitemap it just built.
permissions:
  contents: write

jobs:
  sitemap:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Write sitemap.xml and robots.txt
        run: python sitemap1.py

      - name: Commit them, but only if they changed
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add site/sitemap.xml site/robots.txt
          # --quiet returns non-zero when there is something staged, so this
          # commits only when there is a real change. Without it the job
          # fails on every push that did not touch a page.
          git diff --staged --quiet || git commit -m "sitemap: rebuild"
          git push
LineWhat it does · ทำอะไร · 做什么
permissions:A workflow can read the repository. Writing needs saying so.เวิร์กโฟลว์อ่านรีโพได้อยู่แล้ว แต่การเขียนต้องระบุไว้工作流默认能读仓库。要写就得写明。
git diff --staged --quiet || git commitCommit only when something changed, or every push fails.คอมมิตเฉพาะตอนที่มีอะไรเปลี่ยน ไม่งั้นทุกการพุชจะล้มเหลว只在有变化时提交,否则每次推送都会失败。
A WORKFLOW THAT PUSHES DOES NOT TRIGGER ITSELF This job commits to the same branch that triggered it. That looks like a loop. It is not one. A push made with the built-in token starts no new workflow run. That is deliberate. So a second workflow will never react to this commit. งานนี้คอมมิตกลับไปที่แบรนช์เดียวกับที่เรียกมันขึ้นมา ซึ่งดูเหมือนวงวน แต่มันไม่ใช่ การพุชด้วยโทเคนที่มีมาให้จะไม่เริ่มเวิร์กโฟลว์รอบใหม่ และนั่นเป็นการออกแบบไว้อย่างตั้งใจ แปลว่าถ้าวันหนึ่งคุณอยากให้เวิร์กโฟลว์ตัวที่สองตอบสนองต่อคอมมิตนี้ มันจะไม่ทำงาน 这个任务会提交回触发它的那个分支,看起来像个死循环。它不是。用内置令牌做的推送不会再启动一次工作流,这是故意设计的。这也意味着,如果你哪天想让第二个工作流响应这个提交,它不会响应。

4Tell Bing the moment something changes · บอก Bing ทันทีที่มีอะไรเปลี่ยน · 东西一变就告诉 Bing

🇬🇧 English

Google's ping endpoint is gone. Bing and Yandex share one called IndexNow, and it is two steps.

Put a key file on the site, then name the pages that changed. Check the key file is live before sending.

🇹🇭 ไทย

ปลายทางสำหรับ ping ของ Google หายไปแล้ว ส่วน Bing กับ Yandex ใช้ตัวเดียวกันชื่อ IndexNow และมีสองขั้นตอน

วางไฟล์คีย์ไว้บนเว็บ แล้วบอกชื่อหน้าที่เปลี่ยน ตรวจว่าไฟล์คีย์ใช้งานได้จริงก่อนส่ง

🇨🇳 中文

Google 的 ping 端点没了。Bing 和 Yandex 共用一个叫 IndexNow 的,分两步。

把一个 key 文件放到站上,再报出变了的页面。发送前先确认 key 文件是活的。

Show the answerดูเฉลย看答案

📄 tell1.py

"""tell1.py — tell a search engine a page changed, without waiting.

Google used to take a ping at /ping?sitemap=. That endpoint now answers
404; they removed it and say they read sitemaps on their own schedule.
Bing and Yandex kept a way in, called IndexNow, and it is two steps: put
a key file on your site, then name the pages that changed.
"""
import json
import secrets
import sys
import urllib.error
import urllib.request
from pathlib import Path

SITE = "https://nari.github.io/"
FOLDER = Path("site")
INDEXNOW = "https://api.indexnow.org/indexnow"

sys.stdout.reconfigure(encoding="utf-8")


def key_file():
    """Make the key once and keep it. It lives in the site, so it is public."""
    saved = FOLDER / "indexnow-key.txt"
    if saved.exists():
        return saved.read_text(encoding="utf-8").strip()

    # Any 8 to 128 hex characters. It proves you can write to the site,
    # which is the only thing it needs to prove.
    key = secrets.token_hex(16)
    saved.write_text(key, encoding="utf-8")
    (FOLDER / (key + ".txt")).write_text(key, encoding="utf-8")
    print("made a new key and wrote", key + ".txt")
    return key


def live(url):
    """Is this address actually reachable? Ask before claiming anything."""
    try:
        with urllib.request.urlopen(url, timeout=20) as response:
            return response.status == 200
    except (urllib.error.URLError, urllib.error.HTTPError):
        return False


def main():
    key = key_file()
    proof = SITE + key + ".txt"
    print("key file should be at", proof)

    if not live(proof):
        # Sending before the key file is published gets the whole request
        # rejected, and the reason is not obvious from the error.
        print("that address does not answer yet.")
        print("Commit the key file, push, wait for Pages to build, then rerun.")
        return

    changed = [SITE, SITE + "python.html"]
    body = json.dumps({"host": SITE.split("/")[2], "key": key,
                       "urlList": changed}).encode()
    request = urllib.request.Request(
        INDEXNOW, data=body, headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            print("told IndexNow about", len(changed), "pages:", response.status)
    except urllib.error.HTTPError as refused:
        # 422 means it found the key file but did not like something.
        print("refused:", refused.code, refused.read().decode()[:200])


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · before the key file was published

made a new key and wrote b44cba0f1f6002f53edc1da1eea9a2e1.txt
key file should be at
  https://nari.github.io/b44cba0f1f6002f53edc1da1eea9a2e1.txt
that address does not answer yet.
Commit the key file, push, wait for Pages to build, then rerun.
LineWhat it does · ทำอะไร · 做什么
if not live(proof):Sending before the key is published fails, unhelpfully.ถ้าส่งก่อนที่คีย์จะถูกเผยแพร่ มันจะล้มเหลวแบบไม่บอกเหตุผล在 key 发布之前发送会失败,而且不告诉你为什么。
secrets.token_hex(16)The key is public. It only proves you can write to the site.คีย์นี้เป็นสาธารณะ มันแค่พิสูจน์ว่าคุณเขียนลงเว็บได้这个 key 是公开的。它只证明你能往这个站写东西。

🇬🇧 English

Eight files, and most of the work is two small text files and a browser tab.

The part worth remembering is the order. Make the pages readable, list them, check the list as served, then ask.

Asking first is what everybody does, and it is why they sit refreshing a page that says Discovered.

🇹🇭 ไทย

แปดไฟล์ และงานส่วนใหญ่คือไฟล์ข้อความเล็ก ๆ สองไฟล์กับแท็บเบราว์เซอร์หนึ่งแท็บ

สิ่งที่ควรจำคือลำดับ ทำให้หน้าอ่านได้ก่อน แล้วทำรายชื่อ แล้วตรวจรายชื่อในแบบที่ถูกเสิร์ฟ แล้วค่อยขอ

การขอก่อนคือสิ่งที่ทุกคนทำ และนั่นคือเหตุผลที่พวกเขานั่งกดรีเฟรช หน้าที่เขียนว่า Discovered

🇨🇳 中文

八个文件,而大部分的活就是两个小文本文件和一个浏览器标签页。

值得记住的是顺序:先让页面读得懂,再列清单,再按被送出的样子检查清单,最后才去要。

先去要,是所有人都会做的事,也是他们对着写着「已发现」的页面一直刷新的原因。

You wantคุณอยากได้你想要Look upไปหาคำว่า去查
Your own domain on Pagesใช้โดเมนตัวเองกับ Pages在 Pages 上用自己的域名a CNAME file, then a Domain propertyไฟล์ CNAME แล้วค่อยใช้ property แบบ Domain一个 CNAME 文件,然后用网域资源
Which searches found youคนค้นอะไรถึงเจอคุณ人们搜什么找到你the Performance report, after a weekรายงาน Performance หลังผ่านไปหนึ่งสัปดาห์一周后看效果报告
A page to stay outไม่อยากให้หน้าไหนถูกเก็บ让某一页别被收录<meta name="robots" content="noindex">
Bing's own consoleคอนโซลของ Bing เองBing 自己的后台Bing Webmaster Tools, imports from GoogleBing Webmaster Tools นำเข้าจาก Google ได้Bing 网站管理员工具,可从 Google 导入

Fifteen lines, your own address, and an app password. That is the whole trick. · สิบห้าบรรทัด อีเมลของคุณเอง และ app password หนึ่งอัน เคล็ดลับมีแค่นี้ · 十五行代码、你自己的邮箱地址,加一个应用专用密码。诀窍就这些。

All eight files on this page were run on 26 September 2026 and every output shown is copied from those runs. The measurements are from live sites: a bare GitHub Pages site returns 404 for both robots.txt and sitemap.xml and sets no X-Robots-Tag; google.com/ping?sitemap= returns 404, so the advice still in most guides no longer works; IndexNow answers 422 to a bad key, meaning it is alive. Challenge 1 ran against this site, 499 pages, and found three that a sitemap would have claimed and the host does not serve - all of them folders with an index.html inside, which GitHub Pages would serve and S3 behind CloudFront does not. The Search Console steps themselves are browser work and were not executed; the constraint they rest on is that a Domain property needs a DNS record, and on a github.io address you do not control DNS.