Find Expired Hugo Content with a Reproducible CSV Check
You will finish with a CSV list of Hugo content whose expiry date has passed, without building the site or editing any content. This guide targets the installed Ubuntu package, Hugo 0.123.7. Allow about 10 minutes if the site is already familiar, or 20 minutes if you need to identify its source directory first.
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. You do not need root privileges for a project you can read. Avoid using sudo unless the project is deliberately restricted, because the command only reads the project and may create Hugo's local build lock file.
Hugo decides that content is expired from its expiryDate front matter. An item with no expiry date is not an expired item. The comparison is made against the current clock unless you supply --clock. That override is useful for testing, audits and scheduled jobs, where an implicit wall clock can make a result difficult 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 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 finds a different path, stop and check that it is the version you intend to audit. Hugo's output format and available flags can differ between releases.
2. Run the basic expired-content query
Change to 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 begins with this header:
path,slug,title,date,expiryDate,publishDate,draft,permalink
Each following row identifies one expired item. In a test project, an item with expiryDate: 2020-01-01T00:00:00Z appeared as content/old.md, with its title, expiry timestamp, draft state and permalink. The command does not render pages, publish changes or remove files. An empty result may still contain the header, so do not treat the header alone as a match.
Checkpoint: confirm what Hugo is reading
If the result is unexpectedly empty, verify the project root and its content tree before changing 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. If you are running the command from somewhere else, use --source to name the project directory. This flag is a path, not a content directory guess:
$ hugo list expired --source /path/to/hugo-project
Hugo reports an error when the source path does not exist. Treat that as a path problem rather than evidence that the project has no expired content.
3. Make the date comparison repeatable
Use --clock with an RFC 3339 timestamp when you need a result that another person or a later job can reproduce. 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 in 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 was present one second later. If that boundary matters to an audit, record the clock value and use a timestamp just after the boundary 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 changes the reference time for the run only.
4. Read and save the CSV without damaging data
For a one-off inspection, leave the output in the terminal. For a report, redirect it to a new file and choose a destination that is not inside 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 the number of rows only when titles and other fields do not contain embedded newlines. For robust processing, use a CSV-aware parser rather than splitting every line on commas: titles and paths can contain characters that make naive shell parsing unsafe.
For a quick shell check that only reports whether at least one data row exists, use:
$ if tail -n +2 "$report" | grep -q .; then
> echo "expired content found"
> else
> echo "no expired content"
> fi
This check is deliberately not a parser. It is suitable for a notification condition, not for extracting titles or paths.
5. Investigate a surprising result
First inspect the matching source file and its front matter. Confirm that the field is spelled expiryDate, that its value is a valid date, and that you are examining the same source directory passed to Hugo. Also check whether the content is nested below a branch page or is affected by project configuration. The list command reports content Hugo can load; it does not explain a malformed front matter value in the CSV.
Use verbose logging only when ordinary output is not enough:
$ hugo list expired --logLevel info
The installed binary accepts --quiet, --debug, --logLevel, --config, --configDir, --environment, --themesDir, --destination, --renderToMemory and --ignoreVendorPaths as inherited flags. They are not needed for the normal query. The only command-specific option is --help.
Common traps
- Running from the wrong directory: use
--sourceor change to 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 repair expiry from this command: it is read-only with respect to content. Edit front matter separately, then rerun the query and review the resulting diff.
Done means
hugo versionidentifies the intended Hugo installation.hugo list expiredwas run against the correct project source.- The report header and rows were treated as CSV.
- Any audit or scheduled check records an explicit
--clockvalue. - No content, configuration or generated site files were changed by the query.