🤟

Gatsby + Notionでいい感じのブログをつくる

🕣2021.11.28

ブログ書きたいけどめんどくさい

ブログ書きたいなと思い始めて4,5年経ちました。。

Wordpressで作ってみたりいろいろ試してみたのですが、結局デザインとかテーマは凝るのに公開できない。

なぜなのか

それは記事を書くのがめんどくさいからです。

Markdownでブログ書きたい なんならNotionで書いてそのまま公開したい

自分のアウトプットに普段使っているNotionで記事を書いて、そのまま公開できる仕組みを作れば少しは記事を書くハードルが下がるかも。

そんな願いを込めてGatsbyとNotionを組み合わせてブログをつくってみました。

今見ているブログがそうです。こんな感じのブログが作れます🤞

Githubに今回作ったブログのソースを置いておくので、みなさんもいい感じのブログを作ってみてください 🌝

環境

Gatsby CLI version : 4.1.0

Node.js : v17.0.1

※ 2021.11.28現在

使ったプラグイン

今回は Gatsby Source Plugin Notion API というプラグインを使いました。

Gatsby + Notion でブログを作る場合「gatbsy-source-notionso」というプラグインが一般的に使われてるっぽいのですが、Githubの更新を見ると2020年1月で更新が止まっているみたいでした。

Gatsby Source Plugin Notion APIは2021年10月現在が最新で、一応Notion公式APIがリリースされて以降にも更新があったようなのでこちらを採用しました。

ちなみにこのプラグインを使って作られている公式のデモサイトがこちらです。

Gatsbyの準備

ともあれNode.jsとGatsbyのコマンドラインツールの準備をしましょう。

やり方はリファレンスに沿って1行コマンドを叩くだけです。

コマンドラインツールのインストール

npmが入っている場合は

npm install -g gatsby-cli

でGatsbyのコマンドラインツールがインストールできます。

※ Node.js自体入っていない場合はネット上の記事のほうが詳しいと思うので調べてみてください 🙃

Notion側の設定

トークンの設定

Integrationの発行

まずはAPIでアクセスできるようにトークンを発行します。

Notionの「Settings & Members」から左メニューにある「Integrations」を選択します。

「Develop your own integrations」を押すとブラウザ上でIntegrationを発行する画面に移ります。

「+ New Integration」でIntegrationを新規作成しましょう。

名前を適当に設定して「Associated workspace」でワークスペースを選択します。

最後に「Submit」でIntegrationの出来上がりです。

APIトークンを確認する

再びNotionに戻り「Integrations」を確認すると先程追加したIntegrationsが見れるはずです(無い場合はリロードしてみましょう)

試しに「uhouho」というIntegrationを作成してみました。

メニューボタンから「Copy internal integration token」を押すとAPIトークンが表示されます。

これは後で使うのですぐに確認できるようにしておきましょう。

ブログ用データベースの作成

次にデータソースとなるデータベースを作成しましょう。

適当にデータベースを作成してみます。Blogという名前でつくってみました。

プロパティは後で詳しく説明するので、まずは作ってしまいましょう。

データベースを Integrations にシェア

データベースを作った後はAPIでアクセスできるようにIntegrationを追加します。

データベースの右上にある「Share」から「Invite」を押すと、さっき作ったIntegrationが表示されるので選択します。

データベースIDの取得

無事Integrationの招待が完了したら、Gatsbyのプラグインで使用するデータベースのIDを確認します。

データベースの右上のメニューから「Copy link」を押してデータベースのリンクを確認しましょう。

リンクはこんな感じになっているはずです。

https://www.notion.so/workspace/hogehogehoge?v=munyamunyamunya

このなかの「hogehogehoge」の部分がデータベースのIDになります。 ※ workspace/ 以降から ?v= の手前までになります。

このデータベースIDもAPIトークンと同じようにGatsby側で設定することになるので、確認できるようにしておきましょう。

ちなみにAPIトークン、データベースIDともに超絶重要社外秘データなので絶対に公開しないようにしましょう。。 Githubとかで間違ってcommitしないように。。

プロパティ

データベースのプロパティはこんな感じです。

必要なものがあれば後々追加してください。※ Gatsby側のGraphQLとかの設定も忘れずに!


Icon (テキスト) : 記事ごとのアイコン

Published (チェックボックス) : 記事の公開/非公開フラグ

PubDate (日付) : 設定した日付以降の記事が公開されます

Slug (テキスト) : 記事のURLになります。ユニークに設定してください。

Tags (複数選択) : 好きなタグを設定してください

CreatedAt (日付) : 作成日(自動で設定される)

UpdatedAt (日付) : 更新日(自動で設定される)


Notion側の設定は以上です。

次はいよいよGatsby側でNotionのデータを取得していきます 😎

Gatsbyスターター

Gatsbyにはスターターといってサイト構築に必要なプラグインとか設定がまるっと1つのパッケージになっている素晴らしい仕組みがあります。ありがたいですねぇ

まずはブログ用に提供されているgatsby-starter-blogをインストールしていきましょう。 myblogの部分は好きな名前で大丈夫です。

gatsby new my-blog https://github.com/gatsbyjs/gatsby-starter-blog

Gatsbyではコマンドを叩くとサーバーが立ち上がってくれます。

cd my-blog
gatsby develop

http://localhost:8000/ からブログを確認してみましょう 😀

おめでとうございます、こんな感じになっていれば成功です。

プラグイン設定

さて、今の状態はNotionからデータを取得しているわけではなく、/content/blogというディレクトリにあるMarkdownファイルをソースに記事を生成しています。

ここからはNotionをデータソースに記事を生成していくためのプラグインの設定をしていきましょう。

Gatsby Source Plugin Notion APIのインストール

記事冒頭で紹介したように今回はGatsby Source Plugin Notion APIというプラグインを使っていきます。

基本的にリファレンスデモサイトに載っている手順通り進めていきましょう。

まずはプラグインのインストールです。

npm install --save gatsby-source-notion-api

次にgatsby-config.jsのpluginsの中に以下の設定を追記しましょう。

ここでtokenには先程取得したAPIトークン、databaseIdにはデータベースのIDを記述しましょう。

plugins: [
	...
	{
    resolve: `gatsby-source-notion-api`,
    options: {
      token: `your token`,
      databaseId: `your database id`,
      propsToFrontmatter: true,
      lowerTitleLevel: true,
    },
  },
	...
],

プラグインの設定はこれでOKです。

gatsby-node.jsの設定

次にgatsby-node.jsファイルの設定を行いましょう。

const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

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

  // Define a template for blog post
  const blogPost = path.resolve(`./src/templates/blog-post.js`)

  // Get all markdown blog posts sorted by date
  const result = await graphql(
    `
    {
      allMarkdownRemark(
        filter: {frontmatter: {Published: {eq: true}}}
        sort: {fields: frontmatter___CreatedAt, order: DESC}
      ) {
        nodes {
          id
          frontmatter {
            title
            Published
            Slug
            Icon
            CreatedAt(formatString: "YYYY/MM/DD")
            UpdatedAt(formatString: "YYYY/MM/DD")
          }
        }
      }
    }
    `
  )

  if (result.errors) {
    reporter.panicOnBuild(
      `There was an error loading your blog posts`,
      result.errors
    )
    return
  }

  const posts = result.data.allMarkdownRemark.nodes

  // Create blog posts pages
  // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
  // `context` is available in the template as a prop and as a variable in GraphQL

  if (posts.length > 0) {
    posts.forEach((post, index) => {
      const previousPostId = index === 0 ? null : posts[index - 1].id
      const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id

      const path = `/blog/${post.frontmatter.Slug}`;

      createPage({
        path: path,
        component: blogPost,
        context: {
          id: post.id,
          previousPostId,
          nextPostId,
        },
      })
    })
  }
}

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  // Explicitly define the siteMetadata {} object
  // This way those will always be defined even if removed from gatsby-config.js

  // Also explicitly define the Markdown frontmatter
  // This way the "MarkdownRemark" queries will return `null` even when no
  // blog posts are stored inside "content/blog" instead of returning an error
  createTypes(`
    type SiteSiteMetadata {
      author: Author
      siteUrl: String
      social: Social
    }

    type Author {
      name: String
      summary: String
    }

    type Social {
      twitter: String
    }

    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
    }

    type Frontmatter {
      title: String
      description: String
      date: Date @dateformat
    }
  `)
}

上記設定のexports.createPagesの処理で、動的な記事詳細ページを生成してくれるというわけです。

細かい解説は省きますが、大事なポイントは

const result = await graphql(

となっている部分です。

GraphQLを使用してNotionAPIでデータを取得するクエリを記述しています。

この中の「nodes > frontmatter」にNotion側で設定したプロパティを記述しています。

Notionでプロパティを変更した場合は、必ずこのクエリの中身も書き換えるようにしましょう。

allNotionではなくてallMarkdownRemarkを使う

まずGraphQLのクエリビルダで、使えるクエリを確認しましょう。

http://localhost:8000/___graphiql を開くとGraphQLのクエリビルダを使うことができます。

これがなかなか便利で、左側のメニューから必要なクエリをポチポチ選択していくと、画面中央のエディタに自動でクエリが生成されていきます。あとはクエリを必要な箇所にコピペするだけでデータ取得してくれるというものです。

クエリビルダのメニューにallNotionとnotionというものがありますが、これは今回追加したプラグインのおかげで使えるようになったクエリです。

ただ、今回これは使用しません。

プラグインのリファレンスにもallNotionを使って書かれたクエリのサンプルがありますが(Query for all nodesという箇所)

query {
    allNotion {
        edges {
            node {
                id
                parent
                children
                internal
                title
                properties {
                    My_Prop_1
                    My_Prop_2
                }
                archived
                createdAt
                updatedAt
                markdown
                raw
            }
        }
    }
}

これを使うとMarkdownがうまくHTMLにパースされませんでした。

allNotionはあくまでNotionのデータを取得するためのものであり、GatsbyでMarkdownをHTMLにパースするための形式で取ってきてくれるものではないからです。

GatsbyではMarkdownはYAML Front-matter という形式で記述するため、allNotionではなくallMarkdownRemarkを使ってデータを取得する必要があるようです。

プラグインのリファレンスにも「Alternatively, you can use MarkdownRemark or MDX directly:」との記載があり、HTMLに変換済みのMarkdownを取得するにはallMarkdownRemarkを使うと良いみたいですね。

query {
    allMarkdownRemark {
        edges {
            node {
                frontmatter {
                    title
                }
                html ← この中にHTMLに変換されたMarkdownの中身が入っている
            }
        }
    }
}

記事一覧ページ

続いて記事一覧を作成していきます。/src/pages/index.js を編集していきましょう。

GraphQL部分は先程編集した、gatsby-node.jsと同じような感じで設定しておきます。

import * as React from "react"
import { graphql } from "gatsby"

import Bio from "../components/bio"
import Layout from "../components/layout"
import Seo from "../components/seo"

import ListItem from "../components/list/item"

const BlogIndex = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`
  const posts = data.allMarkdownRemark.nodes

  if (posts.length === 0) {
    return (
      <Layout location={location} title={siteTitle}>
        <Seo title="All posts" />
        <Bio />
        <p>記事がありません</p>
      </Layout>
    )
  }

  return (
    <Layout location={location} title={siteTitle}>
      <Seo title="All posts" />
      <ol style={{ listStyle: `none` }} className="index-list-container">
        {posts.map(post => {
          const title = post.frontmatter.title || post.frontmatter.Slug
          const link = `/blog/${post.frontmatter.Slug}`;

          return (
            <li key={post.frontmatter.Slug}>
              <ListItem
                icon={post.frontmatter.Icon}
                title={title}
                link={link}
                createdAt={post.frontmatter.CreatedAt}
              >
              </ListItem>
            </li>
          )
        })}
      </ol>
      <Bio />
    </Layout>
  )
}

export default BlogIndex

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(
      filter: {frontmatter: {Published: {eq: true}}}
      sort: {fields: frontmatter___CreatedAt, order: DESC}
    ) {
      nodes {
        excerpt
        id
        frontmatter {
          title
          Published
          Slug
          Icon
          CreatedAt(formatString: "YYYY/MM/DD")
          UpdatedAt(formatString: "YYYY/MM/DD")
        }
      }
    }
  }
`

一覧画面のアイテムコンポーネント

import ListItem from "../components/list/item"

で、記事のリストアイテムのコンポーネントをインポートしています。

コンポーネントの中身は以下のとおりです。src/components/list/item.js というファイルを作成して貼り付けてください。

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

const ListItem = ({icon, title, link, createdAt}) => {
  return (
    <article className="ArticleCard_container">
      <Link to={link} itemProp="url" className="ArticleCard_mainLink">
        <div className="ArticleCard">
          <div className="ArticleCard_emojiContainer">
            <span className="emoji">{icon}</span>
          </div>
          <div className="ArticleCard_infoContainer">
            <div className="ArticleCard_titleContainer">
              <h3 className="ArticleCard_title">{title}</h3>
            </div>
            <small>{createdAt}</small>
          </div>
        </div>
      </Link>
    </article>
  )
}

export default ListItem

「gatsby develop」を再度起動して、一覧画面を確認してみましょう。

Notionで作成した記事が見れていれば成功です 🤟 😎

※ gatsby developの再起動に関して

「gatsby develop」コマンドをコンソールなどで実行すると処理が専有されてしまい、他のコマンドが打てない状態になります。

再起動したい場合は「command + c (Macの場合)」で一度処理を停止した後に、再度「gatsby develop」を実行しましょう。

記事詳細ページ

次に記事詳細ページを編集していきましょう。/src/templates/blog-post.js をいじります。

import * as React from "react"
import { Link, graphql } from "gatsby"

import Bio from "../components/bio"
import Layout from "../components/layout"
import Seo from "../components/seo"

const BlogPostTemplate = ({ data, location }) => {
  const post = data.markdownRemark
  const siteTitle = data.site.siteMetadata?.title || `Title`
  const { previous, next } = data

  return (
    <Layout location={location} title={siteTitle}>
      <Seo
        title={post.frontmatter.title}
        description={post.excerpt}
      />
      <article
        className="blog-post"
        itemScope
        itemType="http://schema.org/Article"
      >
        <header>
          <div className="post-header-wrap">
            <div className="post-icon">{post.frontmatter.Icon}</div>
            <h1 itemProp="headline"> {post.frontmatter.title}</h1>
            <p className="post-created-at">🕣 {post.frontmatter.CreatedAt.replaceAll('/', '.')}</p>
          </div>
        </header>
        <div className="content-wrapper">
          <section
            dangerouslySetInnerHTML={{ __html: post.html }}
            itemProp="articleBody"
          />
        </div>
        <hr />
        <Bio />
      </article>
      <nav className="blog-post-nav">
        <ul
          style={{
            display: `flex`,
            flexWrap: `wrap`,
            justifyContent: `space-between`,
            listStyle: `none`,
            padding: 0,
          }}
        >
          <li>
            {previous && (
              <Link to={`/blog/${previous.frontmatter.Slug}`} rel="prev">{previous.frontmatter.Icon} {previous.frontmatter.title}
              </Link>
            )}
          </li>
          <li>
            {next && (
              <Link to={`/blog/${next.frontmatter.Slug}`} rel="next">
                {next.frontmatter.Icon} {next.frontmatter.title}</Link>
            )}
          </li>
        </ul>
      </nav>
    </Layout>
  )
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query BlogPostBySlug(
    $id: String!
    $previousPostId: String
    $nextPostId: String
  ) {
    site {
      siteMetadata {
        title
      }
    }
    markdownRemark(id: {eq: $id}) {
      id
      html
      excerpt
      frontmatter {
        title
        Slug
        Icon
        CreatedAt(formatString: "YYYY/MM/DD")
        UpdatedAt(formatString: "YYYY/MM/DD")
      }
    }
    previous: markdownRemark(id: { eq: $previousPostId }) {
      frontmatter {
        title
        Slug
        Icon
      }
    }
    next: markdownRemark(id: { eq: $nextPostId }) {
      frontmatter {
        title
        Slug
        Icon
      }
    }
  }
`

GraphQLのクエリでは詳細画面に必要なHTMLなどに加え、記事の前後ページを取得するための「next」「previous」という識別子を付けたクエリを同時に発行しています。

これは後々関連記事とかおすすめ記事を取得するのにも使えそうですね。

また、HTMLに変換されたMarkdownを以下の部分で表示しています。

dangerouslySetInnerHTML={{ __html: post.html }}

こちらも「gatsby develop」から再度ビルドを行い、一覧画面の記事リストをクリックしてみましょう。

記事の詳細が見れていればブログの完成です!!

記事詳細のリンクに関して

このブログの階層は以下のようにしています。

/               # トップページ
	/blog         # 404になってしまうのでなんとかしたい
		/hogehoge1  # 記事詳細ページ1
		/hogehoge2  # 記事詳細ページ2
		...

ブログの詳細画面は/blogで見れるようなURL階層にしていますが、gatsby-node.jsの

const path = `/blog/${post.frontmatter.Slug}`;

で/blog直下に記事詳細を生成するように指定しており、/src/pages/index.js

const link = `/blog/${post.frontmatter.Slug}`;

という箇所で記事詳細へのリンクを生成するようにしています。

これは任意に変更できるので自分の環境というかブログの階層構造に合わせて変更してください。

今回の修正ファイルたち

今回gatsby-starter-blogからNotionをデータソースに変更した修正ファイルはこのコミットで確認できるので、よければ見てみてください(ソース全体はここにアップしています。)。

cloneしてNotionのトークンとデータベースIDを設定すれば「gatsby develop」コマンドですぐにブログが立ち上がるようになっています。

使ってみての感想

メリット

  • Notionで書いてそのまま公開できるのはめっちゃ楽
  • 画像も勝手にNotionから取ってきてくれるのでわざわざサーバーに上げなくてもいい

デメリット

  • 一部使えないコンポーネントがある(テーブルとかコールアウトとか)
  • たまに画像が消えてる
  • なので結局Gatsbyを毎回ビルドして見た目を確認する必要がある

今後やりたいこと

  • aタグでtarget=“_blank”を設定できるようにしたい(自サイトのURLじゃない場合に外部リンク扱いにするような加工が必要)
  • OGPを自動生成する(Zennっぽくしたい)
  • Google Analyticsを設置したい
  • 関連記事とかおすすめ記事を自動で取得したい

とりあえずNotionでブログを書くという当初の目的は達成できそうです。

あとはやる気だけだ。。

みなさんもいい感じにブログ作ってみてください。質問などはTwitterでもらえるとありがたいです 🤟


Profile picture
homio