每次發布時,根據完成的公開頁面集合重新產生網站地圖,然後一併上傳兩者。這套 Mac 工作流程使用簡短的 Python 指令碼,自動納入新增頁面並移除已刪除頁面,並檢查公開網站的結果。

這裡的任務是維護從 Mac 匯出的靜態小型網站的網站地圖。範例使用關於顏色的虛構頁面,並假設你現有的發布流程可以上傳匯出資料夾,並從公開網站移除過時檔案。

選擇網站地圖重新產生流程的位置

先確認你的發布系統是否已經會產生網站地圖。Google 指出,許多內容管理系統會自動提供網站地圖。如果你的系統有,請先新增並移除測試頁面,再檢查網站地圖,然後才導入另一個產生器。瞭解網站地圖。

對於靜態匯出,請依照以下順序操作:完成頁面匯出、從匯出內容產生網站地圖,再發布產生的資料夾。這是一種建議的實作模式:它能讓網站地圖描述與發布系統收到的頁面集合相同。

在發布期間產生網站地圖,也能讓新增和移除遵循一致的規則。完成的匯出內容中有的頁面會列入;沒有的頁面則會省略。你不必另外記住要更新清單。

選擇符合現有設定的整合方式:

現有發布設定 重新產生的位置
CMS 已維護網站地圖 保留該產生器,並測試它如何因應頁面變更
靜態匯出工具支援匯出後執行命令 在上傳前於該處執行產生器
現有命令會發布已完成的資料夾 透過產生器的選用發布參數執行該命令
你透過桌面介面上傳 先產生網站地圖,再上傳完整資料夾;若要無人值守地更新公開網站,則需要受支援的發布整合方式

本機指令碼本身無法變更遠端網站。以下工作流程會自動產生網站地圖,也可以啟動現有的發布命令。實際上傳仍由你既有的發布工具負責。

明確界定哪些頁面應列入網站地圖

對一般頁面 URL,Google 接受每行包含一個完整 URL 的文字格式網站地圖。此任務不必使用 XML。每個頁面都應列出其偏好的公開 URL,而不是同一內容的多種 URL 版本。建立並提交網站地圖。

文字格式需要使用 UTF-8 編碼,且只能包含 URL,不可有標題或註解。單一檔案最多可包含 50,000 個 URL,大小上限為 52,428,800 位元組。請將此範例的網站地圖放在網站根目錄,讓它涵蓋該位置下方匯出的頁面。網站地圖協定:文字檔與位置。

產生器採用刻意保持簡單的匯出慣例:

匯出的檔案 示意公開 URL
public/index.html https://example.com/
public/colors/blue.html https://example.com/colors/blue.html
public/tiles/index.html https://example.com/tiles/

只有在你的主機提供完全相同的 URL 時,才使用這個慣例。如果發布工具會將 blue.html 轉換為 /colors/blue/,或是在 /collection/ 之類的路徑下提供網站,請改用它的路由清單,或在發布前調整對應方式。掃描檔名無法推斷自訂路由。

只保留預定要納入匯出資料夾的頁面。此範例會略過以句點開頭的路徑和名為 404.html 的檔案;它不會檢查頁面中繼資料、標準網址宣告或重新導向規則。若網站還有這些條件,應改為從發布工具提供的合格頁面清單產生網站地圖。

也請持續維護一般導覽連結。WHATWG 將使用 a 元素和 href 屬性建立的超連結列入規範;Google 則說明,妥善連結的頁面通常可透過導覽被發現。更新網站地圖時,也應一併更新指向新增或移除頁面的連結。WHATWG HTML 標準:連結、瞭解網站地圖。

在 Mac 上建立產生器

你需要 Python 3.8 或更新版本,以及純文字編輯器。在終端機執行 python3 --version,查看你可用的直譯器。如果需要 Python,請依照 Python 軟體基金會的 macOS 安裝說明操作。其文件也說明如何在終端機使用 python3 執行指令碼。在 macOS 上使用 Python。

在桌面建立名為 sitemap-demo 的資料夾。將以下完整程式儲存於其中,檔名為 regenerate_sitemap.py。此程式使用 Python 標準函式庫,不需安裝其他套件。

import argparse
from pathlib import Path, PurePosixPath
import subprocess
import tempfile
from urllib.parse import quote, urlsplit


def make_sitemap(paths, origin):
    parsed = urlsplit(origin)
    if (parsed.scheme not in ("http", "https") or not parsed.hostname
            or parsed.netloc != parsed.hostname
            or parsed.path not in ("", "/")
            or parsed.query or parsed.fragment):
        raise ValueError("Use a site origin, such as https://example.com")
    base = origin.rstrip("/")
    urls = set()
    for relative in paths:
        path = PurePosixPath(relative)
        if path.name == "index.html":
            parent = path.parent.as_posix()
            route = "" if parent == "." else parent + "/"
        else:
            route = path.as_posix()
        urls.add(base + "/" + quote(route, safe="/"))
    if not urls:
        raise ValueError("No eligible pages; sitemap was not replaced")
    if len(urls) > 50000:
        raise ValueError("Split the sitemap: more than 50,000 URLs")
    data = ("\n".join(sorted(urls)) + "\n").encode("utf-8")
    if len(data) > 50 * 1024 * 1024:
        raise ValueError("Split the sitemap: larger than 50 MB")
    return data


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("folder")
    parser.add_argument("origin")
    parser.add_argument("publish", nargs=argparse.REMAINDER)
    args = parser.parse_args()
    root = Path(args.folder).resolve()
    if not root.is_dir():
        parser.error("The export folder must already exist")
    paths = []
    for file in root.rglob("*.html"):
        relative = file.relative_to(root)
        if (file.is_file() and file.name != "404.html"
                and not any(p.startswith(".") for p in relative.parts)):
            paths.append(relative.as_posix())
    data = make_sitemap(paths, args.origin)
    target = root / "sitemap.txt"
    temporary = None
    try:
        with tempfile.NamedTemporaryFile(dir=root, delete=False) as handle:
            temporary = Path(handle.name)
            handle.write(data)
        temporary.replace(target)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)
    print(f"Wrote {len(data.splitlines())} URLs to {target}", flush=True)
    if args.publish:
        subprocess.run(args.publish, check=True)


if __name__ == "__main__":
    main()

每次執行時都會重建完整的 URL 清單。程式會排序並去除重複項目、編碼路徑中的空格等字元,並先寫入暫存檔,再取代 sitemap.txt。若產生過程發生錯誤,程式會停止,不會執行選用的發布命令。

若匯出內容為空,程式會刻意引發錯誤,並保留先前的網站地圖。這可以抓出意外選錯資料夾或資料夾內容不完整的情況。若要刻意移除所有頁面,應另外決定如何發布,而不是將空的匯出內容交給此工作流程處理。

在連接發布流程前,先測試新增與移除頁面

在終端機中,切換至示範資料夾:

cd "$HOME/Desktop/sitemap-demo"

使用以下完整設定命令建立兩個虛構頁面。請在新建的示範資料夾中執行;此命令會寫入兩個指定的範例檔案。

python3 - <<'PY'
from pathlib import Path

root = Path("public")
(root / "colors").mkdir(parents=True, exist_ok=True)
pages = {
    "index.html": "<!doctype html><title>Color collection</title><p>Color collection.</p>",
    "colors/blue.html": "<!doctype html><title>Blue tile</title><p>A blue tile.</p>",
}
for name, content in pages.items():
    (root / name).write_text(content, encoding="utf-8")
PY

產生並檢查本機網站地圖。範例網站來源只是示意,這些命令不會將任何內容發布至該處。

python3 regenerate_sitemap.py public https://example.com
cat public/sitemap.txt

預期檔案會剛好包含以下兩行:

https://example.com/
https://example.com/colors/blue.html

現在將虛構的藍色頁面換成綠色頁面:

python3 - <<'PY'
from pathlib import Path

root = Path("public")
(root / "colors/blue.html").unlink()
(root / "colors/green.html").write_text(
    "<!doctype html><title>Green tile</title><p>A green tile.</p>",
    encoding="utf-8",
)
PY
python3 regenerate_sitemap.py public https://example.com
cat public/sitemap.txt

預期結果是首頁 URL,後接 https://example.com/colors/green.html。藍色頁面的 URL 會消失,因為匯出的檔案已不存在。這能檢查更新的兩種情況,不必仰賴搜尋引擎察覺變更。

將重新產生網站地圖納入每次發布

對於你的實際網站,請將示範資料夾和來源換成實際完成的匯出資料夾和公開來源。只在匯出完成後執行產生器,並在發布完成前保持該資料夾不變。各次發布請依序執行。

如果專案資料夾中已有可正常運作且名為 deploy-site.sh 的發布指令碼,以下整合命令會先產生網站地圖,再執行該指令碼。此範例需要既有指令碼;產生器不會建立該指令碼,也不會提供主機設定。

python3 regenerate_sitemap.py public https://example.com sh ./deploy-site.sh

你的發布指令碼必須上傳同一個 public 資料夾,包括 sitemap.txt,並移除公開網站上匯出內容中已不存在的檔案。它不得在產生網站地圖後重建頁面。若發布工具也會建置網站,請改在建置階段和上傳階段之間產生網站地圖。

若產生失敗,發布命令就會略過。若發布失敗,Python 會回報失敗,但遠端網站可能已部分變更。重試前請先檢查公開網站的結果。如果主機支援一次發布完整版本,請使用該功能的官方機制。

若使用桌面上傳工具,請先產生網站地圖,再選取要上傳的匯出資料夾。確認上傳工具會取代現有網站地圖,並妥善處理已移除的頁面。這仍需要執行上傳動作;單靠本機自動產生網站地圖,無法在無人值守的情況下發布至公開網站。

分別驗證公開網站地圖和已移除頁面

成功發布後,請在瀏覽器中開啟實際公開的 sitemap.txt URL。確認新增頁面有列入,已移除頁面則沒有。接著直接造訪每個受影響的頁面。這能區分正確的本機匯出內容和正確的公開版本。

刪除網站地圖項目,並不能決定該頁面舊網址的 HTTP 回應。對於沒有替代頁面的已移除頁面,請確認主機傳回適當的頁面不存在回應。RFC 9110 將 404 定義為資源目前沒有表示內容,將 410 定義為已知資源很可能永久無法使用的情況。RFC 9110:HTTP 語意。

若頁面顯示找不到頁面的訊息,卻傳回 200,Google 可能會將其視為軟性 404。Google 也說明,先前已建立索引而傳回適用 4xx 回應的 URL,會隨時間從其索引中移除。因此,從網站地圖刪除 URL 只是移除頁面的一個步驟。HTTP 狀態碼如何影響 Google 檢索工具。

發現異常時,請依照以下診斷步驟:

觀察到的情況 接下來檢查
本機網站地圖仍列出已移除的頁面 檢查是否有遺留的匯出檔案,或選錯匯出資料夾
本機網站地圖正確,但公開版本不同 檢查上傳的資料夾、目的地和快取
公開網站地圖正確,但已移除的頁面仍可開啟 檢查發布流程是否移除了遠端舊檔案
產生的 URL 開啟錯誤頁面 依據檔名慣例檢查主機的路由對應
新 URL 已公開,但搜尋結果中沒有 將搜尋引擎的發現和建立索引視為另一個流程

快取可能是公開網站地圖看起來沒有變更的原因。RFC 9111 定義了快取回應如何維持新鮮,以及如何重新驗證。如果主機支援設定回應標頭,Cache-Control: no-cache 會要求在重用儲存的回應前先成功驗證;只變更本機檔案並不會設定該標頭。RFC 9111:HTTP 快取。

維持固定的網站地圖網址

透過 Search Console 的 Sitemaps 報表提交公開網站地圖的固定 URL,或在網站既有的 robots.txt 檔案中參照該 URL,讓使用者和搜尋引擎能找到它。Google 說明了這兩種方式,並指出提交只是提示。建立並提交網站地圖。

以示意網站為例,探索用的那一行會是:

Sitemap: https://example.com/sitemap.txt

發布時替換網站地圖內容,但維持其網址不變。實際完成的檢查項目是:公開網站地圖與完成的匯出內容相符、新增頁面可透過列出的 URL 開啟,而已移除頁面則有預期的回應。搜尋引擎會依自己的時程處理這些變更;列入網站地圖不保證一定會檢索或建立索引。瞭解網站地圖。

來源