​Kill the back-end: Bring user-generated content to Jamstack with GitHub Actions

We love static sites, but there's only so much you can do without a server or database… until now. Let's use GitHub Actions to take Jamstack to the next level!

Static sites have been making a comeback these last few years. Deploying to a CDN is easy and cheap (or even free!), and nothing beats a static site with SEO, security, performance, and scalability. Of course, dealing with a lot of static pages used to be a nightmare, but not after the Jamstack approach arrived. It's not about specific languages or frameworks, according to Mathias Biilmann (CEO & Co-founder of Netlify), it's "a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup". Using a static site generator like Gridsome or Gatsby, we can statically generate sites with lots of content, like a blog, and deploy them in a matter of minutes. You can read more about Jamstack it in this excellent post by Josep Jaume.

Here at Codegram we love the Jamstack, but we are aware that it comes with its limitations. One of them is that you need to be in control of the content. In the case of the blog, a common scenario is committing the content in your repository as markdown or JSON files. This is the approach we follow for this very same site where you are reading this. After all, most of the team is familiar enough with technology to write a markdown file, commit and create a pull request on GitHub.

But when working with clients, we often find that the people writing the content is not very technical and they prefer to work with a UI. In these cases, like Davis Cup Finals website, we usually rely on a headless CMS service like Prismic or Contentful.

If you need more flexibility and features than these services provide, like letting users submit their own content and publishing it, you can always write your own backend, if you have the time, knowledge and budget. But wouldn't it be nice to keep things simple and avoid having to worry about servers?

Imagine we are organizing a conference and want to have call for papers. We want users to be able to fill a form with their proposal, and then we as organizers review the proposals and display the accepted ones on the website. Maybe even list the ones that are pending review, if we want to keep the process really open. But how on earth are we going to handle user submissions without a server or a database!? Well, you can check the final site, and read on to know more about how it works… 🍿

GitHub Actions

Let's take a small break and introduce GitHub Actions. You have probably heard about them already, but if not, they are simply a way to automate software workflows. Think CircleCI or Travis but completely integrated with GitHub. Running tests and linting when a PR is opened, and building and deploying when pushing to master are the most typical flows that will probably come to mind, but you can run any piece of code you want, and there are many ways to trigger a workflow. We have this repository of useful workflows if you want to take a look.

As you can imagine, the ability to run any script opens a new world of possibilities for Jamstack sites. So in this post we'll see how we can turn GitHub into our own Content Management System that will receive user submissions from the client-side application, validate them and publish them, all without any backend server involved! 🤯

Jamstack with user-generated content

So let's get on it and build our CFP application. We will be using Gridsome because we love Vue, and we will be deploying to Netlify. Users will be presented with a form to submit their talk. On submit, we trigger a GitHub workflow that generates a pull request that adds a JSON file to the repository. Once this PR is merged, Gridsome generates the site statically listing all the accepted submissions, and it gets deployed to Netlify. The result is a 100% static site that is able to handle user-generated content! Feel free to check out the repository, but if you want to understand how it works, let's review the submission process in a bit more detail:

Trigger a workflow from your client-side app

So a user visits our client-side application and wants to submit a talk. To make it easier to set up and play with, we will not be adding any authentication method, but you could connect the app with a GitHub OAuth app or similar to prevent abuse. So for now, the users will just fill in the details and submit the form. We want this form to open a pull request in the repository with the talk information. As we've seen before, we can use GitHub Actions to create a PR. But how can a form trigger a GitHub Actions workflow? There's a way to trigger a workflow externally, using the repository_dispatch event.

function submit(data) {
  const owner = "codegram";
  const name = "jamstack-cfp";

  return axios.request({
    url: `https://api.github.com/repos/${owner}/${name}/dispatches`,
    method: "post",
    headers: {
      Accept: "application/vnd.github.everest-preview+json",
      Authorization: `token ${process.env.GH_TOKEN_PERSONAL}`,
    },
    data: {
      event_type: "handle-submission",
      client_payload: {
        name: data.name,
        title: data.title,
        description: data.description,
        date: new Date().getTime(),
      },
    },
  });
}

Ok, I must admit, I lied. This process should not be done completely from the client-side. To trigger a repository_dispatch event you need to use a token with repo scope of a user that has write access to the GitHub repository (you can generate the token in your GitHub settings). Doing this on the client-side means that everyone could see the token, and that's no good! We will be handling this bit of logic in a serverless function. Luckily, with Netlify functions writing and deploying a serverless function is super easy. You can check the full function code in the repository.

Create a pull request

When the form is submitted and the Netlify function has done its job, the workflow is triggered on our jamstack-cfp repo. This workflow creates a JSON file with the talk details, then opens a new pull request to add this new file to the repository:

name: "Handle Submission"
on:
  repository_dispatch:
    types: handle-submission

jobs:
  create-pr:
    name: Create PR
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: Use Node.js ${{ env.NODE_VERSION }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ env.NODE_VERSION }}
      - name: Create json file
        run: |
          node actions/process-input.js
      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v2
        with:
          token: ${{ secrets.GH_TOKEN_PERSONAL }}
          branch-suffix: random
          title: "feat(submission): ${{ github.event.client_payload.name }} - ${{ github.event.client_payload.title }}"
          body: |
            # ${{ github.event.client_payload.title }}
            ## by ${{ github.event.client_payload.name }}
            > ${{ github.event.client_payload.description }}
          labels: submission

As we said before, when running GitHub Actions we can run any bit of arbitrary code, and that's what we are doing on the Process input step, adding the logic to create the JSON file with the payload we received from the repository_dispatch event:

const fs = require("fs");

const { client_payload } = require(process.env.GITHUB_EVENT_PATH);

fs.writeFileSync(
  `submissions/${client_payload.name}-${client_payload.title}.json`,
  JSON.stringify(client_payload)
);

With this, we have the main functionality of our app ready! You can try and see this in action by submitting the form and then checking the running workflows. Once the workflow finishes, you will be able to see the open PR with your submission. Once PRs are merged, the application will list them in the accepted submissions section.

On next posts we will be seeing how to list the pending submissions by integrating the GitHub GraphQL API to Gridsome, and how to add another GitHub workflow that automatically validates and merges or closes the PRs. There's still a lot of potential to take this further!

Have we really killed the back-end?

Obviously, this approach has some limitations, so all back-end devs out there, don't worry, you still get to keep your jobs! So, when to use this approach? The submission process can take a few minutes, so if you need immediate feedback it's probably not a good option. Same if the amount of submissions you need to handle in a short period is very high. There's also a privacy issue: if your repository is public you shouldn't store any sensitive data (I'm not sure if storing it in a private repository has any legal issues). As a rule of thumb, this is a good use-case for a proof of concept, MVP, or small application. We've successfully used this approach in production, reaching around 700 submissions, and we were very happy with the result!

If I've convinced you of giving it a try, fork the repo, and let us know your experience! You can reach us on Twitter, we will be very glad to hear it!


Cover photo by Etienne Boulanger