Our API Documentation Journey with Nuxt.js, Netlify, and Github
Storyblok is the first headless CMS that works for developers & marketers alike.
After two years of closed and automated documentation writing we've got so many requests of developers that want to contribute to our documentation by providing examples, adding query parameter descriptions, or fixing a typo that we wanted to enable them to do so.
Before the launch, we had the docs directly integrated with our marketing site, with its content also managed with Storyblok and only editable by our own developers, marketers, and editors. To allow a collaborative approach on the documentation we now use Nuxt.js, Netlify and Github to manage the developer-centric documentation and deployment for the API specific docs of the Storyblok website.
Besides the obvious benefits for developers by splitting the documentation area from the marketing area, we can focus on each of those in more depth. If we have a closer look at both areas, the documentation, and the corporate website, we notice that their requirements go apparat real quick:
API Documentation Inspiration
Before we started developing our documentation setup, we've reviewed multiple other resources and API documentation. Below you can find a list of API documentation that stood out (either technical, design, or feature wise) and that we've used as inspiration for our own.
We also considered using the OpenAPI specification but decided to go with basic Markdown and a folder structure instead.
The Documentation
Content Structure
The documentation content is structured in a folder to differentiate in different origins, eg. types of APIs (Content Delivery and Management in our case). In each origin we do have different language versions, followed by categories, groups, and methods resulting in a folder structure like this:
Content File (Markdown and Frontmatter)
The basic structure of each Markdown ({section}.md
) file contains a FrontMatter section that allows us to specify a title
and additionally a sidebarTitle
for each section.
Each Markdown file also contains two areas for a method, split by a separator string ;examplearea
. This allows us to manage the content of a full method in only one file. Both areas support Markdown, Tables and Code examples the right area also does support Vue.js components that we use the generate our code examples.
Menu File (JSON)
The menu file in the root content
folder will be used as the base for creating the sidebar and navigation for each origin. During the Node.js process, each entry will be enhanced by its Frontmatter title
or sidebarTitle
. Using a separate file rather than using the previous approach with Frontmatter allows us to reuse some sections in other categories more easily by referencing its contentPath.
Node.js: Docgen
Our docgen script started out as a fast prototyping setup, to try different content structure approaches independent to Nuxt.js but already does some preprocessing like preparing Markdown, splitting the sections and adding Prismjs to highlight code examples that were statically added.
In the package.json
you can see that we've extended the default npm run dev
command from nuxt
to use node docgen.js watch=true & nuxt
instead. The script watches on all changes of **/*.md
and **/*.json
inside the content/
folder and handles update
, create
and delete
events.
The task of the docgen.js is to transform all **/*.md
and **/*.json
files from the content
folder in two different sets of output files, that later can directly be consumed by Nuxt.js:
- One Routes JSON file for Nuxt.js generate
- One Menu JSON file per origin and language
- One Methods JSON file per origin and language
After the initial cold start, every content file is transformed into an object and added to the Docgen.sections
object.
If we now change or remove a file all that we need to do is to check for the incoming path, delete one specific property, and trigger the generation of the routes, menu, and methods JSON files.
Routes
The routes file is a string array that will be generated according to the folder structure of content
. As we integrate the documentation in our marketing site we prepend the /docs/api/
path and also remove the default language en
resulting in an array just like this:
As we started with the default language only /de/docs/api/content-delivery
is only an example of how it would look like if we add a new translation. Currently, only the first two entries are available. You can actually see that during our Netlify Deploy Preview which will be created on every Pull Request on Github.
Menu
The menu file which Docgen adds to the /static/
folder is only a copy of /content/{origin}.{lang}.json
, before we switched to this JSON approach it generated a similar JSON structure enhanced with information of the content files.
Methods
The methods file is the result of combining and preparing all Markdown files per origin and language. Every markdown file in the content
folder will be loaded and transformed into an object, where each object represents one section.
The content
and example
property of the above object are the result of using marked to get HTML and Prismjs for highlighting. We still have unresolved Vue.js components in the HTML, eg. <RequestExample></RequestExample>
.
Nuxt.js
We've chosen Nuxt.js as our presentation layer: it allows us to easily generate static HTML files by executing npm run generate
and still enables us to use Vue.js. You might ask yourself why we did not opt for Gatsby, Next.js, or other React based setups. Well: Storyblok itself is built with Vue.js since we started 4.5 years ago, as we still love the simplicity and extensibility we wanted to stay in the ecosystem. We also considered VuePress as it is a great, lightweight setup for basic documentation and extensible enough to easily support our use-case, however, as our team is more familiar with Nuxt.js we went for it. Our presentation layer itself is completely replaceable, which allows us to still make a technology switch at any time; right now we're happy with the documentation result (even tho there is room for improvement) and the roadmap of Nuxt.js.
Consuming generated data
As our data layer is already available through our Docgen script, we can now focus on consuming it on the different levels of our Nuxt.js application. Since the Storyblok documentation will be generated using npm run generate
we will have a look at the nuxt.config.js
and its generate
function. The routes.json
, which was one of the results of our Docgen script enables us to generate all routes necessary, besides that we also require the methods and menu JSON files we've created to save some time during the generation process, as this is way faster than doing the requests in the pages fetch
itself.
In the pages
folder of Nuxt.js, we do have two different set-ups. The main difference is that one of the routes contains the language
parameter and the other page does not.
The payload we passed during generate can be consumed in the fetch
method of Nuxt.js pages. Since we do run Nuxt.js itself during development with SSR (with actual requests) and in production generate (with requiring once) all we have to do it so check if the payload
is available to us, other than that Nuxt.js already takes away all the work we have to do. We assign the menu
and sections
of the payload to our local variables so we can then commit them into the store
. As we reuse the same content structure in multiple components we're not using props to pass information to child components, instead, we're using the Vuex Store to have access to the sections and menu wherever we need it in the application. For the case that we did not receive a payload
, we fall back to two parallel axios GET requests.
Why fetch instead of asyncData?
As our setup uses Vuex as its central data repository and we want to make sure to utilize it while loading data we choose fetch
as it has access to the Vuex store but does not set component data. Yes, asyncData
would also have access to the store but should be used if you're about to set component data.
Layout
The layout of our documentation is completely different from that of our marketing site. The documentation should be able to stay the same even tho we relaunch our website version, so you as a developer will never have to search for an example or documentation part because we switched the URL structure or landing page layout. The Basic layout of the documentation is highly inspired by the Stripe documentation as we ourself had a great experience using it, and most of the time we got the information that it is the best practice API documentation example.
The great thing about Nuxt.js is that the layout structure above is directly reflected in the components we've created:
Navigation
TopHeader and SidebarNavigation contain all navigation elements and also allow you to set the language of the code examples we display. We choose to switch the navigation to a native <select>
on smaller screens, where each option
is treated like a jump link and <optgroup>
are the content categories so we still have the grouping available for smaller devices.
MethodContent
The MethodContent component is fairly simple as it basically only uses v-html
to output the content. As we've already prepared the content and transformed the Markdown to HTML during our Node.js process we've got rid of the complexity in Nuxt.js. Each section has at least one headline which includes the title, depending on the current index we switch from h2
to h1
, but all of them contain an anchor element with a jump link to the current section.
MethodExample
The MethodExample is way more interesting than the MethodContent as it registers new dynamic Vue.js components with the prepared Markdown as template
. This allows us to pass RequestExample
as a child component to the component and if the template now contains a RequestExample
component it will automatically be mounted and allows us to generate all those different code examples. Another great thing is that we can now use <div v-if></div>
syntax in our markdown to hide/show content depending on store values, you can see that in action in Authentication where we added examples for our JavaScript client by using <div v-show="$store.state.technology == 'javascript'">...</div>
.
RequestExample
The RequestExample can be used directly in our Content Markdown Files without having to import it in each file. Below you can see how that would look like in our Retrieve one Story Content File.
The amount props our RequestExample
component allows are fairly small, as we do not want to write every request in some strange format, but rather can copy and paste the actual Response in the docs. We use the actual request and our RequestMixin parses the necessary information from there. As we not only have GET requests but all kind HTTP Methods (POST
, PUT
, DELETE
, and GETOAUTH
; where the latter is an indicator that it is a GET request for the Management API which issues OAuth tokens), we added the prop httpMethod
which is a basic string. As POST
and PUT
contain request objects we introduced the requestObject
prop which is a basic JS object containing the request body object.
Now that we have all information in our RequestExample component we use v-show
depending on the technology to load the JavaScript, Ruby, Java or any other example. Each of those examples uses RequestMixin
, combined with marked
and prismjs
we can now output the dynamically generated and highlighted code examples depending on your choice. Besides that generic example approach, we've introduced the possibility to define the code example in a sdk/{technology}/methods.js
file. If there is a client library that does not follow such a generic approach we can now override RequestExamples depending on the contentPath
. The RequestExample part in MethodExample is fairly experimental and if you find full static generated way to achieve the same flexibility and syntax in the content files feel free to create a pull request on our Github repository, also if you have a suggestion on how this could be improved, feel free to open an issue for a discussion!
Hosting Layer
Github
We were looking for the best place to have developers come together to contribute to open source, run it locally and even allow them to fork the project to create their own documentation if they need it. The obvious choice for us was GitHub, as by their slogan: "GitHub brings together the world's largest community of developers to discover, share, and build better software.". GitHub allows us to make our documentation Nuxt.js set-up available to every developer, besides that it allows us to easily allow contribution via Pull Requests and it enables all of you to check if there were changes since the last time you used a feature.
Netlify
This one was tricky as we've played a while with different services and even used AWS directly to deliver a preview for every Pull Request. We ultimately settled for Netlify as their ecosystem of integrations allows us to do exactly what we wanted. After sign-up we've enabled Netlifys Continuous Deployment by installing the Netlify App on Github for our repository.
The developer experience of Netlify just worked out for us. We were up and running in no time and now we're able could focus on details rather than the overall process. We got our subdomain storyblok-docs.netlify.com redirected to our documentation overview storyblok.com/docs by adding one configuration file for builds on master
, and a routes overview for every other branch. A funny and great gimmick is the Netlify Status Badge which of course we had to add to our documentation.
AWS
The hosting for our production environment is slightly different because we want it to run smoothly within our dynamic setup on AWS. In the last step during the Netlify build, we're additionally transforming the .html
files in .liquid
files and also generate a routes file for our ruby set-up to handle the routing according to the routes available in Nuxt.js. For this, we've again used a Node.js script that does some string replacements and URL adjustments. After that we end up with the liquid files for our own service which automatically will be uploaded to our own service, the assets in /_nuxt/
will still be served by Netlify.
The Marketing Site
Storyblok
For our marketing site, our Content Layer is Storyblok, our own CMS. With our ruby setup, we consume the Content Delivery API and render our website according to the components and content types provided for each route. Every entry is called Story which allows a nested tree of Components. To give you an example on how the content structure could look like for one page, you can have a look at the data behind our For Developers Page by performing a GET request. If you want to know more, you can now dive into the documentation and learn more about stories.
Our marketing team can reuse those components across all entries and content types if we allowed nesting by adding the field type bloks
to the content type. We did restrict some components to be only nested in specific others, benefit
components, for example, should only be able to be added inside a benefits
component. Since a plain data structure with all that nesting might get some confused, Storyblok ships with a live preview for your content that you can enable on every technology stack using our JavaScript Bridge.
Ruby + Liquid
Our own website is running on a Ruby set-up that uses Liquid as it's rendering engine, no additional JavaScript and statically cached in our CDN. As soon as our marketers publish content a Webhook of Storyblok will trigger a cache invalidation for the changed route and resource.
Atlassian: Bitbucket
For our internal source code versioning, we're using Bitbucket by Atlassian. We've also switched our deployment setup from Jenkins to their Bitbucket build pipelines. Similar to what we're doing with Netlify on Github, we did with the Bitbucket build pipelines and AWS. On every branch, we deploy a development
version of our marketing site to an internal URL of Storyblok, so our developers can share the preview with our marketing team at any point in time. Since every development stage is able to use the production content by exchanging the Storyblok access tokens.
CDN
We're using Amazon CloudFront as our content delivery network. Content delivery networks provide a globally-distributed network of proxy servers which cache content more locally to consumers, thus improving access speed for downloading the content. Besides the Amazon CloudFront, we also utilize Netlifys ADN for assets of our documentation itself. For other resources such as our images, we're using our Image Service so we can automatically crop, resize, and optimize all our assets. This image service is also available to you in all our plans, even in the free plan!
The result
Rewriting our whole API documentation from a dynamically generated, slightly manually enhanced to a now fully manually, but detailed documentation was a great journey so far. As always we did this because so many of you suggested changes, wanted to improve the docs and enrich it with examples. Together we can now grow our documentation in a cookbook for all beginners to have them up and running with their projects even faster than before. We want to thank all of you who sent us their feedback on the preview versions we handed out, and of course, we also want to already thank all of you who are willing to contribute to it. So what's next? Check out our roadmap or suggest features for Storyblok itself.