Hugo decides which pages to render when a build runs. Front matter supplies publication metadata, configuration and command flags supply inclusion rules, and the build clock supplies the time used for evaluation. The deployed directory then stays unchanged until another artifact replaces it. A date reaching midnight is not itself a deployment event, and a file without draft: true is not automatically a draft.
Separate publication metadata from build options
Under the default rules, a page marked draft: true is excluded. A future publication date also excludes it, while an expired page is excluded after its expiry date. Hugo resolves dates according to its front matter configuration; an explicit publishDate can differ from the displayed date. Use complete timestamps with a time zone for a boundary that must mean the same thing on a laptop and a build worker.
--buildDrafts includes drafts, --buildFuture includes future content, and --buildExpired includes expired content. Their short forms are -D, -F, and -E. These switches address separate conditions: a future draft needs both draft and future inclusion before the date arrives. Project or environment configuration can enable the same behavior, so checking only a page’s front matter is insufficient.
This site’s configuration deliberately enables buildFuture: true to retain its published article inventory, including articles dated in the future. Consequently, those dates are not an embargo here. A tutorial about Hugo defaults must not be mistaken for a claim about this site’s active settings. Compare the effective options with the expected URL inventory before changing publication policy.
A preview artifact can still be published
A preview flag changes the generated output, not its sensitivity or deployability. If a pipeline uploads a directory built with -D -F, those pages can become public. Use a separate destination for preview builds and construct the release artifact with the intended options. A Git release tag identifies source history; it neither changes Hugo’s inclusion rules nor certifies that a particular generated directory was reviewed.
Scheduled publication requires a later build and deployment after the publication boundary. Conversely, changing a public page back to draft cannot withdraw HTML that is already deployed or cached. Rebuild, replace the served artifact, and inspect the public route. Hugo normally leaves unrelated files in an existing destination, so a fresh output directory or an appropriate clean build is part of verifying that excluded pages are absent.
When it breaks
A preview build copied over a release directory can leave a draft accessible by its direct URL even after the navigation no longer lists it. Inspect generated files, feeds, sitemaps, and the actual served route; hiding an index entry is not removal. A draft section page also does not automatically make all descendants drafts. Use explicit child metadata or the documented cascade mechanism when exclusion must apply to a subtree.
Time-zone mismatches cause a different failure: two workers include different pages from the same commit. Pin the generator, use an explicit build clock for boundary tests, and record the options. Do not repair the discrepancy by changing publication dates until the date resolution rules and worker time zones are understood.
Check it quickly
With Python 3 and Hugo 0.165.0 available, this isolated experiment creates five pages and eight fresh build destinations. Run Python with assertions enabled: omit -O and unset PYTHONOPTIMIZE. It neither changes this repository nor deploys anything. This source review did not execute it.
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
with TemporaryDirectory(prefix="hugo-publication-") as directory:
root = Path(directory)
(root / "content/posts").mkdir(parents=True)
(root / "layouts/_default").mkdir(parents=True)
(root / "hugo.yaml").write_text("baseURL: https://example.org/\n")
(root / "layouts/index.html").write_text("<h1>Publication test</h1>")
(root / "layouts/_default/list.html").write_text("{{ .Title }}")
(root / "layouts/_default/single.html").write_text("{{ .Title }}")
for name, extra in {
"visible": "",
"draft": "draft: true\n",
"future": "publishDate: 2030-01-01T00:00:00Z\n",
"future-draft": "draft: true\npublishDate: 2030-01-01T00:00:00Z\n",
"expired": "expiryDate: 2025-12-01T00:00:00Z\n",
}.items():
(root / f"content/posts/{name}.md").write_text(
f"---\ntitle: {name}\ndate: 2025-01-01T00:00:00Z\n"
+ extra + "---\nPublication fixture.\n"
)
cases = [
("normal", "2026", [], {"visible"}),
("drafts", "2026", ["-D"], {"visible", "draft"}),
("future", "2026", ["-F"], {"visible", "future"}),
("preview", "2026", ["-D", "-F"], {"visible", "draft", "future", "future-draft"}),
("expired", "2026", ["-E"], {"visible", "expired"}),
("all", "2026", ["-D", "-F", "-E"], {"visible", "draft", "future", "future-draft", "expired"}),
("boundary", "2030", [], {"visible", "future"}),
("later", "2031", [], {"visible", "future"}),
]
for name, year, flags, expected in cases:
destination = root / f"output-{name}"
subprocess.run(
["hugo", "--source", str(root), "--destination", str(destination),
"--clock", year + "-01-01T00:00:00Z", *flags],
check=True, capture_output=True, text=True,
)
actual = {p.parent.name for p in destination.glob("posts/*/index.html")}
assert actual == expected, (name, actual, expected)
print(name, sorted(actual))
The assertions verify default exclusion, each inclusion flag, their combination, and publication at and after the boundary. Both drafts remain absent from later normal builds. A future draft appears early only with both -D and -F, while -E independently includes the expired page. This checks Hugo’s rendering decisions under controlled inputs; it does not test the deployment scheduler, CDN invalidation, or this site’s future-inclusive configuration. Those need separate artifact and public-route evidence at release time.
Keep these cases when changing publication options: ordinary, future, draft, future-draft, and expired pages exercise different inclusion conditions. Check newly generated files rather than assuming that missing navigation links prove exclusion.
Sources
- Hugo: Basic usage, draft, future, expired content, and destination behavior, checked 2026-09-06.
- Hugo: Front matter, publication metadata and cascading settings, checked 2026-09-06.
- Hugo command reference, inclusion switches and the build clock, checked 2026-09-06.
- Python assert statement, optimization removes assertions and their expressions, checked 2026-10-18.