Notionから静的サイトを楽につくれるNotionateとNotionslotをつくった

この記事は GMO ペパボエンジニア Advent Calendar 2022 の25日目のクリスマス記事です。イブの記事は、Tatsumi0000の Octokit.rbとGitHub Appを使ってGitHub Releasesを作成する でした。私は最近Rubyを触る機会がめっきり減ってたので、Rubyで書かれたシンプルで短いコードを見て、スッキリしてていいなあと久々Rubyを書いてみたくなりました。

さて、予告ではメール研究について書く予定でしたが、実証実験の実績が乏しいためNotion APIを利用した2つのOSSの話を書きます(テックブログで予告していたし)。なぜ、2つのOSSを作ったのかというと、このブログをいい感じに管理にしたかったというのが元になります。いい感じの管理を言いかえると、「低コストに運用を楽したかった」になり、私がその実現に選択したのは、Notionを使った静的Webサイトの構築です。静的Webサイトをどこでホストするのか、というのは結構なんでも良いのですが、ドッグフーディング的にロリポップを使い、Advancedな使い方としてDNSとCDNでCloudflareを利用しました。

今なお求められる静的Webサイト

エンジニアからすれば結構自明だったりするのですが、そもそもなんで静的Webサイトなんだ?ということから説明します。例えば、ブログやコーポレートサイトなど、頻繁にコンテンツが更新されないWebサイトにおいて、リクエストのたびにコンテンツを生成するような動的Webサイトだと、特にリクエスト数が多いサイトにおいてリソースコストがかかります。さらには、レスポンスタイムなどのパフォーマンスを低下させないようにフロントでキャッシュしなければいけなかったり工夫が必要です。コンテンツの更新が限定的なのでサイトが動的である必要がないわけですから、一般的なStatic Site Generatorを使って静的なWebサイトを作るほうがリーズナブルなのです。もっとも、Jamstackアーキテクチャは、WebサイトのデータやビジネスロジックとWeb Experienceを分離して、スケーラビリティやパフォーマンスや保守性を向上させるというものですから、一つのWebサイトを動的か静的かで分類することがナンセンスではあります。

What is Jamstack? Jamstack is an architectural approach that decouples the web experience layer from data and business logic, improving flexibility, scalability, performance, and maintainability. Jamstack removes the need for business logic to dictate the web experience. It enables a composable architecture for the web where custom logic and 3rd party services are consumed through APIs. https://jamstack.org/

つまり、Jamstackアーキテクチャベースの動的静的を混在させられるフレームワークが台頭しているのです。最近はPre-renderingでJavaScriptのHydrationが不要となるQwikも出てきてたりするので非常に興味深いですね。

Hydration is Pure Overhead
Why hydration is a workaround, not the solution. Dive deeper into hydration. Understand how Resumability works.
iconhttps://www.builder.io/blog/hydration-is-pure-overhead
image

Web experienceに関しては、PCとモバイル関係なくGoogleの検索結果にも影響する要素なので、Jamstackを利用してCore Web Vitalsなどのパフォーマンスを高く維持していかなければならないでしょう。Static Site Generatorに何があるのかは、jamstack.org にまとめてあったりします。

Static Site Generators - Top Open Source SSGs | Jamstack
Check out this showcase of some of the best, open source static site generators. This is community-drive so be sure to submit your favorite today!
iconhttps://jamstack.org/generators/
image

ドキュメント集約先のNotionを使って効率化

私は、Notion を会社だけでなくプライベートでもあらゆるドキュメントの置き場にしています。そのため、ブログやサイトのコンテンツも同様にNotion上で管理したかったのです。Notionの良さは、ここであらためて言及する必要はないでしょう。昨年の日本語対応により、いろんな方面でNotionを使っていますって話を聞くようになった気がします。そして、今年APIがリリースされ、いよいよNotionをDatabaseとして活用することが可能となりました。ドキュメントの書きやすさ、表現力、不足している機能を外部サービスとの連携するなど、直接的なコンテンツホスティングとして優れているサービスではあります。さらにAPI解放により、より抽象的なサービスであるDatabaseとしても機能するようになったのです。よく考えられているな、と感心するのと同時にいろんな既存サービスを飲み込んでいきそうなポテンシャルを感じました。

2022年3月3日 – APIで各種ツールをNotionに連携 🧰
新しいインテグレーションギャラリーをリリースしました。また、ブロック属性で最終更新者を確認できるようになりました。
iconhttps://www.notion.so/ja-jp/releases/2022-03-03
image

Webホスティングでの課題

Vercelでは、Next.jsとNotionを使ってブログを作るテンプレートが用意されています。ホスト先をVercelにする場合はこのテンプレートが使えるのですが、今回はロリポップを使うため使えません。Vercelはコンテナ型のホスティングで、ロリポップはいわゆる旧来のシェアードホスティング。コンテナイメージを使うことはもちろん、Node.jsの環境もありません。とはいえ、断然ロリポップが安いのでランニングコストで分があります(多分、4分の1ぐらいになる)。したがって、シェアードホスティング環境とNotionを使っていかに静的サイトを作るか、が課題になりますが、Next.jsは、Node.jsが不要なHTMLエクスポートができるので、シェアードホスティングだけでなく、S3やGCSなどのクラウドストレージでも何の問題ありません。便利。

Notion-backed Next.js Blog – Vercel
A Next.js site using new SSG support with a Notion backed blog
iconhttps://vercel.com/templates/next.js/notion-blog
image

しかしながら、 next export を使う際にNotionにホストされた画像なども一緒にエクスポートする機能が必要です。これの実現のために notionateというOSSを作りました。

Notionateが提供する機能

Notionateは、画像のエクスポート以外にNotionのページを再現するReactコンポーネントを同梱しています。

import type { GetStaticProps, NextPage } from 'next'
import { FetchBlocks, ListBlockChildrenResponseEx } from 'notionate'
import { Blocks } from 'notionate/dist/components'
import 'notionate/dist/styles/notionate.css'
// Import when enable dark-mode
import 'notionate/dist/styles/notionate-dark.css'

type Props = {
  blocks: ListBlockChildrenResponseEx
}

export const getStaticProps: GetStaticProps<Props> = async (context) => {
  const blocks = await FetchBlocks(process.env.NOTION_PAGEID)
  return {
    props: {
      blocks,
    }
  }
}

export default const Page: NextPage<Props> = ({ blocks }) => {
  return (
    <>
      <Blocks blocks={blocks} />
    </>
  )
}

上記のように、 <Blocks /> コンポーネントにAPIで取得したデータを渡してあげるだけで、Notionのページが再現できます。こんな感じ。

Blocks - Notionate
Notionate
iconhttps://notionate.linyo.ws/blocks
image

こちらのページはGitHub Pages上にあります。そして、Databaseに関しても、List ViewやTable ViewそしてGallery Viewに対応しています。

List - Notionate
Notionate
iconhttps://notionate.linyo.ws/list
image
Table - Notionate
Notionate
iconhttps://notionate.linyo.ws/table
image
Gallery - Notionate
Notionate
iconhttps://notionate.linyo.ws/gallery
image

このように、静的なページに関しては next export で比較的簡単にNotionをHeadless CMSとしたWebサイトを作れます。しかし、Webサイトにほぼ必須と言えるような問い合わせフォームはどうでしょうか?メール返信や通知は、XHRがTCP使えないので無理です。さすがに静的なページだと厳しいということで、TypeFormのようなForm as a ServiceのAPIを使わざるをえません。とはいえ、ランニングコストを下げるためにシェアードホスティングを選んだのです。シェアードホスティングで実行可能な言語と言えばPHP、そしてsendmailコマンド、ちょっとしたスクリプトで解決しそう!ということで作った2つ目のOSSが notionslot です。

Typeform: People-Friendly Forms and Surveys
Build beautiful, interactive forms — get more responses. No coding needed. Templates for quizzes, research, feedback, lead generation, and more. Sign up FREE.
iconhttps://www.typeform.com/
image

Notionslotが提供する機能

Notionslotは、PHP製の問い合わせAPIです。あらかじめ設定した返信メールを問い合わせ者へ送信し、問い合わせ内容は、通知先にメールとNotionのDatabaseに蓄積します。

use \Notionslot\Slot;

$config = [
  'notion_token' => 'secret_**********************************',
  'notion_db_id' => '123456aa-bc12-1234-5678-0987654321aa',
  'site_domain' => 'foo.example',
  'site_name' => 'My Foo',
  'notify_to' => '[email protected]',
  'reply_to' => '[email protected]',
  'mail_from' => '[email protected]',
];
$data = array_merge($_POST, ['ip' => $_SERVER['REMOTE_ADDR']]);

echo Slot::api($config, $_SERVER, $data);

やっていることは単純で、メールを送信することとNotion APIを叩くことです(何年ぶりにPHPを書いただろうか)。NotionのDatabaseは、カラムや型を柔軟に設定できます。問い合わせフォームで様々なデータを受け取ることが可能です。

まとめ

更新頻度の少ないWebサイトを、ランニングコストや運用コストを低くするために、Notionとロリポップ、Cloudflareを使いました(デプロイ周りGitHub Actionsがやっているのですが、気になる人はテックブログをご覧ください)。そして、その実現に next export を使い、必要な機能としてOSSを2つ作りました。

Notionateは、まだまだ対応していないEmbedやViewがあるので、気が無いたら追加対応していこうと思っています。まあ、運用コストかからないように!とあれこれやったわけなので2023年はもっとブログ書いていこうと思った次第でした。メリークリスマス! 🎄