ブログ書きたいけどめんどくさい
ブログ書きたいなと思い始めて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でもらえるとありがたいです 🤟