Frank Robert Anderson

I am a Developer who does web good and likes to do other stuff good too.

  • Blog
  • Tutorials
  • Rants
  • Everything
  • Back to My Site

Add a JSON API to Any Static Site Generator

April 23, 2026
tutorial hugo rest frontpage

Back in 2015, I wrote about adding a JSON API to a Jekyll blog. The idea was simple: if your static site generator can output HTML from templates, it can output JSON from those same templates. No server-side code, no plugins, no runtime dependencies. Just another output format alongside your pages.

That technique is not specific to Jekyll. It works with any static site generator that has a templating system. This blog runs on Hugo now. Hugo has a built-in feature for exactly this kind of thing and it makes the whole process much cleaner. So I figured I would revisit the idea.

The idea, again

A static site generator takes your markdown and pushes it through templates to make HTML files. There is nothing special about HTML here –it is just the output format. If you write a template that outputs JSON instead of HTML, you get .json files. Your web server doesn’t care, it serves them the same way and JavaScript on the client side can fetch them.

Hugo’s output formats

Hugo has this thing called custom output formats. Instead of hacking JSON into a regular HTML template like I had to do with Jekyll, I defined a new output format and gave it its own templates. Hugo generates both the HTML and the JSON for you in the same build.

Config

First, tell Hugo about the JSON format. In hugo.toml:

[outputFormats.JSON]
  mediaType = "application/json"
  baseName = "index"

[outputs]
  home = ["HTML", "RSS", "JSON"]
  section = ["HTML", "JSON"]
  page = ["HTML", "JSON"]

The outputs section is where you decide what gets a JSON version. If you only want a listing endpoint and don’t need individual post JSON, just leave page off.

The list template

Hugo will look for a template called list.json.json for sections and the homepage. Create it at layouts/_default/list.json.json:

[
{{ range $index, $page := .Pages }}
  {{ if $index }},{{ end }}
  {
    "title": {{ .Title | jsonify }},
    "url": {{ .RelPermalink | jsonify }},
    "date": {{ .Date.Format "2006-01-02" | jsonify }},
    "description": {{ .Params.description | default "" | jsonify }},
    "tags": {{ .Params.tags | default slice | jsonify }}
  }
{{ end }}
]

See that jsonify function? That is doing all the heavy lifting. It handles escaping, it handles arrays, it handles nil values. In the Jekyll version I had to loop through tags one at a time and build the JSON array by hand. Here I just pipe to jsonify and Hugo figures it out.

The single post template

For individual posts, create layouts/_default/single.json.json:

{
  "title": {{ .Title | jsonify }},
  "url": {{ .RelPermalink | jsonify }},
  "date": {{ .Date.Format "2006-01-02" | jsonify }},
  "description": {{ .Params.description | default "" | jsonify }},
  "tags": {{ .Params.tags | default slice | jsonify }},
  "content": {{ .Content | jsonify }}
}

Build it

Run hugo and check the output. For every page that has JSON in its output list, there will be an index.json sitting right next to the index.html:

  • /posts/index.json – all your posts
  • /posts/2026/04/23/some-post/index.json – one post
  • /index.json – homepage listing

It is simpler with Hugo. No Gulp task, no separate build step, no copying files around. But this same idea is possible in nearly any Static Site Generator.

The homepage template

My homepage template is different from my default list template. It uses .Site.RegularPages instead of .Pages and limits to 50 posts. If yours is the same, you might want a separate index.json.json in your layouts root:

[
{{ range $index, $page := first 50 .Site.RegularPages }}
  {{ if $index }},{{ end }}
  {
    "title": {{ .Title | jsonify }},
    "url": {{ .RelPermalink | jsonify }},
    "date": {{ .Date.Format "2006-01-02" | jsonify }},
    "description": {{ .Params.description | default "" | jsonify }},
    "tags": {{ .Params.tags | default slice | jsonify }}
  }
{{ end }}
]

What Hugo gives you for free

Comparing this to what I went through with Jekyll:

In Jekyll, I had to loop through tags and categories and manually comma-separate them with forloop.last checks. I had to add nil guards for when tags or categories were empty. I used Liquid’s escape filter on content and hoped for the best. And I had a whole Gulp task that duplicated the posts from _posts into an api/v01/posts directory and swapped the layout from html to json.

With Hugo, I write a template, add three lines to my config, and it just works. The jsonify function handles arrays, escaping, and nil values. The output format system handles the file generation. No Gulp task, no extra build step.

This isn’t just a Hugo thing

The core trick works anywhere. If your static site generator has templates, you can make it output JSON. Hugo happens to have first-class support for it, but the pattern is the same whether you are using 11ty, Astro, or even Jekyll like I was in 2015. Write a template, output JSON, let the build generate the files.

I use it for client-side filtering. Fetch the listing endpoint, match on tags in JavaScript, render a related posts section without a page reload. It is not a replacement for a real API –there is no auth, no pagination, no writes. But for read-only content that is already public on your site, it is free and it ships with every build.

Footer

Social Networks

Blogroll

Blob of contradictions
iCodealot

© 2026 Frank Robert Anderson