出荷したもの
機能を出したら、同じコミットでここに書きます。
変更履歴
新しいものが上です。日付は公開日(JST)。
21.1.0 — 2026-09-15
本物のブラウザで、全画面を開いて確かめました。
サーバが200を返すことと、画面が動くことは別です。画面はJSで /v1 を叩くので、
そこが落ちていてもHTMLは200で返ります。押した人にしか分からない壊れ方をします。
Chrome を headless で動かし、69画面を1枚ずつ開いて、JSの例外・
console.error・400以上の通信・読み込み失敗を集めました。
結果は0件。 さらに、運用者としてログインしてボタンを実際に押し、
クライアントとしてログインして「業種を選ぶ→本文を作る→確認する→出す」まで
通しました。問い合わせフォームも、ブラウザから実際に送って、
保存されるところまで見ています(SMTP未設定なので送信は失敗しますが、
中身は残ります ── 保存が先、送信が後)。
見つかったもの
/favicon.icoが毎回404でした。 ページに宣言があっても、ブラウザは
ルート直下を直接取りにきます(JSONやXMLを開いたとき、APIドキュメントの画面、
ブックマーク、RSSリーダー)。ルート直下で返すようにしました- 1つのレイアウトがアイコンを宣言していませんでした。
レイアウトが増えるたびに忘れるので、検査で見張ります - スマホの「ホーム画面に追加」で、名前もアイコンも出ませんでした。
クライアントは/postを電話で開きます。マニフェストを足しました - 画面の呼び出しは、パスだけでなくメソッドまで照合するようにしました。
経路があってもメソッドが違えば、押した人だけが405を見ます
確かめ方を残しました
tools/browsercheck.py に置きました。sweep で全画面を開き、
click でログインしてボタンを押します。次に同じことを確かめたい人が、
同じ手順を組み直さずに済みます。
カタログ 925 → 931項目。テスト 907 → 918件。
21.0.0 — 2026-09-14
クライアントが毎日押すボタンが、何も送っていませんでした。
かんたん投稿の「出す」は、媒体ごとに成功と表示し、送信しました と言い、
履歴にも残していました。アダプタは1つも呼ばれていませんでした。
1文字も、どこにも届いていません。
この基盤は「送れていないのに送ったと言わない」で通してきたのに、
いちばん人が触るところが、いちばん大きな嘘をついていました。
- 押したら実際に配信します。 選ばれた媒体のうち、認証情報の揃っている
配信先へ、配信エンジン経由で出します - 結果を1件ずつ返します。 送れた先・送れなかった先を、理由つきで。
「配信先が未設定です」「送れませんでした」と出ます - 1つ落ちても、他を偽りません。 片方が401でも、もう片方の成功はそのまま
- ログインしていない操作は、これまでどおり必ずドライランで止まります
一緒に直したこと
- 同じリンクが2回入っていました。 文面にURLを入れたうえで、配信側も
URLを足していました。既に入っていれば足しません(ハッシュタグも同じ) - 同じ秒に2回押すと、あとの投稿が前を上書きしていました。 日本語の見出しから
ではIDに使える文字が取れず、時刻だけのIDになっていたためです - Instagram は選べるのに、配信の口がありませんでした。 アダプタを書きました
(コンテナ作成→公開の2段階)。画像が無い投稿は受け付けられないので、
送る前にそう言います - 業種の呼び名が2つありました。 かんたん投稿は「飲食店」、キーワード雛形は
「飲食」。繋がっていないので、雛形を引くと黙って0件でした。呼び名を寄せ、
全角(EC)や中黒の違いも吸収します。知らない業種は、知らないと言います - 美容の雛形に韓国語(「메뉴」)が混ざっていました
言葉の食い違いは、いちばん静かに壊れます。 認証情報の項目名で同じことが
起きたので、業種名・媒体名についても検査で見張るようにしました。
カタログ 911 → 925項目/53カテゴリ。テスト 862 → 907件。
20.0.0 — 2026-09-14
「実装済み」と書いてあるのに、外から触れない機能が14ありました。
カタログを疑ってかかりました。この数日で「済」のものが壊れているのを
何度も見つけたからです。結果、コードが無い項目はありませんでしたが、
別の穴が出ました ── コードはあるのに、呼び出す経路も画面も無い。
運用する人には、存在しないのと同じです。
| 機能 | どこから触れるようになったか |
|---|---|
| 無料期間の開始・残日数 | 課金画面 / POST GET /v1/billing/{id}/trial |
| 支払い方法の変更・解約 | 課金画面 / POST /v1/payments/{id}/portal |
| 口コミの取り込み・返信・営業時間 | 店舗画面 / /v1/gbp/{id}/reviews/...・/hours |
| Pinterest / TikTok 投稿 | POST /v1/social/pinterest・/tiktok |
| エゴサーチ(語の登録・走査) | SNS画面 / /v1/mentions/{id} |
| 予約の時刻変更 | 予約画面の各行 / PUT /v1/schedule/{id}/time |
| 公開ペースの目標と実績 | 予約画面 / GET PUT /v1/cadence/{id} |
| CDNキャッシュの破棄 | POST /v1/ops/cdn/purge |
| 表記ゆれ検査・定義文の雛形 | GET /v1/entities・POST /v1/entities/definition |
| 記事音声のポッドキャスト配信 | /podcast.xml(合成した音声を回として残すようにしました) |
途中で自分もやりました
/llmo/{slug}が先にあるので、/llmo/entitiesは永遠に届きませんでした。
Starlette は宣言順に照合します。404が返るだけで、誰も気づきません。
後から足した固定の経路が、先のパラメータ付きに 食われていないかを
検査で見張るようにしました- 口コミの取り込みで、返信済みの印を消していました。 取り込み処理は
それ自体が保存まで済ませるので、あとから前の状態を読むと上書き後を見てしまう。
同じ口コミに二度返信するところでした - 画面が
SB.patchを呼ぶのに、SBにpatchがありませんでした。
画面が使う通信手段が揃っているかも検査します
カタログ 893 → 911項目/51カテゴリ。テスト 828 → 862件。
19.1.0 — 2026-09-13
初期設定を終える前のコンソールが、誰にでも開いていました。
「アカウントが1つも無いうちは、中を見てもらう公開デモとして通す」という
作りでした。そこまでは意図どおりです。問題は、通したまま運用が
始められてしまうことでした。
コンソールの画面は、その場でデータを描きます。監査ログ、
問い合わせの本文、取引先の名前、アカウントの一覧。管理者アカウントを
作るまでの間、それが誰にでも見えていました。
- 守るものができた時点で閉めます。 取引先を登録した、問い合わせが届いた、
記事を入れた、APIキーを発行した ── どれか1つでも起きたら、認証必須にします - 入口は閉めません。 初期設定とログインは開けたままです
(閉めると、そもそも設定を終えられません) - 公開サイト(料金・機能・ブログ・問い合わせフォーム)には影響しません
データを出すAPIは、以前から鍵が要ります(そちらは開いていませんでした)。
カタログ 891 → 893項目。テスト 820 → 828件。
19.0.2 — 2026-09-13
退避が効き始めたら、こんどは「永続ストレージがあります」と言い出しました。
付けていないのに。
起動の印が前回のぶん残っているかどうかで、ディスクが残るかを判定していました。
退避から書き戻すようになった以上、印が残るのは当たり前です。
書き戻したことの証拠であって、ディスクの証拠ではありません。
書き戻した起動では、ディスクについては「分からない」と言い、
残る理由は退避のほうだと説明します。前に出していた結論も取り下げます ──
汚れた証拠で出した結論を持ち越すと、いつまでも嘘が残るので。
カタログ 890 → 891項目。テスト 817 → 820件。
19.0.1 — 2026-09-13
退避先を Aisnsio/seobuilder-backups にしました。
最初は組織側(AisnsioDev)に作りましたが、Space に入っているトークンからは
「Repository not found」 で見えませんでした。権限の範囲の話です。
フリート内の他の退避先と同じ、個人の名前空間に置きます。
本番で確認しました。保存すると1分ほどで1枚置かれます。
19.0.0 — 2026-09-13
データが消えなくなりました。 公開を止めていた最後の1つです。
これまで、この Space のディスクは使い捨てでした。デプロイのたびに、
アカウントも設定も問い合わせも消えていました。 有料の永続ストレージは
ブラウザでのカード登録が要り、APIからは付けられません。
そこで、プライベートな HF Dataset を保存先にしました。
- 何かが保存されると、少し待ってからスナップショットを1枚置きます
(書き込みのたびに送らず、まとめて1枚に) - 起動時、手元にDBが無ければ開く前に最新の退避を書き戻します
- 終了の合図を受けたら、最後の1枚を置いてから畳みます。
デプロイ直前の数分は、ここでしか救えません
新しく用意するものはありません。 すでに Space に入っている HF_TOKEN
をそのまま使います(分けたい場合は HF_BACKUP_TOKEN)。
いちばん気をつけたこと
怖いのは消えることではなく、空で上書きすることです。書き戻しに失敗した
直後に自動保存が走ると、向こうにある本物が、空のデータベースで置き換わります。
取り返しがつきません。
なので、書き戻しに失敗している間は自動保存を断ります。 画面には理由を
出します。中身を確かめた上で、手動なら上書きできます(止めるだけでは、
直す手段が無くなるので)。
そのほか:
- 稼働中でも取りこぼしません。 ファイルをコピーすると WAL に残った最新の
書き込みが漏れるので、SQLite の複製APIを使っています - テキストで送ります。 HF はコミットAPI経由のバイナリを拒否します
- 古い退避は自動で間引きます。掃除に失敗しても、保存自体は成功にします
- 運用画面に、最後に退避した時刻・大きさ・この起動で復元したかを出します。
「いますぐ退避する」ボタンも付けました - 外からは中身を見せません。 残るかどうかは公開のままですが、
何件あるか・退避先はどこか・どのSpaceかは運用者だけに返します
(これまで公開の経路から見えていました)
カタログ 879 → 890項目/50カテゴリ。テスト 797 → 817件。
18.6.0 — 2026-09-13
5つの媒体で、画面が集める項目名と、配信時に読む項目名が違っていました。
いちばん気づきにくい壊れ方です。画面には「設定済み」と出るのに、
配信すると「認証情報がありません」で落ちる。入れた人は、正しく入れたはずなのに
動かない、という状態に置かれます。
| 媒体 | 画面が集めていた | アダプタが読む |
|---|---|---|
access_token |
page_access_token |
|
| Ghost | admin_api_url |
site_url |
| Hashnode | access_token |
token |
| WordPress.com | site_id |
site |
| ライブドアブログ | (集めていない) | blog_name(投稿先URLに要る) |
- 画面の項目名を、アダプタが実際に読む名前に揃えました
- 古い名前で登録済みの設定も、そのまま読み続けます。 名前を直したせいで
動いていたものが止まるほうが困ります - 食い違いを検査で見張るようにしました。もう静かには戻りません
Ghost の「接続を試す」は、署名を付けずに投げていました。 鍵が正しくても
必ず401になり、「トークンの期限切れの可能性があります」と嘘をついていました。
鍵の形式が違うときは、そう言います。
カタログ 875 → 879項目。テスト 766 → 797件。
18.5.0 — 2026-09-13
7つの配信先は、実装はあるのに検証が1件もありませんでした。
Blogger / Facebook / ライブドアブログ / Medium / Notion / Shopify /
Squarespace。最初にその媒体を選んだクライアントが、最初の実行者という
状態でした。配信は、この基盤がお金をいただいている部分です。
- 上の7媒体に検証を書きました。何を・どのURLへ・どんな形で送るか
- 手薄だった Wix・YouTube・X・Ghost・メール投稿も埋めました
- 配信アダプタ全体の網羅率 83% → 94%。いちばん低い媒体でも88%
確かめているのは、主にこういうことです。
- 試すつもりの実行が、本当に投稿されないこと(7媒体すべてで、dry-runは
1回も外へ出ません)。取り返しがつかない種類の間違いなので - 下書きのつもりで公開しないこと(Wixは下書きを作ってから公開する2段階。
下書き指定なら2段階目へ進みません) - 転載にcanonicalが付くこと。付け忘れると、自社サイトが自分の記事に負けます
- 動画の概要欄へ追記するとき、過去の関連記事を消さないこと
- 上限を超えたら、切り詰めずに断ること。黙って切ると、後ろが消えたことに
誰も気づきません - できないことは、黙って公開せずスキップと言うこと(Facebook・
ライブドアブログ・X・メール投稿は下書きに非対応)
カタログ 866 → 875項目/49カテゴリ。テスト 727 → 766件。
18.4.0 — 2026-09-13
407ある経路のうち、実際にHTTPで叩かれていたのは6割ほどでした。
残りは、初めて押した人が最初の500を踏む状態でした。全部押しました。
- 137の書き込み経路を、空の本文で叩く検証を足しました。落ちません。
500は「こちらの落ち度」です。使い方が違うなら400番台で、
何が足りないのかを日本語で返すのがこの基盤の約束です - 知らないIDで落ちないことも確かめます(66経路)。消したあとの
ブックマークを踏まれるのは、日常的に起きます - 画面が呼ぶ経路が実在するかを検査するようにしました。経路の名前を
変えると、呼んでいる画面は黙って壊れます。押した人にしか分かりません
いま押して壊れたものはありませんでした。これから壊れたときに気づけます。
カタログ 863 → 866項目。テスト 723 → 727件。
18.3.0 — 2026-09-13
見た目は正常なのに、中身が届いていない画面が2つありました。
- 監査ログが、ずっと空でした。 「誰が何をしたか」は取ってきていたのに、
画面へ渡していませんでした。渡していないので Jinja は空欄で描き、
「まだ記録がありません」と出ていました。記録はありました - AI検索対応の画面だけ、フッタのバージョンが消えていました。 検査結果を
siteという名前で渡したせいで、レイアウトが使う全体設定を隠していました
どちらも例外を出しません。未定義の変数を空文字にするのは Jinja の仕様なので、
渡し忘れても画面は出ます。だから見つけるには、描いている最中に未定義へ触ったかを
見るしかありません。
- 全画面を1枚ずつ描いて、渡し忘れを検出する検証を足しました(51画面)
- 静的検査(pyflakes)を検証に組み込みました。 18.2.0 で見つけた
「存在しない名前を呼ぶ」バグは、これなら一瞬で見つかります。
「計算して捨てている」も同時に見ます - 使われていない行を2か所削りました
カタログ 859 → 863項目。テスト 717 → 723件。
18.2.0 — 2026-09-13
画面から登録したクライアントが、再起動のたびに配信エンジンから消えていました。
起動時にDBのクライアントをエンジンへ載せ直す処理が、存在しない名前を
呼んでいました(client_store/このモジュールでは store)。広い
except が NameError を warning に変え、ログの1行として流れていました。
1本ずつのテストは全部通っていました。Tenants.apply() も、画面も、APIも
動いていた。繋ぎ目だけが切れていて、誰も歩いていなかったので、
気づけませんでした。
- 起動フックを直しました。登録したクライアントは、再起動しても残ります
- 通しで歩く検証を足しました。空の状態から、管理者を作り、クライアントと
配信先を登録し、招待し、本人がパスワードを決め、投稿を作り、ボットを出す
まで。途中で止まったら落ちます - 起動時の失敗を、検証で見張るようにしました。握り潰した warning は誰も読みません
- 同梱のデモ2社で「配信できます」が緑になっていました。 デモには認証情報が
無いので、緑でも1本も出せません。実際に出せる配信先が1つ以上あるときだけ
緑にします - クライアントが運用コンソールへ迷い込めないことも、通しで確かめます
カタログ 854 → 859項目/48カテゴリ。テスト 709 → 717件。
18.1.0 — 2026-09-12
前回の作りで残っていた細かい穴を埋めました。
- 招待メールを、こちらから送るようにしました。 これまでは
リンクを作ったあと、手元のメールソフトで送る必要がありました。
メールが繋がっていなければURLを返すので、これまでどおり手で渡せます。
送れていないのに送ったとは言いません(落ちても招待そのものは残ります) - メールにパスワードは載せません。 載せた時点で、メールボックスが鍵になります
- クライアント画面の説明が古いままでした。「設定は
data/clients.yamlから
読み込まれます」と書いてありましたが、いまは画面からも登録できます。
どちらが優先されるか(画面のほうが勝つ)まで書きました - かんたん投稿から応対ボットへ行けませんでした。 クライアントは2つの画面を
使うのに、片方からしか行き来できていませんでした
カタログ 850 → 854項目。テスト 702 → 709件。
18.0.0 — 2026-09-11
クライアントを迎えるのに、リポジトリを触らなくてよくなりました。
これまで1社増やすには data/clients.yaml を編集してデプロイする必要があり、
運用する人が自分でクライアントを増やせませんでした。
クライアントを迎える(/console/onboard)
- 登録する — ID・会社名・サイトURLを入れるだけ。その場で配信エンジンに反映されます
- 配信先を足す — 25媒体から選び、足りない認証情報を名指しで出します
(WordPressならsite_url/username/app_password) - 招く — 一度きりのリンクを作って本人に送ります
既存の data/clients.yaml は残ります。YAMLを先に読み、画面で登録したものを後から重ねます。
認証情報の扱い
- 暗号化して保存します。 DBを覗いても平文は出ません
- 画面には項目名だけを返します。値は保存後に読み出せません
- 空欄のままにした項目は変更しません。入れ直さないと消える作りにはしません
- 配信先を外すと、その認証情報も一緒に消えます
招待(/console/invite/{token})
パスワードを人づてに渡すのは、渡す側にも受け取る側にも危ない。
一度きりのリンクを送り、本人に決めてもらいます。
- 7日で期限が切れ、1度使うと無効になります
- 弱いパスワードは、招待経由でも受け取りません
- 招待リンクはログイン無しで開けます。 招かれた人はまだアカウントを持っていないので、
門番で塞いだら誰も入れません
- 受諾したらそのまま入れます。client 権限はかんたん投稿の画面へ、それ以外はコンソールへ
クライアントを止めるとき
消さずに無効にします。記録が消えると、あとから何があったか分からなくなります。
カタログ 826 → 850項目 / 47カテゴリ。テスト 675 → 702件。
17.1.0 — 2026-09-10
出す直前に見つけた不具合を直しました。
鍵が設定された本番では、クライアントがブラウザからボットを作れませんでした。
/bot の画面が書き込み系APIを叩くのに、ブラウザは X-API-Key を持っていないためです。
コンソールのフォームで一度踏んだのと同じ形でした。
- 鍵かログイン、どちらでも通すようにしました。クライアントにAPIキーを渡させるのは
無理がありますし、渡させれば漏れます client権限は自分のクライアントのボットしか触れません
(作成・学習・試用・削除のすべて)- 鍵を1本も置いていない環境(ローカル開発)は、これまでどおり通します
カタログ 824 → 826項目。テスト 669 → 675件。
17.0.0 — 2026-09-10
クライアントが自分のボットを出せるようになりました。 サイトのURLを渡すと、
そのサイトを読んで、訪問者の質問に答えるボットができます。
公開までの3ステップ(/bot)
- 何を覚えさせますか — URLを1本。同じサイトの中の主要ページまで辿ります(最大30ページ)
- どう話しますか — 名前・あいさつ・分からないときの返事・問い合わせ先
- 試して、出す — 実際に聞いてから公開。貼るのは
<script>1行だけ
ボットが守ること
ボットは事業者を代表して話します。だから守りたいのは1つです。
渡した資料に無いことを答えないこと。
- 価格・在庫・営業時間・キャンペーンを推測で答えると、その責任は事業者が負います。
資料に無いことは「こちらでは分かりかねます」と伝え、問い合わせ先を案内します
- 医療・健康・法律・税務の判断はしません
- アシスタントより厳しい基準で答えます。曖昧な一致で答えるより、人に渡すほうが害が小さいためです
- 公開するまで外からは見えません。 作った瞬間に出ることはありません
- 公開URLは16バイトの乱数です。訪問者には client_id も公開IDも渡しません
貼り方
<script src=".../static/js/bot.js" data-bot="..." defer></script> の1行です。
見た目は Shadow DOM の中に閉じてあります — 貼り先のCSSと喧嘩しません。
サイトに貼れない相手には、そのまま開けるURLも渡せます。
途中で直したもの
- 23ページ読んで、塊が23個しかできていませんでした。 段落だけで切っていたため、
改行の少ないページが1枚まるごと1つの塊になっていました。文の切れ目でも割るようにして
118個になり、どこが根拠かを絞れるようになりました - RSSとsitemapまで学習していました。 記事の断片が本文に混ざり、
見当違いの引用が出ていました - 「営業時間を教えてください」が「営業時間は?」より当たりませんでした。
丁寧な言い回しを手がかりに数えていたためです
カタログ 802 → 824項目 / 45カテゴリ。テスト 646 → 669件。
16.0.0 — 2026-09-08
クライアントの入力を、URLからジャンル選択に変えました。 URLを用意しないと
始められないのでは、まだ手間が残ります。
新しい3ステップ
- どこに出すか選ぶ — 何を書くかより、どこに出すかのほうが先に決まります
- ジャンルかキーワードを言う — 12業種のボタンから選ぶだけでも作れます。
選ぶと投稿テーマの候補(「季節のメニュー」「初めての方へ」など)も出ます - 見て、出す
「お店・会社のこと」を1行書くと、その範囲でだけ具体的になります。
書かないこと
元になるページが無いということは、裏を取れる材料が無いということです。
- AIには「あなたはこの事業者の実績・価格・所在地・受賞歴を知りません。
知らない情報を具体的に書くと、事実と違う投稿を出させることになります」と
指示しています。書くのはその業種の読み手に役立つ話(コツ・選び方・季節の話)です
- 「業界No.1」「必ず」「絶対」は使いません。医療・健康・法律の断定もしません
- AIが未設定のときは、それらしい文章を作りません。「ここに書いてください」という
空欄を用意します。機械が作った文章は、確かめないまま出てしまうためです
- 下書きと一緒に「実績・価格・住所は書いていません」と伝えます
残したもの
すでにページがある場合の「URLから作る」も残してあります(ステップ2の下)。
カタログ 796 → 802項目。テスト 636 → 646件。
15.1.0 — 2026-09-07
公開の準備を、画面だけで終えられるようにしました。
最初の設定(/console/setup)
ログイン画面には「POST /v1/accounts/bootstrap で最初の管理者を作ってください」と
書いてありました。これでは運用する人が自分で始められません。
- 画面から管理者を作れます。作った直後にセッションを渡すので、ログインし直す必要はありません
- 配信用のAPIキーも同時に発行できます(一度しか表示しないことを画面に明記しています)
- 作り終わると、この画面は出てこなくなります
- ログイン画面そのものは門番で塞ぎません(塞ぐと誰も入れなくなります)
直したもの
- テストが開発中のDBの状態に左右されていました。 手元でアカウントを1つ作っただけで、
コンソールがログイン必須になり、無関係な17件が落ちました。
テストは専用のDBで動かします
確認したこと
- GETルート185本を全部叩いて、5xxはゼロでした。
唯一遅いのは/v1/logs/stream(SSE)で、これは接続を保つのが仕様です
カタログ 791 → 796項目。テスト 632 → 636件。
15.0.0 — 2026-09-07
クライアントがやることを3つに減らしました。 運用コンソールには50を超える画面が
ありますが、それは運用する側に要るもので、クライアントに見せるものではありません。
かんたん投稿(/post)— 1画面で完結
- 出したいページのURLを入れる — 会社サイトでも商品ページでも。内容はこちらで読み取ります
- 出す先を選ぶ — 設定済みの配信先があればそれを、無ければ媒体そのものを
- 見て、出す — 文章・画像・媒体ごとの見え方を確認して送信
- 下書きはこちらで用意します。 AIが「ページに書いていないことを足さない・
誇張しない」という指示のもとで書きます。気に入らなければその場で書き換えられます - AIが未設定でも下書きは出ます。 ページ自身の文章から組み立てます。
未設定を理由に画面を空にしません - 画像は必ず1枚は選べます。 ページに画像が無くても、題名を入れたカードを作ります。
ページの画像も候補に出します(アイコンやスプライトは除きます) - 出る前に、出るものをそのまま見せます。 Xは280字、LINEは500字。
切られてから気づくのを避けます。切ってもリンクは必ず残し、
文の途中ではなく切れ目で切ります client権限のアカウントは、ログインすると自動でこの画面に着地します。
コンソールには入りません
データが残るかを、自分で確かめて言う
永続ディスクの無いホスティングでは、再起動のたびにデータベースごと初期化されます。
消えること自体より、消えると分からないことが問題なので、起動時に確かめて画面に出します。
- ホスティングのAPIに聞ける場合は聞きます。聞けない場合は、起動をまたいで観測します
- 消えると困るものの件数(問い合わせ・アカウント・APIキー・計測・順位)を出します
- 消える前に、資格情報を除いた中身を持ち出せます
カタログ 768 → 791項目 / 44カテゴリ。テスト 621 → 632件。
14.1.0 — 2026-09-06
アシスタントが「関係のない質問にも、それらしく答えてしまう」状態でした。
- ありふれた語を手がかりにしない。 「教えて」「ます」「して」はどの資料にも
出てくるので、当たっても何も分かりません。珍しい語がどれだけ当たったかで測るようにしました
(「おすすめの映画を教えて」が記事の「教え」に当たって高得点になっていました) - 丁寧に長く聞いても答えが出るようにしました。 絶対値だけで足切りすると、
語が増えるほど通らなくなります。「対応している媒体は?」は通るのに
「対応している媒体を教えてください」は通らない、という状態でした。
一番よく当たった資料の点数を見てから、その近傍だけを残します - 資料が少ない環境でも動きます。 入れたばかりで記事が数本しかないとき、
どの語も「珍しい」ことになって判定が効かなくなっていました
手元の資料で実測して、答えるべき8問すべてで正しい資料を先頭に出し、
答えるべきでない5問のうち3問を落とします。残る2問はAIを繋いでいれば
「その情報は持っていません」と答えます(そう指示してあります)。
カタログ 765 → 768項目。テスト 618 → 621件。
14.0.0 — 2026-09-06
クライアントを迎えられる状態にしました。 認証の中身(パスワード・二要素・
セッション)は前から揃っていたのに、画面から入る手段がありませんでした。
つまり、クライアントにURLを渡せる状態ではありませんでした。
ログイン(/console/login)
- アカウントが1つも無い間は、コンソールは公開デモのまま誰でも開けます。
最初の管理者を作った時点で、以後は必ずログインが要ります
(APIキーが「共有鍵 → スコープ判定」に切り替わるのと同じ考え方です) - 二要素認証とリカバリコードに対応。セッションは HttpOnly / SameSite=Lax の Cookie
- HTTPSのときだけ Secure を付けます。 ローカルの http で付けると、
Cookie が保存されず一生ログインできません - 戻り先は自サイト内のパスだけ許します(
//evil.exampleで外へ飛ばさない) client権限のアカウントは、担当クライアントのデータしか見られません。
これがクライアントを招くときの入口です
管理画面(/console/admin)
設定・機能・アカウント・連携・監査を1枚にまとめました。
- 公開の準備チェックリスト — クライアントを迎える前に埋めることを、
できていない順に出します。できていることを数えるための一覧ではありません
- 機能のオン・オフ盤 — 26の機能をスイッチで。分類ごとにまとまり、絞り込みつき。
切り替えはその場で保存されます
- 設定を画面から入れられます。 SMTPやAPIキーのために Space の Secrets を触って
再起動する必要がなくなりました。読む順番は DB → 環境変数 → 既定値 で、
ホスティングの Secrets は上書きしません(そちらを正とします)
- 秘密の値は暗号化して保存し、保存後は画面に返しません。空欄で保存しても消えません
- 接続を実際に試せます。 メール・AI・通知は本当に外へ出して結果を返すので、
「保存はできたが届かない」が起きません
- 監査ログ — 誰がいつ何を変えたか
クライアント向けアシスタント
サイトとコンソールの右下に常駐します。この基盤が持っている材料
(記事・用語集・機能カタログ・対応媒体・画面の役割)だけで答えます。
- 数字を創作させません。 資料に無い数値は出さず、無いものは無いと言います
- 答えには必ず出典を添えます
- AIキーが無くても黙りません。 関連する箇所を出典つきで抜き出します。
「AIが未設定です」で終わらせると、訪問者にとっては壊れているのと同じです
- 関係のない質問には答えません(薄い一致で自信ありげな答えを返さない)
- 自社データは権限のある人にだけ。 client_id を含む回答には鍵かログインが要り、
client 権限は自分のクライアント以外を見られません
- 答えられなかった質問は記録されます。次に書く記事の題材になります
カタログ 726 → 765項目 / 42カテゴリ。テスト 591 → 618件。
13.0.0 — 2026-09-06
お問い合わせフォームを実装しました。 これまで /contact には
「送信フォームは未実装のため、直接お送りください」と書いてあるだけでした。
フォーム(/contact)
- 名前・会社・メール・相談内容に加えて、CMS / 運用中の外部媒体 / LINE公式アカウント /
月あたりの記事本数を受け取ります - 届いた内容は
contact@ai-sns.ioにメールで通知します
(環境変数CONTACT_TOで変更できます) - Reply-To に送信者を入れてあります。 受け取った側は、そのメールに返信すれば
相談者へ届きます - テキストとHTMLの両方を送るので、どのメールソフトでも読めます
落とさないこと
- 保存が先、送信が後。 SMTPが未設定でも落ちても、問い合わせそのものは失いません。
逆順にすると「送れなかったので無かったことにする」が起きます - 送れていないものを「送りました」と言いません。 訪問者には「受け付けました」と伝え、
運用側の画面には未送信として出します - 公開ページには直接の宛先も出しています。フォームが動かないときに訪問者が詰まらないように
迷惑投稿よけ(CAPTCHAは入れない)
訪問者の負担のわりに、取りこぼした問い合わせは戻ってこないためです。代わりに:
- 人には見えない欄(埋まっていたら自動投稿)
- 表示から数秒で送られたものを弾く
- 同一IPからの連投を止める。止めるときは直接の連絡先を添えて逃げ道を残します
- 名前とメール欄の改行を拒否(メールヘッダインジェクション — 通すと宛先を勝手に足されます)
受信箱(/console/contact)
届いた問い合わせ・通知の成否・未対応の件数を1画面で見られます。
通知が送れていなかったぶんは、あとから送り直せます。
メールが未設定のときは、不足している環境変数まで画面に出します。
設定
CONTACT_TO は Space の変数に設定済みです。実際にメールが出るには、
Space の Secrets に SMTP_HOST / SMTP_USER / SMTP_PASSWORD / MAIL_FROM
(必要なら SMTP_PORT)が要ります。 設定するまでは、問い合わせは保存されますが
通知は出ません(その状態は受信箱に表示されます)。
カタログ 708 → 726項目 / 39カテゴリ。テスト 562 → 591件。
12.0.0 — 2026-09-06
スマートフォンで使える状態にしました。 前回は「横にはみ出さない」ところまででしたが、
はみ出していないことと、指で操作できることは別の話でした。
ヘッダの「コンソールを開く」
白地・細めにして、ロゴとナビの邪魔をしない大きさに収めました。
指で押せる大きさ
- ボタン・メニュー・単独で置かれたリンク(「すべて見る →」など)の当たり判定を 40〜44px に
- iOSの自動拡大を止めました。 16px未満の入力にフォーカスすると画面が勝手に拡大します
- 入力欄の指定を型の除外で書き直しました。 型を並べて書いていたため、
取り込み画面のinput[type=url]だけが素の見た目(高さ24px)で浮いていました
ナビゲーション
- サイトのナビとコンソールのサイドバーを、状態を持つ引き出しにしました。
aria-expandedを出し、Esc と外側のタップで閉じ、画面が広がったら自動で閉じます - コンソールの引き出しには背景を敷き、開いている間は背後を触れないようにしました。
引き出しの中のリンクを押したら閉じます
表と図
- 表は縮めずに横へ流します。 縮めると1セル1文字ずつ折れて、かえって読めなくなります
- ボタンとバッジは折り返しません。折ると文字が縦に1文字ずつ積まれます
- 表と図の右端をぼかして、横に動かせることを伝えます
文字組みと余白
- 見出し・本文・行間を画面幅に合わせ、余白を詰めて1画面あたりの情報量を保ちました
- ヒーローのCTAは縦に積んで幅を揃えます(3つ並ぶと2+1に折れて不格好になるため)
- ヘッダを56pxに下げ、スクロール時の本文の面積を取り戻しました
直したもの
- コンソールの
.grid2に、縮められない1fr 1frが残っていました
サイト・コンソールの主要34画面を 320 / 390 / 768px で実測し、
横スクロールが出ないこと・小さすぎる操作要素が無いことを確認しました。
カタログ 697 → 708項目。テスト 546 → 562件。
11.0.0 — 2026-09-06
AI-SNS.IO のロゴを入れました。 そして入れる過程で、狭い画面の表示が
壊れていることが分かりました。
ロゴ
- 明るい背景用(黒文字)と暗い背景用(白文字)の2枚を持ちます。
製品サイトのヘッダは前者、コンソールのサイドバーとフッタは後者を使います - ヘッダ用には横並びのロックアップを別に作りました。 元は縦積みなので、
ヘッダの高さに縮めると社名も「SEO BUILDER」の行も読めなくなります - マーク単体も切り出し、ファビコン(16/32/48)・ホーム画面アイコン(192/512)・
apple-touch-icon(180)を作りました - アイキャッチ画像にもロゴが載ります。共有されたときにどこの記事かが分かります
狭い画面(実測で見つけた不具合)
実機相当の390pxで測ったところ、トップページが 518px 幅で描かれていました(128pxはみ出し)。
- 原因1:
1fr。 CSS グリッドの1frはminmax(auto,1fr)なので、
中身が縮まないと列が広がり、グリッドごと親をはみ出します。
minmax(0,1fr)とmin-width:0に直しました - 原因2: 区切りの無い長い語。
「メール投稿(WordPress/Blogger/Tumblr/Ameba等)」が列より広く描かれていました。
収まらないときだけ途中で折り返すようにしました - 見出しの大きさの下限を画面幅に追従させました。固定の下限では、
狭い画面で日本語の見出しが1行に収まりません - コンソールでは、表はパネルの中だけで横に流し、横並びの入力は縦に積みます
- サイドバーのロゴが上下で切れていました。縦のflexの中では
heightだけだと縮みます
サイト・コンソールの主要34画面を 320px と 390px で実測し、横スクロールが
出ないことを確認しました。
カタログ 681 → 697項目 / 38カテゴリ。テスト 525 → 546件。
直したもの
- 画像が Hugging Face の2値ファイル上限を超えて push が弾かれました。
全画像を256色に落として100KB未満に収め、上限を超えていないかをテストで見ます
10.0.0 — 2026-09-06
正規URLの食い違いを検査できるようにしました。 自分のサイトを自分で調べていて、
このサイト自身が踏んでいることに気付いたのがきっかけです。
何が起きるのか
HTTPレスポンスの Link: <...>; rel="canonical" は、HTMLの
<link rel="canonical"> と同じ強さで読まれます。ホスティングやCDNが
これを付けている場合、サイト側でHTMLをどれだけ正しく書いても打ち消されます。
そしてこのヘッダは、ページのHTMLを見ても分かりません。
実際にリクエストして、返ってきたヘッダを見るまで気付けない種類の問題です。
検査(/console/technical、GET /v1/technical/canonical?url=...)
- HTTPヘッダとHTMLの両方から正規URLを拾い、食い違いを error として出す
- HTMLに canonical が2本ある状態、1本も無い状態も検出する
- 別ページを指す canonical は info に留める(転載ページなら正しい設定のため)
- 末尾スラッシュとスキームの違いは同一とみなす
- 画面を開くだけでは外に通信しません。 押したときだけ取得します
- 届かないURLは落ちずに理由を返します
直したもの
- 上の検査を、画面表示のたびに実行する形で入れかけました。
コンソールを1枚開くたびに外部へHTTPリクエストが飛ぶことになるので、
押したときだけ動く形に変えています
カタログ 672 → 681項目 / 36カテゴリ。テスト 517 → 525件。
9.0.0 — 2026-09-06
このサイトの共有リンクには、これまで画像が出ていませんでした。
og:image が SVG を指しており、Facebook・X・LINE・LinkedIn・Slack はどれも
SVG の og:image を描画しません。配信を仕事にする基盤としては筋が通らない状態でした。
アイキャッチをPNGで生成する
- 1200×630 のPNGを、ページごとに生成する。記事・用語ページ・運用の一周は、
それぞれ自分のタイトルが入った画像になる - 日本語は単語の切れ目が無いので、1文字ずつ実寸で幅を測って折り返す。
行数が増えたら字を詰めるので、長いタイトルでも枠から溢れない - 上端のグラデーションはサイトのトークンと同じ3色
og:image:width/height/altも出す
置き場所
/v1/media/og.png ではなく /og.png に置きました。
robots.txt で /v1 を拒否しているため、そこに置くとSNSのクローラが画像を
取りに来られず、結局プレビューが空になります。
環境への依存を切る
- コンテナに IPAゴシック(
fonts-ipafont-gothic)を入れました。
Noto CJK は100MB超あるのに対し、これは数MBで JIS第1・第2水準を含みます - フォントが見つからないときは黙って空を返さず、何を入れればよいかと
代わりに使える SVG 版を案内します - 同じ引数なら中身は変わらないので、ETag と1日のキャッシュを付けています
記事に cover_image_url があればそちらが優先されます。自動生成は既定として働きます。
カタログ 661 → 672項目 / 35カテゴリ。テスト 506 → 517件。
8.0.0 — 2026-09-06
この基盤のブログを、記事3本から9本にした。 SEO運用基盤を名乗るサイトのブログが
3本しかない状態で、棚卸しも内部リンクも一括操作も、動かして見せられませんでした。
追加した記事
- コンテンツの棚卸し — 伸ばす記事と畳む記事の決め方
- 制作指示書の作り方 — 上位ページから構成を組み立てる
- 強調スニペットの取り方 — 4つの型と、型に合わせた書き方
- 内部リンクの設計 — テーマ単位で束ね、孤立ページを作らない
- AI検索最適化(LLMO) — 回答に引用されるための書き方
- 記事移行のやり方 — 二重投稿を作らずに持ち込む
全9本がこの基盤自身の診断で 97〜100 点、error と warn は 0 件です。
数値には出典を添えるか、自社の基準であることを明記しています。
記事どうし・記事と用語集を内部リンクで繋ぎ、媒体別の別原稿と短文も持たせています。
書いていて自分で踏んだ不具合
記事を書き、この基盤に載せ、診断と用語抽出を通したことで見つかりました。
- 伏字を用語として登録していた。 「〇〇とは」という説明用の例から
〇〇 という用語ページができていた
- 片括弧の用語ができていた。 「AI検索最適化(LLMO」— 閉じ括弧の手前で
「とは」が来ると、開いたままの語を拾う
- ほぼ同じ用語ページが2枚できていた。 「LLMO(AI検索最適化)」と
「AI検索最適化(LLMO)」を別物として扱っていた。括弧の外と中を同じ部品として
並べ替えてから比べるようにした
- 登録済み用語の別名に当たる語を、記事本文から再び拾っていた
言い回しの変更
- 指示書の「必ず触れる話題」を「外せない話題」に改めた。
この基盤の診断が「必ず」を断定表現として指摘するため、自分の画面が自分の指摘に
引っかかっている状態でした
カタログ 651 → 661項目 / 34カテゴリ。テスト 502 → 506件。
7.0.0 — 2026-09-06
既存記事を取り込めるようにした。 これまで記事は「この基盤で書いたもの」しか
扱えなかった。実際の導入先は「既にWordPressに200本ある」という状態のほうが普通で、
そこが空いていると棚卸しも内部リンクも診断も動かせない。
取り込み(/console/import)
- WordPress REST API(
/wp-json/wp/v2/posts)・RSS / Atom・記事JSONの配列・
フロントマター付きMarkdown の4経路 - HTMLはMarkdownに落とす。見出し・リンク・画像のalt・強調・箇条書き・引用・コードを保つ。
変換できなかったタグは、タグだけ外して中身を残す — 消すと本文が痩せ、
文字数も見出し数も狂って、そのまま診断が誤警告するため - クエリ文字列だけのURL(
?p=123)でも、同じURLなら同じslugになる
二重投稿を作らないこと
これが取り込みでいちばん怖いところです。
- 取り込んだ記事は「公開済み」として入り、元URLが正規URLになります。
下書きとして入れると、その記事は転載元を名乗れません
- 自社の配信先を指定すると「その記事は既にそこに出ている」と記録します。
この記録が無いと、次の配信で同じ記事が自社サイトへもう一度投稿されます
入れないもの
- 既にある slug は上書きしません。取り込みは足すだけの操作です
- 同じ取り込みの中で slug が重なった場合も、片方だけを入れます
- タイトルか本文が空のものは、理由を添えて飛ばします。
空のページが増えるほうが、飛ばすより害が大きいためです - 1本が落ちても、残りは取り込みます
正直に返すこと
- 既定は下見。何が入り、何が飛ばされるかを先に全部見せます
- 取り込む前に分かる問題(本文が短い / 元URLが無い / 公開日が無い)を1件ずつ出します
- 取り込み元に届かないときは0件ではなく、接続できなかったのか、
REST APIが無効なのか、item が1つも無かったのかを区別して返します
カタログ 633 → 651項目 / 33カテゴリ。テスト 474 → 501件。
6.0.0 — 2026-09-06
製品サイトが配信エンジンの話しかしていなかった。第4波・第5波で作ったもの
(指示書・エディタ・棚卸し・用語集・一括操作)は、公開面のどこにも出ていなかった。
運用の一周(/workflow)
- 決める・書く・出す・測る・畳む を1ページで説明する。ナビの先頭に置いた
- 各工程から、実際にその機能が動くコンソール画面へ繋ぐ
- 設計上の判断を理由ごと書く。「なぜCore Web Vitalsを平均でなく75パーセンタイルで見るのか」
「なぜCVに取得元が付かないのか」「なぜ棚卸しは提案までで実行しないのか」 - トップページからも導線を追加
ドキュメント
/docsに「記事を書く・畳む」の節を追加。指示書・エディタ・版・差分・棚卸し・
用語集・一括操作のエンドポイントを一覧に載せた
直したもの
- SSEのエンドポイントを
<a href>で貼っていた。 押すと接続が開いたままになり、
タブが固まったように見える(自前のリンクチェッカーが実際にここで止まった)。
画面の中で購読し、止められるボタンに変えた - 日本語の用語でURLが衝突していた。 英字だけを抜いていたため
「AIによる問い合わせ対応の自動化」も「AI活用の進め方」も/glossary/aiになり、
あとから登録したほうが前のページを差し替えていた。
抜き出した英字が語を代表していない場合は、語ごとに変わる短い印を足す - 同じ slug に別の用語を登録しようとしたとき、既存の用語名を添えて止めるようにした
(黙って上書きしない) - フッタの「機能カタログ(500)」が固定値だった
テスト
- 462 → 474件。自サイトを自分で巡回する回帰テストを追加した。
リンク切れだけでなく「返ってこないリンク」も落とす(60秒で終わらなければ失敗)
カタログ 623 → 633項目 / 32カテゴリ。
5.0.0 — 2026-09-06
記事を書く場所を作った。 ここまでの機能はすべて「既にある記事」を診断し、配信し、
仕分けるものだった。書く場所が無く、記事JSONを手でディレクトリに置くしかなかった。
SEOの基盤を名乗るなら、そこが空いているのはおかしい。
記事エディタ(/console/editor)
- 新規作成・編集・複製・削除。制作指示書からは、構成をH2として置いた状態で起こせる
- 打鍵しながら診断が回る。 保存しなくてもスコアと指摘がその場で出る。
書き終えてから別画面で診る形では、結局直さない - 見出しの入れ子を隣に出し、H2の次にH4が来るような階層の飛びをその場で指摘する
- 保存前に必ずスキーマ検証。落ちたら「どの項目がなぜ駄目か」を日本語で返す。
壊れたJSONをディスクに書いてから気付く、という順番にはしない
版管理
- 保存のたびに直前の版を控える。 上書きで失うものを作らない
- 版ごとに日時・メモ・文字数・診断スコアを残し、本文の差分を行単位で見せる。
タイトルやタグなど本文以外の変化も並べる - 古い版に戻せる。戻す前の状態も版として残すので、戻し過ぎも取り消せる
- 1記事30版まで(古いものから破棄)。変更が無ければ版を作らない
- 記事を削除しても版はDBに残る。削除には slug をそのまま入力させる
コンソールの書き込み経路
- 見つけた不具合: コンソールに置いた「実行」ボタンのうち3つが、
<form>から直接/v1にPOSTしていた。<form>はヘッダを付けられないため、
APIキーが設定された本番では黙って401になっていた(押しても何も起きない) - fetch に寄せ、運用キーは そのタブの sessionStorage にだけ置く。
サーバにも localStorage にも残さない。タブを閉じれば消える - キーの状態(設定済み / 未設定 / 鍵不要)を常に右上に出す。
401のときは「どこにキーを入れればよいか」まで書く
機能カタログ
- 592 → 623項目 / 31カテゴリ
テスト
- 441 → 462件。「保存で何も失わない」「戻し過ぎを取り消せる」
「消しても中身は残る」「slug に../を書けない」を回帰として固定した
4.1.0 — 2026-09-06
用語集
canonical/ 重複コンテンツ / 強調スニペット / 関連質問 / 共起語 / 検索意図 / E-E-A-T /
構造化データ / Core Web Vitals / LLMO / 内部リンク / 孤立ページ / noindex /
カニバリゼーション / 棚卸し / リライト / トピッククラスタ / SERP / hreflang / UTM /
IndexNow / サイテーション / ティザー / 一次データ / クロールバジェット / alt /
滞在時間 / CTA / A/Bテスト など 30語を同梱。
辞書の丸写しではなく、この基盤の中でどう効くかまで書いてある- 起動時に足りない語だけを追加する。既にある語には触れないので、
人が書き直した定義が起動のたびに戻ることはない
直したもの
- 用語が1語も無いのに
/glossaryを sitemap に載せていた(中身の無いページを索引させない) - サイトのフッタが v1.0.0 のまま止まっていた。変更履歴の最新見出しから取るようにした
- 変更履歴が古い順に並んでいた。新しい順に直した
カタログ 588 → 592項目。テスト 438 → 441件。
4.0.0 — 2026-09-06
記事を「作る前」と「作ったあと」を埋める回。これまでは1本の記事を配信するところは
できていたが、何を書くか決める工程と、増えた記事をどう畳むかの工程が無かった。
制作指示書(/console/briefs)
- キーワードを渡すと、構成・必ず触れる話題・答えるべき質問・共起語・内部リンク候補・
自社の一次データ・上位ページの一覧を1枚にまとめて出す - 構成はSERP上位3件のH2を突き合わせて合成する。包含関係の見出しは1本にまとめ、
「ステップ1」のような中身の無い断片は落とす - SERPが未取り込みなら検索意図別の雛形を使い、そう明記する(材料を偽らない)
- 仮タイトルは検索結果で切れない長さまで自動で詰める
- そのままMarkdownで書き手に渡せる。書き上がった原稿との照合もできる
コンテンツ棚卸し(/console/inventory)
- 全記事を「そのまま / 改稿 / 更新 / 統合 / 削除検討 / 様子見 / 判断不能」に仕分ける
- 判断には必ず理由を添える。公開90日未満は評価せず、公開日が無い記事は判断不能にする
- 4〜20位の記事を改稿の最優先に置く(1ページ目まであと一歩が最も効くため)
- 閲覧数は絶対値でなく全体分布の35パーセンタイルで相対評価する
- 提案までで、実行しない。 記事にもDBにも書き込まない
- 計測データが無いときは「流入に基づく判断ができていない」と明記する
用語集(/glossary、/console/glossary)
- 1語1ページで公開。DefinedTerm / DefinedTermSet 構造化データ付き
- 記事本文の「〇〇とは、…です」から候補を抽出する。ひらがな終わりの断片や
指示語を除いたうえで、定義が短すぎるものは登録を拒否する - 本文の初出だけを用語ページへ自動リンク。見出し・コード・既存リンクには触れない
- 用語と記事を相互に紐付け、用語ページから記事への導線にする
一括運用(/console/bulk)
- タグ・カテゴリ・CTA・配信先・fallback・ワークフロー・再配信・撤回・アーカイブの12操作
- 既定はすべて dry-run。 対象ごとに何が何になるかを先に見せる
- 外部に出る操作と取り返しがつかない操作は、計画時に発行した確認トークン無しでは実行しない。
トークンは一度使うと失効する - 絞り込みはタグ / カテゴリ / slug / 本文の語 / 文字数 / 棚卸しの判定結果、すべてAND
公開面
- サイト内検索(
/search)— 本文まで検索し、用語集も同時に探す。0件のときはそう言う。
検索結果ページ自体はnoindex、robots.txtでも除外 - カテゴリ別・タグ別RSS(
/feed/category/{値}.xml、/feed/tag/{値}.xml)。該当0件なら404 - 著者ページ(
/authors/{id})— Person 構造化データ、執筆記事と監修記事を分けて表示 sitemap.xmlに用語ページ・著者ページ・/sdk/domains/statusを追加
機能カタログ
- 500 → 588項目 / 29カテゴリ。第4波で実際に動くようになった88項目を追加(すべて実装済み)
直したもの
- 仮タイトルの生成が、副題を持たない長いキーワードで文字数上限を超えていた
(「検索結果で切れない長さ」と言いながら切れる案を出していた)
テスト
- 392 → 436件。第4波ぶんの検証を
tests/test_seo4.pyに追加。
「棚卸しは何も書き込まない」「危険な一括操作はトークン無しでは実行しない」
「自動リンクは見出しとコードを壊さない」を回帰として固定した
3.4.1 — 2026-09-05
公開中のサイトを自分のリンクチェッカーでクロールして見つけたもの。
直したもの
- POST専用のAPIを
<a href>で指していた — 押すと 405 が返るだけだった
(LINE運用の「記事から取り込む」、発掘・競合の「いま取り込む」)。form + button に変更。
テンプレートを静的に見て同じ間違いを防ぐテストを追加。 - デモ記事のCTAが存在しないURL(
lin.ee/XXXXXXX)を指していた — 公開ページに
404のボタンが載っていた。プレースホルダのまま出荷しない。自サイトの問い合わせへ向けた。
3.4.0 — 2026-09-05
- 記事を3本に — 「LINE公式アカウントの自動応答を、よくある質問20問から作る」を追加。
記事が増えたことで、関連記事の推薦・ピラー/クラスタ構造・内部リンクの点検が
実データで動くようになった。 -
収録した3本はすべて自分の診断にかけて指摘を潰してから入れている
(診断 87 / 93 / 97、AI検索適性 75 / 88 / 100)。
自分で「出典を添えろ」「断定を避けろ」と言う道具が、自分の記事で警告を出したままでは筋が通らない。 -
3本とも診断の warn/error をゼロにした(診断 97 / 97 / 97、AI検索適性 88 / 88 / 100)。
記事どうしを内部リンクで繋いだので、孤立記事なし・平均被リンク1.33・
カテゴリごとのピラー構造が実データで出るようになった。 - 記事の中身に依存していたテスト3件を合成データに置き換えた。
記事を直すたびにテストが落ちるのは、テストが中身を固定してしまっていたため。
直したもの
- 記事の meta ブロックに書いたメタ説明が読まれていなかった。 記事ライブラリは
トップレベルのmeta_descriptionとmeta.descriptionしか見ておらず、
生成タスクが使うmeta.meta_descriptionを素通りしていた。
結果、ページの<meta name="description">が抜粋で代用され、診断も
「メタ説明にキーワードが無い」と誤って警告していた。ありうる置き場所を順に見るよう変更。
収録記事が診断の warn/error をゼロに保つことをテストで固定した。
3.3.1 — 2026-09-05
- アクセシビリティ — コンソール上部のクライアント/デバイス選択など、
12ファイル分の入力欄にラベルが無く、スクリーンリーダーでは何の欄か分からなかった。
すべてにaria-labelを付け、主要14ページで
「ラベルの無い入力欄が無い」「alt の無い画像が無い」「h1 が1つ」「lang がある」を
テストで固定した。
3.3.0 — 2026-09-05
- エラー画面 — 未処理の例外で素の "Internal Server Error" だけを返していた。
参照IDを振ってログに出し、その番号を画面に見せる。番号を伝えてもらえればログから
該当の1件を引ける。例外の中身は画面に出さない(内部構造が漏れるため)。
API パスは同じ参照IDつきのJSONを返す。 - 機能カタログにカテゴリのジャンプ — 500行を上から探すしかなかったのを、
24カテゴリへの目次と「先頭へ戻る」で移動できるようにした。
3.2.1 — 2026-09-05
直したもの
- 応答を圧縮していなかった。 機能カタログのページは254KBあり、転送だけで1秒以上
かかっていた(キャッシュを入れて描画は2msになったのに速くならず、計測して判明)。
gzip を有効にして 254KB → 34.7KB(86%減)。1KB未満の応答は、
ヘッダ分で逆に増えるため圧縮しない。
3.2.0 — 2026-09-05
- 導入の進み具合 (
onboarding.py//console/start) — コンソールが38画面になり、
初めて触る人がどこから手を付ければよいか分からなくなっていた。
実際の状態を見て次にやることを1つだけ示す。チェックは手で付けさせない
(付けたつもりで設定できていない、が起きるため)。概要画面にも次の一手を出す。 - 静的ページのキャッシュ (
cache.py) — 500行の機能カタログの描画に2秒かかっていたのを
25ms→2msに。対象は「入力が同じなら出力も同じ」と言い切れるページだけで、
計測・順位の数字はキャッシュしない(古い数字を見せるくらいなら遅いほうがまし)。
直したもの
- 認証情報の設定判定が甘かった — 宣言された値のうち1つでも埋まっていれば
「設定済み」としていたため、site_urlだけ書いてある状態を設定済みと誤判定していた。
すべて埋まっていることを条件に変更(${ENV}は未設定だと空文字に展開されるため、
この判定でないと通ってしまう)。
3.1.1 — 2026-09-05
直したもの
- コンバージョンが取得元チャネルに紐付いていなかった。 CVイベント自体にはリファラが無いため、
そのまま数えると全部「サイト内」に落ちていた(デモデータを入れて画面を見て気づいた)。
訪問者の最初の閲覧チャネルを取得元とみなして集計する。訪問者IDが無く紐付けられないCVは、
隠さず「取得元不明」として別枠で出す。
3.1.0 — 2026-09-05
- デモデータの投入 (
demo_seed.py) — コンソールの多くの画面はデータが無いと
「空です」としか出ないため、機能を確かめるための一式を入れられるようにした。
60日ぶんの計測(曜日の波・AI検索からの流入つき)・順位6語×6時点(1語はわざと下落)・
Search Console・SERP・被リンク・配信ジョブ・一次データ・ワークフロー・コメント。
乱数の種を固定しているので何度入れ直しても同じ数字になる。
投入分には印が付いておりDELETE /v1/demoで全部消える。
入っている間はコンソール全画面に注意書きを出す(実測と誤読させないため)。 - 記事を追加: 「canonical の設定で、転載した記事の評価を自社に集める」
- 媒体数を文言に直書きしていたのをやめ、カタログの実数から出すようにした
(増やしたときに古い数字が残らない。テストでも固定)
直したもの
- 英単語のキーワードが日本語に隣接すると数えられていなかった —
「SEOの話」で\bSEO\bが境界にならず0件になっていた。英数字が続かないことを条件に変更 - 複合キーワードの見出し判定が厳しすぎた — 3語すべてを1つの見出しに含めるのは
現実的でないため、過半数の語が入っていれば「その語を狙っている」とみなすようにした - キーワード密度が水増しされていた — 語間の空白を文字数に含めて計算していた
3.0.0 — 2026-09-05
機能カタログ 500/500 を実装済みにした(420 → 500)。テスト 260 → 339。
残っていたのは「外部の有料データが要る」「ブラウザ/エンコーダの同梱が要る」もの。
前者は取り込み口を開けて分析側を全部作る、後者は外部に委ねつつ手前を全部作るという
方針で埋めた。未設定のときは必ず理由を返し、0件を「存在しない」とは言わない。
認証・セキュリティ
- 人のアカウント (
accounts.py) — PBKDF2-HMAC-SHA256(24万回・個別ソルト)、
TOTP二要素認証を hmac で自前実装(Google Authenticator でそのまま使える)、
使い捨てリカバリコード、5回失敗で15分ロック、パスワード変更で全セッション失効 - OAuth セルフ接続 (
oauth.py) — Google / LinkedIn / Facebook / WordPress.com / Shopify。
state を必ず検証(CSRF対策) - シークレットの暗号化 (
secrets_store.py) — PBKDF2 + Encrypt-then-MAC を標準ライブラリだけで。
鍵が無ければ保存を拒否する(平文でこっそり保存しない) - セキュリティヘッダを推奨するだけでなく実際に付けるようにした
検索データ
- SERP分析 (
serp.py) — 取り込みはプロバイダ/CSV/JSON、分析は全部内製。
上位の構成・共通話題・網羅率の採点・強調スニペットの型・難易度・検索意図の変化検知・
PAA収集・被リンクとドメイン評価 - Search Console / GA4 (
gsc.py) — あと一歩の語、表示だけされている語、
順位のわりにクリックされない語、キーワード→CVの紐付け、インデックス状況 - 順位と流入の相関、上位化した語の共通構成
画像
- 実際のピクセル処理 (
imaging.py, Pillow) — リサイズ・圧縮・WebP/AVIF変換・
SNS別の切り出し・内容ハッシュでの重複排除・メディアライブラリ。12KB→3KB を確認 - ストックフォト検索(Unsplash / Pexels の無料API)、AI画像生成、CDNキャッシュ破棄、
スクリーンショット、数値からのグラフSVG生成
収益・自動化
- Stripe決済 (
payments.py) — Checkout へ委ね、カード情報を自社サーバに通さない。
署名検証を自前実装(再送攻撃も弾く)、Webhookは冪等 - 自動化 (
automation.py) — if-thenトリガー、タイトルA/B、自動アーカイブ、
承認済みの自動昇格、インバウンドWebhook(HMAC署名)、Zapier用ポーリング、
SSEでの実行ログ配信、接続テスト、トークンの期限監視と自動更新
そのほか
- 一次データ (
primary_data.py) — n数・調査時期・方法を必須にした自社データ。
表・グラフ・Dataset構造化データ・引用文を生成 - GBP / 地図 (
gbp.py) — 投稿の下書き、口コミの取り込みと返信案、
地点グリッドでの地図順位、掲載先チェックリスト - AI引用監視 (
aicite.py) — 流入からの実測と、Claude/Perplexity への能動確認 - テナント分離の強制 (
tenant_guard.py) — 絞り忘れが起きない窓口、
混在していないことの検証、データの書き出しと完全削除 - 短尺動画の台本と絵コンテ、コメントの一元受信、パイプライン定義(承認ゲート)
- 軽量版ページ
/lite/blog/{slug}(外部リクエスト0・44%小さい)、独自ドメインの手順 - 新しい配信先: Shopify / Wix / Squarespace / YouTube概要欄(25媒体に)
直したもの
Store.runが DELETE/UPDATE でもlastrowidを返しており、削除件数を取り違えていた- キーワードから作る仮slugに日本語が入り、自分で定めたURL規約に違反していた
- サジェスト収集が約40秒かかっていた(並列化して1.4秒)
2.1.1 — 2026-09-05
- サジェスト収集が遅かった(既定の修飾語20個 × 2エンジンを順番に叩いていたため約40秒)。
並列化して 1.4秒にした。深い階層(depth=2)も同じ経路で速くなる。
2.1.0 — 2026-09-04
実装済み 347 → 420 / 500(84.0%、一部実装を含めて90.6%)。テスト 163 → 260。
「外部の有料データが要る」以外のものを、ほぼ実装に変えた。
実ユーザー計測(static/js/sb.js)
記事に1行入れるだけの計測タグ(依存なし・Cookieを使わない)。これ1つで
Core Web Vitals(LCP/INP/CLS)・滞在時間・スクロール深度・クリック位置・CTAクリック・
離脱の兆候・A/B割り当てを集める。同意が取れるまで送信しない。
CWVは平均でなく75パーセンタイルで評価する(遅い端末を平均で薄めないため)。
新モジュール
suggest.py— 検索サジェストの収集。Google / DuckDuckGo の公開JSONエンドポイントを使う
(SERPのスクレイピングはしない)。五十音展開・疑問形の抽出・業種別の定番セット・
自社コーパスの共起語・問い合わせログからのテーマ起こし・季節変動・指名/一般の分離・
競合のRSS新着監視。取得できないときは0件ではなく理由を返す。cta.py— LINE以外のオファー(フォーム / 電話 / 資料DL / 予約 / チャット)と、
検索意図による出し分け。A/B・スクロール率連動・離脱時ポップアップ・電話タップ。
記事のオファー名とLINEあいさつ文の言葉のズレを検出する。dedupe.py— 4文字シングルのJaccardで重複率を測る。別原稿が実質コピーになっていないか、
teaserが本文の何割を露出しているか、転載の遅延、引用の囲み、AI生成比率の開示。technical.py— hreflang・ページネーション・タグページの間引き・URL設計の検査・
サイト構造ツリー・画像のwidth/height補完・HTTPヘッダ・クローラの巡回とエラー記録。line_ops.py— あいさつ文・よくある質問の辞書(記事のFAQ節から取り込める)・
AIに答えさせない範囲(見積もり/苦情/医療/法律は必ず人へ)・営業時間での人/AI切替・
narrowcast・push・通数の見積もりとまとめ配信・友だち数の推移・Flexプレビュー。social.py— 媒体別の投稿文(上限・タグ数・リンクの効き方)・連投生成・トンマナ検査・
ブランドタグ・再告知・反応の取り込み・Instagram / Pinterest / TikTok アダプタ。authoring.py— 全角半角と送り仮名のゆれ・著者と監修者(E-E-A-T)・更新履歴・
鮮度スコア・編集ロック(期限つきなので放置されても詰まらない)・出典セクション。tenancy.py— クライアント追加ウィザード(設定の下書きと必要な環境変数を出す)・
顧客に渡す閲覧専用リンク/c/{token}・代理店の階層と横断比較・分離の実測。ops.py— 構造化ログ(1行1JSON)・Prometheus/metrics・媒体別レート制御・
同時実行の門番・SQLiteのオンラインバックアップと世代管理。
SDK
/static/sdk/seobuilder.js(ESM)と /static/sdk/seobuilder.py。どちらも依存パッケージなし。
使い方は /sdk。
そのほか
- レポートを印刷(PDF保存)前提の体裁に。ロゴ・担当者の所見欄・改ページ制御を追加
- 顧客向けポータル
/c/{token}と/metrics、/healthzの中身を実装 - 301リダイレクトとクローラ記録をミドルウェア化
直したもの
/v1/social/engagementが/v1/social/{slug}に食われて404になっていた(登録順を修正)- 同じ秒に2回バックアップを取るとファイル名が衝突し、上書きされていた
2.0.0 — 2026-09-04
機能カタログの「計画」を実装に変えた回。実装済みは 199 → 347 / 500(69.4%、一部実装を含めて76.4%)。
新しく13のモジュールと74のAPIエンドポイント、15の管理画面を追加した。
外部サービスが要る機能はアダプタ+正直な未設定表示にしてあり、資格情報が無い状態でも
「何が使えて何が使えないか」が画面で分かる。
追加した基盤
- SQLite永続化層 (
store.py) — 順位・計測・ワークフロー・予約・課金の状態を持つ。
書けない環境では /tmp へ自動フォールバック。 - 記事診断エンジン (
analysis.py) — 日本語向け。タイトル長(全角換算)・キーワード配置・
読みやすさ(1文長/漢字比率/受身率)・表記ゆれ・出典の無い数値・薬機法/景表法の断定表現・
altの欠落・弱いアンカー。外部API不使用。 - 構造化データ (
schema.py) — Article / BreadcrumbList / FAQPage / HowTo / LocalBusiness /
VideoObject。必須項目を埋められない型は出さない。 - キーワード台帳 (
keywords.py) — 意図推定・クラスタリング・カバレッジ・カニバリ検出・
優先度スコア・地域展開・CSV入出力。 - 内部リンク (
linking.py) — 孤立記事・弱いアンカー・ピラー/クラスタ・リンク切れ確認・301管理。 - 画像 (
media.py) — alt点検と生成案・OG画像のSVG生成・SNS別の切り出し指示・出典台帳。 - ワークフロー (
workflow.py) — 状態遷移(不正な遷移は拒否)・担当・チェックリスト・
コメント・版管理と差分。 - 予約と自動化 (
scheduler.py) — 予約公開・繰り返し・営業日回避・リトライ・
デッドレター・編集カレンダー・公開頻度ルール。 - 計測 (
analytics.py) — 自前のイベント収集。AI検索(ChatGPT/Perplexity 等)を独立チャネルとして分離。
CTA位置別クリック・KPI・異常検知・同意管理・削除要求への対応。 - 順位計測 (
ranks.py) — 推移・変動アラート・競合比較・シェアオブボイス・SERP機能・
改稿優先度・アルゴリズム更新の年表。 - レポート (
reports.py) — 月次レポートのHTML/JSON生成、前月比、次の打ち手の自動導出、共有URL。 - 認可 (
auth.py) — スコープ付きAPIキー・レート制限・IP制限・監査ログ。 - 課金 (
billing.py) — プラン・利用量メータリング・上限警告・インボイス対応の請求明細。 - AI検索最適化 (
llmo.py) — 引用されやすさの診断・llms.txt・AIクローラ方針・表記統一。 - ローカルSEO (
localseo.py) — NAP一貫性・店舗ページ生成・LocalBusiness・口コミ返信案。 - 執筆補助 (
writing.py) — 構成案・タイトル案・メタ説明・FAQ・alt は規則ベースで常に動作。
本文生成・リライト・翻訳は Claude 設定時のみ(未設定なら理由を返して失敗)。 - 配信管理 (
delivery.py) — 失敗分のみ再実行・更新配信・撤回・二重投稿防止・
配信先グループ・媒体別成功率。 - 外部連携 (
integrations/) — GA4 / Search Console / IndexNow / Slack / SMTP / Stripe /
Googleビジネスプロフィール / 順位プロバイダ / Claude。
サイト
- 公開ステータスページ
/status— 媒体別の配信成功率を実測から出す /llms.txt、robots.txt にAIクローラ方針を明記- リダイレクト(301)をミドルウェアで適用
- レポートの共有URL
/r/{token}(閲覧に認証不要)
直したもの
- 媒体の成功率で
skipped(送らないと判断したぶん)を失敗に数えていた。
下書き記事をSNSに出さなかっただけで danger 表示になっていたのを、分母から外した。
テストは 70 → 163。
1.0.1 — 2026-09-04
- Space を
Aisnsio/SEOBuilderから組織AisnsioDev/SEOBuilderへ移動したことに伴い、
canonical / sitemap / OGP の既定URLをaisnsiodev-seobuilder.hf.spaceに更新。
旧サブドメインは応答しないため、そのままだと全ページの正規URLが死んだURLを指していた。
1.0.0 — 2026-09-04
初回リリース。AutoPost 配信エンジンの上に、製品サイトと運用コンソールを載せた。
配信エンジン(AutoPost)
- 21媒体へのアダプタ:WordPress / WordPress.com / Ghost / はてなブログ / ライブドアブログ /
Qiita / Zenn / dev.to / Hashnode / Medium / Blogger / Notion / LinkedIn / X / Threads /
Facebookページ / LINE公式アカウント / Webhook / メール投稿 / note / Ameba - 役割順(primary → syndicate → variant / manual → social → line)の配信と、
自社公開URLの後続への引き回し - 重複コンテンツ対策:canonical対応媒体にのみ全文転載、非対応媒体は別原稿、無ければ要約+リンク
- CTAの本文差し込み(冒頭 / 中盤 / 末尾)と、位置別
utm_contentを含む計測パラメータの自動付与 - ドライラン、指数バックオフつきリトライ、媒体単位の失敗隔離、ジョブの永続化
- note / Ameba の手動投稿パッケージ生成と、投稿後URLの書き戻し
製品サイト
- ランディング、仕組み、対応媒体一覧、料金、ドキュメント、ブログ、変更履歴、
お問い合わせ、利用規約 / プライバシー / 特定商取引法 - 記事ライブラリ:
data/articles/の記事JSON・フロントマター付きMarkdownを読み込み、
目次・読了時間・パンくず・Article / BreadcrumbList 構造化データ付きで公開 - テクニカルSEO:
robots.txt、sitemap.xml、RSS、自己参照canonical、OGP / Twitterカード、404ページ
機能カタログ
- SEO運用基盤に必要な500機能を24カテゴリに整理し、実装状況(実装済み / 一部実装 / 計画)つきで公開
/featuresで検索・状態フィルタ、/v1/featuresでJSON取得
運用コンソール
- 概要ダッシュボード、配信(ドライラン固定)、ジョブ履歴と詳細、記事ライブラリ、
クライアント設定、配信先マトリクス、LINE配信のリクエスト生成、機能カタログの進捗
API
/v1/features、/v1/library、/v1/library/{slug}を追加- Swagger UI を
/docs-apiへ移動(/docsは製品ドキュメントのページ)