These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
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.
Sign up for the Logbook, Strapi's Monthly newsletter
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
.titleinstead 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:
| Tool | Minimum Version | Recommended |
|---|---|---|
| Node.js | 20.x | 24.x |
| Hugo (extended) | 0.146.0 | 0.159.0 |
| npm | Bundled with Node.js | Latest |
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 laterStep 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 developThe 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 Name | Type | Notes |
|---|---|---|
title | Text (Short text) | Required |
slug | UID | Attached to title |
body | Rich text (Blocks) | Main content |
featuredImage | Media (Single) | Cover image |
author | Text (Short text) | Author name |
publishedAt | Auto-managed | Handled 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.jsonNow 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:
- Go to Settings → Global settings → API Tokens
- Click + Create new API Token
- Set the name to
Hugo SSG Token - Choose Read-only as the token type
- Set duration to Unlimited (rotate manually) or 90 days
- Click Save and copy the token immediately. Without an
encryptionKeyconfigured 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-siteConfigure 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 quicklyFor 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.mdAdd 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:
| Field | Type |
|---|---|
title | Short text |
slug | UID |
body | Rich text (Blocks) |
excerpt | Long text |
featuredImage | Media (Single) |
author | Short text |
categories | Relation (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.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.