Power your blog with Notion and host for FREE with Netlify
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
- Pick your preferred look and feel for your website. This tutorial will teach you how to pull Notion data using any starter template: https://www.gatsbyjs.org/starters/?v=2
- You can find the resources I used by going to https://conradlin.com/info/
- Set up your development environment (this is easier than you think): https://www.gatsbyjs.org/tutorial/part-zero/
- Install code editor: VS Code
Recommended
- Install Git
Mandatory
- Install Gatsby CLI
Mandatory
- Create a new site, referring to one of the starter templates
- 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 designJune 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