SERIES: Hugo SEO
- Part 1: How I Fixed Google Search Console Indexing Issues With a Hugo and Netlify Site
- Part 2: Netlify Redirects for Hugo Sites: A Practical Cleanup Guide
Redirects look simple until they start showing up in Google Search Console.
On a static site, a bad redirect is easy to miss. The page might work in the browser. The new URL might return 200. The sitemap might look fine. But Google may still be holding old URLs from a previous site structure, old slugs, renamed posts, or older internal links.
That is exactly where Netlify redirects become useful.
This post is the practical redirect cleanup guide I wish I had written before fixing my own Hugo site. It follows the cleanup work from How I Fixed Google Search Console Indexing Issues With a Hugo and Netlify Site, but focuses only on the redirect layer.
The examples here come from my actual netlify.toml and static/_redirects files.
The Problem Redirects Solve
Hugo generates static files. Netlify serves them.
That sounds simple, but URLs can change for many reasons:
- a post title changes
- a front matter
slugis corrected - old content was moved out of a folder
- a category folder was removed from the public URL
- a typo in a slug was fixed
- an old
/writing/page became/post/ - an API route needs to point to a Netlify Function
When those old URLs already exist in Google, social links, bookmarks, or other sites, deleting them without a redirect creates avoidable 404 noise.
The cleanup goal is not to make every old URL indexable. The goal is:
old URL -> direct 301 -> current canonical URL -> 200
No redirect chains. No duplicate pages. No old URLs in the sitemap.
Where Netlify Reads Redirects From
Netlify supports two file-based redirect locations:
_redirectsnetlify.toml
For a Hugo site, static/_redirects is copied into the final published output as:
public/_redirects
That makes it a convenient place for simple one-line redirects.
The netlify.toml file stays at the project root and is better when the rule benefits from structured TOML syntax, branch deploy settings, functions configuration, or more complex routing.
Netlify’s redirect engine processes _redirects rules first, then netlify.toml rules. That ordering matters if the same source path appears in both files.
My Current Split
I use both files.
In my site, static/_redirects handles simple legacy public paths:
/writing/ /post/ 301
/writing /post/ 301
/connect/ /contact/ 301
/connect /contact/ 301
/about-us/ /about/ 301
/about-us /about/ 301
These are straightforward page moves. The old URL has one obvious destination.
For post-level cleanup, most of my rules live in netlify.toml:
[[redirects]]
from = "/post/cyber-security/dark-web/"
to = "/post/inside-the-dark-web/"
status = 301
[[redirects]]
from = "/post/docker-without-sudo/"
to = "/post/how-to-execute-docker-commands-using-non-root-users/"
status = 301
[[redirects]]
from = "/post/installing-jupyter-notebook-with-virtualenv/"
to = "/post/installing-jupyter-notebook-in-virtual-env/"
status = 301
I prefer netlify.toml for these because it keeps redirect cleanup next to the Netlify build and functions configuration.
Rule 1: Redirect To The Canonical Hugo URL
The most important redirect decision is the target.
Do not redirect to the page that “looks close enough.” Redirect to the exact canonical URL Hugo currently generates.
For example, this old URL existed historically:
/post/cyber-security/dark-web/
The current post is:
/post/inside-the-dark-web/
So the redirect should be direct:
[[redirects]]
from = "/post/cyber-security/dark-web/"
to = "/post/inside-the-dark-web/"
status = 301
I did not recreate the old /post/cyber-security/dark-web/ page. I also did not redirect it to the homepage. It has a real replacement, so it gets a direct 301.
Rule 2: Fix Typos As Redirects, Not As Permanent URL Debt
One issue in my site was a typo:
/post/api-authentication-and-authroization/
The corrected canonical URL is:
/post/api-authentication-and-authorization/
The redirect is simple:
[[redirects]]
from = "/post/api-authentication-and-authroization/"
to = "/post/api-authentication-and-authorization/"
status = 301
There is also an older title-based URL:
[[redirects]]
from = "/post/api-authentication-vs-authorization/"
to = "/post/api-authentication-and-authorization/"
status = 301
That gives Google, readers, and old links one clear answer: the corrected URL is the only page that should survive.
Rule 3: Avoid Redirect Chains
A redirect chain happens when an old URL points to another old URL, which then points to the current URL.
This is weaker:
/old-a/ -> /old-b/ -> /current/
This is better:
/old-a/ -> /current/
/old-b/ -> /current/
That is why my API redirects all point directly to the final page:
/api/ -> /post/unsung-heros-of-interconnected-world-apis/
/posts/api/ -> /post/unsung-heros-of-interconnected-world-apis/
/post/api/ -> /post/unsung-heros-of-interconnected-world-apis/
The static/_redirects version handles old root-level and /posts/ variants:
/api/ /post/unsung-heros-of-interconnected-world-apis/ 301
/api /post/unsung-heros-of-interconnected-world-apis/ 301
/posts/api/ /post/unsung-heros-of-interconnected-world-apis/ 301
/posts/api /post/unsung-heros-of-interconnected-world-apis/ 301
The netlify.toml version handles the old Hugo post path:
[[redirects]]
from = "/post/api/"
to = "/post/unsung-heros-of-interconnected-world-apis/"
status = 301
The destination is the same. That is intentional.
Rule 4: Keep Slash And Non-Slash Variants In Mind
In static/_redirects, I keep both versions for some older page routes:
/writing/ /post/ 301
/writing /post/ 301
I do this for short public routes that may have been shared either way.
For post URLs, I normally use the trailing slash version because Hugo’s canonical post URLs are slash-terminated:
/post/inside-the-dark-web/
The practical rule is:
- for broad public pages, cover both if old links may exist
- for Hugo posts, match the canonical shape Hugo actually serves
Rule 5: Use 200 Rewrites Only When You Mean Rewrite
Most old content URLs should use 301.
But this rule in my netlify.toml is different:
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
That is not an SEO redirect. It is a rewrite.
The browser keeps the /api/... URL, while Netlify serves the matching function behind the scenes.
That is useful for application routes and serverless APIs. It is not what I want for old article URLs.
For old articles, use 301.
For clean API paths backed by Netlify Functions, use 200.
Rule 6: Do Not Use force Unless You Need It
Netlify redirects have a force option. I avoided it for this cleanup.
A forced redirect can override existing content at the source path. That is useful in some situations, but it is also easy to misuse.
For stale post cleanup, I want a simpler rule:
- if Hugo still generates a page at that path, fix the Hugo source first
- if Hugo no longer generates a page at that path, add a direct redirect
That keeps redirects from hiding source problems.
How I Audit A Redirect Before Adding It
Before adding a redirect, I answer four questions.
First, does the old URL still exist in source?
rg -n "cyber-security/dark-web|docker-without-sudo|api-authentication-and-authroization" `
"$SITE_ROOT\content" `
"$SITE_ROOT\layouts" `
"$SITE_ROOT\static" `
"$SITE_ROOT\netlify.toml"
Second, what canonical URL does Hugo generate now?
hugo --source "$SITE_ROOT" `
--config "$SITE_ROOT\hugo.toml" `
--cleanDestinationDir `
--minify
Third, does the final target page exist?
Test-Path "$SITE_ROOT\public\post\inside-the-dark-web\index.html"
Fourth, does the stale URL appear in the generated sitemap?
rg -n "cyber-security/dark-web|docker-without-sudo|api-authentication-and-authroization" `
"$SITE_ROOT\public\sitemap.xml"
The ideal result:
- old URL found only in redirect rules
- canonical page exists in
public/ - stale URL does not appear in
sitemap.xml - canonical URL appears in
sitemap.xml
A Cleanup Pattern I Reuse
When I find a stale URL in Search Console, I use this decision tree:
1. Search the repo for the old slug.
2. If the old slug is still in content/front matter, fix the source.
3. If the old slug is still in layout/static links, fix the link.
4. If the page moved and has a real replacement, add a direct 301.
5. If the page has no replacement, let it 404.
6. Rebuild with --cleanDestinationDir.
7. Check the generated sitemap.
8. Deploy.
9. Test the old URL and final URL in Search Console.
The counterintuitive part is step 5. Not every old URL deserves a redirect. Redirecting deleted, unrelated pages to the homepage creates noise. It can also make debugging harder later.
My Current Redirect Examples
These are examples from my current cleanup set.
Renamed article:
[[redirects]]
from = "/post/docker-without-sudo/"
to = "/post/how-to-execute-docker-commands-using-non-root-users/"
status = 301
Folder path removed from public URL:
[[redirects]]
from = "/post/cyber-security/online-scams/"
to = "/post/dont-click-that-link/"
status = 301
Old title changed:
[[redirects]]
from = "/post/ai-powered-workflow-for-building-websites/"
to = "/post/from-idea-to-live-site-how-i-build-ai-powered-websites-in-a-day/"
status = 301
Old section landing page:
/writing/ /post/ 301
/writing /post/ 301
Serverless function rewrite:
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
These rules are not all the same kind of redirect. That is the point. Article cleanup, navigation cleanup, and application rewrites should be treated differently.
What I Check After Deployment
After deployment, I do not immediately trust the Search Console Pages report. That report can lag.
Instead, I test the live behavior:
old URL -> 301
canonical URL -> 200
canonical URL appears in sitemap
old URL does not appear in sitemap
For important pages, I inspect the final canonical URL in Google Search Console and request indexing there.
I do not request indexing for old redirect-source URLs. Those URLs are supposed to disappear from Google’s index over time.
Redirect Cleanup Checklist
This is the version I would reuse on any Hugo and Netlify site:
1. Export stale URLs from Search Console.
2. Search the source tree for every old slug.
3. Confirm the canonical Hugo URL.
4. Prefer fixing front matter and internal links before adding redirects.
5. Add direct 301 redirects only to real replacement pages.
6. Keep redirect sources out of the sitemap.
7. Avoid redirect chains.
8. Avoid homepage redirects for unrelated deleted pages.
9. Use 200 rewrites only for app/API routing.
10. Rebuild with --cleanDestinationDir.
11. Test old and new URLs after deploy.
12. Validate in Search Console.
Final Takeaway
Good redirect cleanup is not about collecting old URLs forever.
It is about making the current site unambiguous.
For my Hugo site, the clean state is:
- Hugo generates one canonical URL per article
- old article URLs use direct
301redirects - API paths use
200rewrites only when they are meant to hit functions _redirectshandles simple legacy public pathsnetlify.tomlhandles structured Netlify routing- the sitemap contains only current URLs
Once that is true, Search Console can take its time catching up. The site itself is already sending the right signals.