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
- Publication conditions fail: inspect
draft, the effectivepublishDate, andexpiryDate. - No page file exists: inspect a fresh output directory, then build options and templates.
- 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.
- Author: yanzhi
- Original article: https://www.bobobk.com/en/hugo-missing-post-draft-dates.html
- License: This work is licensed under CC BY-NC-ND 4.0. See the license for attribution, noncommercial use and no-derivatives terms.
Related articles
- Troubleshoot Linux Scheduled Jobs: Python Paths, Working Directories, and Environment
- Fixed-Interval Python Tasks with monotonic: Avoid Drift and Skip Missed Slots
- Build URL Query Parameters in Python: Unicode, Plus Signs, and Repeated Keys
- Fix Duplicate Python Logs: Repeated Handlers and Logger Propagation
- Fix Python CSV Field Limits: Set a Character Cap and Test Boundaries