OpenAPI Reference Documentation in Hugo: A Better Alternative to Swagger

Chris Concannon
4 min readFeb 5, 2021

I’ve recently been working on an overhaul of API documentation, and as part of the overhaul I am building the strict reference sections entirely from an OpenAPI definition. While OpenAPI-based reference documentation is commonly built with Swagger UI, I quickly realized that using Swagger UI was not the best choice for me. The documentation site I’m working on is built with the Hugo Framework, a static site generator.

Why Not Swagger?

Swagger UI is a standalone webpage. It’s a great product, and I’m a huge fan. However, it’s not designed to be modular or extremely configurable when it comes to the output.

Hugo also generates standalone webpages. Sites built in Hugo have their own custom templates, partial views, CSS themes, etc. Trying to merge a Hugo site with a Swagger UI output will result in the Swagger UI portions being discontinuous in design aesthetic for the site. Plus, Swagger UI is a large and (relative to static html) slow product because it dynamically renders all components of an OpenAPI specification in the browser.

The Better Choice

I think the better choice for those using Hugo or other static site generators is to use the build process of these generators to parse an OpenAPI specification file, and generate their own .html from this process. I provide a working code example at the end of this post.

The Result

The end result of using this shortcode is an .html file that contains exactly the properties that you want to display in your reference documentation. You can customize it for your needs, which avoids extra bloat and allows lightning-fast performance for your documentation website.

The result contains everything you need, and nothing that you don’t

Since the result is a static .html file, any updates to the OpenAPI definition will require a re-build for this .html file. On the flip side, since the result is a static .html file, you can apply the same CSS framework and element organization to this shortcode in order to maintain a fluid aesthetic while automating your reference documentation builds.

Benefits

I checked the asset sizes and page load performance numbers for my shortcode vs. Swagger UI for Twitter’s and Github’s OpenAPI definitions.

Asset Size Reduction

There was a 2,250x reduction in total size of served assets when I processed Twitter’s OpenAPI definition with my shortcode vs Swagger UI.

The difference in asset sizes was less pronounced for the massive Github OpenAPI definition, but it was still meaningful at a 7x reduction in total assets served for the reference documentation.

Page Performance Improvements

Improvements of 45% and 67% were observed in Lighthouse Performance scores for the shortcode-generated .html when compared to Swagger UI for Twitter and Github's OpenAPI definitions.

References

If you want to dive deeper into how this is possible with Hugo, check out my in-depth blog post at my personal website.

If you want to download the working sample, pre-configured in a bare-minimal Hugo site, you can find that on Github

Conclusion

If you find this useful, please give it a clap! Thanks for reading.

Shortcode

Here’s the shortcode, copied and pasted for your convenience. It was built using Hugo v0.80.0, but should work in prior versions with perhaps slight modifications:

<!-- openapi-ref-docs.htmlhugo OpenAPI reference documentation generatorParams:
url: the public URL of the OpenAPI .json specification
Generates reference documentation based on the provided OpenAPI specification. Use with: {{/* {{< openapi-ref-docs url="your-.json-file-url" >}} */}} Example: {{/* {{< openapi-ref-docs url="https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json" >}} */}}If your OpenAPI spec is not hosted at an accessible url endpoint, you can
store a copy of your OpenAPI specification in the root-level site /data folder.
Then delete the first and last lines of the code block below, and replace the
$openapi variable declaration at the beginning with: {{/*
{{ $openapi := .Site.Data.fileName }}*/}}where fileName is the file's name with `.json` removed from the end
-->

{{ with $.Params.url }}
{{ $openapi := getJSON $.Params.url }}
<div class="$openapi-spec-content">
<h2>{{ $openapi.info.title }} version {{ $openapi.info.version }}</h2>
<div class="description">
<p>{{ $openapi.info.description }}</p>
</div>
<div style="display: flex; flex-direction: column; align-items: stretch; width: 80%;">
{{ range $path, $pathMethods := $openapi.paths }}
{{ range $pathMethod, $pathDetails := $pathMethods }}
<div style="display: flex; flex-direction: column; width: 100%; border: 1px solid black;">
<div style="display: flex; flex-direction: row; width: 100%; max-height: 400px; overflow: auto;">
<div style="width: 50%; padding-right: 15px;">
<h4><span style="text-transform: uppercase;">{{ $pathMethod }}</span>&nbsp;&nbsp;<span style="color: darkslategray;">{{ $path }}</span></h4>
<h5>{{ $pathDetails.summary }}</h5>
<p><a href="{{ $pathDetails.externalDocs.url }}">{{ $pathDetails.externalDocs.description}}</a></p>
<p>Description: {{ $pathDetails.description | markdownify }}</p>
</div>
<div style="width: 50%; display: flex; flex-direction: column;">
{{ range $responseCode, $responseDetails := $pathDetails.responses }}
</br>
{{ if isset $responseDetails "content" }}
{{ range $exampleName, $example := (index $responseDetails.content "application/json").examples }}
{{ if isset $example "$ref" }}
{{ $refPath := index $example "$ref" }}
{{ range $index, $component := (split $refPath "/") }}
{{ if (index $openapi.components.examples $component) }}
<div style="font-weight: bold; max-height: calc(80%); overflow: auto;">
<span>Example for Response Code: {{ $responseCode }}</span>
<pre><code>{{(index $openapi.components.examples $component).value | jsonify (dict "indent" " ") }}</code></pre>
</div>
{{ end }}
{{ end }}
{{ else }}
<code style="flex-wrap: nowrap; overflow-x: scroll;">Example Response: {{ $example }}</code>
{{ end }}
{{ end }}
{{ else }}
<span>No Example for Response Code: {{ $responseCode }}</span>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}

--

--