Notion API(v0.4.4)で画像を扱う
以前の記事
で、「Notion APIが画像に対応してない」と書いたんですが、2021/8/15ぐらい? にimageに実は対応していることが発見されました。
僕の個人ブログでも対応しようと思ってたんですが、8月、9月は手がつけられず、今月に入ってようやく着手しました。
前提
notion-sdk-js: 0.4.4
※ これより前でもimageブロックを取ることはできると思います。たぶん……
あと個人ブログの記事ページはSSGで生成してます。
まだ変更があるのであまり大々的に発表してない?
そもそもimageブロックに対応した、という情報を得たのがTwitter経由で、公式のChangelogを漁ってもそれについての発表が見つけられませんでした。
記憶だと公式でも発表してたような気がしたんですが、気のせいだったかもしれません。
リリースは控えめですが、ドキュメントは↓のように更新されています。
画像ブロックは、このような形式で返ってくるとのこと。
{
"type": "image",
//...other keys excluded
"image": {
"type": "external",
"external": {
"url": "https://website.domain/images/image.png"
}
}
}
そしてちょっとわかりづらい書き方ですが、File objectとして返すパターンもあります。
これは何かというと、Notionにアップデートした画像です。
普通にNotionを使っていると意識しませんが、Notionアプリで表示している画像は、同じ画像に見えても、
- 内部画像
- 外部リファレンス画像
の二種類があります。
外部リファレンスというのは、↓ここでURLを指定した画像です。
で、問題は内部画像についてなんですが、Notion APIで画像URLを取得すると、この画像は1時間で消えます。
内部画像の仕様についての推察
なんで消すねん、という話はまず思ったんですが、調べてみると、
Notionの画像というのは単に画像サーバーに置いて、そのURLを返してるだけなイメージでしたが、
そうではなく、認証済みのURLを都度返すという仕様になっているようです。
もともとNotionはプライベートな情報が多いと思うので、このセキュリティ要件はユーザーにとって好ましいでしょう。
問題はこれがAPIとして解放されたときに、どう設計するかです。
本来的には、画像単位でNotionユーザーが権限を設定できるようにすべきです。
デフォルトはプライベート設定で、公開したい画像についてはパブリック設定、みたいな選択ができるようにすべきでしょう。
ただこれをやるには既存の画像認証の仕組みにガッツリ手を加えることになる上に、ミスるとセキュリティ事故になるので、慎重になっているのだと思われます。
対応方針
Notionが将来的にどう対応するつもりなのかは知りませんが、とりあえず現状はこんな仕様になっています。
個人ブログでNotion画像を扱う場合、こんな対応方針があるかと思います。
- 画像をすべて外部でホスティングする
- 1時間に1回デプロイする
- サイトのSSGをやめてSSRにする
- デプロイ時に一時画像ファイルを開発者側で別のアクセス可能な場所に置く
それぞれ、簡単に説明します。
画像をすべて外部でホスティングする
ブログ向けの画像をあらかじめS3とかにあげておいて、Notionの記事からそのURLを参照させる、という方法です。
正直これが一番開発コストの面では楽だと思います。
僕の場合、そんな画像使わないし、既存の記事で使われている画像の総数も10ファイル程度だったので、
理性的に考えればこの対応で必要十分ではありました。
ただブログを執筆するときに、
「よしここは画像貼ろう」
「あ、画像は外部に置かなきゃだな……」
みたいに画像を使うたびに一手間かかるのを想像して、ちょっと憂鬱になったので、この方法はやめました。
1時間に1回デプロイする
この方↓がやっていた方法です。
デプロイのコストはあるものの、1時間に1回ぐらいならそこまでではないんで、ごちゃごちゃ変なことやるぐらいなら、これでもいいなと思いました。
サイトのSSGをやめてSSRにする
ブログ記事のページはNext.jsのgetStaticProps
を使って静的につくってましたが、これをSSRにして、
ブラウザ更新のたびにページ生成するようにすれば、画像のURLが1時間で切れても問題ありません。
とはいえ、たかが画像のために全体のパフォーマンス犠牲にするのは悪手でしょう。
SSGの中で一要素だけは動的に取得する、みたいなのができたらいいんですが、なさそうでした。
もしやり方あるようでしたら、教えてください。
デプロイ時に一時画像ファイルを開発者側で別のアクセス可能な場所に置く
開発コストは高くなりますが、APIの仕様を考えるとこれがベストかなあと思いました。
デプロイのときにNotion APIから渡される画像URLから画像ファイルを取得して、それを永続的にアクセスできる場所に置きましょう、というアイディアです。
S3に置く
Notionが1時間で切れるS3のアドレスを渡してくれるのを、自分が管理してるAWS S3のバケットに置き、そこのURLを参照する方針を実装しました。
↑詳しくはこちらのプルリクを見てください。
ざっくり書くと以下のことをします。
- IAMを設定する
- S3にブログ記事の画像用バケットをつくる
- バケットを公開設定とする
- @aws-sdk/client-s3をimportする
- 'ListObjectsCommand'で画像があるか確認する
- なかったら'PutObjectCommand'でアップロードする
S3上の公開されているファイルのパスは、
'https://s3.{リージョン名}.amazonaws.com/{バケット名}/{ファイル名}.png(またはjpeg)
として規則的に取得できます。
なので、HTML要素を生成するタイミングでアップロードしてその成功を待つのではなく、成功した前提で、↑の規則からURLを導き出すことにしました。
ファイル名をNotionのブロックIDにするように工夫しました。
拡張子を取得するのが地味に大変でしたが、Notionが渡してくる一時URLの中にjpegかpngか入ってるのを見つけたので、そこから取るようにしています。
Webサーバー上に置けばよかったことに気づく
で、↑の実装をやったあとで、別にS3に置かなくても、普通にpublicフォルダの中に置けば参照できることに気づきました。
つまりVercel上に直置きする、ということになります。
↑方針を変えて実装したプルリクがこれです。
Node.jsのfsを使って、こんな感じで書けました。
if (!isImageExist(block.id)) {
const binary = (await blob.arrayBuffer()) as Uint8Array
const buffer = Buffer.from(binary)
saveImage(buffer, block.id)
}
const saveImage = (imageBinary: Uint8Array, keyName: string) => {
fs.writeFile(imagesPath + '/' + keyName + '.png', imageBinary, (error) => {
if (error) {
console.log(error)
throw error
}
})
}
public/blogImagesの中にブロックID付きで画像ファイルが保存されます。
Vercel上に置くデメリット
結局僕はVercel上に直置きを採用しました。
多くの場合は、これでいいんじゃないかと思いますが、デメリットってなんでしょうね?
Vercelの無料プランを使っていると、データ量によっては、なんか上限に触れるんじゃないかと思って調べてみたんですが、
どうやらVercelに容量制限みたいなものはないみたいで、そこは大丈夫そうでした。
S3(や、その他外部ファイルサーバー)でやりたい要件が何か特別にあれば、そっちを使えばいいかなと。
残った課題
APIの話からちょっとズレるんですが、現在画像の縦サイズ横サイズを固定長にしています。
これはnext/imageを使ったときに指定しないとダメだったので、こうしています。
HTMLの <img>
を使うと継承できるんですが、今度はモバイル端末でディスプレイが小さくなったときに、
画像の縮小が上手く効いてくれない問題があって、現状で妥協しています。
next/imageをもうちょい勉強しないとダメかと思っています。
まとめ
というわけで、Notion APIの画像対応やってみた、記事でした。
いずれに方法を選ぶにせよ、そもそもNotion API側のイマイチ感があるので、将来的な改善を待ちつつ、今は上で紹介した対応でしのぐ感じになりますかね。
Discussion
Notion × Next.js でブログを作っていて、画像まわりの実装悩んでいたので参考にさせて頂きました!ご共有ありがとうございます。
記事を書かれたときは分からないのですが、現在はvercelでのビルド後のファイル構成にはpublicディレクトリが含まれず、「Vercel上に置く」方法だとうまく画像が保存できません。こちらの公式ドキュメントでも、
と書かれており、「S3に置く」方針が現状としてはベストかなと思います。