When working with Hugo as a Static Site Generator (SSG), you often face the problem: some content is dynamic, for example from a Headless CMS like Strapi. But you still want to benefit from the performance advantages of static pages.
An elegant solution: use a second, small Hugo instance as a prebuild step to automatically generate Markdown or content files from the dynamic data — your main site processes these like "normal" Hugo content. This way you can combine the best of both static and dynamic models.
Why a prebuild with a second Hugo instance at all?
- Performance & SEO
Statically generated pages are fast, reliable and good for SEO (e.g. fast loading times, lower server load). With the prebuild, you avoid (or minimize) runtime queries to CMS or APIs on page requests. - Consistent content handling
Your main Hugo only knows "Markdown & Frontmatter". You don't need to complicate your Hugo templates with complex API logic — the prebuild translates the API data into the format Hugo expects. - Separation of concerns
The prebuild process can run independently (e.g. in CI/CD, cronjob or build pipeline). Your frontend stays lean and focused. - Flexibility for dynamic content model
Content can be managed in the CMS. When changes occur, you trigger the prebuild (e.g. via webhook). This keeps your page content current without having to make an API call on every visit.
Architecture overview & flow
CMS (e.g. Strapi)
↓ (API / Webhook)
Prebuild Hugo instance → generates Markdown files
↓
Main Hugo instance → renders static pages
↓
Deployment (e.g. CDN, Netlify, Vercel)
Step by step
1. Set up Prebuild Hugo project
- Create a separate Hugo project for the prebuild
- Only the logic for Markdown generation is needed
- Go Template in layout: /layouts/index.html
2. Fetch JSON from CMS
Example: Save JSON from Strapi as file `data/posts.json`.
3. Hugo template for Markdown generation
{{ with resources.GetRemote "" }}
{{ $items := unmarshal .Content }}
{{ range $items }}
{{ $string := "+++\n" }}
{{ $string = printf "%s title = \"%s\"\n" $string .title }}
{{ $string = printf "%s url = \"/%s\"\n" $string .slug }}
{{ $string = printf "%s type = \"%s\"\n" $string .type }}
{{ $string = printf "%s date = \"%s\"\n" $string .date }}
{{ $string = printf "%s%s" $string "+++\n" }}
{{ $string = printf "%s %s\n" $string .content.rendered }}
{{ $filename := printf "posts/%s.md" (urlize .id) }}
{{ $resource := resources.FromString $filename $string }}
{{ $file := $resource.RelPermalink }}
{{ end }}
{{ end }}
Explanation:
- `resources.GetRemote` reads the data from the CMS
- Loop `range` generates Markdown with frontmatter for each post
- Hugo writes this content to the `public/` directory during build
4. Use main Hugo project
The generated Markdown files are copied to the content directory of the main Hugo instance:
prebuild/public/posts/*.md → main-site/content/posts/
Then run `hugo --gc --minify` as usual.
SEO & Performance tips
- Set canonical URLs and slugs correctly
- Maintain meta titles, descriptions and OG tags in frontmatter
- Store images locally and optimize with Hugo Image Pipes
- Update sitemap and robots.txt
- Error handling in case JSON is not available
Conclusion
With Hugo + Go as a prebuild instance, dynamic content can be automatically converted to Markdown. This way you benefit from maximum performance, SEO friendliness and a simple, Go-based workflow.