Building This Site

I had a Wordpress site before this one. It was okay. It got the job done, but I have been working with React more. So, I decided that I'll rebuild it in React. Also, I was inspired by my friend's website.

 

Our sites are 90% the same. However, there are some major differences in how we built them. I'm using JavaScript and Cloudflare Worker. So I highly, recommend you check out his post too.

 

I followed his post to create my website, but honestly, I had a hard time following it because I'm still new to React and JavaScript. So, my plan is to break down the steps in greater details for newbies like myself.

Blazing Fast

First, let me just say, "I love Next.js." It's so freaking fast. Though, of course it is. All my pages are currently statically generated. That said, on top of that, Next.js is also doing cool things like image optimization and code-splitting right out of the box.

 
notion image
 

In fact, Derek Sivers even commented on it ☺. I still can't believe someone who has been on the Tim Ferris Podcast visited and praised my site. Freaking awesome, if you ask me.

 
notion image
 

Let's Get Started

For this to work, you'll need some basic understanding of Next.js. I highly recommend you complete Create a Next.js App tutorial before continuing. That's what I did.

 

After that, create a brand new Next.js app for your site.

//console
npx create-next-app personal-site
 

One of the first things I did was install and import react-bootstrap:

//console
npm install react-bootstrap
 

Then, I started to put together the UI components for the site. Don't worry about any of the data stuff for now. Template out a static site first. Then we'll worry about the data.

 

Homepage

import Head from 'next/head'
import Navbar from '../components/Navbar'
import Container from 'react-bootstrap/Container';
import HomeStyle from '../styles/Home.module.css'
export default function Home() {
  return (
    <div>
      <Head>
        <title>David Nguyen - Fullstack .NET Developer, Microsoft Certified Azure Developer, and Entrepreneur</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      
      <Navbar/>
      <Container className={HomeStyle.main}>
          <img className="rounded-circle mx-auto d-block" src="/images/me.jpg" width="250"/>
          <h1 className="text-center font-weight-bold mt-2">David Nguyen</h1>
          <div className="row justify-content-center">
            <div className="col-lg-6">
              <p>I build interesting projects, work for cool companies, and write sporadically. I specialize in .NET, ASP.NET, and Microsoft Azure Cloud Services.</p>
            </div>
          </div>        
      </Container>

    </div>
  )
}
 

Card component

export default function Card() {
    return (
        <div className="col-sm-4">
            <div className="card">
            <div className="card-body">
                <h5 className="card-title font-weight-bold">Card title</h5>
                <h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
                <p className="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
                <a href="#" className="card-link">Read more...</a>
            </div>
            </div>
        </div>
    );
}
 

Navbar

import Link from 'next/link'

import Container from 'react-bootstrap/Container';
import Navbar from 'react-bootstrap/Navbar'
import Nav from 'react-bootstrap/Nav'


export default function AppNavbar() {
    return (
      <Navbar className="shadow-sm" bg="light" expand="md" fixed="top">
        <Container>
          <Navbar.Brand href="/">
            <Link href="/" passHref>
              <Nav.Link className="navbar-brand">
                <img src="/images/dn.png" width="40" height="40" alt="" />
              </Nav.Link>
            </Link>
          </Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="ml-auto">
              <Link href="/" passHref>
                <Nav.Link className="mr-3">Home</Nav.Link>
              </Link>

              <Link href="/blog" passHref>
                <Nav.Link  className="mr-3">Blog</Nav.Link>
              </Link>

              <Link href="/now" passHref>
                <Nav.Link  className="mr-3">Now</Nav.Link>
              </Link>

              <Link href="/about" passHref>
                <Nav.Link>About</Nav.Link>
              </Link>
            </Nav>
          </Navbar.Collapse>
        </Container>
      </Navbar>
    );
}
 

Now, this was actually a bit tricky. Next.js has its own Link component. In order to get bootstrap navbar to work correctly, I needed to combine bootstrap Nav.Link component and Next.js Link component.

https://medium.com/@simon.schilling/the-trick-to-use-next-js-link-with-react-bootstraps-nav-link-7f7ba2470042

 

After that, I spent a crap load of time obsessing over the footer. I recommend you don't do that. Anyways, let's move on to the fun stuff.

Setting up Cloudflare Worker

I used notion-api-worker and Cloudflare Worker to be able to pull data from my Notion account. This part confused me the most when I was doing this on my own. So, I'll do my best to guide you here.

 

You need to understand Cloudflare Worker first. I recommend their official documentation. Basically, it's a serverless environment where you can upload code without worrying about the actual server. It's very similar to Azure Functions and AWS Lambda.

 

After having a basic understanding of Cloudflare Worker, you'll want to clone notion-api-worker. This is the code you'll put onto Cloudflare Worker.

 

To get started, create a wrangler.toml file. Here's mine:

 
notion image
 

After that, the process to publish the code onto Cloudflare Worker is straightforward.

 
  1. Login
notion image

2. Publish

notion image
 

Getting your Notion Token

Notion doesn't have a public API yet. So you need to set up notion-api-worker with your notion token. To grab your Notion token, log in to Notion and open your DevTools and find your cookies. There should be a cookie called token_v2, which is used for authorization.

 
notion image
 

After getting your token, you'll want to visit https://dash.cloudflare.com and access your Cloudflare Worker settings.

 
notion image
 

Here you'll be able to setup your NOTION_TOKEN variable.

 

Once that is done, your notion-api-worker is all set! Fire up Postman and give your API a try. You should be able to request your Notion page or table.

Using Next.js to Pull Data

Okay, with that out of the way, let's get to the heavy lifting:

  • /src/pages/posts/[path].js - a dynamic route file with static generation
  • /src/lib/posts.js - fetches data from Notion
 
notion image
 

Let's look at [path].js:

import Head from 'next/head'
import { getAllPostIds, getPostData } from '../../lib/posts'
import "react-notion/src/styles.css";
import { NotionRenderer } from 'react-notion'

import Container from 'react-bootstrap/Container';
import HomeStyle from '../../styles/Home.module.css'

import Navbar from '../../components/Navbar'
import Footer from '../../components/Footer'
import Date from '../../components/Date'

// Section #1
export default function Post({ blockMap, blog }) {
  return (
    <div>
      <Head>
        <title>{blog.Title} - David Nguyen</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Navbar />

      <Container className={HomeStyle.main}>
        <h1>{blog.Title}</h1>
        <h6 className="card-subtitle mb-4 ml-1 text-muted">
          <Date dateString={blog.PublishedDate} />
        </h6>
        <NotionRenderer blockMap={blockMap} />
      </Container>

      <Footer />
    </div>
  );
}

// Section #2
export async function getStaticPaths() {
    const paths = await getAllPostIds();
    return {
      paths,
      fallback: false
    }
  }

// Section #3
export async function getStaticProps({ params }) {
    const {data, blog} = await getPostData(params.path);

    return {
        props: {
          blockMap: data,
          blog: blog
        }
    }
}
 

Section #1 is the JSX code to display my Notion page data. We'll dive deeper into that later.

 

Section #2 contains getStaticPaths, which is an important function in Next.js. It's the function that generates dynamic routes for your pages.

 

Section #3 contains getStaticProps, which is another important function in Nextjs. It generates the props for your dynamic pages. In this case, the content of my blog posts.

 

Now both getStaticPaths and getStaticProps are calling functions from /lib/src/post.js to get their data. Let's jump into that.

 
async function getBlogTableData(){
    var uri = "https://my-notion-cloudflare-worker.workers.dev/v1/table/" + process.env.TABLE_ID;
    const response = await fetch(
        uri
      ).then(res => res.json());

    var data = response.filter(a => a.Published)
    .filter(a => a.Path)
    .filter(a => a.PublishedDate)
    .filter(a => a.Title)
    .filter(a => a.Path != 'now');
    
    return data;
}

export async function getSortedPostsData() {
  const data = await getBlogTableData();

  // Sort posts by date
  return data.sort((a, b) => {
    if (a.PublishedDate < b.PublishedDate) {
      return 1;
    } else {
      return -1;
    }
  });
}

export async function getAllPostIds() {
    const data = await getBlogTableData();

    return data.map(blog => {
      return {
        params: {
          path: blog.Path
        }
      }
    })
}

export async function getPostData(id) {
    const blogData = await getBlogTableData();
    var blogArr = blogData.filter((a) => a.Path === id);
    var blog = blogArr[0];
    var pageUri = "https://my-notion-cloudflare-worker.workers.dev/v1/page/" + blog.id;
    const data = await fetch(
        pageUri
      ).then(res => res.json());
      return{
        data,
        blog
    }
}

export async function getNowData() {
  var pageUri =
    "https://my-notion-cloudflare-worker.workers.dev/v1/page/" + process.env.NOWPAGE_ID;
  const data = await fetch(pageUri).then((res) => res.json());
  return {
    data
  };
}
 

Let's start with getAllPostIds. It's calling a function call getBlogTableData that gets the data from my Notion table.

 
notion image
 

And, within that table is a column called Path. It simply returns those values. Notice within the params object is a keyvalue of path: "something" and it matches /src/post/[path].js filename. This is important.

 

Now let's move on to getPostData. With this function I ran into a little issue, since I was using Path to generate my URL but I needed the Notion page ID to get the page data. I needed to make an extra call to getBlogTableData to get the Notion page ID. Only after getting the page ID could I fetch the page data from my API. I'm sure there is a better way to do this, but the site isn't taking a performance hit because this is only getting run during build time. So I didn't care too much.

React-Notion

Okay, let's jump back into Section #1.

import { NotionRenderer } from "react-notion";

// Section #1
export default function Post({ blockMap, blog }) {
  return (
    <div>
      <Head>
        <title>{blog.Title} - David Nguyen</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Navbar />

      <Container className={HomeStyle.main}>
        <h1>{blog.Title}</h1>
        <h6 className="card-subtitle mb-4 ml-1 text-muted">
          <Date dateString={blog.PublishedDate} />
        </h6>
        <NotionRenderer blockMap={blockMap} />
      </Container>

      <Footer />
    </div>
  );
}
 

The key to react-notion is <NotionRenderer blockMap={blockMap} />. The prop blockMap comes from getStaticProps.