春江暮客

春江暮客的个人学习分享网站

Hugo 删文后旧页面还在:用 Python 对比全新构建与残留 HTML

2026-11-04 技术
Hugo 删文后旧页面还在:用 Python 对比全新构建与残留 HTML

Markdown 已经删除,重新运行 Hugo 后,旧地址却还能打开。先别急着清浏览器缓存:旧的 HTML 可能根本没有从输出目录消失。本文面向初级到中级读者,用一个独立小站复现残留文件,再用 Python 对比已有输出与全新构建的 HTML 路径清单。

检查器只读目录,不自动删除、同步或发布。它解决的是“多了一份旧产物”,不是文章因草稿或日期而没有生成的问题。

1. 方法:先确认旧文件是否还在

Hugo 基础用法说明,普通构建不会先清空目标目录;它覆盖同名产物,但可能保留已经不再生成的文件。因此删文、改永久链接或把已发布文章改回草稿后,旧 HTML 仍可能留在原处。

本教程先复现,再在可丢弃副本上演示清理参数。实际项目更适合先构建到一个全新的目录,确认当前应该有哪些文件,再审查旧目录多出的路径。不要把清理命令直接指向线上目录。

2. 准备:建立独立演示站

需要已有的 Hugo 和 Python 3.10 或更新版本,不安装依赖。以下命令在 Linux、Hugo 0.164.0、Python 3.11.4 下验证,适用于 bash/zsh。重复实验时换一个新目录名;后续命令都在 hugo-stale-demo 内运行,不能在真实站点根目录复制执行初始化代码。

  1. 创建最小配置、模板和一篇临时文章:
hugo version
python3 --version
mkdir hugo-stale-demo
cd hugo-stale-demo
mkdir -p content layouts/_default static
cat > hugo.toml <<'TOML'
baseURL = 'https://example.org/'
title = 'Stale output demo'
disableKinds = ['taxonomy', 'term', 'RSS', 'sitemap']
TOML
cat > layouts/_default/single.html <<'HTML'
<!doctype html><html><head><title>{{ .Title }}</title></head>
<body><h1>{{ .Title }}</h1>{{ .Content }}</body></html>
HTML
cat > layouts/_default/list.html <<'HTML'
<!doctype html><html><head><title>{{ .Title }}</title></head>
<body>{{ range .Pages }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}</body></html>
HTML
cat > content/old.md <<'MARKDOWN'
+++
title = 'Old page'
+++
Only a disposable demo page.
MARKDOWN

这里保留一个空 static 目录,以兼容本文测试版本的清理行为。模板只用来生成可核对的文件,没有引入主题、网络资源或真实站点配置。

3. 复现与修复:删除源文件并不等于删除产物

  1. 构建一次,只删除刚创建的演示文章,再构建到相同目录:
hugo --quiet --destination out-existing
test -f out-existing/old/index.html
python3 - <<'PY_DELETE'
from pathlib import Path
Path("content/old.md").unlink()
PY_DELETE
hugo --quiet --destination out-existing
test -f out-existing/old/index.html && printf 'STALE: old/index.html\n'

应看到 STALE: old/index.html。这证明残留发生在本地磁盘,尚未涉及浏览器或 CDN。演示站没有 404 模板时可能出现缺少布局的警告;本例只核对首页与文章页。

  1. 复制输出,在副本上测试清理。out-cleaned 必须不存在,copytree 会拒绝覆盖已有目录:
python3 - <<'PY_COPY'
import shutil
shutil.copytree("out-existing", "out-cleaned")
PY_COPY
hugo --quiet --cleanDestinationDir --destination out-cleaned
test ! -f out-cleaned/old/index.html && printf 'CLEAN: old/index.html absent\n'

应看到 CLEAN: old/index.html absent,原来的 out-existing 仍保留旧页。Hugo 命令参考列出了 --destination 和 --cleanDestinationDir;后者可能删除目标里手工放入的文件,不要把它当成只删旧文章的安全过滤器。不同版本的清理细节有差异,先在副本上验证。

4. 实现:只读比较 HTML 清单

将以下代码保存为当前目录的 audit_output.py:

"""Compare HTML path inventories, without modifying either tree. Python 3.10+."""
import argparse
import json
import os
from pathlib import Path
import stat
import sys


def scan_error(error):
    raise error


def html_paths(root):
    result = set()
    for folder, dirs, files in os.walk(root, onerror=scan_error, followlinks=False):
        for name in dirs + files:
            if (Path(folder) / name).is_symlink():
                raise ValueError(f"symbolic link not allowed: {Path(folder) / name}")
        for name in files:
            path = Path(folder) / name
            if path.suffix.lower() != ".html":
                continue
            if not stat.S_ISREG(path.stat().st_mode):
                raise ValueError(f"not a regular HTML file: {path}")
            result.add(path.relative_to(root).as_posix())
    return result


def directory(value):
    path = Path(value)
    if path.is_symlink():
        raise ValueError(f"symbolic link not allowed: {path}")
    path = path.resolve(strict=True)
    if not path.is_dir():
        raise ValueError(f"not a directory: {path}")
    return path


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--existing", required=True)
    parser.add_argument("--fresh", required=True)
    args = parser.parse_args()
    try:
        existing = directory(args.existing)
        fresh = directory(args.fresh)
        if existing == fresh or existing in fresh.parents or fresh in existing.parents:
            raise ValueError("directories must be separate and non-nested")
        old_paths, new_paths = html_paths(existing), html_paths(fresh)
        if not new_paths:
            raise ValueError("fresh build has no HTML files")
        extra = sorted(old_paths - new_paths)
        missing = sorted(new_paths - old_paths)
        print(json.dumps({"extra_html": extra, "missing_html": missing},
                         ensure_ascii=True, sort_keys=True))
        return 1 if extra or missing else 0
    except (OSError, ValueError) as error:
        print(f"ERROR: {error}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())

检查只比较相对路径,不比较文件内容、大小或修改时间。extra_html 表示已有目录比参考构建多出的 HTML 候选,missing_html 表示参考构建中有而已有目录里没有的 HTML。两个同名文件即使内容不同,也不会被这个工具发现。

根据 os.walk 官方说明,目录扫描错误默认可能被忽略。这里通过 onerror 让失败中止,并拒绝扫描树内的符号链接;不把漏扫目录当成检查通过。工具只用于可信、检查期间不变化的本地目录,不提供对恶意路径或并发替换的安全保证。

5. 验证:用退出码区分残留与扫描失败

  1. 查看参数,然后比较原输出和清理后的副本:
python3 audit_output.py --help
if python3 audit_output.py --existing out-existing --fresh out-cleaned; then
    status=0
else
    status=$?
fi
printf 'exit=%s\n' "$status"

预期结果如下,--help 的帮助文本除外:

{"extra_html": ["old/index.html"], "missing_html": []}
exit=1
  1. 退出码约定如下。脚本发生扫描错误时只向标准错误写诊断,不输出成功 JSON:
退出码 含义
0 两边的 HTML 路径集合一致。
1 存在多余或缺失的 HTML 路径,需要审查。
2 参数错误、目录不存在、目录重叠、符号链接、空参考清单或扫描失败。

这不是自动删除清单。手工添加到输出目录的 HTML、旧版合法页面、不同配置生成的页面,也会成为差异候选;先核实用途和发布策略。

6. 自动化:每次使用全新的参考目录

将以下代码保存为 preview_check.py,与 audit_output.py 放在一起。它在当前项目根目录调用 Hugo,既不清空也不重建 --existing 指定的目录:

"""Build into a fresh temporary directory, then audit an existing output."""
import argparse
from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--existing", default="public")
parser.add_argument("--config", default="hugo.toml")
args = parser.parse_args()

workspace = Path("/tmp/codex-work")
workspace.mkdir(parents=True, exist_ok=True)
with TemporaryDirectory(prefix="hugo-output-", dir=workspace) as temporary:
    preview = Path(temporary) / "site"
    subprocess.run(
        ["hugo", "--quiet", "--config", args.config,
         "--destination", str(preview)],
        check=True, timeout=120,
    )
    result = subprocess.run(
        [sys.executable, "audit_output.py", "--existing", args.existing,
         "--fresh", str(preview)],
        check=False, timeout=120,
    )
    raise SystemExit(result.returncode)
  1. 在演示站运行:
python3 preview_check.py --existing out-cleaned
if python3 preview_check.py --existing out-existing; then
    status=0
else
    status=$?
fi
printf 'exit=%s\n' "$status"

第一次应输出两个空列表并返回 0;第二次应报告 old/index.html 并打印 exit=1。TemporaryDirectory在上下文退出时清理本次参考目录;构建失败或超时也不会继续比较,已有目录保持不变。强制杀死进程或断电时不能依赖自动清理。

  1. 用于自己的项目时,将这两个 Python 文件放在项目根目录,再显式指定实际配置,例如 python3 preview_check.py --config hugotest.toml --existing public。参考构建必须使用相同的源文件、语言、配置、环境变量和构建选项;不要一边构建一边扫描。构建较慢时按需要调整 timeout=120,不要去掉 check=True。

通过差异审查以后,还要用站内链接与图片检查检查页面引用,用 sitemap 检查核对当前发布清单。部署端是否移除了旧路径,要另行通过 HTTP 验证;本地通过不等于线上旧页已经消失。

7. 排障:按差异来源处理

现象 直接处理
extra_html 非空 检查对应源文件是否删除、改 URL 或变成草稿;确认是否属于手工维护页面,再决定单独移除还是部署完整新目录。
missing_html 非空 确认比较方向、构建配置和发布步骤;不能只把这些项当成缓存问题。
symbolic link not allowed 使用独立、真实目录中的构建产物,不要为绕过检查而自动跟随链接。
fresh build has no HTML files 检查模板、内容资格和目标路径;空参考不能证明已有文件全都该删。
本地旧页已消失,HTTP 仍返回旧正文 先核对服务器实际服务目录与响应正文,再检查部署遗漏、代理或 CDN 缓存;不要仅凭状态码判断。

检查器只扫描 .html 后缀,大小写不敏感;不会检查图片、XML、CSS、JSON 或服务器重写。它将路径保存在集合中,适合中小型静态站点,也不验证输出内容正确性。

8. 总结

先在本地确认旧 HTML 是否残留,再用全新构建作为参考,能把“缓存没刷新”和“部署还留着旧文件”分开。让差异检查只读、错误返回非零,并在审查后另行部署。不要因为一次构建成功,就认为旧路径已经下线。

标签

1024 12306 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv venv vhhbert vite web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它