サーバーも MySQL も無い。サイトは手元のフォルダ(記事=シリーズ単位の AsciiDoc、履歴=git)で、公開は静的HTML(Cloudflare Pages 等、月0円)。 WordPress の管理画面の操作感のまま、壊れない・攻撃されない・保守しない サイト運用をするためのデスクトップアプリ(Python/Flet)。
| WordPress | aiseed-builder |
|---|---|
| サーバー+PHP+MySQL | 手元のフォルダ+git(無し) |
| 管理画面(投稿一覧・編集・下書き/公開・メディア) | 同じ操作感の Flet アプリ |
| テーマ | サイト側の tools/templates/ + site.json |
| プラグイン | forms(問い合わせフォーム)/ audit(サイト監査) |
| 更新・脆弱性対応 | 無い(静的HTMLに攻撃面がない) |
| リビジョン | git(「変更を記録」ボタン) |
配布は PyPI ではなくデスクトップアプリ(各OSのバンドル)。
Releases から自分のOSのものを取って実行する。ビルドは GitHub Actions
(.github/workflows/build.yml)が linux / macOS / Windows 分を作る。
# 自分でビルドする(Flutter SDK が要る)
uv sync && uv run flet build linux # → build/linux/aiseed-builder
# ソースから動かす(開発時)
uv run aiseed-builder ~/dev/website # サイトフォルダを指定
uv run aiseed-builder # 前回のサイト(初回は選択画面)サイト側には Python と、そのサイトのビルドエンジン(tools/)が要る
——アプリはエンジンを持たないため(下記の設計原則)。
- エンジンは持たない。 ビルド・パースは対象サイトが持つ
tools/(build_article.py / build/series.py / serve.py)を使う。 パーサの正はサイト側に一つだけ。aiseed-builder は UI とファイル操作。 - サイト固有の知識は
site.jsonへ。 シリーズ一覧・URL規則・ Cloudflare プロジェクト名などはコードに書かない。シリーズは site.json で自由に定義できる(組み込みの名前に縛られない):
{
"builder": {
"cf_project": "aiseed-dev",
"publish_dir": "html",
"preview_port": 8000,
"series": [
{"file": "blog.adoc", "label": "Blog", "url_base": "/blog"},
{"file": "claude-debian.adoc", "label": "Debian入門",
"url_base": "/claude-debian", "slug_strip": "claude-debian-"},
{"file": "pages.adoc", "label": "固定ページ", "url_base": "/pages",
"subtitle": "日付のない、独立したページ"}
]
}
}- 公開の手段はアプリが持つ。 「サイトを公開」は
cf-publish(PyPI・このアプリの
依存)で
publish_dirを Cloudflare Pages へ直接アップロードする—— wrangler も npm も要らない。サイト側にデプロイスクリプトが無くても公開 できるので、ビルドだけできるサイトなら公開できる(公開先はcf_project、既定の公開ディレクトリはhtml/)。認証は環境変数か~/.config/cloudflare/pages.env(CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID)。 - 壊さない。 すべての保存は本文/フロントマターの行単位差し替え+ 保存後の再パース検証つき。壊れる編集は拒否して自動復元する。
- 起動時にプレビューサーバー(ライブリロード付き)を自動起動。 「更新」→数秒でビルド→開いているプレビューが自動で最新になる。
site.json の builder.plugins(例 ["forms", "audit"])に名前を入れた
ものだけが、その順でサイドバーに出る。各プラグインは
plugins/<name>/plugin.py に PLUGIN(name/label/icon/screen)を宣言し、
plugins/__init__.py の登録表に1行足すだけ。main.py はプラグインごとの
分岐を持たない。
"audit" を入れると、サイドバーに サイト監査 が出る。WordPress の
「サイトヘルス」に当たるが、静的サイトなので全ページ・全参照をローカルで
完全走査できる(実測: 390ページ・13,579参照を約1秒)。結果は JSON で
書き出せる。
ビルド出力 html/ を丸ごと走査する:
- リンク切れ …… 内部リンク・画像・PDF が実在するか(最重要)
- メタデータ …… title / description / OGP の欠落、説明文の重複
- 代替テキスト …… alt の無い画像
- 孤立ページ …… どこからもリンクされていないページ
- 重い画像 …… 表示を遅くする大きなファイル
verify_migration.py(新旧の一致を見る道具)とは目的が違う。あちらは
「移行が正しいか」、こちらは「サイトが今正しいか」——両方で壊れている
ものは前者では原理的に検出できない(実際、この監査で「本文のリンク名と実
ファイル名の食い違いで移行前からPDFが読めていない」箇所が見つかった)。
~/dev/test/dns-inventory の CLI を移植。 自分のドメインを入れて棚卸しすると:
- 基本レコード(A/AAAA/MX/NS/TXT/SOA/CAA)
- メール認証(SPF/DMARC/DKIM)を良好/要改善で色分け(DMARC の
p=noneも警告) - 逆引き(IP → ホスト名)
- AXFR ゾーン転送テスト(開いていたら重大)
- crt.sh でサブドメイン補完(外部通信。チェックで on/off、既定 on)+ ダングリング CNAME 検出(サブドメイン乗っ取り候補)
WordPress をやめて攻撃面を減らした次の一手——「公開面を自分で点検する」。
dnspython に依存(未インストールなら画面にその旨を表示)。
site.json の builder.plugins に "forms" を入れると、サイドバーに
フォーム受信箱が出る(FormRescue の forms/flet を移植):
- 受信箱(未確認/確認済み/対応済み)——Cloudflare Worker + D1 からプル型で 取得。「確認」でリモートから削除(データは端末に残る)、「対応済み」で整理
- 接続設定(Worker URL / PULL_TOKEN——
cf-publishの deploy.py が発行) - 「フォーム定義をコピー」——記事本文に貼る
[.form]の雛形 - データは
<site>/.builder/forms/inbox.db(gitignore 推奨・日次 バックアップ30日保持)
公開ページ側の [.form] 描画はサイトのビルドエンジン側で対応済み
(website の場合: pywashi の form プラグインを adoc レンダラに合成し、
form-render.js / form.css / Turnstile をフォームのあるページだけに読み込む)。
これで一気通貫:
記事に [.form](雛形コピー)→ 公開ページでフォーム描画 → 送信は
Worker+D1 → aiseed-builder の受信箱で管理。
未移植: QRスキャン(モバイル専用機能)。
サイドバーの「新しいサイトを作る」で、雛形を選んでサイトを増やせる:
| 雛形 | 中身 |
|---|---|
default |
素のサイト(記事2本・テンプレート・エンジン一式) |
contact |
上記 + 固定ページ(pages.adoc: このサイトについて / お問い合わせ)+ 問い合わせフォーム([.form]、フォーム編集ツール tools/form-builder/、plugins:["forms"]) |
雛形とその展開は今開いているサイトのエンジン(tools/init_site.py)が
行う。アプリは雛形を持たない——エンジンと雛形が一緒に育つようにするため。
作られたサイトにはエンジン一式が入るので、単体でビルドでき、このアプリでも
開ける(そのサイトからさらに新しいサイトも作れる)。
フェーズ1 — site-editor の汎用化(済。website(旧website-adoc)が最初の利用者)サイトの新規作成(雛形)(済) / テーマ切替UIforms プラグイン(済。[.form]のサイト側描画も対応済み)audit プラグイン(サイト監査 + 公開面DNS)(済)- flet-printing 連携(記事を印刷)、外部リンクの生存確認、他プラグイン
uv run pytest -qFlet 1.0 系は API の変更が続いており、LLM の記憶は古い(実際この開発でも
Tabs の構造・Dropdown.on_select・ft.Padding.symmetric で三度踏んだ)。
公式の Flet MCP サーバーを繋ぐと、インストール済みバージョンの
コントロール・プロパティ・イベント・アイコン・実例を、その場で引ける。
pip install flet-mcp # flet と同じバージョンが入るプロジェクト直下に .mcp.json を置く(flet は絶対パスで指定——
MCP サーバーは venv を有効化していない環境で起動されるため):
{
"mcpServers": {
"flet": {
"command": "/path/to/.venv/bin/flet",
"args": ["mcp"],
"env": { "FLET_MCP_ENABLE_EXAMPLES": "1" }
}
}
}既定で有効なのは API とアイコン。FLET_MCP_ENABLE_EXAMPLES=1 で実例検索も
入る。パスが環境ごとに違うので .mcp.json は git に入れない
(.gitignore 済み)。設定を変えたら Claude Code を再起動する。
flet build のバンドルは自前の Python ランタイムを抱える。そのため:
[tool.flet.app] pathはパッケージの親(src)を指す。パッケージ本体を 指すと中身がフラットに展開され、実行時にModuleNotFoundErrorになる- パッケージ内は絶対 import(
from aiseed_builder... import) - サイトの python / git を起動するときは
LD_LIBRARY_PATHPYTHONHOMEPYTHONPATHを外した環境を渡す(残すとサイト側の python がアプリの ライブラリを掴んで起動に失敗する) - サイトのエンジンから import するのは依存ゼロの層だけ
(
tools/build/series.pyとfrontmatter.py)。レンダリング(markdown-it / jinja2 / Pillow)を引きずる層に触れると、アプリのバンドルにその依存が 無いため落ちる。ビルド・デプロイは import ではなく subprocess で行う