Example Hugo Project Structure#

Here’s a typical Hugo project layout:

/my-hugo-site
├── archetypes/          # Default front matter templates
├── assets/              # Processed assets (SCSS, JS, images)
├── content/             # Site content (Markdown)
│   ├── _index.md        # Homepage content
│   ├── blog/
│   │   ├── _index.md    # Blog section
│   │   ├── post-1.md    # Blog post 1
│   │   ├── post-2.md    # Blog post 2
├── data/                # Structured data (JSON, YAML, TOML)
├── layouts/             # Custom layouts and overrides
│   ├── _default/
│   │   ├── baseof.html  # Base template
│   │   ├── single.html  # Layout for single pages
│   │   ├── list.html    # Layout for section pages
│   ├── partials/
│   │   ├── header.html  # Header template
│   │   ├── footer.html  # Footer template
│   ├── index.html       # Custom homepage layout
├── static/              # Unprocessed static files
│   ├── img/             # Images
│   ├── js/              # JavaScript files
├── themes/              # Installed themes
│   ├── mytheme/         # Theme folder
├── config.toml          # Site configuration
├── public/              # Generated site output

Understanding Key Concepts#

1. Base URL (baseURL)#

Defines the root URL of your site. Example:

baseURL = "https://example.com/"
  • Used to generate absolute URLs for pages, images, and assets.

  • If you’re working locally, you can override it:

    hugo server --baseURL=http://localhost:1313/
    

Controls how URLs are structured. Example:

permalinks = { 
  blog = "/blog/:slug/" 
}
  • /blog/my-first-post/ instead of /blog/my-first-post.md.
  • Other placeholders:
    • :year/blog/2025/my-first-post/
    • :title/blog/my-first-post/

3. Menu & Navigation (menu)#

Defines site navigation. Example:

[menu]
  [[menu.main]]
    identifier = "home"
    name = "Home"
    url = "/"
    weight = 1

  [[menu.main]]
    identifier = "blog"
    name = "Blog"
    url = "/blog/"
    weight = 2
  • weight determines menu order.

  • Displayed in layouts/partials/header.html:

    <nav>
      {{ range .Site.Menus.main }}
        <a href="{{ .URL }}">{{ .Name }}</a>
      {{ end }}
    </nav>
    

4. baseof.html (Base Template)#

baseof.html acts as a wrapper for all pages.

<!DOCTYPE html>
<html>
<head>
  <title>{{ .Title }}</title>
  <link rel="stylesheet" href="{{ "css/main.css" | relURL }}">
</head>
<body>
  {{ partial "header.html" . }}
  <main>{{ block "main" . }}{{ end }}</main>
  {{ partial "footer.html" . }}
</body>
</html>
  • {{ block "main" . }} → Content goes here based on single.html or list.html.
  • {{ partial "header.html" . }} → Inserts a reusable header.

How Themes Work & Customisation Without Ejecting#

How Themes Work in Hugo#

Themes live in /themes/ and provide:

  • Layouts (themes/mytheme/layouts/)
  • Static files (themes/mytheme/static/)
  • Config settings (themes/mytheme/config.toml)

If a theme is used (theme = "mytheme" in config.toml), Hugo looks for files in this order:

  1. Your project (layouts/) → Custom overrides
  2. The theme (themes/mytheme/layouts/) → Default files

Customizing a Theme Without Ejecting#

Instead of modifying theme files directly:

  1. Override layouts:

    • Copy a file from themes/mytheme/layouts/ to layouts/ and edit it.

    • Example: Override themes/mytheme/layouts/_default/single.html

      cp themes/mytheme/layouts/_default/single.html layouts/_default/single.html
      
  2. Override CSS/JS:

    • Add custom styles in assets/css/custom.css:

      body { background-color: #f5f5f5; }
      
    • Load it in baseof.html:

      <link rel="stylesheet" href="{{ "css/custom.css" | relURL }}">
      
  3. Override partials:

    • Copy and modify themes/mytheme/layouts/partials/header.html
    • Place it in layouts/partials/header.html

Handling Image Optimisation in a Large Hugo Site (1000+ Images)#

Problem#

If your site has 1000 images for example, pushing all of them every time you update a post is very inefficient.

Solution: Separate Image Storage & Processing#

1. Use static/ or External Storage#

  • Store images outside the repo (static/images/).

  • Git ignores them (.gitignore):

    static/images/*
    

2. Optimize with Hugo Pipes#

Example: Compress images before deployment.

{{ $img := resources.Get "images/large-photo.jpg" | images.Resize "800x" | images.Optimize }}
<img src="{{ $img.RelPermalink }}" alt="Optimized Image">

3. Use an External CDN#

  • Upload images to Cloudinary, BunnyCDN, or S3.

  • Store URLs in Markdown instead of raw images:

    ![CDN Image](https://cdn.example.com/images/photo.jpg)
    

4. Deploy Site Without Re-uploading Images#

Instead of deploying images every time:

  1. Host images externally → Use a CDN

  2. Ignore images in Git → Avoid repo bloat

  3. Automate uploads → Sync only new images with rsync

    rsync -av --ignore-existing static/images/ remote:/var/www/images/