Power your blog with Notion and host for FREE with Netlify

📅 2020-May-07

This guide is currently outdated with the release of the official Notion API. We’ll be updating this in May 2022.


2020-Sep-17 Changelog:

Updated plugins for feature improvements and bug fixes

Fixed ES Modules error for newer versions of node.js

Fixed image loading errors resulting from Notion code change

Fixed fetch errors for Netlify deployments

Related Readings:

More Ways To Build A Personal Website For Free

Video Walkthrough (~10 Mins)


Host a GatsbyJS Blog (with Notion as a CMS) for $0 with Netlify

Why?

This is my preferred option. You may want a lightweight and cheap CMS (and let's be honest, you're probably drafting your content in notion anyway), but with your own personal touch. You're unique and want to express yourself, and having control over the styling is important to you.

Pros

  • Complete control over how you want to present your content
  • Extend wherever you like with custom code
  • Detailed Analytics
  • High SEO (searchability on google)

Cons

  • Requires some understanding of code
  • Most likely a weekend project if you are applying detailed customization

Requirements

  • Notion.so account FREE
  • Your own domain ~$10/year
  • GitHub account FREE
  • Netlify account FREE
  • Coding Knowledge: 2/5 (Enough to know where a chunk of code starts and ends)

Disclaimer: I had little to no knowledge of coding in React before I started this project, and I learned a lot while testing things out. Building my entire conradlin.com website took me between 8-10 hours, but only because I was very particular about styling. I truly believe anyone can learn to code - and this is a fun way to get started. Don't be afraid to try!

Getting Started

First, some background about Gatsby. Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps. Building in Gatsby means that your website will be running on the latest technologies, and be able to pull data from anywhere. In this article, I'll be demonstrating how to pull data from Notion.

First Steps

  1. Install code editor: VS Code Recommended
  2. Install Git Mandatory
  3. Install Gatsby CLI Mandatory
  4. Create a new site, referring to one of the starter templates
  5. Install Node.js LTS (stable) version

Watch the Video Step-By-Step Walkthrough! →

Note: You may sometimes hear me use yarn commands (a package manager) in my video tutorial, if you are new to developing just use the default commands instead (gatsby or npm).

Integrating Gatsby with Notion

Building in gatsby is akin to working with building blocks. You can use my gatbsy-notion-demo which to get started quickly (with the integration built-in), but I recommend you follow below so that you understand how to make any gatsby starter into one that is notion powered.

Step 1: Install the following plugin by following the steps in this link: https://github.com/conradlin/gatsby-source-notion-database

Important! The original creator has stopped providing support for this plugin, so to keep up with the latest Notion API changes, we have forked the project to deliver latest improvements and bug fixes. If our changes are merged into the master in the future, we will refer you to the original code instead.

Step 2: Add this piece of code into your gatsby-config.js file

You should change the link for table to refer to your own notion table link, however I recommend you stick with my link in the setup stage so you can verify everything is working.

plugins: [
    {
      resolve: `@conradlin/gatsby-source-notion-database`,
      options: {
        sourceConfig: [
          {
            name: 'posts',
            table: 'https://www.notion.so/conradlin/1aa283fcd5ae4a73ba0f73c062de745e?v=6a40014bee144152b55203e2caf0c02e',
            cacheType: 'html'
          }
        ]
      }
    }
]

Step 3: Add this piece of code into your gatsby-node.js file

This piece of code took me a while to nail down, because in the starter kit, the assumption is made that we are only querying one table of data to load into the site. For me, I want to also load in data for previous issues of my newsletter, so this is how you would handle that case.

// graphql function doesn't throw an error so we have to check to check for the result.errors to throw manually
const path = require(`path`)

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions

  const blogPost = await graphql(`
  query {
      allPosts(filter: {status: {eq: "published"}, content_type: {eq: "article"}}) {
          nodes {
            slug
            url
          }
        }
      }
    `).then(result => {
      if (result.errors) {
        Promise.reject(result.errors);
      }
      
      result.data.allPosts.nodes.forEach(({ slug, url }) => {
        createPage({
            path: `blog/posts/${url}`,
            component: path.resolve(`./src/templates/blogPost.js`),
            context: {
                // Data passed to context is available
                // in page queries as GraphQL variables.
                slug: slug,
            },
        });
    });
  });
  const newsPost = await graphql(`
  query {
      allPosts(filter: {status: {eq: "published"}, content_type: {eq: "newsletter"}}) {
          nodes {
            slug
            url
          }
        }
      }
    `).then(result => {
      if (result.errors) {
        Promise.reject(result.errors);
      }
      
      result.data.allPosts.nodes.forEach(({ slug, url }) => {
        createPage({
            path: `subscribe/posts/${url}`,
            component: path.resolve(`./src/templates/blogPost.js`),
            context: {
                // Data passed to context is available
                // in page queries as GraphQL variables.
                slug: slug,
            },
        });
    });
  });

  return Promise.all([blogPost, newsPost]);
};

Key information to understand about this code:

  • This code is telling gatsby to create new pages only for posts that are published, and are newsletters or articles.
  • In path, we can determine where we want the newly created pages to resolve
  • In component, we are determining with which template we want the newly generated pages to be handled by (and have the appropriate styling, etc.)

Step 4: Create the following items:

Your starter should have existing pages that are similar, so I will just mention the specific code you need for the notion integration to work. You can beautify the look and feel by borrowing your starter components and styling.

blog.js (in src/pages) - this will be the page where you can see a list of blogs

import React from 'react'
import { graphql } from 'gatsby'
import PostItem from "../components/postItem"
import Layout from '../components/layout'

const Blog = (props) => {
  const { data: { allPosts } } = props
  return (
    <Layout>
      <div id= "main">
        {
          allPosts.nodes.map(node => <PostItem data={node} />)
        }
      </div>
    </Layout>
  )
}

export default Blog
export const query = graphql`
  query {
    allPosts(filter: {status: {eq: "published"}, content_type: {eq: "article"}}  sort: { fields: [publish_date___startDate], order: DESC }) {
      nodes {
        title
        tags
        desc
        content_type
        status
        url
        read_time
        cover_image
        slug
        publish_date{
          startDate(formatString: "YYYY-MMM-DD", fromNow: false)
        }
      }
    }
  }
`

subscribe.js (in src/pages) - this will be the page where you can see a list of newsletters

import React from 'react'
import { graphql } from 'gatsby'
import NewsItem from "../components/newsItem"
import Layout from '../components/layout'

const Subscribe = (props) => {
  const { data: { allPosts } } = props
    return (
    <Layout>
      <div id= "main">
        {
          allPosts.nodes.map(node => <NewsItem data={node} />)
        }
      </div>
    </Layout>
  )
}

export default Subscribe
export const query = graphql`
  query {
    allPosts(filter: {status: {eq: "published"}, content_type: {eq: "newsletter"}} sort: { fields: [publish_date___startDate], order: DESC }) {
      nodes {
        title
        tags
        desc
        content_type
        status
        url
        read_time
        cover_image
        slug
        publish_date{
          startDate(formatString: "YYYY-MMM-DD", fromNow: false)
        }
      }
    }
  }
`

blogPost.js (in src/templates) - this is the template which all new blogs/newsletters will be following.

import React from 'react'
import { graphql } from 'gatsby'
import Layout from '../components/layout'
import { parseImageUrl } from '@conradlin/notabase/src/utils'


export default ({ data }) => {
  const { posts: { title, tags, publish_date, html, url, slug, desc, color, cover_image } } = data

  return (
    <Layout>
      <div id = "main">
        <div>{tags && tags.join(', ')}</div> 
        <h1>{title}</h1>
        <div dangerouslySetInnerHTML={{ __html: html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    posts(slug: { eq: $slug }) {
      html
      title
      tags
      publish_date{
        startDate(formatString: "YYYY-MMM-DD", fromNow: false)
      }
      url
      desc
      color
      cover_image
    }
  }
`

postItem.js (in src/components) - this is the component to determine the look and feel of each new 'row' of blogs items

import React from "react"
import { Link } from "gatsby"

export default ({ data }) => {
    const { title, tags, cover_image, publish_date, desc, read_time, url, slug } = data

    return (
        <div style={{ margin: 10 }}>        
            <Link to={`posts/${url}/`}>
              <h1 style = {{ color: "black" }}>{title}</h1>
              <div style = {{color: "grey", margin: '-30px 0px 0px 0px'}}>Tags: {tags && tags.join(', ')}<br></br>Published: {publish_date.startDate}<br></br>Read Time: {read_time} mins</div>
              <p style = {{ color: "black", margin: '15px 0px 30px 0px' }} dangerouslySetInnerHTML={{ __html: desc }}></p>
            </Link>
        </div>
    )
}

Optional: Use this code instead if you want to also show the cover image for your blog posts

import React from "react"
import { Link } from "gatsby"
import { parseImageUrl } from '@conradlin/notabase/src/utils'

export default ({ data }) => {
    const { title, tags, cover_image, publish_date, desc, read_time, url, slug } = data
    let coverimageURL = parseImageUrl(cover_image[0], 1000, slug)

    return (
        <div style={{ margin: 10 }}>        
            <Link to={`posts/${url}/`}>
              <img 
                alt={`${title} cover image`}
                style={{ width: '100%' }}
                src={coverimageURL}
              />
              <div style = {{color: "grey"}}>Tags: {tags && tags.join(', ')} • Published: {publish_date.startDate} • {read_time} MIN READ</div>
              <h2>{title}</h2>
              <p style = {{ color: "black" }} dangerouslySetInnerHTML={{ __html: desc }}></p>
            </Link>
        </div>
    )
}

newsItem.js (in src/components) - this is the component to determine the look and feel of each new 'row' of newsletter items

import React from "react"
import { Link } from "gatsby";

export default ({ data }) => {
    const { title, tags, cover_image, publish_date, desc, read_time, url, slug } = data

    return (
        <div style={{ margin: 10 }}>        
            <Link to={`posts/${url}/`}>
              <h1 style = {{ color: "black" }}>{title}</h1>
              <div style = {{color: "grey", margin: '-30px 0px 0px 0px'}}>Tags: {tags && tags.join(', ')}<br></br>Published: {publish_date.startDate}<br></br>Read Time: {read_time} mins</div>
              <p style = {{ color: "black", margin: '15px 0px 30px 0px' }} dangerouslySetInnerHTML={{ __html: desc }}></p>
            </Link>
        </div>
    )
}

Recommended: Add this code to your global CSS file, to ensure optimal mobile behaviour

[data-block-id] {
    max-width: 100%!important;
 }

Testing it Out!

  • You should now be able to run gatsby develop in your terminal and be able to see data populate your pages.
  • Navigating to http://localhost:8000/blog should showcase a list of all blogs
  • Clicking any of the entries should lead you into the detailed blog articles
  • Navigating to http://localhost:8000/subscribe should showcase a list of all newsletters
  • Clicking any of the entries should lead you into the detailed newsletter entries

Pushing Code to Production

  • The gatsby official wiki explains this much better than I can, so you can follow the guide here:
    https://www.gatsbyjs.org/docs/deploying-to-netlify/
  • Netlify has a fantastic free tier that will be more than enough for your needs as a personal site.
  • Once your netlify site is deployed, check it out, and link your custom domain to netlify:
    [https://docs.netlify.com/domains-https/custom-domains/](https://docs.netlify.com/domains-https/custom-domains/)

Cool Things You Can Do

These are some cool integrations I've set up for my site, let me know if you'd like a follow-up guide!

  • Set up a Netlify deploy widget on your phone with IFTTT to update your website whenever you add a new blog (leveraging buildhooks);
  • Add a comments section at the end of your blogs;
  • Add a subscribe form for your mailing list with convertkit.

Known Issues

  • Initial Columns and embeds break responsive design
  • June 11 2020 After Notion's inline emojis update, emoji support has stopped working

Things I'm Working On

  • Starter packages for Co-x3 Patrons with custom styling, comments integrations, and more.
  • Pre-process images from notion by using the Sharp library (typically available for local images only), which automatically process images to be performant, with features like lazy-loading.

If you are a front-end developer and want to help improve this project, please do write in and let me know! Kindly reach out at lets.talk@conradlin.com

Did this resonate with you?