Hugo

Hugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again.

Hugo

What Is Hugo?

Hugo is a static site generator written in Go. It compiles Markdown, Go templates, and data into fully static HTML, CSS, and JavaScript at build time — no runtime server required.

The key distinction for this integration: all external data fetching from APIs like Strapi occurs during the build process. The output is a directory of static files you can deploy to any CDN or static host. Hugo's Go runtime is known for fast builds, which matters when webhook-triggered rebuilds need to propagate content changes quickly.

Hugo v0.159.0 was released on March 23, 2026.

Why Integrate Hugo with Strapi

Hugo handles rendering and deployment. Strapi handles content modeling, editing, and API delivery. Together, they give you a content-driven static site with a clean separation of concerns.

Here's what makes this pairing practical:

  • Fast builds keep feedback loops tight. Hugo's Go runtime compiles sites quickly. When Strapi fires a publish webhook, the full pipeline can complete fast enough to keep editorial workflows responsive.
  • Static output eliminates Strapi from runtime. Once Hugo builds, your production site serves pre-rendered HTML from a CDN. Zero database queries, zero Node.js process overhead per page view. A high-traffic site generates no Strapi API requests at runtime.
  • Strapi v5's flat API response reduces template complexity. The new response format puts content attributes at the top level. In Hugo's Go templates, this means accessing .title instead of .data.attributes.title, resulting in less chaining and fewer nil pointer errors.
  • Draft and publish workflow decouples content from code. Strapi's admin panel gives editors a visual interface for managing content and publishing drafts. Hugo builds can be configured to trigger on Strapi publish events, helping ensure draft content does not reach production.
  • Both tools support i18n natively. Strapi's internationalization capabilities pair with Hugo's multilingual content pipeline. Language routing resolves at build time into distinct URL paths, with no runtime detection or server-side routing needed.
  • Infrastructure scales per layer independently. CDN scales with traffic, Hugo builds scale with content volume, and Strapi scales with editorial team size. Each layer operates in isolation, keeping costs proportional to actual usage.

How to Integrate Hugo with Strapi

This section covers the end-to-end setup: from creating both projects to fetching Strapi content in Hugo templates.

Prerequisites

Before starting, confirm you have these installed:

ToolMinimum VersionRecommended
Node.js20.x24.x
Hugo (extended)0.146.00.159.0
npmBundled with Node.jsLatest

You also need a terminal, a text editor, and basic familiarity with REST APIs and Go template syntax.

# Verify installations
node --version    # Should output v20.x or v24.x
hugo version      # Should output v0.146.0 or later

Step 1: Create a Strapi v5 Project

Start by scaffolding a new Strapi project:

npx create-strapi@latest my-strapi-project
cd my-strapi-project
npm run develop

The CLI prompts for a Strapi Cloud login. Skip this for local development. Once the server starts, open http://localhost:1337/admin and create your first admin account.

The Content-Type Builder is only available in development mode, which is the default for locally created projects.

Step 2: Create Content Types in Strapi

Navigate to Content-Type Builder in the admin panel and create a new Collection Type called Article with these fields:

Field NameTypeNotes
titleText (Short text)Required
slugUIDAttached to title
bodyRich text (Blocks)Main content
featuredImageMedia (Single)Cover image
authorText (Short text)Author name
publishedAtAuto-managedHandled by draft/publish

Click Save after adding all fields. Strapi must be manually restarted to register the new schema, which is stored at:

src/api/article/content-types/article/schema.json

Now add a few articles through the Content Manager. Create entries, fill in the fields, and click Publish to make them available via the API. Draft entries won't appear in API responses by default.

Step 3: Configure API Permissions and Generate a Token

Two options exist for API access. For a Hugo integration, token-based authentication is cleaner than public permissions.

Generate an API token:

  1. Go to Settings → Global settings → API Tokens
  2. Click + Create new API Token
  3. Set the name to Hugo SSG Token
  4. Choose Read-only as the token type
  5. Set duration to Unlimited (rotate manually) or 90 days
  6. Click Save and copy the token immediately. Without an encryptionKey configured in your admin settings, the token displays only once

If you prefer public access instead, go to Settings → Users & Permissions → Roles → Public, then enable find and findOne for your Article content type.

Verify API access with a quick curl:

curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://your-strapi.com/api/articles?populate=*"

The response uses Strapi v5's flat format. Fields sit directly on each object in the data array, with no .attributes wrapper:

{
  "data": [
    {
      "documentId": "abc123def456ghi789jkl012",
      "title": "My First Article",
      "slug": "my-first-article",
      "body": [{ "type": "paragraph", "children": [{ "type": "text", "text": "Hello world" }] }],
      "author": "Ari",
      "featuredImage": { "url": "/uploads/cover_abc123.jpg", "alternativeText": "Article cover" }
    }
  ],
  "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 3 } }
}

Step 4: Create a Hugo Site

In a separate directory, scaffold a new Hugo project:

hugo new site my-hugo-site
cd my-hugo-site

Configure the site to store your Strapi connection details. Open hugo.yaml and add:

baseURL: "http://localhost:1313"
languageCode: "en-us"
title: "Hugo + Strapi Site"

params:
  strapiBaseUrl: "http://localhost:1337"
  strapiToken: ""  # Inject via HUGO_PARAMS_STRAPITOKEN env var. Don't hardcode.

caches:
  getresource:
    maxAge: "10s"  # Short for local dev so Strapi changes appear quickly

For production, inject sensitive values through environment variables using the HUGO_ prefix:

export HUGO_PARAMS_STRAPIBASEURL="https://api.example.com"
export HUGO_PARAMS_STRAPITOKEN="your_production_token"

Step 5: Fetch Strapi Content with a Content Adapter

Content adapters can generate pages from remote API data. Create a _content.gotmpl file in your content directory:

content/
└── articles/
    ├── _content.gotmpl
    └── _index.md

Add a minimal _index.md for the section listing:

---
title: "Articles"
---

Now create the content adapter at content/articles/_content.gotmpl:

{{/* Fetch all articles from Strapi v5 */}}
{{ $data := dict }}
{{ $token := site.Params.strapiToken }}
{{ $baseUrl := site.Params.strapiBaseUrl }}
{{ $url := printf "%s/api/articles?populate=author,featuredImage,categories&pagination[pageSize]=100" $baseUrl }}

{{ $opts := dict
  "headers" (dict "Authorization" (printf "Bearer %s" $token))
}}

{{ with try (resources.GetRemote $url $opts) }}
  {{ with .Err }}
    {{ errorf "Strapi API error: %s" . }}
  {{ else with .Value }}
    {{ $data = . | transform.Unmarshal }}
  {{ else }}
    {{ errorf "No data returned from Strapi API: %s" $url }}
  {{ end }}
{{ end }}

{{/* Generate Hugo pages from Strapi articles */}}
{{ range $data.data }}
  {{ $content := dict "mediaType" "text/markdown" "value" .body }}
  {{ $dates := dict "date" (time.AsTime .publishedAt) }}
  {{ $params := dict
    "strapiId"       .documentId
    "author"         .author
    "featuredImage"  .featuredImage
    "strapiBaseUrl"  $baseUrl
  }}
  {{ $page := dict
    "content" $content
    "dates"   $dates
    "kind"    "page"
    "params"  $params
    "path"    .slug
    "title"   .title
  }}
  {{ $.AddPage $page }}
{{ end }}

This adapter does three things: fetches the JSON response via resources.GetRemote, parses it with transform.Unmarshal, and calls $.AddPage for each article. Hugo treats each generated page like any other content file, with proper dates, params, and section placement.

Important: Older tutorials use getJSON for this purpose. That function was deprecated in Hugo v0.123.0 and later removed in a subsequent release. Use resources.GetRemote for fetching remote resources.

Step 6: Create Layout Templates

Add a layout for individual articles at layouts/articles/single.html:

{{ define "main" }}
<article>
  <header>
    <h1>{{ .Title }}</h1>
    {{ with .Params.author }}
      <p class="author">By {{ . }}</p>
    {{ end }}
    <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
  </header>

  {{/* Featured image with Hugo image processing */}}
  {{ with .Params.featuredImage }}
    {{ $imgUrl := "" }}
    {{ if hasPrefix .url "http" }}
      {{ $imgUrl = .url }}
    {{ else }}
      {{ $imgUrl = printf "%s%s" $.Params.strapiBaseUrl .url }}
    {{ end }}
    {{ with try (resources.GetRemote $imgUrl) }}
      {{ with .Value }}
        {{ $resized := .Resize "800x webp" }}
        <img
          src="{{ $resized.RelPermalink }}"
          width="{{ $resized.Width }}"
          height="{{ $resized.Height }}"
          alt="{{ $.Params.featuredImage.alternativeText }}"
          loading="lazy"
        >
      {{ end }}
    {{ end }}
  {{ end }}

  <div class="content">
    {{ .Content }}
  </div>
</article>
{{ end }}

Project Example: Developer Blog with Automated Deploys

Let's build a practical developer blog that fetches articles, categories, and images from Strapi v5, processes images through Hugo's pipeline, and deploys automatically when content editors hit publish.

This project uses content adapters for page generation, handles Strapi's pagination for large collections, renders the Blocks rich text format, and wires up a GitHub Actions workflow triggered by Strapi webhooks.

Content Model in Strapi

Extend the Article content type from earlier. In the Content-Type Builder, add a Category Collection Type with a name (Text) and slug (UID) field, then add a Relation field on Article pointing to Category (Article belongs to many Categories).

Your final Article model includes:

FieldType
titleShort text
slugUID
bodyRich text (Blocks)
excerptLong text
featuredImageMedia (Single)
authorShort text
categoriesRelation (many-to-many with Category)

After saving and restarting, add a few articles and categories through the admin panel. Publish them to make the content available.

Paginated Content Adapter

For blogs that might grow beyond Strapi's default page size of 25, the adapter needs to handle pagination:

content/articles/_content.gotmpl:

{{/* Paginated fetch from Strapi v5 */}}
{{ $baseUrl := site.Params.strapiBaseUrl }}
{{ $token := site.Params.strapiToken }}
{{ $page := 1 }}
{{ $pageSize := 100 }}
{{ $hasMore := true }}
{{ $allArticles := slice }}

{{ range seq 1 50 }}
  {{ if $hasMore }}
    {{ $url := printf "%s/api/articles?populate=*&pagination[page]=%d&pagination[pageSize]=%d" $baseUrl $page $pageSize }}
    {{ $opts := dict "headers" (dict "Authorization" (printf "Bearer %s" $token)) }}
    {{ with try (resources.GetRemote $url $opts) }}
      {{ with .Err }}
        {{ errorf "Strapi API error on page %d: %s" $page . }}
      {{ else with .Value }}
        {{ $response := . | transform.Unmarshal }}
        {{ $allArticles = $allArticles | append $response.data }}
        {{ $totalPages := $response.meta.pagination.pageCount }}
        {{ if ge $page $totalPages }}
          {{ $hasMore = false }}
        {{ end }}
        {{ $page = add $page 1 }}
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

{{/* Generate pages from all fetched articles */}}
{{ range $allArticles }}
  {{ $article := . }}

  {{ $bodyValue := "" }}
  {{ if (reflect.IsSlice .body) }}
    {{ $bodyValue = . | jsonify }}
  {{ else }}
    {{ $bodyValue = .body }}
  {{ end }}

  {{ $content := dict "mediaType" "text/markdown" "value" $bodyValue }}
  {{ $dates := dict "date" (time.AsTime .publishedAt) }}
  {{ $params := dict
    "strapiId"       .documentId
    "author"         .author
    "excerpt"        .excerpt
    "categories"     .categories
    "featuredImage"  .featuredImage
    "strapiBaseUrl"  $baseUrl
  }}
  {{ $page := dict
    "content" $content
    "dates"   $dates
    "kind"    "page"
    "params"  $params
    "path"    .slug
    "title"   .title
  }}
  {{ $.AddPage $page }}
{{ end }}

The range seq 1 50 loop acts as a safety limit. It won't fetch more than 5,000 articles. The $hasMore flag stops iteration once we've retrieved all pages of results.

Rendering Strapi v5 Blocks Rich Text

Strapi v5 offers rich text editors that can output either structured JSON in the Blocks format or plain Markdown, depending on whether you use the Rich Text (Blocks) or Rich Text (Markdown) field. Hugo needs a custom partial to render this. No official Hugo renderer exists for this format, so here's a working implementation.

layouts/partials/strapi-blocks.html:

{{ range . }}
  {{ $type := .type }}
  {{ if eq $type "paragraph" }}
    <p>{{ partial "strapi-inline.html" .children }}</p>
  {{ else if eq $type "heading" }}
    {{ $tag := printf "h%d" .level }}
    <{{ $tag }}>{{ partial "strapi-inline.html" .children }}</{{ $tag }}>
  {{ else if eq $type "list" }}
    {{ if eq .format "ordered" }}<ol>{{ else }}<ul>{{ end }}
    {{ range .children }}
      <li>{{ partial "strapi-inline.html" .children }}</li>
    {{ end }}
    {{ if eq .format "ordered" }}</ol>{{ else }}</ul>{{ end }}
  {{ else if eq $type "image" }}
    <img src="{{ .image.url }}" alt="{{ .image.alternativeText }}" loading="lazy" />
  {{ else if eq $type "code" }}
    <pre><code>{{ range .children }}{{ .text }}{{ end }}</code></pre>
  {{ end }}
{{ end }}

layouts/partials/strapi-inline.html:

{{ range . }}
  {{ if .bold }}<strong>{{ end }}
  {{ if .italic }}<em>{{ end }}
  {{ if .underline }}<u>{{ end }}
  {{ if .code }}<code>{{ end }}
  {{ if .url }}<a href="{{ .url }}">{{ end }}
  {{ .text | safeHTML }}
  {{ if .url }}</a>{{ end }}
  {{ if .code }}</code>{{ end }}
  {{ if .underline }}</u>{{ end }}
  {{ if .italic }}</em>{{ end }}
  {{ if .bold }}</strong>{{ end }}
{{ end }}

Only use safeHTML on content from a trusted source. If your CMS editors can input arbitrary HTML, add sanitization before rendering.

Article Page Template with Responsive Images

layouts/articles/single.html:

{{ define "main" }}
<article class="blog-post">
  <header>
    <h1>{{ .Title }}</h1>
    <div class="meta">
      {{ with .Params.author }}<span class="author">By {{ . }}</span>{{ end }}
      <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
    </div>

    {{/* Category tags */}}
    {{ with .Params.categories }}
      <div class="categories">
        {{ range . }}
          <span class="tag">{{ .name }}</span>
        {{ end }}
      </div>
    {{ end }}
  </header>

  {{/* Responsive featured image with srcset */}}
  {{ with .Params.featuredImage }}
    {{ $imgUrl := "" }}
    {{ if hasPrefix .url "http" }}
      {{ $imgUrl = .url }}
    {{ else }}
      {{ $imgUrl = printf "%s%s" $.Params.strapiBaseUrl .url }}
    {{ end }}
    {{ with try (resources.GetRemote $imgUrl) }}
      {{ with .Err }}
        {{ errorf "Image fetch failed: %s" . }}
      {{ else with .Value }}
        {{ $sm := .Resize "400x webp" }}
        {{ $md := .Resize "800x webp" }}
        {{ $lg := .Resize "1200x webp" }}
        <img
          src="{{ $md.RelPermalink }}"
          srcset="{{ $sm.RelPermalink }} 400w,
                  {{ $md.RelPermalink }} 800w,
                  {{ $lg.RelPermalink }} 1200w"
          sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
          loading="lazy"
          alt="{{ $.Params.featuredImage.alternativeText }}"
        >
      {{ end }}
    {{ end }}
  {{ end }}

  {{/* Render Blocks rich text */}}
  <div class="content">
    {{ with .Params.body }}
      {{ partial "strapi-blocks.html" . }}
    {{ else }}
      {{ .Content }}
    {{ end }}
  </div>
</article>
{{ end }}

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and Hugo documentation.

Frequently Asked Questions

Hugo is a fast static site generator that compiles Markdown content into static HTML, which is ideal for performance. When integrated with Strapi, Hugo pulls content via Strapi's API and generates pre-rendered static pages. This enhances the speed of your website while allowing content management through Strapi's headless CMS.

Combining Hugo and Strapi offers the best of both worlds: Hugo provides ultra-fast static site generation, while Strapi manages your content efficiently. This integration ensures that your website is both highly performant (thanks to Hugo's static pages) and easily maintainable (with Strapi’s flexible content management system).

To use Hugo and Strapi together, you need:

  • Hugo v0.68+
  • Node.js 14+
  • A database system like PostgreSQL or MySQL
  • Git for version control
  • Basic understanding of the command line and API usage
  1. Create a Strapi project with the command:
    npx create-strapi-app my-project --quickstart
  2. Set up content types in the Strapi admin panel (e.g., Articles, Pages).
  3. Configure CORS settings in Strapi to allow connections from Hugo (development and production environments).
  4. Use Strapi's REST or GraphQL API to fetch the content you need for Hugo.

Hugo fetches content from Strapi using a custom script or plugin that communicates with the Strapi API. You configure your Strapi API URL in the Hugo project's configuration, and Hugo makes HTTP requests to retrieve content. For instance, you can use a Node.js script to pull content and convert it into markdown for Hugo.

Yes, you can use GraphQL to fetch content from Strapi in Hugo. After enabling the GraphQL plugin in Strapi, you can configure your Hugo project to send GraphQL queries to Strapi for more complex or specific data retrieval, which gives you finer control over the data structure and reduces the amount of data fetched.

Strapi supports multilingual content through its i18n plugin, which allows you to manage translations for each content type. Hugo, when configured properly, can then fetch this multilingual data from Strapi and render it according to the user’s language preferences. Make sure to configure your Strapi i18n settings and set up content in different languages, and then use dynamic routing in Hugo to serve the correct language version.

You can optimize performance by:

  • Using Hugo’s built-in image optimization and lazy loading features.
  • Caching content in Hugo using its built-in caching features or through a CDN.
  • Leveraging Strapi’s API caching and pagination to minimize large API responses.
  • Using a CDN to serve static content and reduce server load.

To resolve CORS issues, make sure you configure Strapi’s config/middleware.js to allow requests from your Hugo site’s domain. You can add the following settings to enable CORS for development:

module.exports = {
  settings: {
    cors: {
      enabled: true,
      origin: ['http://localhost:4000'], // Your Hugo site's URL
    },
  },
};