Guide

How to use Notion as a CMS for your custom site and host for FREE

This is a living document, details may be updated as improvements are released. 2020-Sep-17 Changelog: ✓ Updated plugins for feature improvements and bug fixesFixed 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 Blazing Fast Website For Free Video Step By Step 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
May require 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/
1.
Install Node.js LTS (stable) version
2.
Install code editor: VS Code Recommended
3.
Install Git Mandatory
4.
Install Gatsby CLI Mandatory
5.
Create a new site, referring to one of the starter templates
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' } ] } } ]
JavaScript
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]); };
JavaScript
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) } } } } `
JavaScript
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) } } } } `
JavaScript
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 } } `
JavaScript
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> ) }
JavaScript
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={`blog/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> ) }
JavaScript
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={`subscribe/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> ) }
JavaScript
Recommended: Add this code to your global CSS file, to ensure optimal mobile behaviour
[data-block-id] { max-width: 100%!important; }
JavaScript
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/
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