How to create Hugo blog post urls without subfolders in an SEO-friendly way

If you want blog post urls of the form, here’s how to do that without hurting your SEO.

Ron Erdos • Updated January 2, 2021
Tested with Hugo version 0.80.0


Do you want your Hugo blog post urls to have the form, without any subdirectories?

I’ll show you how you to do this without causing any SEO issues.

Before hitting on this solution, I tried a few other methods, but they all produced duplicate content (the same content on different urls), which is not great for SEO.

And Google does have a habit of finding duplicate content, even if you don’t link to it.

Let’s dive in to the solution!


The best, most SEO-friendly way to remove subfolders from your blog post urls is quite simple: Just put all your blog posts and all your pages in the root (top level) of your /content/ folder.

In other words, don’t use any subfolders (such as /posts/ or /pages/) inside your /content/ folder.

However, there are two tradeoffs. I do have effective workarounds for those tradeoffs, though.

Tradeoff 1: Messy repo

The first tradeoff is that you’ll end up with all your blog posts and all your pages (e.g. your “About” and “Contact” pages) mixed together in your /content/ directory, rather than neatly organised in folders.

It means that when you want to work on just your blog posts or just your pages, it’s a lot more work.

Your /content/ folder might look like this, with pages and posts mixed together:

📁 content

While that’s fine from Hugo’s perspective, it can make it hard from a human perspective to quickly find the piece of content you’re looking for.

Workaround to Tradeoff 1

Let’s fix this and bring order back to our repo.

Step 1: Add an underscore to the start of page filenames

Simply add a leading underscore to the filename of pages.

When you do this in our example above, our /content/ folder looks like this:

📁 content

Voila! All our pages are up top; all our posts are down below.

We’re not finished yet though; we need to override the url of each page, otherwise they will have unsightly underscores, like this:

Step 2: Override the url of each page using YAML front matter

You can override this default behaviour by using a url field in your page front matter. It can be named either url or slug.

So in the first page above,, here’s how our YAML front matter might look:

title: About Us
date: 2020-03-09 07:00:00 +11:00
url: "/about/"

Now our About page will have a nice, clean url that looks like this:

I don’t need this url YAML front matter field for my blog posts, because their filenames don’t have a leading underscore, so I’m happy for Hugo to use blog post filenames as the URI.

So my blog post urls look like this:


Now there’s just one more tradeoff we need to know about and manage.

Tradeoff 2: Pages will appear in your list of blog posts

OK, so now your content team can quickly tell which content files are blog posts, and which are pages.

However, Hugo won’t know the difference when it comes time to make a list of blog posts (e.g. on the homepage) unless we give it something to work with.

And unless we help Hugo in this way, your pages (such as About and Contact) will end up in your list of blog posts. Which means they could end up featured way too prominently on your homepage.

Workaround to Tradeoff 2

We’re going to give Hugo a way to tell pages from posts.

Step 1: Add a new YAML front matter field to your pages

In my pages (About and Contact), I’m going to add YAML front matter that says post: false.

Here’s my YAML front matter for my About page, for example:

title: About
date: 2020-03-09 07:00:00 +11:00
url: "/about/"
post: false

I don’t have to add YAML front matter saying post: true to my posts though. It’s good enough just to add the opposite value, post: false to pages; we’ll save a lot of time that way.

Step 2: Filter out pages from our list template

Our list template (at /layouts/_default/list.html) determines which content will show up on our homepage.

We’ll need to add some logic to make sure our pages (About and Contact) are excluded.

Here’s how you could do that:

<div id="list">
	{{ $paginator := .Paginate (where .Site.RegularPages "" "!=" false) }}
	{{ range first 10 $paginator.Pages }}
		{{ .Render "li" }}
	{{ end }}

Let’s walk through this code, line by line.

<div id="list"> is our containing div with an id of “list” for CSS styling.

The second line is where we declare our $paginator variable.

In the Go programming language upon which Hugo is based (hence the name Hugo), variables are preceded by a $ (dollar sign).

The relevant part to this tutorial is a bit later on in the same line: (where .Site.RegularPages "" "!=" false). This limits our newly-created paginator variable to only those pages which have a YAML front matter field where post is not set to false.

In other words, remember how our About and Contact pages have a YAML front matter field of post: false? Well, that’s how we’re going to exclude those pages from our homepage list.

We can’t use code which says “where the post front matter field is set to true”, because we earlier saved ourselves all that work of not adding post: true to each post. So if we use code that says “where post is set to true”, then we won’t get any results at all, and our homepage list will be blank.

Next, we open up a range of the first 10 pieces of content that meet the description we set above. In other words, no pages, only posts.

Then we render the li.html template for each of those 10 posts.

In the second-last line, we close out the range loop with {{ end }}

Finally, we close out the <div> with a closing </div> tag.


Once again, this is the best, most-SEO friendly way I’ve found to publish Hugo blog post urls without any subfolders. There are a few one-time tasks to configure, but once they’re done, you’ll reap the benefits forever.

I'll also send you useful Hugo web dev tips every now and then.

The planets in our solar system