📘 hkob-astro-notion-blog

これまではてなブログにて情報発信をしていましたが、令和5年3月22日より、こちらでの情報発信を始めました。2019年以前の古い記事は過去の Middleman 時代のものなので、情報が古いです。記録のためだけに残しています。

astro-notion-blog の画像を外部リンクに変換するスクリプト

1. はじめに

このブログは astro-notion-blog で記述しています。これは Notion で記述したページを自動的にブログの形に変換してくれるフレームワークです。このフレームワークは astro という技術を用いており、静的なページを作成してくれます。

過去の middleman のブログを使っていたときは、画像は Gyazo というサービスに置き、外部リンクにしていました。astro-notion-blog では外部リンクはそのまま取り込んでくれるので、問題なく表示されていました。また、ビルド自体もかなり高速でした。

一方、astro-notion-blog に移行後は普通に Notion に画像をアップロードしていました。astro-notion-blog はこのようなアップロード画像に対しては以下のような処理をしてくれます。

  1. ファイルをローカルにダウンロードする
  2. リンクをダウンロードフォルダに向ける

この結果、リンクが1時間で切れてしまう Notion 画像に対しても静的なページからアクセスできるようになります。

しかし、ページが増えるに従い、astro-notion-blog に時間がかかるようになってしまいました。画像自体はほとんど変わらないにも関わらず、このダウンロードなどの処理に時間がかかっているようです。今回は、この Notion 内のページを middleman 時代のように外部リンクに変換することを目的とします。

2. 実装

私の場合には、職場の単なるWebサーバに rsync でフォルダごと展開しています。一度でも astro-notion-blog がビルドされていれば、Web サーバ上にその画像が存在しているはずです。そこで、Notion page の Image block の URL を外部リンクに変更すればいいはずです。ここでは、astro-notion-blog のフォルダに image_export.rb というスクリプトを実装することにします。

2.1 NotionRubyMapping の設定と定数定義

最初に NotionRubyMapping の読み込みとインテグレーションキーの設定を行います。また、外部サーバの URL と astro-notion-blog のデータベース ID を定数にしておきます。

#! /usr/bin/env ruby

require "notion_ruby_mapping"
include NotionRubyMapping
NotionRubyMapping.configure { |c| c.token = ENV["NOTION_API_KEY"] }

EXPORT_URL = "https://www2.metro-cit.ac.jp/~hkob/notion/"
ASTRO_DB_ID = "c9cfd957e44443c59bc67173401f6c6b"

2.2 object URL の差し替え

次に object を渡して、その URL を差し替えるメソッド export_image を作成します。ここで object は ImageBlock のオブジェクトだけでなく、OG 画像用の File Object をとることもあります。どちらのブロックもポリモーフィズムにより url= メソッドで書き換えが可能となっています。この辺りは型を気にしなくていい Ruby のいいところですね。url が amazonaws だった場合に、ファイル名の部分を取り出し、URL を外部 Web サーバのものに置き換えるだけです。ImageBlock と File object では更新方法が異なるので、ここでは Ruby インスタンス変数の置き換えまでを実施することになります。置き換えがあった場合には、true が返されるので、呼び出し側で保存処理を実施します。

def export_image(object)
  url = object.url
  ans = false
  if url =~ /https:\/\/s3.us-west-2.amazonaws.com\/secure.notion-static.com\/([^?]+)?/
    filename = $1
    if File.exist?("public/notion/#{filename}")
      object.url = "#{EXPORT_URL}#{filename}"
      print "## Exporting #{filename}\n"
      ans = true
    else
      puts "File not found: #{filename}"
      exit 1
    end
  end
  ans
end

2.3 ページ全体の書き換え

オブジェクトに対する URL の書き換えはできているので、実際にページ内のすべての画像に対して、置き換えを実施します。書き換えが終わったページを再処理しないように ImageExported というチェックボックスプロパティを用意して、ここにチェックが入っていないもののみを処理するようにします。

Notion のブロックには、CalloutBlock や ToggleBlock など子ブロックを持つブロックも存在します。そのため、再起的な処理ができるように blocks という処理バッファを用意し、子ブロック情報を処理バッファに追記する処理を行っています。ここでは再起的にすべてのブロックをスキャンし、ImageBlock があった時のみ、export_image を実施します。置き換えがあった場合には、true が帰るので、その時に block.save を実施します。

ブロックの処理が終わったら、FeaturedImage プロパティにある og image の書き換えも実施します。こちらはプロパティ内の最初の File object を取得し、export_image を呼び出します。NotionRubyMapping では File object の書き換えでプロパティの書き換えを検知できないので、プロパティ自体の files を上書きしています。また、外部書き出しが終わったので、ImageExported のチェックボックスも true に変更しておきます。その後、page を保存することで画像 URL の変更及び処理完了フラグも書き換えも行われます。

def export_page(page)
  pp = page.properties["ImageExported"]
  unless pp.checkbox
    print "# Exporting page #{page.title}\n"
    blocks = page.children.to_a
    until blocks.empty?
      block = blocks.shift
      if block.is_a? ImageBlock
        block.save if export_image block
      elsif block.has_children
        blocks += block.children.to_a
      end
    end
    pf = page.properties["FeaturedImage"]
    file = pf.files[0]
    if file
      pf.files = [file] if export_image file
    end
    pp.checkbox = true
    page.save
    print "#### Done\n\n"
  end
end

2.4 ブログ全体の書き換え

最後にこのプログラムの呼び出し部を記載します。ページ ID を一つ設定した場合には、そのページのみを処理します。一方、何も指示しなかった場合には、ブログ全体のページをすべて処理します。

if ARGV.length < 1
  db = Database.find ASTRO_DB_ID
  dp = db.properties["ImageExported"]
  db.query_database(dp.filter_equals(false)).each do |page|
    export_page page
  end
else
  page = Page.find ARGV[0]
  export_page page
end

3. ハマったところ

3.1 URL の書き換えに失敗した

NotionRubyMapping の FileObject には internal と external の二種類があります。Ver 0.8.0 では、Internal URL を書き換えようとするとエラーになるように設定していました。今回の処理は internal の URL を書き換えたかったので、url の書き換えとともに external に変更する処理に変えました。これを NotionRubyMapping 0.8.1 としてリリースしました。

    def url=(url)
      @url = url
      @type = "external"
      @expiry_time = nil
      @will_update = true
    end

3.2 OG image が消えた

実際に Notion データベースは変更されたものの、astro-notion-blog を実行したところ OG image がデフォルトのものに変わってしまいました。今日の昼休みに、おとよさんとやりとりをしたところ、external の処理が抜けてしまっていたことがわかりました。私の方でもパッチを作り確認をしましたが、その後おとよさんも早速パッチを作ってくれました。

https://github.com/otoyo/astro-notion-blog/pull/143

中身はこんな感じ

4. 実行

astro-notion-blog でビルドすると、こんな感じでちゃんと外部 URL になっています。

これを受けて今回のスクリプトを実行すると FeaturedImage の URL がちゃんと書きかわりました。

5. おわりに

この変更によりブログのビルドが格段に高速化しました。ビルドが遅くてちょっと躊躇していたのですが、これからもっと頻繁に記事を投稿していきたいと思います。