Right, let’s talk about the public/ directory. This is where the magic happens, or more accurately, where the result of the magic is dumped. It’s the single most important directory in your project, and yet, you will almost never directly touch a file inside it. Intrigued? You should be.

Think of your Hugo project as a kitchen. Your content/, layouts/, and static/ directories are your countertops, ingredients, and recipes. The public/ directory is the clean, polished plate of food you serve to your guests. You don’t prepare the food on the plate; you prepare it and then put it on the plate. This directory is that final, served plate. It contains the entire, fully rendered, static website. Every HTML file, every CSS file, every image that’s been processed, every JavaScript asset—all of it, perfectly arranged and ready to be uploaded to a web server.

Hugo generates this entire structure from scratch every time you run hugo or hugo server. This is its core job. It takes your content (written in Markdown), your templates (written in Go HTML), and your static assets, and it compiles them down to pure, vanilla HTML, CSS, and JS.

The hugo Command is a Wrecking Ball

Here’s the first thing you need to burn into your brain: running the hugo command (without the server flag) will completely obliterate the entire public/ directory and then rebuild it from the ground up. It does not “update” or “patch” the directory. It’s a scorched-earth policy. This is why it’s so fast and so reliable—it’s not trying to be clever about diffs. It’s a clean build every single time.

This has a critical implication: Never, ever manually edit a file in the public/ directory. It’s a complete waste of your time. Your changes will be vaporized the next time Hugo builds the site. I’ve seen people do this. Don’t be that person. If you need to change something, you change it in the source (layouts/, static/, content/), not the output.

What You’ll Find Inside

The structure of public/ mirrors the structure of your content/ directory, but translated for the web. A content file at content/posts/my-awesome-post.md will become public/posts/my-awesome-post/index.html. Hugo does this by default to create “pretty URLs,” which is a fantastic choice they made. It means your URLs don’t end in .html and each post lives in its own folder, which is great for organizing related assets.

Your static/ directory gets copied over verbatim. If you have static/images/logo.png, you’ll find it at public/images/logo.png. It’s a straight copy-paste job.

Here’s a quick example. Given this ridiculously simple site structure:

.
├── archetypes/
├── content/
│   └── posts/
│       └── hello-world.md
├── layouts/
├── static/
│   └── css/
│       └── style.css
└── config.toml

Running hugo would generate a public/ directory that looks like this:

public/
├── posts/
│   └── hello-world/
│       └── index.html
├── css/
│   └── style.css
└── index.html

Notice how static/css/ became public/css/? And how the post became a folder with an index.html inside? That’s Hugo working as intended.

The .gitignore Rule of Law

Because the public/ directory is entirely generated, you should never commit it to your git repository (unless you have a very specific, and probably wrong, reason to do so). It’s not source code; it’s a build artifact. It’s the equivalent of committing the .exe file from your C++ project.

Your .gitignore file should absolutely include this line:

/public/

This keeps your repository clean and focused on the actual source files that matter. You build the site during your deployment process (e.g., on Netlify, Vercel, GitHub Pages, or your own CI/CD server), not on your local machine before you push.

Checking the Output (Without a Server)

Sometimes you don’t want to run a local server; you just want to build the site to check the final output. Maybe you’re debugging a minification issue or checking the generated meta tags. Simply run:

hugo

Then, you can open the public/index.html file directly in your browser. Be warned: because you’re using the file:// protocol, any links that use absolute paths (like /css/style.css) will break. To properly test the fully built site locally, you can use Python’s built-in server:

cd public
python3 -m http.server 1313

Then navigate to http://localhost:1313. It’s a perfect, final check before you deploy. This directory is exactly what will be uploaded to your host. If it works here, it will work there. It’s the ultimate source of truth.