Right, so you’ve got your site building locally. That’s cute. But the whole point of this exercise is to get it on the actual internet, preferably without you having to manually drag files onto a server like some sort of digital peasant. This is where preview deployments and branch deploys come in—the machinery that turns your Git workflow into a publishing powerhouse. It’s the difference between a static site and a professional-grade deployment pipeline.

The Magical World of Deploy Previews

Here’s the core concept: every time you open a pull request (PR) on GitHub or push to a branch that isn’t your main production branch (commonly main or master), your hosting provider can automatically build that branch and give you a unique, temporary URL to see it. This isn’t just a convenience; it’s a game-changer. You can send a link to your client, your colleague, or your cat for review, and they’re seeing exactly what will go live, without you ever having to merge the code.

Netlify is the undisputed king of this. Their deploy previews are so good they feel like cheating. The moment you open a PR, Netlify’s bots spring into action. They’ll clone your repo, check out your branch, run your build command (hugo), and if it succeeds, slap the generated public folder onto a global CDN. The URL usually looks like https://deploy-preview-123--your-site.netlify.app. The best part? It adds a comment to your PR with a direct link. It’s a beautiful, beautiful dance.

For GitHub Pages, this magic is a bit more manual. You don’t get automatic previews for PRs from forks, but you can set up a GitHub Action to build and deploy every branch to a unique subdirectory. It’s more work, but it gets the job done. Here’s a taste of what that GitHub Actions workflow (.github/workflows/gh-pages-previews.yml) might look like:

name: Deploy Branch Previews to GitHub Pages

on:
  push:
    branches: 
      - '*' # This runs on push to any branch except main
      - '!main'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        submodules: recursive # Crucial for Hugo themes

    - name: Setup Hugo
      uses: peaceiris/actions-hugo@v2
      with:
        hugo-version: '0.125.7' # Pin your Hugo version. Trust me.

    - name: Build
      run: hugo --minify

    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./public
        destination_dir: preview/${{ github.ref_name }} # This puts each branch in its own folder

Your preview would then live at https://your-username.github.io/your-repo/preview/your-branch-name/. It’s not as slick as Netlify’s one-click, but it’s yours, and it works.

Taming the Build: Environment Variables and Context

This is where most people faceplant. Your production site might use Google Analytics, but you absolutely do not want its noisy data polluting your previews. Or maybe you have a “contact form” that actually emails you; you probably don’t want that firing off from every random preview build.

This is solved with environment variables and build context. Both Netlify and Cloudflare Pages let you set environment variables that are scoped to specific contexts (Production, Deploy Previews, Branch Deploys). You check for these in your Hugo config.

In your config.toml (or better yet, using Hugo’s config directory pattern), you can do:

# config/_default/config.toml
[services.googleAnalytics]
id = "UA-PRODUCTION-ID" # Default, for production

# config/production/config.toml (only loaded in production build)
[services.googleAnalytics]
id = "UA-PRODUCTION-ID"

# config/development/config.toml (loaded in dev and previews)
[services.googleAnalytics]
id = "UA-TEST-ID" # Or just leave it completely blank

Hugo automatically loads settings based on the environment (HUGO_ENV). Netlify sets HUGO_ENV to “production” for your main site, but for deploy previews, it’s not set, so it falls back to the “development” settings. You can also use plain old environment variables. In your build command on Netlify, you can set: hugo --minify --environment preview

And then in your config, use a variable:

enableRobotsTXT = {{ if eq (getenv "CONTEXT") "production" }}true{{ else }}false{{ end }}

This means search engines will only index your production site, not every single preview you’ve ever built. This is a critically important best practice.

The Pitfalls: When the Magic Smoke Escapes

It’s not all rainbows and unicorns. The most common face-melter is build timeouts. Netlify gives you 15 minutes on their starter plan; Cloudflare gives you 15. If you have a massive site (thousands of pages), your Hugo build might exceed this. The solution is almost always to optimize your build—prune unnecessary content, simplify templates, or upgrade your plan.

The second gotcha is caching. Your local build works, the preview fails. 99% of the time, it’s because you forgot to check out your theme as a submodule. See that submodules: recursive line in the GitHub Action above? That’s not a suggestion. It’s a requirement. Your hosting platform’s build environment is a blank slate. It doesn’t have your theme locally unless you tell Git to go get it.

Finally, be ruthless with your draft content. Hugo doesn’t build drafts by default, but some CI environments can be weird. Explicitly set --buildDrafts=false in your production build command to ensure that post you’re still working on doesn’t accidentally ship to the world. Use the previews for the drafts instead! That’s what they’re for.