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, run this isolated experiment. It creates three pages in a temporary project and builds five independent destinations without touching this repository or deploying anything.
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",
}.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"}),
("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 after the boundary. The draft remains absent from the later normal build. 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.
For a site using expiry dates, add an expired fixture and exercise -E independently. Include a future draft as well when the workflow relies on overlapping exclusion conditions.
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.