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 an API to Your Jekyll Blog

October 25, 2015
tutorial jekyll rest reactjs frontpage

Jekyll generates static HTML, which is great until you want a dynamic content listing or a search feature. The trick is to also generate a JSON endpoint alongside your HTML – no plugins required, no server-side code, just Liquid templates outputting JSON instead of markup. In this post I will build a RESTful API for a Jekyll blog that works on vanilla GitHub Pages.

I really like github pages. I built my blog on it, even though I host it myself. Markdown is so easy, liquid is so easy. What isn’t easy is dynamic lists of content. I want to learn Reactjs –so I will build a content listing with that. That will require a RESTful API for my content.

The easy way

To be clear I didn’t write this snippet –but only because someone else did. And working code wins.


    ---
    layout: nil
    ---

    [
    {% raw %}
    {% for post in site.posts %}
        {
          "title"    : "{{ post.title }}",
          "url"     : "{{ post.url }}",
          "date"     : "{{ post.date | date: "%B %d, %Y" }}",
          "content"  : "{{ post.content | escape }}"
        } {% if forloop.last %}{% else %},\{% endif %}
    {% endfor %}
    {% endraw %}
    ]

With this snippet (added for convenience) we get most of the way there. My plan is to modify it a bit.

What I would do, or the not so easy way


    ---
    layout: nil
    ---

    [
    {% raw %}
    {% for post in site.posts %}
        {
          "title"    : "{{ post.title }}",
          "url"     : "{{ post.url }}",
          "date"     : "{{ post.date | date: "%B %d, %Y" }}",
          "tags"  : {{ post.tags }},
          "categories"  : {{ post.categories }},
          "description"  : "{{ post.description | escape }}"
        } {% if forloop.last %}{% else %},{% endif %}
    {% endfor %}
    {% endraw %}
    ]

From here you should see the change that I made. I changed the content to description and added tags and categories to the listing. This will provide a good everything list for the blog api. (This should all be in the /api/v01/list.json file).

Hmmmm, that isn’t working as expected. Instead of giving me a list of categories and tags it is concatenating them into a single string. So it looks like another modification is necessary.

I also added a check incase there isn’t anything in tags or categories.

---
layout: nil
---

[
{% raw %}
{% for post in site.posts %}
    {
      "title"    : "{{ post.title }}",
      "url"     : "{{ post.url }}",
      "date"     : "{{ post.date | date: "%B %d, %Y" }}",
      {% if post.tags %} "tags"  : [
        {% for tag in post.tags %} "{{ tag }}"
        {% if forloop.last %}{% else %},{% endif %}
        {% endfor %}
        ],
      {% endif %}
      {% if post.tags == nil %} "tags"  : [],  {% endif %}
      {% if post.categories %} "categories"  : [
        {% for category in post.categories %} "{{ category }}"
        {% if forloop.last %}{% else %},{% endif %}
        {% endfor %}
        ],
      {% endif %}
      {% if post.categories == nil %} "categories"  : [],  {% endif %}
      "description"  : "{{ post.description | escape }}"
    } {% if forloop.last %}{% else %},{% endif %}
{% endfor %}
{% endraw %}
]

This gives me a single listing page. Now we need posts.

Aside note. Jekyll is having a really hard time with this post. It keeps wanting to parse the liquid in the code examples. Thankfully Jekyll Liquid now has a proper escaping system. Use liquid{% raw %}{% raw %}{% endraw %}.

Building the posts. NPM to the rescue (maybe)

Not npm exactly, Gulp instead saved the day. Ever since I moved my utility functions to Gulp I have been a fan. It is very well suited for building small repeatable tasks. So I add a new Gulp task to the gulpfile.

gulp build-api

With this command I duplicate the content in the _post directory over to the api/[version]/v01/posts directory. Throw in a little gulp piping to do a string replace (this is to change the layout template from html to json. This is the whole gulp command.

gulp.task('build-api', function() {
  return gulp.src('_posts/*')
    .pipe(replace(/layout\: post/, 'layout: json'))
    .pipe(vfs.dest('api/v01/posts', { overwrite: true }));
});

It is simple enough, ignore the vfs.dest part (I am using vinyl directly so I can use the overwrite option).

All that is left is to add a unique identifier, I figure the post url should work for that. I do that with the json template.

---
layout: json_default
---

{
  {% raw %}
  "title"    : "{{ page.title }}",
  "url"     : "{{ page.url }}",
  "permalink": "/posts/{{ page.date | date: "%Y/%B/%d" }}/{{ page.path | replace: 'api/v01/posts/', '' | replace: '.md', '' }}.html",
  "date"     : "{{ page.date | date: "%B %d, %Y" }}",
  {% if page.tags %} "tags"  : [
    {% for tag in page.tags %} "{{ tag }}"
    {% if forloop.last %}{% else %},{% endif %}
    {% endfor %}
    ],
  {% endif %}
  {% if page.tags == nil %} "tags"  : [],  {% endif %}
  {% if page.categories %} "categories"  : [
    {% for category in page.categories %} "{{ category }}"
    {% if forloop.last %}{% else %},{% endif %}
    {% endfor %}
    ],
  {% endif %}
  {% if page.categories == nil %} "categories"  : [],  {% endif %}
  "content"  : "{{ content | escape }}"
  {% endraw %}
}

Footnotes

Jekyll complains about using ruby nill for a layout type. It doesn’t break, but I also create two layout templates json_default.json and json.json. Now that it is working, I will rename the templates to something a bit more meaningful:

default.json
post.json

I think these are better names.

With this setup, any Jekyll blog now has a JSON listing endpoint and individual JSON post endpoints – all generated at build time with no plugins and no server-side dependencies. From here, building a React or vanilla JS front-end that consumes this API is straightforward. The content stays in markdown, the API stays in Liquid, and GitHub Pages serves it all for free.

Footer

Social Networks

Blogroll

Blob of contradictions
iCodealot

© 2026 Frank Robert Anderson