Notion API + Deno deploy + chrome extensions
モチベ
web ページを見ていて「いいな」と思った瞬間に1アクションで内容を notion に記録したい
- その web ページだけで完結させたいので、chrome 拡張を使う
- リンク貼り付けてもどうせ読まないので、web ページの中身を notion に転写する
要するに
アイコンを押すと「その web ページの HTML から notion API 用の block を作成して pages.create する chrome 拡張」を作る
Deno deploy
notion API は CORS に対応していないのでブラウザから呼んでも動かない。対策として Deno deploy に仲介させる
なので『ブラウザ → extention → Deno deploy → notion』という流れになる
Zenn を notion に転写した際の問題
- notion はわりと行間が狭い
- details の枠がなくなり、見辛くなる
- 埋め込み系はだいたい上手く転写できない (marmeid も不可)
- notion が拡張子のない画像を拒否するので対策として embed で転写しているが、かなり微妙
- bookmark に タイトルも含めた preview が追加されないので、中身がわからない
- code block が diff に対応していない
- 注釈のリンクがそのまま残っている
chrome 拡張
- html も CSS もやりたくないので、UI が存在しないストロングスタイル
-
document.body
を投げるだけ。後はひたすら response のハンドリング - Deno deploy の URL などの情報は、環境変数っぽく別ファイルから読み込ませる
Manifest
{
"name": "test",
"description": "test",
"version": "0.0.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_title": "test"
},
"permissions": ["activeTab", "scripting"],
"web_accessible_resources": [
{
"resources": [ ".env" ],
"matches": [ "https://zenn.dev/*", "https://note.com/*" ]
}
]
}
- 拡張のアイコンを押したときに、そのタブの web ページで指定した関数を実行させるために、
"permissions": ["activeTab", "scripting"]
とbackground.js
内の ↓ を組み合わせるchrome.action.onClicked.addListener((tab) => { chrome.scripting.executeScript({ target: { tabId: tab.id }, function: arrange_scrap, }) })
- Token 系を別ファイル[1]から読み込ませるために、
web_accessible_resources
を設定する
-
.env
になっているけれど、dotenv 型の処理は上手くいかなかったので実態は json ファイル ↩︎
Client 側の CORS 対応
fetch API に必要な設定を与えれば上手くやってくれるので、自分で"Access-Control-Request-Headers"
などを書く必要は無いっぽい
Deno Deploy その1
本来はまとめて deploy するべきだと思うけど、playground 形式で別個に立てていたものを使う
通信関連はよくわからないのでかなり雑
ルーティングに sift を使ってみた。ルーティングだけだと恩恵はいまいちかも
パスをパラメータとして使う場合について、ドキュメントに結構ひどいミスがある
- 間違い:
(request, params) => ...
- 正しい:
(request, _connect-info, params) => ...
ドキュメントそのままの書き方だと、param
という変数名で違う値を取得してしまう
Server 側の CORS 対応
-
'Access-Control-Allow-Method'
と'Access-Control-Allow-Headers'
に欲しい物を書く - Authorizationを使うなら
'Access-Control-Allow-Credentials'
も true にする
CORS 関連は他にも色々情報が転がっているが、以下の2つ以外は時間の無駄
特に、やってみた系はだいたい Qiita の記事の劣化なので本当に無駄。
Preflighted requests
ルーティングでは、↑の扱いに非常に困った[1]
- Preflighted requests では、レスポンスヘッダーに関係なく
response.ok = false
の場合は CORS が拒否されたものとして扱われる - CORS が拒否された場合のエラーは、
response.ok = false
ではなく fetch の失敗として扱われ、(たぶん) fetch そのものをtry {}
していないと処理できない
その結果、存在しない URL へのアクセスに対して 404 エラーを返す設定にすると、
- Preflighted requests に 404 が返る
-
response.ok = false
なので CORS 拒否扱いになる - fetch 失敗扱いになり、404 用の処理に移行しない
となって、処理が止まってしまう。
なのでどこへのアクセスだろうがとにかく「request.method == 'OPTIONS'
にはヘッダーを設定して 200 を返す」処理を差し込むことになり、ルーティングとは?という感じになってしまった
-
あと、Preflighted requests で Auth のチェックはできないよ、っていうのも読み落としていて大変だった ↩︎
複数の CORS
headers.origin
を見て manually に'Access-Control-Allow-Origin'
を設定する
セキュリティっぽい何か
Deno depoly に notion のトークンを保持させることになるが、この辺のセキュリティをどう処理するのが正しいのか全然わかんない
とりあえず、notion のやつとは別のトークンを extention から渡して deploy 側で突き合わせるようにしてみたものの、なんか真似事感がすごい
Deno Deploy その2
block 作成自体は関数に切り出して、deploy 自体は notion への POST に集中する
といっても API 自体は sdk が呼び出すので、ひたすらエラーのハンドリング
なんかもう疲れたので、sdk を信じてエラーはそのまま chrome拡張に返すことにした
Notion API
なぜか change log には書いていないっぽいのだが、「自分+子要素+孫要素」の2段階のネスト付きで blocks.children.append が可能になった[1]
- この Update によって
- 例えばこのようなリストでもまるごと
- 一度の API call で追加することができる
- 一度の API call で追加することができる
- 例えばこのようなリストでもまるごと
何よりも最高なのがこれが pages.create における children にも適用されることで、
これによって、「新規のページ + その内容である 100個以上の 2段階のネスト付きの block」を 1回の API call で作成できるようになった
ネスト付きの block の追加は table しか許されなかったベータ版を思えば大きな進歩で、かなり使えるようになったのではと思う
-
そしておそらくそれにともなって append の100個上限がなくなった ↩︎
3段以上のネストがある block をどう append するか
- 「自分ー子ー孫」を追加する → その後「孫」に「曾孫ーその下」を追加する
利点:直感的でわかりやすく、ネストの深さによらず逐次的に処理できる
問題:「孫」の notion id を調べるのが面倒で、2回目の追加までに手間がかかる
- 「自分」だけを append する → その後「自分」に「子ー孫ー曾孫」を追加する
利点:「自分」の notion id はすぐに調べられるので、2回目の追加までが早い
問題:4段以上のネストになると、手間が増える
2の方法を採用した上で、4段以上のネストは拒否することにした
Deploy 側の流れ
- html を block 化処理に渡す
- block 化処理から「block の情報の record」と「1回目に追加する block の index」と「2回目に追加する block の index」と「その他諸々」が返される
- 「1回目に追加する block の index」を使って block を取り出し、これを"children"として新規のページを作成する
- 「2回目に追加する block の index」が空でない場合
- 作成したページに対して blocks.children.list() を実行し、追加した一番上の階層の block の notion 上での id を得る[1]
- 「2回目に追加する block の index」を使って、親 block ごとに children をまとめる
- 親 block ごとに上で取得した notion 上の id と children をセットにして、それぞれで blocks.append.children を実行する
- 成功/失敗にかかわらず、とりあえず結果をまとめて適当な status とともに extention 側に response を返す
-
children.list() は相変わらず 100個しか返せないので、cursor を使って複数回呼ぶ ↩︎
block 化処理
基本の流れ
- title やら author やらの element をクラス名指定経由で取得し、データを得る
- 記事のメイン構成部分 (<p>が並列に並ぶところ)をクラス名指定経由で取得し、逐次 block 化する
- block 情報の辞書、1回目に追加する block のindex のリスト、2回目に追加する block の index のリストを準備する
- 空白線や区切り線の block を作成し、適当な index で辞書に登録しておく
- block ごとに
-
crypto.randomUUID()
で識別 index を作成し、1回目の index のリストに push する - またこの index をキーとして、辞書に block の情報を登録する
- ネストの深さを調べ、2以下なら何もせず、3以上なら children を取り除く
- 取り除いた children は親の識別 index を情報として持たせつつ、別の block として識別 index を作成して辞書に登録する。この index は2回目の index のリストに push する
-
- ↑のなかで、適宜必要なところで空白行や区切り線の index を1回目のリストに push しておく
- 「tltile などのページの情報、1回目のリスト、2回目のリスト、block 情報の辞書、ネストの最大値」を返す
block 化
基本は以下の2つを組み合わせる
- 与えられた材料と type 指定から block を作る関数
- element の nodeName を見て block 作成の指示を決める関数
block 作成関数は仕事が単純なので汎用化できたが、指示出し関数は属ドメイン化した
平文の装飾
このような aaabbbccc な装飾タグがネストされている平文においてcode
dddeeefffggg
- <a> や <code> を独立 block とは別の要素だと判定し
- 単語ごとに適切な annotation を設定した rich text を作成し
- どれだけタグがあっても 1つの paragraph block にまとめる
のはかなり面倒で、いろいろやってみたが、結局は
- text node を基準に、親を辿って annotation を判断して rich text を作る
- rich text は貯めておき、パラグラフが終わったと判断されたときにそこから paragraph block を作る
という方針にした