• development

Create your blog with Notion API and Next.js

August 9, 2022 · 15 min read
post cover

Table of Contents

    👋 Hi there again! In this post I’ll talk about how to code your blog website with Next.js and Notion API. I’ll assume that you know already know what Notion is, even though I’ll tell you why I chose Notion for writing & storing my content.

    Why Notion API

    There are many ways to develop your own blog website, as I explained in my previous post. Storing your posts on a Github repo It’s the most widely used solution out there, but I decided to use Notion’s API for many reasons.

  • Write anywhere. You can write your articles on the mobile app while on your way to work for instance, and changes get automatically saved. You just need an internet connection.
  • Images and assets storage. With Notion you can use either Unsplash or upload your own images. If you upload yours, they get automatically optimized and Notion API generates a URL for each asset. You can get unlimited file uploads with the ‘Personal Pro’ plan, starting at 4$/month. More info here.
  • Content export. Even with the free plan you get the ‘Bulk export’ function. With this, you can export your content as HTML, Markdown or CSV. Files and images get exported as well alongside with your article.
  • Content automatically published. No more git commits each time we want to upload our post; content gets published when I change it at Notion.
  • Prepare your Notion environment

    First thing that you’ll have to do before start coding is preparing your Notion.

    Create Notion database

    Go to your Notion app and create a new page. There, select ‘Table’ database type. You can learn more about how to create your database here.

    Once you’ve created your database, try to imagine this data table as a ‘parent object’ of your API, where you’ll store all of your blog posts. Each column represents a different article property key, and each row a different value. You can add as much properties as you need. Notion API calls these fields properties.

    💡You can also view this database as a spreadsheet with special functions.

    Database fields

    My database currently looks like this:

    Between all the fields, there’s one that you might add to your database as well. The published field is a checkbox type field that works like a filter, so later on with Notion’s API I can make a query to this database and only retrieve published articles. This is quite useful since it would allow us to preview the articles without having to 'publish’ them or show them on the website, as in a CMS. I’ll explain you how to achieve this later in this article.

    You’ll also need to use one field to store the article itself. In my case I used name field as a title type field. This type of field creates a Notion page with the title that you set as a value at the database. If you hover the value, the button to access the article itself pops up.

    👉Notion team created a guide reference in order to learn more about databases here

    Create API’s integration

    Go to my integrations and hit the ‘Create new integration’ button. You can leave the defaults here but make sure that your integration is using your current Notion workspace.

    This will give you the secret key that will allow us to use the API.

    Give integration access to the database

    You have to specify which database you want your integration to have access to. Go to your database page and hit the button ‘Share’ on the top right corner. If you don’t see your integration name there, click on the input and add it.

    Now it should look like this:

    Find your database ID

    In order to query our database, we need to know beforehand its ID. Click again the ‘Share’ button and then hit ‘Copy link’:

    Paste that URL into your browser or text editor. It’ll look something like this:

    https://www.notion.so/yourdatabaseid?v=whateverargument

    We’ll need this ID later on in this article.

    Scaffolding your project

    Init a Next.js project with create-next-app:

    npx create-next-app@latest
    # or
    yarn create next-app
    # or
    pnpm create next-app

    You can use Next.js with Typescript as well:

    npx create-next-app@latest --typescript

    Notion Client set-up

    Once you’ve started your project let’s install Notion SDK:

    npm install @notionhq/client

    Go to my integrations and copy your token and your database id that we previously got. Paste it into your .env file:

    # .env
    
    NOTION_TOKEN=your_secret
    NOTION_DATABASE_ID=your_database_id

    Make sure that you add it to .gitignore file.

    👉You can find the API documentation here

    Instantiate Notion Client

    Create a config.js file like this:

    // lib/notion/config.js
    
    import { Client } from '@notionhq/client'
    
    export const databaseId = process.env.NOTION_DATABASE_ID
    export const notion = new Client({
      auth: process.env.NOTION_TOKEN
    })

    Thanks to the Notion SDK, we can easily init our client. Pretty straightforward, isn’t it? In my case, I’ve created the lib/notion folder, where we will put all the code related to Notion.

    👉Learn more about authentication here.

    Project sitemap/routing

    For this example, we’ll create just two Next.js routes:

  • / ⇒ On our home page we’ll request all the articles to our Notion API’s database.
  • /[postId]/[slug] ⇒ On our article page we’ll request a specific Notion page.
  • Retrieving all articles

    According to the API reference, if we want to retrieve a list of Pages contained in a database, we need to call the method Client.databases.query() :

    const response = await notion.databases.query({
          database_id: databaseId
        })

    As you can see, our query object only requires the database_id parameter. If we do console.log(response) we get this:

    {
      object: 'list',
      results: [
        {
          object: 'page',
          id: 'd84b779b-fb7f-4f3e-a9fd-a51bbb9c487f',
          created_time: '2022-07-26T11:31:00.000Z',
          last_edited_time: '2022-08-08T09:44:00.000Z',
          created_by: [Object],
          last_edited_by: [Object],
          cover: [Object],
          icon: null,
          parent: [Object],
          archived: false,
          properties: [Object],
          url: 'https://www.notion.so/Create-your-blog-with-Notion-API-and-Next-js-d84b779bfb7f4f3ea9fda51bbb9c487f'
        },
    		/* ... more pages ... */
      ],
      next_cursor: null,
      has_more: false,
      type: 'page',
      page: {}
    }

    From this response object, we just need results and next_cursor. We can extract them like this:

    const { results, next_cursor } = await notion.databases.query({
          database_id: databaseId
        })

    next_cursor is a string used to retrieve the next page of results by passing the value as the start_cursor parameter to the same endpoint.

    Then, if next_cursor is NOT null, means that there’s the next page of results.

    We can pass the start_cursor to our payload like this:

    const posts = []
    let cursor = undefined
    
      while (true) {
        const { results, next_cursor } = await notion.databases.query({
          database_id: databaseId,
          start_cursor: cursor,
          filter: {
            property: "published",
            checkbox: {
              equals: true,
            },
          },
        });
        posts.push(...results)
        if (!next_cursor) {
          break
        }
        cursor = next_cursor
      }

    This code pattern iterates until next_cursor is null because for this example we want to retrieve all the articles.

    We’ve also added a filter object, so now we’re filtering by the published property; only published articles will get retrieved. Filter object documentation.

    👉In a larger project with a large amount of content we would do it differently, but just for this tutorial, we do it this way. You can learn more about pagination here.

    Now we can wrap up this code in a function:

    // lib/notion/post.js
    
    export const getPosts = async () => {
      const posts = []
      let cursor = undefined
    
      while (true) {
        const { results, next_cursor } = await notion.databases.query({
          database_id: databaseId,
          start_cursor: cursor,
          filter: {
            property: "published",
            checkbox: {
              equals: true,
            },
          },
        });
        posts.push(...results)
        if (!next_cursor) {
          break
        }
        cursor = next_cursor
      }
      return posts
    }

    Unfortunately, there’s an extra step that we need to take before putting this code into our Next.js page.

    Since a recent Notion API’s release, page properties must be retrieved using the page properties endpoint. This means that the post fields that we defined in our database require an extra API call. Honestly, It is a pity because when I created this website, this step was not necessary (Notion team, you owe us an apology 😒😂 just kidding).

    Take a look at the following console.log(posts[0].properties) output:

    // console.log(posts[0].properties)
    
    {
      description: { id: '%3BlgR' },
      lastedited_time: { id: 'A_Qp' },
      creation_time: { id: 'EB%3Dv' },
      categories: { id: 'YWAU' },
      published: { id: 'ZgDh' },
      read_time: { id: 'fzyZ' },
      publication_date: { id: 'uTP%3A' },
      name: { id: 'title' }
    }

    The only thing we currently have is just IDs. Let’s use those IDs to retrieve their values:

    // lib/notion/page.js
    
    export const getPagePropertyValue = async ({
      pageId,
      propertyId
    }) => {
      const propertyItem = await notion.pages.properties.retrieve({
        page_id: pageId,
        property_id: propertyId
      })
      if (propertyItem.object === 'property_item') {
        return propertyItem
      }
      // Property is paginated.
      let nextCursor = propertyItem.next_cursor
      const results = propertyItem.results
      while (nextCursor !== null) {
        const propertyItem = await notion.pages.properties.retrieve({
          page_id: pageId,
          property_id: propertyId,
          start_cursor: nextCursor
        })
        nextCursor = propertyItem.next_cursor
        results.push(...propertyItem.results)
      }
      return results
    }

    This getPagePropertyValue() function will return the following: If the property is paginated, returns an array of property items. Otherwise, it will return a single property item. API reference here.

    Now we can create a function that returns all the properties that we need

    Let’s divide this function into 3 parts:

    export const getPageProperties = async (
      page,
      pageId
    ) => {
    	const titlePropertyId = page.properties['name'].id
      const descriptionPropertyId = page.properties['description'].id
      const readTimePropertyId = page.properties['read_time'].id
      const categoriesPropertyId = page.properties['categories'].id
      const publicationDatePropertyId = page.properties['publication_date'].id
    	/* ... */
    }

    The first part is pretty straightforward. We’re just retrieving the property's ids. Those are required for the next part:

    export const getPageProperties = async (
      page,
      pageId
    ) => {
    /* ... */
    const [
        titlePropertyItems,
        descriptionPropertyItems,
        readTimePropertyItem,
        publicationDatePropertyItem,
        categoriesPropertyItem
      ] = await Promise.all([
        getPagePropertyValue({
          pageId,
          propertyId: titlePropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: descriptionPropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: readTimePropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: publicationDatePropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: categoriesPropertyId
        })
      ])
    /* ... */
    }

    In the second part of the function, we’re calling getPagePropertyValue that we created beforehand and wrapping those calls into a Promise.all() code pattern.

    Now you might think that we have the properties already, but the truth is that those properties are quite complex objects.

    Each property might be a different property type, so you need to extract the values from each property individually. Following the Property item object documentation, we need to do the following:

    
    export const getPageProperties = async (
      page,
      pageId
    ) => {
    /* ... */
    	const title = titlePropertyItems
        .map((propertyItem) => propertyItem.title.plain_text)
        .join('')
      const description = descriptionPropertyItems
        .map((propertyItem) => propertyItem.rich_text.plain_text)
        .join('')
      const readTime = readTimePropertyItem.number
      const publicationDate = publicationDatePropertyItem.date.start
      const categories = categoriesPropertyItem.multi_select
      /* Cover's page property works differently from the rest */
    	const pageCover = page.cover
      const coverUrl =
        pageCover.type === 'external'
          ? pageCover?.external?.url
          : pageCover?.file?.url
    
      return { title, description, readTime, publicationDate, categories, coverUrl }
    }
    

    The final code of the function would look like this:

    // lib/notion/page.js
    
    /* ... */
    
    export const getPageProperties = async (
      page,
      pageId
    ) => {
      const titlePropertyId = page.properties['name'].id
      const descriptionPropertyId = page.properties['description'].id
      const readTimePropertyId = page.properties['read_time'].id
      const categoriesPropertyId = page.properties['categories'].id
      const publicationDatePropertyId = page.properties['publication_date'].id
      const [
        titlePropertyItems,
        descriptionPropertyItems,
        readTimePropertyItem,
        publicationDatePropertyItem,
        categoriesPropertyItem
      ] = await Promise.all([
        getPagePropertyValue({
          pageId,
          propertyId: titlePropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: descriptionPropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: readTimePropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: publicationDatePropertyId
        }),
        getPagePropertyValue({
          pageId,
          propertyId: categoriesPropertyId
        })
      ])
    
      const title = titlePropertyItems
        .map((propertyItem) => propertyItem.title.plain_text)
        .join('')
      const description = descriptionPropertyItems
        .map((propertyItem) => propertyItem.rich_text.plain_text)
        .join('')
      const readTime = readTimePropertyItem.number
      const publicationDate = publicationDatePropertyItem.date.start
      const categories = categoriesPropertyItem.multi_select
      const pageCover = page.cover
      const coverUrl =
        pageCover.type === 'external'
          ? pageCover?.external?.url
          : pageCover?.file?.url
    
      return { title, description, readTime, publicationDate, categories, coverUrl }
    }

    It's a little bit long but quite easy code to understand. With this function, you can add more properties in the future to your database with not much effort at all.

    Let’s refactor our getPosts() function:

    // lib/notion/post.js
    
    export const getPosts = async () => {
      const pages = []
      let cursor = undefined
    
      while (true) {
        const { results, next_cursor } = await notion.databases.query({
          database_id: databaseId,
          start_cursor: cursor,
          filter: {
            property: "published",
            checkbox: {
              equals: true,
            },
          },
        });
        pages.push(...results)
        if (!next_cursor) {
          break
        }
        cursor = next_cursor
      }
      const posts = []
      for (const page of pages) {
        const pageId = page.id
        const {
          title,
          description,
          readTime,
          publicationDate,
          categories,
          coverUrl
        } = await getPageProperties(page, pageId)
        posts.push({
          pageId,
          description,
          title,
          readTime,
          publicationDate,
          categories,
          coverUrl
        })
      }
      return posts
    }

    Iterate through each post and just retrieve the information that we need for the front-end. Easy, right? Let’s export those functions into our lib/notion entry point:

    // lib/notion/index.js
    
    import { getPosts } from "./post";
    export { getPosts };

    Next.js data fetching

    As you might know, Next.js offers different data fetching strategies. In my case, I wanted a fast and SEO-optimized solution. The data is not changing that much often, but I do not want to commit each time I publish a post.

    So I decided that, in order to avoid posts having to be updated in build-time, but still maintain a good SSO, ISR (Incremental Static Regeneration) was the solution I needed. With this solution, ‘Next.js allows you to create or update static pages after you’ve built your site.’ Honestly, I love this feature from the framework, makes it stand out from the rest.

    👉You can learn more about ISR here.

    Home page

    Let’s move to our / route file, located in /pages/index.js, and add the following code at the end of your file:

    // pages/index.js
    
    /* ... */
    
    export const getStaticProps = async () => {
      const posts = await getPosts()
    
      return {
        props: {
          posts
        },
        // Next.js will attempt to re-generate the page:
        // - When a request comes in
        // - At most once every second
        revalidate: 1 // In seconds
      }
    }

    This will allow Next.js to try to re-generate your page when either a new request comes in or every second. Now, if we modify our content in our database, we just need to wait a little bit and reload the page to see the changes.

    Now we can access our posts prop at our Client-side code. Let’s add some React code:

    // pages/index.js
    
    import { getPosts } from "../lib/notion";
    import Head from "next/head";
    
    const HomePage = ({ posts }) => {
      return (
        <>
          <Head>
            <title>Your Blog</title>
          </Head>
          <main>
            {posts.map((post) => {
              return (
                <div key={post.pageId}>
                  <h2>{post.title}</h2>
                  <p>Published: {post.publicationDate}</p>
                  <p>Read time: {post.readTime} min</p>
                </div>
              );
            })}
          </main>
        </>
      );
    };
    
    export default HomePage;
    
    export const getStaticProps = async () => {
      const posts = await getPosts();
      return {
        props: {
          posts,
        },
        // Next.js will attempt to re-generate the page:
        // - When a request comes in
        // - At most once every second
        revalidate: 1, // In seconds
      };
    };

    Let’s run the project locally:

    npm run dev

    Go to http://localhost:3000/ and you should see your published posts#:

    Retrieve a single article

    Each article represents a Notion’s Page in our database according to the API’s reference. If we have access to this page’s ID, we can retrieve its content:

    const page = await notion.pages.retrieve({
        page_id: postPageId
      })

    Now, let’s wrap it up on a function as we did previously:

    // lib/notion/post.js
    
    /* ... */
    
    export const getPost = async (postPageId) => {
      const page = await notion.pages.retrieve({
        page_id: postPageId
      })
    
      const pageId = postPageId
      const {
        description,
        title,
        readTime,
        publicationDate,
        categories,
        coverUrl
      } = await getPageProperties(page, pageId)
    
      return {
        pageId,
        title,
        description,
        readTime,
        publicationDate,
        categories,
        coverUrl
      }
    }
    
    /* ... */

    Update the entry point:

    // lib/notion/index.js
    
    import { getPost, getPosts } from "./post";
    export { getPost, getPosts };

    Article page

    Create a file with the following structure: /pages/[postId]/[slug].js . This routing structure called ‘dynamic routing’ gives us access to the postId URL param, needed to retrieve the post data. The slug param is used just for SEO purposes. You can skip it if you want.

    So, any route like /1/my-post-slug will match our /pages/[postId]/[slug].js file, and we’ll have access to the following params at getStaticProps :

    { "postId": "1", "slug": "my-post-slug" }

    To achieve this, let’s create our getStaticPaths function like this:

    // pages/[postId]/[slug].js
    
    /* ... */
    export const getStaticPaths = async () => {
      const posts = await getPosts();
      return {
        paths: posts.map((post) => ({
          params: {
            slug: getSlug(post.title),
            postId: post.pageId,
          },
        })),
        fallback: "blocking",
      };
    };

    Here is where we specify to Next.js which paths should be pre-rendered. paths object should have their own set of params, matching the parameters set in our page name.

    👉When exporting a function called getStaticPaths from a page that uses Dynamic Routes, Next.js will statically pre-render all the paths specified by getStaticPaths.

    In this case, our page name is pages/[postId]/[slug], so params should contain postId and slug.

    We also need to create a function for creating a slug with a post title. Install slugify with npm install slugify and then create the following file:

    // lib/slug.js
    import slugify from 'slugify'
    
    const getSlug = (postTitle) => slugify(postTitle).toLowerCase()
    
    export default getSlug

    fallback property, on the other hand, is pretty important, so It can change the behavior of getStaticPaths. As we’re going to use ISR, we need to set it to fallback: 'blocking'. Then, when we push new articles, they will get SSR for the first request, and then cached for future requests. Fallback documentation.

    Below our getStaticPaths function, let’s add getStaticProps :

    // pages/[postId]/[slug].js
    
    /* ... */
    export const getStaticProps = async (context) => {
      const { slug, postId } = context.params;
      const post = await getPost(postId);
    
      /* Redirect to 404 */
      if (!post) return { notFound: true };
    
      return {
        props: {
          post,
          slug
        },
        revalidate: 1,
      };
    };

    Nothing really interesting to comment on here. We just extracted our routing params, slug and postId, from the context prop. Then we call our getPost method previously defined and we return it alongside the slug.

    Let's modify the home page client code so that we can navigate from the home page to the article page:

    // pages/index.js
    
    import getSlug from "../lib/slug";
    
    /* ... */
    
    const HomePage = ({ posts }) => {
      return (
        <>
          <Head>
            <title>Your Blog</title>
          </Head>
          <main>
            {posts.map((post) => {
    					/* Generate the href for each post */
              const postHref = `/${post.pageId}/${getSlug(post.title)}`;
              return (
                <Link href={postHref} key={post.pageId}>
                  <a>
                    <h2>{post.title}</h2>
                    <p>Published: {post.publicationDate}</p>
                    <p>Read time: {post.readTime} min</p>
                  </a>
                </Link>
              );
            })}
          </main>
        </>
      );
    };
    
    /* ... */

    Now you can go to the home page and navigate to an article:

    We’re just rendering the page properties, the same information that we had on the Home page. The difference is that the article page is where we should retrieve the actual page content.

    Retrieve article content

    A Notion’s page content is basically made out of Blocks.

    👉A block object represents content within Notion. Blocks can be text, lists, media, and more. A page is a type of block, too!’. Learn more here.

    It might be confusing at the beginning but, trust me, it’s a no-brainer.

    So, if a page it’s a block itself, we can retrieve all its children blocks with the page id. Create a file /lib/notion/block.js with this code:

    // lib/notion/block.js
    
    import { notion } from "./config";
    
    export const getBlocks = async (blockId) => {
      const blocks = [];
      let cursor = undefined;
    
      while (true) {
        const { results, next_cursor } = await notion.blocks.children.list({
          block_id: blockId,
          start_cursor: cursor,
        });
        blocks.push(...results);
        if (!next_cursor) {
          break;
        }
        cursor = next_cursor;
      }
    
      return blocks;
    }

    Now modify our getStaticProps to use the getBlocks function like this:

    // pages/[postId]/[slug].js
    
    /* ... */
    export const getStaticProps = async (context) => {
      const { slug, postId } = context.params;
      const post = await getPost(postId);
      /* Redirect to 404 */
      if (!post) return { notFound: true };
    
      const blocks = await getBlocks(post.pageId);
    
      return {
        props: {
          post,
          slug,
          blocks,
        },
        revalidate: 1,
      };
    };

    Make sure to return the blocks property within the props object 😉.

    Render article content

    Now we need to modify our client code:

    // pages/[postId]/[slug].js
    
    /* ... */
    
    const ArticlePage = ({ post, blocks }) => {
      return (
        <>
          <Head>
            <title>{post.title}</title>
          </Head>
          <article>
            <Link href="/">Go Back</Link>
            <h1>{post.title}</h1>
            <p>Published: {post.publicationDate}</p>
            <img width="400" height="auto" src={post.coverUrl} />
    
            <section>
              {blocks.map((block) => (
                <Fragment key={block.id}>
                  <BlockRenderer {...block} />
                </Fragment>
              ))}
            </section>
          </article>
        </>
      );
    };
    
    /* ... */

    We’re now extracting the blocks prop and rendering it with a <BlockRenderer /> component that we are going to create right now to render the blocks:

    // components/BlockRenderer.jsx
    
    import Text from "./Text";
    
    const BlocksEnum = {
      paragraph: "paragraph",
      heading_1: "heading_1",
      heading_2: "heading_2",
      heading_3: "heading_3",
      image: "image",
      unsupported: "unsupported",
    };
    
    const ImageComponent = ({ value }) => {
      const src = value.type === "external" ? value.external.url : value.file.url;
      const caption = value.caption ? value.caption[0]?.plain_text : "";
      return (
        <figure>
          <img width="200" height="auto" src={src} alt={caption && caption} />
          {caption && (
            <figcaption className="text-primary-medium mt-1 italic font-normal">
              {caption}
            </figcaption>
          )}
        </figure>
      );
    };
    
    const BlockRenderer = (block) => {
      const { type, id } = block;
      const value = block[type];
    
      const BlockTypes = {
        [BlocksEnum.paragraph]: (
          <p key={id}>
            <Text text={value.rich_text} />
          </p>
        ),
        [BlocksEnum.heading_1]: (
          <h1 key={id}>
            <Text text={value.rich_text} />
          </h1>
        ),
        [BlocksEnum.heading_2]: (
          <h2 key={id}>
            <Text text={value.rich_text} />
          </h2>
        ),
        [BlocksEnum.heading_3]: (
          <h3 key={id}>
            <Text text={value.rich_text} />
          </h3>
        ),
        [BlocksEnum.image]: <ImageComponent value={value} />,
      };
    
      const unsupportedBlock = (
        <div>
          <span role="img" aria-label="unsupported">
          </span>{" "}
          Type {type} unsupported {type === "unsupported" && "by Notion API"}
        </div>
      );
    
      return BlockTypes[type] || unsupportedBlock;
    };
    
    export default BlockRenderer;

    With this component, we can render all the different Notion block types. As you can see, instead of creating a switch statement, I’ve created a BlocksEnum object, and afterward, I’m using this enum to create the BlockTypes array with all the block types that we have for now.

    We can consider the BlockTypes array as an ‘array of components’ mapping the different BlocksEnum key values. Then we do return BlockTypes[type] || unsupportedBlock at the end of our component, so we make sure that we don’t get any error when an unsupported block is rendered.

    For this example, I just created 5 basic blocks. You can check all of them in the documentation and implement the rest.

    👉Most of the time, those blocks contain a rich text object, so we can use the value.rich_text property. But, for special blocks, like an image or video, it’s not the same property. Check the docs mentioned above for each block carefully.

    In order to render the value.rich_text property, we need to think about rich text as an ‘array of split sentences’ because each word can have different annotations, like a bold style or strikethrough or whatever:

    [
        {
            "type": "text",
            "text": {
                "content": "Hello! Hola! Welcome to my first post 👋😄  As this is ",
                "link": null
            },
            "annotations": {
                "bold": false,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "code": false,
                "color": "default"
            },
            "plain_text": "Hello! Hola! Welcome to my first post 👋😄  As this is ",
            "href": null
        },
        {
            "type": "text",
            "text": {
                "content": "my very first post",
                "link": null
            },
            "annotations": {
                "bold": true,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "code": false,
                "color": "default"
            },
            "plain_text": "my very first post",
            "href": null
        },
        {
            "type": "text",
            "text": {
                "content": " on the internet, I’m going to introduce myself a little before getting into today’s topic.",
                "link": null
            },
            "annotations": {
                "bold": false,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "code": false,
                "color": "default"
            },
            "plain_text": " on the internet, I’m going to introduce myself a little before getting into today’s topic.",
            "href": null
        }
    ]
    Rich-text block value example

    As you can see in this example, Notion API splits the sentence into different objects depending on the annotations: the non-bolded part is clearly separated from the bolded part into different objects in this array.

    Due to this, we need to create a separate component, <Text /> :

    // components/Text.jsx
    
    const Text = ({ text }) => {
      if (!text) {
        return null;
      }
      return text.map((value, index) => {
        const {
          annotations: { bold, code, color, italic, strikethrough, underline },
          text,
        } = value;
        const getClassName = () => {
          const classArray = [];
          if (bold) classArray.push("bold");
          if (italic) classArray.push("italic");
          if (strikethrough) classArray.push("strikethrough");
          if (underline) classArray.push("underline");
          if (code) classArray.push("bold");
          return classArray.join(" ");
        };
        return (
          <span
            key={index + text.content}
            className={getClassName()}
            style={color !== "default" ? { color } : {}}
          >
            {text.link ? (
              <a target="_blank" rel="noreferrer" href={text.link.url}>
                {text.content}
              </a>
            ) : code ? (
              <code>{text.content}</code>
            ) : (
              text.content
            )}
          </span>
        );
      });
    };
    
    export default Text;

    We extract the common properties at the beginning and then we render the special cases in our JSX, like for example the link case. Learn more about Rich-text object here.

    Now we should import <Text /> component in <BlockRenderer /> and we’re ready to go!

    As you can see, our page content now is finally getting rendered! 🥳

    Suggested Deployment

    I highly recommend that if you decided to use this example, you should deploy your code at Vercel.

    Next steps

    I’ll give you a list of a few next steps you can take:

  • Add styling to the project
  • Add more block types
  • Retrieve 2-level-blocks from article content
  • Add an article search page
  • Deployment with Vercel
  • I’ve created a Github repo so you can check out the final code and clone it if you want.

    👋 Thank you for reading and I’ll see you in the next one!

    Was it good? Found any typing error?

    Send me a message on Twitter @jordicasesnoves for letting me know.