Why Localize
Localization broadens the markets your site can reach. Translating content opens your product to users who previously couldn't use it, which tends to lift conversion rates. A localized site also tends to rank better in region-specific search results, which in turn reduces the cost of marketing to those audiences. Brand recognition compounds as your reach expands across markets.
Localization itself is the process of adapting a site or product built for one market so it works in another. That can mean translating text, but it also covers removing content that could be considered offensive, adjusting visual design for different writing systems, or respecting regional color preferences. None of this is possible if the underlying architecture can't support multiple markets without duplicating the entire site.
That architectural capability is internationalization: building a site that can switch languages, content, and interface elements between markets. Before you start, you need a backend that can manage content across locales. Strapi — an open-source headless CMS built with Node.js — provides this through its Internationalization feature. You define content types in its admin panel, and it generates a customizable API for each one. Its Role-Based Access Control (RBAC) lets you assign custom permissions so different people can manage content only for the locales they're responsible for.
On the frontend, Hugo is a static site generator written in Go. It applies your data and content to templates and outputs static pages, which are fast to deliver. Hugo typically completes builds in a second or less. It supports multiple content types, theme integration, multi-language sites, and markdown authoring, with additional support for Google Analytics, Disqus comments, code highlighting, and RSS.
The example in this tutorial is a documentation site for a product used in Canada, Mexico, and the United States. The documentation needs to appear in English, French, and Spanish. The site will have three pages: home, about, and terms. Strapi will manage the content for those pages in all three languages and serve markdown versions through its API. The Hugo site will pull that content and display it based on the user's language selection.
Prerequisites
- Hugo installed. Pre-built binaries are available for macOS, Windows, Linux, and other systems, or you can install via command line. Installation guides are on the Hugo website. This tutorial uses v0.68.
- Node.js installed. Strapi requires Node.js 12 or higher but recommends version 14. Don't install anything newer than 14, as Strapi may not support it. Installers are on the Node.js downloads page.
Step 1: Set Up Strapi
The Strapi app will be called docs-server. In your terminal, change to the directory where you want the app and run the install command. When prompted, choose Quickstart as the installation type and select No when asked about a template.
npx [email protected] docs-server
? Choose your installation type Quickstart (recommended)
? Would you like to use a template? (Templates are Strapi configurations designed for a specific use case) No
This creates a Strapi quickstart project, installs dependencies, and runs the app at http://localhost:1337. Register an administrator at http://localhost:1337/admin/auth/register-admin. After entering your names, email, and password, you'll be redirected to the admin panel.
The admin panel is where you create content types, add entries, and manage its settings. With the app running, create content types for each of the three pages.
Step 2: Create Content Types
Strapi supports two kinds of content types. A collection type handles content with a single structure but multiple entries (like blog posts). A single type models unique content with only one entry — an "about" page is a typical case as a site usually has just one. You'll create single types for home, about, and terms, each with a title and content attribute as a starting point.
Stop Strapi before running the creation commands, since they will crash a running app. Use the CLI to generate the types.
npm run develop
for page in home about terms; do npm run strapi generate:api $page title:string content:richtext ;done
The command generates the three content types with their attributes and APIs inside the api/ folder, including models, services, controllers, and configuration for each.
api
├── about
│ ├── config
│ │ └── routes.json
│ ├── controllers
│ │ └── about.js
│ ├── models
│ │ ├── about.js
│ │ └── about.settings.json
│ └── services
│ └── about.js
├── home
│ ├── config
│ │ └── routes.json
│ ├── controllers
│ │ └── home.js
│ ├── models
│ │ ├── home.js
│ │ └── home.settings.json
│ └── services
│ └── home.js
└── terms
├── config
│ └── routes.json
├── controllers
│ └── terms.js
├── models
│ ├── terms.js
│ └── terms.settings.json
└── services
└── terms.js
CLI-generated types are collection types by default, so you'll convert them and enable localization. In api/about/models/about.settings.json, change the kind to singleType, add a description, and enable localization by setting localized to true under the i18n property in pluginOptions for the type and both attributes.
{
"kind": "singleType",
"collectionName": "about",
"info": {
"name": "about",
"description": "The about page content"
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"pluginOptions": {
"i18n": {
"localized": true
}
},
"type": "string"
},
"content": {
"pluginOptions": {
"i18n": {
"localized": true
}
},
"type": "richtext"
}
}
}
Collection types get routes for find, find one, count, delete, update, and create. A single type only needs routes to retrieve, update, and delete its single entry — no count, create, or find-one routes. Replace the routes file accordingly.
{
"routes": [
{
"method": "GET",
"path": "/about",
"handler": "about.find",
"config": {
"policies": []
}
},
{
"method": "PUT",
"path": "/about",
"handler": "about.update",
"config": {
"policies": []
}
},
{
"method": "DELETE",
"path": "/about",
"handler": "about.delete",
"config": {
"policies": []
}
}
]
}
The home content type needs the same adjustments. Update its model settings and routes.
{
"kind": "singleType",
"collectionName": "home",
"info": {
"name": "Home",
"description": "The home page content"
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"content": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
{
"routes": [
{
"method": "GET",
"path": "/home",
"handler": "home.find",
"config": {
"policies": []
}
},
{
"method": "PUT",
"path": "/home",
"handler": "home.update",
"config": {
"policies": []
}
},
{
"method": "DELETE",
"path": "/home",
"handler": "home.delete",
"config": {
"policies": []
}
}
]
}
Apply identical changes to the terms type in api/terms/models/terms.settings.json and its routes.
{
"kind": "singleType",
"collectionName": "terms",
"info": {
"name": "Terms",
"description": "The terms content"
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"content": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
{
"routes": [
{
"method": "GET",
"path": "/terms",
"handler": "terms.find",
"config": {
"policies": []
}
},
{
"method": "PUT",
"path": "/terms",
"handler": "terms.update",
"config": {
"policies": []
}
},
{
"method": "DELETE",
"path": "/terms",
"handler": "terms.delete",
"config": {
"policies": []
}
}
]
}
Step 3: Add Locales
Now add the locales for your target markets: English (America) — en-US, French (Canada) — fr-CA, and Spanish (Mexico) — es-MX. With Strapi running via npm run develop, go to the Internationalization settings under Settings → Global Settings. Click the blue Add a locale button and pick each locale from the dropdown.
| Locale | Local Display Name |
|---|---|
| en-US | English(America) |
| es-MX | Spanish(Mexico) |
| fr-Ca | French(Canada) |
Add a locale pop-up in the Strapi Internationalization Settings. (Large preview)Set one of these as the default locale via Advanced Settings in the pop-up. If you don't, the first entry will fall back to en. If you don't need that locale, delete it after setting an alternative default.
With locales configured, you're ready to add content — first for the default locale, then for the others — which the Hugo frontend will consume and serve according to the user's selection.
Add Content For Every Page And Locale
In the Strapi admin panel, you can now populate the About, Home, and Terms content types through their respective content entry forms. Each page has a title field and a content area. Plan placeholder text in each language, and use distinguishing flag emojis so you can identify which locale a page belongs to at a glance.
Before saving, verify the locale selector shows the correct language. After modifying a locale, click the bright green Save button, followed by the Publish button in the top right. To add content for another language, pick it from the Locales dropdown in the Internationalization panel and repeat the save-and-publish process.
Expose The Content Endpoints
Strapi’s default permissions block anonymous read access, so hitting /home, /about, or /terms returns a 403 Forbidden error. To make the pages public, head to Users & Permissions Plugin → Public Roles. Under Permissions in the Application section, tick the find checkbox for Home, About, and Terms, and click the bright green Save button.
The routes are now accessible, and you can select a language with the _locale query parameter. For instance, http://localhost:1337/home?_locale=fr-CA returns the Canadian French version of the home page. Requests without that parameter return the default locale.
Scaffold The Hugo Project
In a directory separate from docs-server (the Strapi app), create the new Hugo site named docs-app:
- Run the
hugo new sitescaffold command. - Launch the local dev server.
- Visiting
http://localhost:1313/shows an empty shell because there is neither a theme nor content yet.
Install A Theme That Speaks Many Languages
Hugo supports themes as git submodules, so the project first needs an initialized git repository inside docs-app. Add the hugo-book documentation theme from the official theme showcase, but confirm any alternative theme you consider supports internationalization before using it.
After the submodule is cloned into the themes folder, restart the Hugo server to apply the theme. The site will still render nearly empty, but you should see the book shell and its default layout.
Configure The Languages In config.toml
Enabling internationalization involves more than picking a theme. In the Hugo config, replace the config.toml contents entirely to define three languages, each with its own content directory and label. The language name you declare here is what users see in the language dropdown. The config also declares StrapiServerURL, pointing to http://localhost:1337 for local development.
That endpoint setting works with Hugo’s getJSON template function to fetch Strapi data. Because getJSON caches responses aggressively, set the maxAge config property to 10s so content edits on the Strapi side show up quickly during development. Once the site is deployed, raise this to a reasonable value based on your rebuild cadence and how often content changes.
Overriding The Theme For Dynamic Content
You can override a Hugo theme’s template by creating a file with the same path in the top-level layouts/ folder. For the hugo-book theme, you will replace the behavior of themes/book/layouts/partials/docs/inject/content-after.html. Hugo inserts whatever this partial renders after the main content on every page.
After creating layouts/partials/documentation/strapi-content.html, its partial does the following:
- Reads an endpoint page variable defined in front matter.
- Assembles a fallback object with a placeholder title and body.
- Checks that an endpoint, a Strapi server URL, and a page exist in a locale-aware data structure.
- Builds the full URL and uses the
getJSONfunction to return the fetched data; otherwise returns the default structure.
The custom layouts/partials/docs/inject/content-after.html partial then invokes the new partial, wraps the response title in a heading inside an article tag, pipes the body through the markdownify filter, and prints the result. The next config step wires page modules for each language to these partials, but this is where Strapi’s content becomes visible on the frontend.
Creating Content Pages
Each language has its own content folder: content for English (US), content.es-mx for Español (Mexico), and content.fr-ca for Français (Canada). Every content file must declare an endpoint front matter variable, which points to the Strapi endpoint that supplies the content in that language. This variable is added through two archetype files: archetypes/default.md and archetypes/docs.md.
Archetypes serve as templates for content files, defining default front matter and content when you use hugo new. archetypes/default.md handles all _index.md files, while archetypes/docs.md applies to files inside docs/ folders, a structure specific to the hugo-book theme. Create the docs archetype:
touch archetypes/docs.md
Then replace the contents of both archetype files with:
---
title: "{{ replace .Name "-" " " | title }}"
endpoint: "/"
---
<br/>
The title appears as the page heading and in the table of contents. The endpoint tells Hugo where to fetch content in Strapi. The <br/> tag prevents the page from being treated as blank during builds.
Generate the language-specific content directories:
mkdir content.es-mx content.fr-ca
Next, populate each directory with the content files:
for cont in "_index.md" "docs/about.md" "docs/terms.md"; do hugo new $cont; done && for langDir in "content.es-mx" "content.fr-ca" ; do cp -R content/* $langDir; done
This creates an _index.md, a docs/about.md, and a docs/terms.md inside each content folder:
content
├── docs
│ ├── about.md
│ └── terms.md
└── index.md
content.es-mx
├── docs
│ ├── about.md
│ └── terms.md
└── index.md
content.fr-ca
├── docs
│ ├── about.md
│ └── terms.md
└── index.md
Add the following front matter and content to each file.
Home (index.md)
content
---
title: "Home"
endpoint: "/home?_locale=en-US"
---
<br/>
content.es-mx
---
title: "Hogar"
endpoint: "/home?_locale=es-MX"
---
<br/>
content.fr-ca
---
title: "Accueil"
endpoint: "/home?_locale=fr-CA"
---
<br/>
About (docs/about.md)
content
---
title: "About"
endpoint: "/about?_locale=en-US"
---
<br/>
content.es-mx
---
title: "Sobre"
endpoint: "/about?_locale=es-MX"
---
<br/>
content.fr-ca
---
title: "À propos"
endpoint: "/about?_locale=fr-CA"
---
<br/>
Terms (docs/terms.md)
content
---
title: "Terms"
endpoint: "/terms?_locale=en-US"
---
<br/>
content.es-mx
---
title: "Condiciones"
endpoint: "/terms?_locale=es-MX"
---
<br/>
content.fr-ca
---
title: "Conditions"
endpoint: "/terms?_locale=fr-CA"
---
<br/>
Now start the Hugo server. First confirm Strapi is running with npm run develop in a separate terminal inside the docs-server folder so Hugo can pull content during the build:
hugo serverBelow are views of the site rendered in each language:
Automated Rebuilds
Because Hugo generates static pages, content is fetched from Strapi at build time, not on each request. To keep pages in sync with the CMS, schedule regular rebuilds—for instance, on Netlify—or trigger them whenever content changes.
Wrapping Up
Hugo builds fast static sites with built-in multilingual support. Through its internationalization configuration, you can define multiple languages and have Hugo generate a version of the site for each. Strapi handles content management through an admin interface and exposes a customizable API consumed by external frontends. Its internationalization plugin stores content in different locales.
In this walkthrough you set up a Strapi application with three single content types for the home, about, and terms pages, each localized for English (US), Español (Mexico), and Français (Canada). You also configured public read routes for those API endpoints.
On the Hugo side, you added a documentation theme, enabled internationalization, created language-specific content files, and adapted the theme to render content fetched from Strapi. If you extend the project, consider adding more page types with richer content structures or supporting another locale.
Consult the Hugo documentation for deeper customization options, and visit the Strapi website for details on its broader feature set.



