Find Expired Hugo Content with a Reproducible CSV Check
hugo list expired hunts down every page whose expiry date has already passed, without touching the site. It prints a CSV, not a build, so nothing gets rendered or edited. This guide targets the installed Ubuntu package, Hugo 0.123.7. Allow ten minutes if the site is already familiar, or twenty if you still need to find its source directory.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
- You need Hugo installed and a Hugo project containing the content you want to inspect.
- No root required for a project you can read; avoid
sudounless access is deliberately restricted, since the command only reads the project and may create Hugo's local build lock file. - Expiry comes from front matter. Hugo decides content is expired from its
expiryDatefield; an item with no expiry date is not expired. - The clock is live unless you override it. The comparison runs against the current clock unless you pass
--clock, which matters for testing, audits and scheduled jobs where an implicit wall clock makes a result hard to reproduce.
1. Check the installed command
Confirm which executable will run and record its version. The local manpage describes Hugo 0.123.7, and the installed package here is 0.123.7-1ubuntu0.3+esm2.
$ command -v hugo
/usr/bin/hugo
$ hugo version
hugo v0.123.7+extended linux/amd64 BuildDate=2026-03-17T19:51:14Z VendorInfo=ubuntu:0.123.7-1ubuntu0.3+esm2
If command -v turns up a different path, stop and check it is the version you actually mean to audit. Hugo's output format and available flags can differ between releases.
2. Run the basic query
Change into the Hugo project directory, then run the subcommand exactly as shown:
$ cd /path/to/hugo-project
$ hugo list expired
The command prints comma-separated output. A typical result opens with this header:
path,slug,title,date,expiryDate,publishDate,draft,permalink
Each row after that names one expired item. In a test project, an item with expiryDate: 2020-01-01T00:00:00Z appeared as content/old.md, complete with its title, expiry timestamp, draft state and permalink. The command does not render pages, publish changes or remove files. An empty result can still contain the header, so do not treat the header alone as a match.
3. Point Hugo at the right project
If the result is unexpectedly empty, verify the project root and its content tree before touching any front matter. From the directory you believe is the project root, run:
$ pwd
$ find content -maxdepth 2 -type f -print | sort
$ hugo list expired
The default source is the current project. Running from somewhere else, name the project directory explicitly, since the flag takes a path, not a content-directory guess:
$ hugo list expired --source /path/to/hugo-project
Checkpoint
Hugo reports an error when the source path does not exist. Treat that as a path problem, not evidence the project has no expired content.
4. Make the date comparison repeatable
Add --clock with an RFC 3339 timestamp whenever a result needs to be reproduced by someone else, or by a later job. This example lists content expired by midnight UTC on 1 January 2021:
$ hugo list expired --clock 2021-01-01T00:00:00Z
A page expiring exactly at the supplied instant is included by the installed version's query. In a temporary project, an expiry of 2020-01-01T00:00:00Z was absent at exactly that instant when the clock was set to 2020-01-01T00:00:00Z, and present one second later. Record the boundary case if it matters to an audit, and use a timestamp just past it when checking the result.
To inspect content relative to a local offset, include the offset in the timestamp:
$ hugo list expired --clock 2021-01-01T00:00:00+01:00
Do not confuse --clock with a change to page dates. It only changes the reference time for the run.
5. Read and save the CSV without damaging data
For a one-off look, leave the output in the terminal. For a report, redirect it to a new file and pick a destination outside the site's content tree:
$ report=/tmp/hugo-expired-2021-01-01.csv
$ hugo list expired --clock 2021-01-01T00:00:00Z > "$report"
$ sed -n '1,10p' "$report"
$ wc -l "$report"
The first line is the header. Subtract one from the line count to estimate rows, but only when titles and other fields contain no embedded newlines. For anything that needs to hold up, reach for a CSV-aware parser instead of splitting every line on commas: titles and paths can carry characters that make naive shell parsing unsafe.
For a quick shell check that only reports whether at least one data row exists:
$ if tail -n +2 "$report" | grep -q .; then
> echo "expired content found"
> else
> echo "no expired content"
> fi
That check is deliberately not a parser. It suits a notification condition, not extracting titles or paths.
6. Investigate a surprising result
Start with the source file and its front matter. Confirm the field is spelled expiryDate, that its value is a valid date, and that you are looking at the same source directory you passed to Hugo. Also check whether the content sits below a branch page or is affected by project configuration. The list command reports what Hugo can load; it will not explain a malformed front matter value sitting in the CSV.
Reach for verbose logging only when ordinary output falls short:
$ hugo list expired --logLevel info
The installed binary also accepts --quiet, --debug, --config, --configDir, --environment, --themesDir, --destination, --renderToMemory and --ignoreVendorPaths as inherited flags. None of them are needed for the normal query. The only command-specific option is --help.
Common traps
- Running from the wrong directory. Use
--sourceor change into the project root. - Expecting an ordinary table. The installed command emits CSV with a fixed header, not a prose report.
- Assuming a blank screen means success. Without a match, Hugo can still print the header; check for data rows.
- Using the machine clock in an audit. Add
--clockand record the exact value. - Trying to fix expiry from this command. It is read-only with respect to content: edit front matter separately, then rerun the query and review the diff.
Done means
hugo versionidentified the intended Hugo installation.hugo list expiredran against the correct project source.- The header and rows were treated as CSV, not as a plain list.
- Any audit or scheduled check records an explicit
--clockvalue. - Nothing changed. No content, configuration or generated site files were touched by the query.