0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ObsidianをCMSにしてFlask-FrozenとClaude Codeで快適なブログ環境を作った

0
Posted at

TL;DR

  • ObsidianのMarkdownファイルをそのままブログ記事として使う
  • Flask-Frozenで静的HTML化 → Cloudflare Pagesで無料公開
  • Claude Codeのカスタムスキルで記事管理をAI補助
  • OGP画像の自動生成・サイト内検索・sitemap.xml生成まで一通り揃えた
  • MITライセンスでテンプレートを公開中

モチベーション

WordPressは管理コストが高い。Hugoは速いがカスタマイズがつらい。Notionはいいが独自ドメインが有料。

結局「Markdownファイルが手元にあって、それがそのままブログになる」構成が一番シンプルだと思い、Obsidian + Flask-Frozenで自作した。

システム構成

content/posts/*.md   ← Obsidianで編集
       ↓ frontmatter + wikilink解決
app.py (Flask)
       ↓ flask-frozen
build/               ← 静的HTML
       ↓ git push
Cloudflare Pages     ← 自動デプロイ

技術選定

役割 採用 理由
エディタ/CMS Obsidian ローカルファイル、プロパティUI、グラフビュー
Webフレームワーク Flask Pythonで書ける、シンプル
静的化 Frozen-Flask Flaskのルートをそのまま静的化できる
ホスティング Cloudflare Pages 無料、カスタムドメイン対応
AI補助 Claude Code カスタムスキルで記事管理

実装のポイント

Obsidianとの統合

Obsidianのvaultを content/ に設定し、以下を .obsidian/ で構成済みにした。

// app.json
{
  "newFileFolderPath": "posts",
  "attachmentFolderPath": "assets"
}

これでObsidianから新規ノート作成 → content/posts/ に保存、画像貼り付け → content/assets/ に保存、が自動で行われる。

Wikilinkの解決

ObsidianのWikilink記法([[ファイル名|表示テキスト]])をPython側で処理する。
ファイル名→slugのマップを作り、/posts/{slug}/ に変換する。

def _get_slug_map():
    result = {}
    for fp in glob.glob(os.path.join(POSTS_DIR, '*.md')):
        p = frontmatter.load(fp)
        fn = os.path.splitext(os.path.basename(fp))[0]
        result[fn] = p.get('slug', fn)
    return result

def convert_wikilinks(text, slug_map):
    def repl_piped(m):
        slug = slug_map.get(m.group(1).strip(), m.group(1).strip())
        return f'[{m.group(2).strip()}](/posts/{slug}/)'

    def repl_plain(m):
        ref = m.group(1).strip()
        slug = slug_map.get(ref, ref)
        return f'[{ref}](/posts/{slug}/)'

    text = re.sub(r'\[\[([^\]|]+)\|([^\]]+)\]\]', repl_piped, text)
    text = re.sub(r'\[\[([^\]|]+)\]\]', repl_plain, text)
    return text

ファイル名を日本語にしておくとObsidianのグラフビューに日本語タイトルが表示されて見やすい。URLはフロントマターの slug フィールドで制御する。

OGP画像の自動生成

Pillowを使ってビルド時に全記事分のOGP画像(1200×630px)を生成する。日本語フォントはmacOSのHiragino、Linux環境ではNoto CJKを自動検出する。

FONT_CANDIDATES = [
    '/System/Library/Fonts/ヒラギノ角ゴシック W6.ttc',
    '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',
    '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf',
]

def _find_font(size):
    for path in FONT_CANDIDATES:
        if os.path.exists(path):
            return ImageFont.truetype(path, size)
    return ImageFont.load_default()

Cloudflare PagesのビルドコンテナはLinuxなのでNoto CJKをインストールしておく必要がある。

# build command
apt-get install -y fonts-noto-cjk && pip install -r requirements.txt && python freeze.py

検索

クライアントサイドJSによる全文検索。ページロード時のパフォーマンスを落とさないため、JSONインデックスはボタンクリック時にlazy fetchする。

@app.route('/search-index.json')
def search_index():
    return jsonify([{
        'slug': p['slug'],
        'title': p['title'],
        'date': p['date'].isoformat() if p['date'] else '',
        'tags': p['tags'],
        'summary': p['summary'],
    } for p in get_posts()])
button.addEventListener('click', () => {
    if (index) { search(); return; }
    fetch(indexUrl)
        .then(r => r.json())
        .then(data => { index = data; search(); });
});

Frozen-Flaskは /search-index.json もパラメータなしルートとして静的ファイル化してくれる。

記事の表示順

日付降順だけでなく、order フロントマターで明示的な順序を指定できるようにした。チュートリアル系記事に便利。

def get_posts(include_drafts=False):
    ...
    return sorted(posts, key=lambda p: (
        p['order'] if p['order'] is not None else 9999,
        -(p['date'].toordinal() if p['date'] else 0)
    ))

Claude Codeカスタムスキル

.claude/skills/blog.md にプロジェクトローカルのスキルを定義する。/blog と入力するだけでClaude Codeが記事作成・一覧確認・ビルドなどを案内してくれる。

<!-- .claude/skills/blog.md の冒頭 -->
# ブログ管理スキル

このスキルが呼ばれたら、以下を確認してユーザーに提案せよ:
1. 新規投稿の作成
2. 下書き一覧の確認
...

フロントマター仕様

---
order: 1          # 表示順(省略時は日付降順で末尾)
title: 記事タイトル
slug: url-slug    # URLに使用。省略時はファイル名
date: 2026-05-01  # YYYY-MM-DD(クォートなし)
tags:
  - タグ名
summary: 概要文   # OGP・一覧・meta descriptionに使用
draft: false      # trueで非公開
---

デプロイ設定(Cloudflare Pages)

項目
Build command apt-get install -y fonts-noto-cjk && pip install -r requirements.txt && python freeze.py
Build output directory build
環境変数 SITE_URL https://<project>.pages.dev
環境変数 SITE_NAME ブログ名

mainブランチへのpushで自動デプロイ。

まとめ

  • 書く環境: Obsidianのローカルファイル編集 + プロパティUI + グラフビュー
  • ビルド: Flask-Frozenで静的HTML化、OGP画像も同時生成
  • 運用コスト: 月額0円(GitHub + Cloudflare Pages無料枠)
  • AI補助: Claude Codeのカスタムスキルで記事管理を自動化

テンプレートはMITライセンスで公開中です → https://github.com/pons-llc/obsidian_blog

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?