オレオレCMSの概要
本ブログは、Obsidianで書いたものが自動的に配信されるようなシステムになっている。サーバーは自宅のProxmoxマシン上のUbuntu VMで、記事の一覧を読み取ってREST APIで配信するバックエンドと、SSRアプリのフロントエンドに別れ、それぞれDockerコンテナで動いている。そして、記事は基本的に自分が更新しなければ内容は変わらないので、すべてのページをCloudflareにキャッシュしている。
Obsidianの快適な編集環境を有効活用しながら、なおかつレスポンスも良いブログとしても配信できるシステムに非常に満足している。ここに紹介もかねて記事としてまとめる。
オレオレCMSのoutline
Obsidianを使った記事管理
このブログは、Obsidianを使ったautomaticなCMSになっている。
このようにObsidianで記述した内容がそのままブログに反映される
実際に上記のように記事を書いている。Vault直下にBlog
というディレクトリを作り、これをCMSのルートにしている。必ず一つのサブフォルダを挟み、update/2024-11-12_obsidian-oreore-cms.md
というようにファイルを作ると https://endaaman.com/update/obsidian-oreore-cms
として公開されるようになっている。
Syncthingを使った同期
そもそもObsidianは無料だとエディタしか使えないので、同期するためには自分でインフラを構築する必要がある。そのために Syncthing を採用した。
Syncthingの管理画面。デフォルトはlocalhost:8384
P2Pで動作するプライベートDropboxみたいなものだ。Syncthingの最大の長所は、スマートフォンと同期できることだろう。LANはNATトラバースでデバイスを検知し、スマホでもPCでも即座に書いたものが同期される。NASなど一つ中央集権的なデバイスを用意すれば、その他の使い勝手はDropboxとほとんど変わらない。VPNも使えばデバイス間のやりとりはより柔軟なものになるだろう。
Vaultをサーバーと同期する
Syncthingで同期するデバイスの一つに自宅のQNAP NASがあり、同一LANに存在するサーバーVM上にnfsでVaultのディレクトリをマウントしている。具体的にはNAS上に /share/Obsidian
を、VM側では /mnt/Obsidian
にマウントする。この /mnt/Obsidian/Blog
を Dockerコンテナに volume で引き渡すことでサーバーアプリケーションからアクセスできるようにしている。開発時はローカルで同期したディレクトリのシンボリックリンクを張るだけである。
静的ファイルのあつかい
/mnt/Obsidian/Blog
と一緒に/mnt/Obsidian/static
もVMから見えるようになるので、これも volume にしてDockerに渡してNginxのalias
で使って配信する。 /static/images/obsidian-oreore-cms/obsidian-edit.png
のように参照すると、ObsidianでもHTMLでもちゃんと画像が表示される(Obsidianは曖昧に画像が参照できてとてもえらい)。
記事をREST APIで配信
サーバーアプリケーションはPythonとFastAPIで作っている。採用理由はコード記述量が少なく、最近機械学習でPythonばかり書いているためだ。Blog
以下のMarkdownファイルをまるっと読み込むわけだが、このパース部分の仕様が一番重要である。仕様さえ守られていればアプリケーションは何でもよい。実際にアプリケーション部分はこれまではnode.js+Koa
→Go+BeeGo
→ Python+FastAPI
と変遷しているが、記事を捨てること無くやりくりしてきている。
パースのルールは
- サブディレクトリをカテゴリとして扱う
- ファイル名は
YYYY-MM-DD_slug.md
とする- 日付と
slug
を必須とする - この書式に従わないファイルは無視する
- 日付と
- 個別ページのパスは
/{category}/{slug}
とする - 本文は
body
とし、その他のプロパティをFrontmatterで管理- 必須項目はなし
- 内容のバリデーションができないから
title
,draft
,tags
,digest
などを設定できる
- 必須項目はなし
日付を頭に付けるのは、タイムスタンプだと引っ越しのときに情報が脱落する可能性があること、ソートできる一覧性のためである。
JavaScriptのほうがMarkdown関連は充実しているのでHTMLへのパースはフロントエンドで行う。実際に https://endaaman.com/api/articles/ を開くと一覧が得られる。
変更の監視とキャッシュパージ
watchdogでルートディレクトリを監視し、記事が変更されるたびにメモリ上にキャッシュする。変更があれば必ずこのパース処理が走るので、そのタイミングでCloudflareのAPIからキャッシュパージを行う。念のためGETで手動でキャッシュパージする秘密のエントリポイントも作っている。
SvelteKitでWebサービス化
本ブログは、トップページの一覧と、各記事詳細の、2種類のページしか無い。トップページでは一覧のみを取得する。一覧のときはペイロードが大きくなるので body
を含めない。個別ページごとに対応するREST APIを触るようにしている。
HTMLへの変換には markdown-itを使っている。ここでObsidianの独自機能などを対応させる。拡張の一例を上げると GitHub Alerts は特に便利で多用している。
GitHub Alertsとは
このようなハイライトされた特殊なブロックである。GitHub独自のシンタックスだが、Obsidianも対応しており、markdown-itにもそのようなプラグインがある
あとは QiitaやZennのようにコードブロックにファイル名を表示するのもちょっと細工が必要だったりする。
> [!NOTE] GitHub Alertsとは
> このように特殊なブロックを埋め込める。GitHub独自のシンタックスだが、
> Obsidianも対応しており、markdown-itにもそのようなプラグインがある
nginxで配信・Cloudflareのキャッシュ設定
/
- フロントエンド/api
- バックエンド/static
- 静的ファイル(Nginx)
となっているが、これは全部Cloudflareでキャッシュする。
server {
listen 80;
server_name endaaman.com;
add_header Cache-Control "public, max-age=2592000, s-maxage=2592000";
location / {
proxy_pass http://localhost:8000;
}
location /api {
proxy_pass http://blog-backend:3000;
}
location /static {
alias /app/static;
gzip on;
gzip_types image/jpeg image/png image/webp image/gif image/svg+xml;
}
}
このように全部一ヶ月(2592000秒)にしている。毎回自動的にキャシュパージが入るので、かなり長くしても全く問題ないのである。
ここで重要なのは、変にnginxでキャッシュを持たないことだ。キャッシュのライフタイムがわからず、完全に意味不明なことになる。
SvelteKitのprefetchとブラウザ キャッシュ TTL
Cloudflareではブラウザ キャッシュも有効にしている。SveteKitを使っていると、リンクにホバーするだけで +page.js
の load
が発火し、ページ遷移前にfetchなどが飛ぶようになっている。事前にコンテンツを取得することで高速化を図るものだが、これも設定しておけばリクエストはCloudflareエッジにすら行かず、ブラウザのディスクキャッシュから即座に解決されるようになる。
まとめ
個人的にはかなり快適なコンテンツ作成環境が整ったともう。 このようなブログシステムにせずとも、Obsidian + Syncthing (+NASもあるとなお良い)は革命的に便利なので、この組み合わせだけでもぜひ試してみてほしい。