A 'Static' Tumblog Using Next.js and Airtable

I miss Tumblr. Well, I can't say I really miss having another website to endlessly scroll. I mostly miss having a personal archive of music/movies/articles I was enjoying. A place for friends to go to see what I was enjoying and a place for me to periodically visit when I wanted to see what inspired me months or years in the past.

As an engineer who enjoys building things on the web, of course I've been tempted many times to build my own personal version. But I never took the plunge because I could never decide on the best approach. The obvious choice would be to build one using a web framework. I probably could have built something in Django in a couple of hours. But I didn’t really want to have to pay a few bucks to AWS or Heroku each month to host it. It felt like overkill. On the other end of the spectrum, a static site was appealing for its simplicity, speed, and negligible hosting costs however I wanted it to be relatively easy to add new posts. I really didn’t want to have to git push every time I wanted to add a new post. So the sweet spot for me was something in between: statically generated but easy to update. I've been trying out Next.js lately and it, together with a CMS, seemed like a really good fit for this use case.

Next.js

Next.js has been a great way to build single page applications or static sites for a while now but the last couple of releases included features that made this static site with dynamic content sweet spot possible. Specifically, in version 9.3 they added new data fetching functions that are called when the site is built to fetch data that is used to generate content for the application. They also include a build web hook. Used in combination, the idea is that you could use a headless CMS to host your app’s page data and when the site was built, the data would be fetched from the CMS. The resulting application would then be statically deployed to the server. You could then have the headless CMS be configured to call the build web hook to rebuild the page or pages if the content in the CMS changed. The idea is that you get the best of both worlds: the flexibility and publishing tooling from a CMS with the performance benefits of a static site.

With the 9.4 release, they added the ability to trigger new builds of the site from within the deployed Next.js app itself. Instead of using the build web hook, the same function in Next.js that gets called at build time to fetch data for the page is called in the background and triggers a new build if the data has changed. The result is that the first visitor to the site after the data changes will see the old site but any users after that will see the new data. And each request is still served statically so page load is still fast and hosting is cheap.

The CMS

I took a look at a few of the headless CMS options and while they all seem like great products, they were all way too complicated for this little app I had in mind. I've been using Airtable at work lately and had the idea to try that out as the data store for the blog. Nice iPhone app for adding rows. Super easy API to use. Seemed like a good fit. You would probably never use Airtable for a dynamic site as they only allow 5 requests per second but since Next.js isn't fetching data from Airtable on each request, this won't be a problem.

Here's what my Airtable base definition looks like (in the form of a typescript interface):

export interface AirtableItem {
  id: string
  fields: {
    Name: string
    Type: ItemTypes
    Comment: string
    Created: string
    Link: string
    'Created (Override)': string
  }
}

Pretty straightforward. There's a "type" a la Tumblr, and the post is basically just a name ("Name") with an optional external URL ("Link") and also an optional "Created (Override)" for when I want to override the auto-generated-by-Airtable timestamp (for when I'm importing posts posts from elsewhere).

Putting it all Together

It's pretty remarkable how easy this was to get working. There are two main parts to the Next.js app: 1) a single index.tsx page for listing the items from Airtable, and 2) React presentational components for rendering them. I won't go into the presentational components; the interesting stuff happens in the index.tsx Next.js page. Since we need to fetch the data from Airtable in order to render the page but we want the page to be statically generated, we should use the getStaticProps function to fetch the items from Airtable and make them available to the page as props. Here is that function:

export const getStaticProps: GetStaticProps = async (context) => {
  let more = true
  let items = []
  const baseUrl = process.env.AIRTABLE_BASE
  const queryParams = new URLSearchParams()
  queryParams.append('view', process.env.AIRTABLE_VIEW)
  while (more) {
    const url = baseUrl + '?' + queryParams.toString()
    const resp = await fetch(url, {
      method: 'GET',
      headers: {
        Authorization: `Bearer ${process.env.AIRTABLE_KEY}`,
      },
    })
    const data = await resp.json()
    items = [...items, ...data.records]
    if ('offset' in data) {
      queryParams.set('offset', data.offset)
    } else {
      more = false
    }
  }
  return { props: { items }, revalidate: 10 }
}

The function is pretty straightforward: it paginates through the records in Airtable and returns the array of records. (Note: I'm using an Airtable view so that I don't have to handle filtering and ordering the data using query parameters but this isn't necessary). If the object returned by this function only included the props field, this function would only be called at build and so anytime I added new records to the Airtable base, I would have to trigger a build in Vercel manually or via a webhook. By adding the revalidate field, this function gets called in the background and rebuilds the site statically if the data changed. This is how we get our "static" site to get updated and rebuilt automatically.

Conclusion

I'm really happy with this setup. Adding posts is really easy. If I'm on my phone I can add a post through the Airtable iOS app, or if I'm on my laptop, I can add it through the Airtable web app. And the new posts show up almost instantly yet the app is served statically so the site loads extremely fast. The only time I have to break out VSCode and update the Next.js app is when I want to make design tweaks. As an added bonus, since the Airtable API is so easy to work with, I can easily migrate posts from my old Tumblr blog or set up integrations to pull in posts automatically from other services I use like Instapaper or Goodreads.

Here's the finished product and here's the full GitHub repo.