The most surprising thing about deploying static sites with Netlify and Hugo is that the "static" part is a bit of a misnomer; Netlify is constantly watching your Git repository and rebuilding your site on every commit.

Let’s see what that looks like. Imagine you have a Hugo site. You’ve written some content, set up your theme, and you’re ready to deploy.

Your Hugo project might look something like this in your file structure:

my-hugo-site/
├── archetypes/
├── content/
│   └── posts/
│       └── first-post.md
├── layouts/
├── static/
├── themes/
├── config.toml
└── go.mod

You push this to a Git repository, say, on GitHub.

git add .
git commit -m "Initial Hugo site setup"
git push origin main

Now, you connect this repository to Netlify. In Netlify’s dashboard, you’d go to "Add new site" -> "Import an existing project." You select your Git provider (GitHub, GitLab, Bitbucket) and then pick your repository.

Netlify then asks for build settings. For Hugo, these are typically:

  • Build command: hugo --minify
  • Publish directory: public

You click "Deploy site," and Netlify spins up a build environment. It clones your repository, installs Hugo, runs the hugo --minify command, and then deploys the contents of the public directory to its global CDN.

Here’s a snippet of what the build log might show:

10:15:35 AM: Build starting in Netlify, build-image version: v3.7.1
10:15:35 AM: Cloud provider: aws
10:15:35 AM: Directory listing: /opt/build/repo
10:15:36 AM: Installing Hugo (v0.111.3)
10:15:40 AM: Hugo installed successfully
10:15:40 AM: Running build command: hugo --minify
10:15:45 AM: hugo --minify executed successfully
10:15:46 AM: Creating deploy upload manifest
10:15:48 AM: Starting deploy upload, with 100.0 MiB.
10:15:55 AM: Deploy successful!

The public directory is where Hugo generates all your HTML, CSS, and JS files. Netlify takes that entire directory and distributes it across its edge servers.

The magic happens because Netlify sets up a webhook. When you push a new commit to your connected Git branch (e.g., main), Git notifies Netlify. Netlify then automatically triggers another build process, just like the first one. It pulls the latest code, runs Hugo, and deploys the updated public directory. This means your site is always current with your Git repository.

The problem this solves is the tedious manual deployment process. Instead of building locally and uploading files via FTP or some script, Netlify automates the entire lifecycle from commit to CDN. It handles the build environment, the build process, and the global distribution.

Internally, Netlify uses a fleet of build agents. When a deploy is triggered, one of these agents is provisioned with a clean environment. It checks out your repository, installs any dependencies specified in your package.json (if you were using Node.js for a frontend build, for example) or your go.mod (for Hugo). Then it executes your build command. The output directory is then uploaded to Netlify’s storage, and the CDN is updated to point to this new version.

The exact levers you control are primarily the build command and the publish directory. You can also specify environment variables within Netlify’s UI, which are crucial if your Hugo site relies on API keys or configuration that changes between development and production. For instance, setting HUGO_ENV=production might be handled automatically by Netlify for production deploys, but you can override it.

What most people don’t realize is how Netlify manages caching between builds. By default, Netlify caches the node_modules directory (if applicable) and other build artifacts. This significantly speeds up subsequent builds because Netlify doesn’t have to re-download everything each time. For Hugo, this typically means the Hugo binary itself and potentially cached theme assets if they are managed in a way that Netlify can recognize.

The next concept you’ll run into is handling dynamic elements or server-side logic, as Netlify is primarily for static assets.

Want structured learning?

Take the full Netlify course →