Bobobk

Practical Python, Linux and automation guides

Missing Hugo Posts: Diagnose Drafts, Publication Dates, Expiry, and Output Paths

2026-10-26 Technology
Missing Hugo Posts: Diagnose Drafts, Publication Dates, Expiry, and Output Paths
On this page

Hugo reports a successful build, but a new article is missing. It may still be a draft, its publication time may be in the future, or the page may exist at a different URL. This beginner-to-intermediate tutorial inspects Hugo’s resolved metadata, then uses fresh output directories to distinguish missing files from missing homepage entries.

This follows the pre-publish internal link checker. Diagnostic builds write only to temporary directories and require no theme or server configuration changes.

1. Method: distinguish three cases

  1. Publication conditions fail: inspect draft, the effective publishDate, and expiryDate.
  2. No page file exists: inspect a fresh output directory, then build options and templates.
  3. The file exists but the homepage omits it: inspect theme filters, pagination, and page collections.

hugo list all includes drafts, future, and expired content; hugo list published excludes these categories. These inventories describe content eligibility, not live HTTP availability.

2. Setup

Use an existing Hugo executable and Python 3.9+, without third-party Python packages. The experiment was tested on Linux with Hugo 0.164.0 and Python 3.11.4. Start in your project root:

hugo version
python3 --version
hugo list all --help
hugo list published --help

Commands use bash/zsh. Set hugo_config to the configuration used by your publishing process; this website uses hugotest.toml. Match other options, including --environment, to the real build so you inspect the same configuration.

3. List excluded articles

Run in the same terminal:

hugo_config=hugo.toml
audit_dir=$(mktemp -d)
trap 'rm -rf -- "$audit_dir"' EXIT
hugo list all --config "$hugo_config" > "$audit_dir/all.csv"
hugo list published --config "$hugo_config" > "$audit_dir/published.csv"
python3 - "$audit_dir" <<'PY_AUDIT'
import csv
from pathlib import Path
import sys

root = Path(sys.argv[1])
with (root / "published.csv").open(encoding="utf-8", newline="") as stream:
    published = {row["path"] for row in csv.DictReader(stream)}
with (root / "all.csv").open(encoding="utf-8", newline="") as stream:
    for row in csv.DictReader(stream):
        if row["kind"] == "page" and row["path"] not in published:
            print(row["path"])
            for key in ("draft", "date", "publishDate", "expiryDate", "permalink"):
                print(f"  {key}: {row[key]}")
PY_AUDIT

The output shows excluded paths and relevant fields. For example:

content/posts/new-guide.md
  draft: false
  date: 2024-01-01T00:00:00Z
  publishDate: 2030-01-01T00:00:00Z
  expiryDate: 0001-01-01T00:00:00Z
  permalink: https://example.org/new-guide/

This is illustrative; your paths and domain will differ. 0001-01-01... is a zero value you may see for an unset expiration date, not an expiration in year 1. csv.DictReader preserves column alignment when titles contain commas. Empty output means only that this difference contains no matching pages.

Date resolution settings determine effective values. Publication time defaults to publishdate, pubdate, published, then date. Changing date therefore does not override a future pubdate. Also inspect custom [frontmatter] settings. Read the resolved inventory rather than relying on one Markdown field.

4. Fix the cause and build into a fresh directory

Finding Action
draft: true Set draft: false when the article is ready.
publishDate is ahead of now Compare date -Is and timezone offsets; correct the effective publication field for immediate release.
A real expiryDate has passed Confirm the policy; remove an accidental expiration or choose an appropriate date.
Eligible content returns 404 at the old address Follow the inventory’s permalink; check url and the language path.

pubdate aliases publishDate. Choose one spelling per article. Include a timezone offset such as +08:00 or Z to make timestamps unambiguous.

After fixing your target article, run a normal build:

preview_dir=$(mktemp -d)
hugo --config "$hugo_config" --destination "$preview_dir"
find "$preview_dir" -type f -name '*.html' | head -n 20
printf 'Preview directory: %s\n' "$preview_dir"

Locate the file using its actual URL. For root-hosted sites, /guide.html typically maps to guide.html, and /guide/ to guide/index.html. Account for multilingual and subpath configuration. Hugo’s URL documentation explains how url overrides the default path.

To compare rendering with hidden content included, use another directory:

diagnostic_dir=$(mktemp -d)
hugo --config "$hugo_config" --buildDrafts --buildFuture --buildExpired \
  --destination "$diagnostic_dir"
printf 'Diagnostic directory: %s\n' "$diagnostic_dir"

These build flags are for local diagnosis here. A page that appears only in the diagnostic build needs its publication conditions reviewed. Keep that directory out of deployment. Existing output may contain files from an earlier build and cannot prove that the current build rendered a page.

If the normal build still lacks HTML, inspect logs and templates. Check build.render and build.list in the article and inherited cascade: render: never prevents rendering, while list: never removes a page from collections. When HTML exists but a homepage entry is missing, inspect theme filters for type or section, plus pagination.

After inspection, remove only the two directories created above:

rm -rf -- "$preview_dir" "$diagnostic_dir"

5. Validation: a repeatable four-article experiment

Save this as test_hugo_visibility.py. It creates four fixtures in a temporary site, fixes Hugo’s diagnostic clock, and checks inventories plus five build combinations. It does not read or modify your site’s articles. The temporary site is removed automatically when the program exits.

"""Verify Hugo visibility rules in a temporary site. Python 3.9+."""
import csv
import io
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory

CLOCK = "2026-01-01T00:00:00Z"


def run_hugo(root, *args):
    result = subprocess.run(
        ["hugo", "--source", str(root), "--clock", CLOCK, *args],
        check=True, capture_output=True, text=True, encoding="utf-8", timeout=60,
    )
    return result.stdout


with TemporaryDirectory(prefix="hugo-visibility-") as work:
    root = Path(work)
    (root / "content").mkdir()
    layouts = root / "layouts" / "_default"
    layouts.mkdir(parents=True)
    (root / "hugo.toml").write_text(
        'baseURL = "https://example.org/"\n'
        'disableKinds = ["home", "taxonomy", "term", "RSS", "sitemap"]\n',
        encoding="utf-8",
    )
    (layouts / "single.html").write_text(
        "<!doctype html><title>{{ .Title }}</title>{{ .Content }}",
        encoding="utf-8",
    )
    cases = {
        "visible": "",
        "draft": "draft: true\n",
        "future": "publishDate: 2030-01-01T00:00:00Z\n",
        "expired": "expiryDate: 2020-01-01T00:00:00Z\n",
    }
    for name, extra in cases.items():
        (root / "content" / f"{name}.md").write_text(
            f"---\ntitle: {name}\ndate: 2024-01-01T00:00:00Z\n"
            f"url: /{name}.html\n{extra}---\n\nFixture: {name}.\n",
            encoding="utf-8",
        )
    all_rows = list(csv.DictReader(io.StringIO(run_hugo(root, "list", "all"))))
    published = list(csv.DictReader(io.StringIO(run_hugo(root, "list", "published"))))
    assert {row["title"] for row in all_rows} == set(cases)
    assert {row["title"] for row in published} == {"visible"}
    builds = [
        ([], {"visible"}),
        (["--buildDrafts"], {"visible", "draft"}),
        (["--buildFuture"], {"visible", "future"}),
        (["--buildExpired"], {"visible", "expired"}),
        (["--buildDrafts", "--buildFuture", "--buildExpired"], set(cases)),
    ]
    for index, (flags, expected) in enumerate(builds):
        destination = root / f"output-{index}"
        run_hugo(root, "--destination", str(destination), *flags)
        actual = {path.stem for path in destination.glob("*.html")}
        assert actual == expected, (flags, actual, expected)
        for name in expected:
            assert f"Fixture: {name}." in (destination / f"{name}.html").read_text()
    print("PASS: inventories and 5 isolated builds")

Run:

python3 test_hugo_visibility.py

Expected output: PASS: inventories and 5 isolated builds. The fixed clock is only for this experiment; production builds should use actual time. Each combination writes to a fresh directory and checks HTML content. An assertion failure or command timeout exits unsuccessfully; inspect the error before changing a publishing script.

6. Troubleshooting

  • The file appears in neither inventory: inspect content directories, language configuration, extensions, and exclusion rules. Confirm the file is inside the content actually read by this build.
  • Preview works but deployment returns 404: check the deployed directory, domain, and response body using the curl website verification tutorial.
  • Local builds work but automation omits articles: log hugo version, configuration, and current time. Compare the environment with the Hugo automated deployment tutorial.

7. Summary

Compare content inventories, then inspect HTML in a fresh directory to locate publication, URL, and homepage issues. After correcting effective metadata, verify a normal build and the actual HTTP response. Keep the automated experiment to recheck behavior after Hugo upgrades or workflow changes.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring 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 economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable 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 probability 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 seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sql sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接