個人開発で neglegere(ネグレゲレ) というWebサービスを作っています。
コンセプトは、
地味に時間を取られる作業を少しだけ任せて、大事なところに力を残す
です。
最初は、自分の声を使って文章を読み上げる「AIボイス」から始めました。
そこから、
- 音声加工
- 背景除去
- 画像変換
- OCR
- 読み上げ
- 電子サイン
- マイク
- カメラ
- rPPG
- AI要約
- 文字起こし
など、ブラウザでできそうなものを少しずつ増やしています。
現在は主に、
- AIボイス
- 鬼ボイス
- 背景メーカー
- 画像変換
- 音読アシスト
- 手書き電子サイン
- ことばレンズ
- 騒音チェック
- コンディションチェック
などを公開しています。
この記事では単なる機能紹介ではなく、
「その処理をBrowser / Cloudflare / GPUのどこで実行するか」
という設計を中心に、個人開発で使っている技術や運用方法までまとめます。
技術スタック
全体はNext.jsを中心に構成しています。
| 領域 | 技術 |
|---|---|
| Framework | Next.js 16 |
| UI | React 19 / TypeScript |
| CSS | Tailwind CSS 4 |
| UI Components | shadcn/ui / Radix UI |
| Component Development | Ladle |
| Next.js Adapter | OpenNext for Cloudflare |
| Runtime | Cloudflare Workers |
| DB | Cloudflare D1 |
| ORM | Drizzle ORM |
| Storage | Cloudflare R2 |
| Realtime | Durable Objects / SSE |
| Authentication | NextAuth |
| Validation / API Schema | Zod / zod-openapi |
| API Docs | Swagger UI |
| PDF表示 | PDF.js |
| PDF編集 | pdf-lib |
| DOCX | Mammoth |
| OCR | Tesseract.js |
| 背景除去 | IMG.LY Background Removal |
| AVIF変換 | jSquash |
| 手書き | Signature Pad |
| Voice Clone | Qwen3-TTS |
| AI API | OpenAI GPT / Transcription |
| Unit Test | Vitest |
| E2E / VRT | Playwright |
| Error Monitoring | Sentry |
| Uptime Monitoring | Better Stack |
| Dependency Update | Renovate |
| CI/CD | GitHub Actions |
| AI Coding | Codex / Claude Code Pro |
一番意識しているのは「この処理、本当にサーバーでやる必要ある?」
neglegereでは、何でもAPIへ送る構成にはしていません。
処理によって大きく3種類に分けています。
軽い処理・プライバシー重視
↓
Browser
認証・保存・外部API
↓
Cloudflare Workers
GPUが必要
↓
GPU
現在の役割分担をざっくり書くと、
Browser
├─ 画像変換
├─ 背景除去
├─ PDF表示
├─ OCR
├─ 音声加工
├─ 騒音解析
├─ rPPG
└─ Speech Synthesis
Cloudflare
├─ Next.js
├─ Auth
├─ D1
├─ R2
├─ Durable Objects
├─ Cron
├─ Email
├─ URL Fetch
└─ AI API連携
GPU
└─ Qwen3-TTS
という構成です。
例えば画像変換なら、
画像
↓
Browser
↓
WebAssembly
↓
AVIF / WebP
↓
Download
だけで完結します。
サーバーへアップロードする必要がありません。
一方、Qwen3-TTSのような大きなモデルはブラウザやCloudflare Workersでは厳しいため、GPU側へ逃がしています。
Next.jsをCloudflare Workersで動かす
Next.jsはOpenNextを使ってCloudflare Workersへデプロイしています。
OpenNext for Cloudflare:
ざっくり、
Next.js
↓
OpenNext
↓
Cloudflare Workers
です。
Cloudflare側では、
Workers
├─ Next.js
├─ D1
├─ R2
├─ Durable Objects
├─ Cron Triggers
└─ Send Email
を使っています。
Node.js前提ではなくEdge / Cloudflare前提でライブラリを選ぶ
Cloudflare Workersで動かすため、ライブラリ追加時には、
Node.jsで動くか
だけではなく、
Cloudflare Workersで動くか
Web Standard APIで動くか
Node.js専用APIへ依存していないか
WASMを使えるか
を見るようになりました。
特に画像・PDF・音声系はNode.js用ライブラリが多いため、この違いはかなり大きいです。
Cloudflareのbindingも型生成する
CloudflareのD1やR2などはWranglerから型生成しています。
pnpm cf-typegen
これで、
wrangler.jsonc
↓
wrangler types
↓
cloudflare-env.d.ts
という形になります。
D1、R2、Durable Objectsなどのbindingを手書きで型定義し続けなくてよいので、Cloudflare中心の構成では便利です。
APIはZodからOpenAPIへつなげる
APIではZodをvalidationに使っています。
さらにzod-openapiを使い、API仕様へつなげています。
Zod Schema
↓
Runtime Validation
↓
OpenAPI Schema
↓
Swagger UI
という形です。
Validation用schemaとAPIドキュメントを別々に管理すると、どこかでズレます。
そのため、
実際にAPIが受け付けるschemaをできるだけAPI仕様の元にもする
という構成にしています。
Swagger UIも用意していますが、内部向けの情報も含むため認証をかけています。
お問い合わせにはCloudflare Turnstile
お問い合わせフォームではCloudflare Turnstileを使っています。
Form
↓
Turnstile
↓
Cloudflare Workers
↓
Email
という構成です。
Cloudflare中心でインフラを組んでいるので、この辺りもCloudflare側へ寄せています。
1. AIボイス:GPU処理だけ別系統へ逃がす
AIボイスでは、録音した本人の声を参照して文章を読み上げる音声を生成します。
音声生成にはQwen3-TTSを使っています。
大まかな構成は、
Browser
↓
Cloudflare Workers
↓
D1 + R2
↓
Cron
↓
GitHub Actions
↓
GPU
↓
Qwen3-TTS
↓
Cloudflare
↓
Browser
です。
重い処理をHTTPリクエストの中で待たない
音声生成のような処理を、
POST /generate
↓
GPU処理
↓
数十秒待つ
↓
Response
とすると、タイムアウトや再試行、ページ離脱への対応が面倒になります。
そこで、
POST /generate
↓
D1へJob登録
↓
R2へ必要ファイル保存
↓
Response
までで一旦終了します。
実際の音声生成は非同期ジョブとして扱います。
pendingがあるときだけGPU側を起動する
Cloudflare Cronで待機中ジョブを確認します。
const pendingCount =
await countPendingVoiceClonesSnapshot(env.recordings);
if (pendingCount === 0) {
return;
}
await triggerGpuJob();
つまり、
Cron
↓
pendingなし
↓
終了
です。
仕事があるときだけ、
Cron
↓
pendingあり
↓
GitHub Actions
↓
GPU処理
へ進めます。
GPU系は「何も仕事がない時間に動かさない」のがかなり重要でした。
GitHub ActionsはGPU処理の実行ブリッジとして使う
定期処理そのものをGitHub Actionsのschedule任せにはせず、
Cloudflare Cron
↓
GitHub REST API
↓
workflow_dispatch
↓
GPU
という形にしています。
GitHub ActionsはCI/CDだけでなく、外部のGPU処理を起動するためのブリッジとしても使っています。
Durable Objects + SSEで生成完了を通知する
生成状況確認を毎秒pollingするのではなく、SSEを使っています。
Cloudflare Workersは複数instanceに分かれる可能性があるため、接続先をDurable Objectsへ集約します。
Browser A
↑
│
Complete API → Durable Object → Browser B
│
↓
Browser C
これで生成完了イベントをブラウザへpushできます。
ページを閉じている場合もあるので、メール通知も併用しています。
2. 鬼ボイス:生成AIではなくWeb Audio API
録音した声を鬼っぽく変える機能もあります。
こちらは生成AIを使っていません。
Microphone
↓
Recording
↓
Web Audio API
↓
Pitch / Reverb / Effect
↓
Playback
です。
つまり、
今ある音声を加工する
→ Browser
新しい音声を生成する
→ Qwen3-TTS
と分けています。
AIでできる処理でも、普通の信号処理で十分ならAIを使わない方が安くて速いです。
3. 背景メーカー:AIモデルをブラウザで動かす
背景メーカーでは、
- 背景透過
- 背景色変更
- 背景画像差し替え
などをブラウザ側で処理しています。
背景除去にはIMG.LY Background Removalを使っています。
特徴は、
画像をサーバーへ送らずにモデルをブラウザで実行する
ことです。
Image
↓
Browser
↓
Background Removal Model
↓
RGBA
↓
Canvas
↓
Output
です。
重い処理は必要になってからロードする
背景除去ライブラリはdynamic importしています。
const { removeBackground } =
await import("@imgly/background-removal");
つまり、
ページを見ただけ
→ Model不要
背景除去を実行
→ Model Load
です。
ローディングUIを描画してから重い処理へ入る
ブラウザでWASMやAIモデルを読み込むと、初期化時に重くなることがあります。
そのため、
クリック
↓
Processing表示
↓
一度paint
↓
Model Load
↓
処理
という順にしています。
ユーザーからすると、
押したのに何も反応しない
時間を減らせます。
背景除去結果を使い回す
背景除去を一度実行したら、その結果をRGBAとして保持します。
Background Removal
↓
RGBA
├─ 透明背景
├─ 白背景
├─ 色背景
└─ 画像背景
背景を変更するたびにAIモデルを再実行する必要はありません。
非同期処理の古い結果を捨てる
画像系UIでは、
白背景
↓
すぐ赤背景
↓
さらに青背景
のような操作があります。
処理完了順が入れ替わると、最後に選んだ色ではなく古い結果が表示される可能性があります。
そこでversionを持たせ、
const version = ++renderVersionRef.current;
const result = await render();
if (version !== renderVersionRef.current) {
return;
}
のように、
最新操作以外の結果を捨てる
ようにしています。
Object URLも必ず解放する
画像処理では、
const url = URL.createObjectURL(blob);
を多用します。
不要になったら、
URL.revokeObjectURL(url);
で解放します。
画像を何度も編集するSPAでは、こういう細かいメモリ管理も効いてきます。
4. 画像変換:WebAssemblyでWebP / AVIFへ
画像変換では、
PNG / JPEG
↓
WebP / AVIF
へ一括変換できます。
AVIFにはjSquashを使っています。
File
↓
Decode
↓
ImageData
↓
WASM Encoder
↓
AVIF Blob
という流れです。
サーバーへアップロードしないため、
- サーバーCPU不要
- 転送量不要
- 元画像を送信しない
というメリットがあります。
5. 音読アシスト:入力形式をTextへ正規化する
音読アシストでは、
- 直接入力
- TXT
- DOCX
- PDF URL
- Webページ
などを読み込めます。
PDFにはPDF.js。
DOCXにはMammoth。
を使っています。
入力は違っても最終的には、
File / URL / Text
↓
Text
↓
Sentence[]
↓
Speech Synthesis
へ正規化しています。
長文を一気に読み上げない
長い文章を1つのSpeechSynthesisUtteranceへ渡すと、環境によって不安定になります。
そのため文章を分割します。
Sentence 0
Sentence 1
Sentence 2
Sentence 3
現在位置を持たせることで、
- 読んでいる文をハイライト
- 途中から再生
- タップした文から再生
- 自動スクロール
などができます。
URLの文章は必要なときだけAI要約
Webページはまず本文を取得します。
その上で、
全文を読む
または、
GPTで要約
↓
短くして読む
を選べるようにしています。
普通に読めるものまで毎回AIへ投げないのも、コスト面で重要です。
6. ことばレンズ:PDF.jsで独自Viewerを作る
ことばレンズでは、PDFやWebページを読みながら、その場で分からない単語を調べられます。
PDF.jsを単純に埋め込むのではなく、独自Viewerを作っています。
PDF
├─ Page Render
├─ Thumbnail
├─ Zoom
├─ Fit Width
├─ Fullscreen
├─ Mouse Lookup
├─ Touch Selection
├─ Magnifying Lens
└─ OCR Fallback
全ページを最初から描画しない
100ページあるPDFを全部Canvas化すると重くなります。
そこでIntersectionObserverを使い、
Viewport付近
↓
Render
としています。
少し先のページまで先読みすることで、スクロール時の待ち時間を減らしています。
PDFの座標から単語を推定する
PDF.jsのTextItemには座標情報があります。
ユーザーがクリックした位置とTextItemの矩形を比較して、どの文字列を押したのかを探します。
さらに、
const charIndex = Math.floor(
((pageX - item.left) / item.width) *
item.text.length
);
のように文字位置も推定します。
スマートフォンでは正確な1点タップが難しいため、範囲選択にも対応しています。
日本語PDF特有の空白も補正する
PDFによっては、
日 本 語
のように文字間へスペースが入ります。
日本語script同士の不自然な空白を除去して、
日本語
へ戻してから検索へ使います。
テキストを持っていないPDFだけOCRする
PDFには、
Text Layerあり
と、
スキャン画像だけ
があります。
まずPDFのText Layerを使い、取れなかった場合だけTesseract.jsへfallbackします。
PDF Textあり
→ そのまま使う
PDF Textなし
→ OCR
です。
最初から全ページOCRすると重すぎるため、必要な場合だけ実行しています。
OCRは実行中Promiseまでキャッシュする
OCR結果だけでなく、処理中のPromiseもキャッシュします。
1回目
↓
OCR開始
↓
Promise保存
2回目
↓
同じPromiseを待つ
これで同じ領域に対してOCRが重複実行されるのを防いでいます。
7. 手書き電子サイン:Canvas + pdf-lib
手書きサインにはSignature Padを使っています。
Canvasで書いた署名を画像化し、pdf-libでPDFへ埋め込みます。
PDF
↓
Browser
↓
Signature
↓
位置・サイズ調整
↓
PDFへ合成
↓
Download
までブラウザ側で行います。
サインのためだけにPDFをサーバーへアップロードする必要はありません。
8. 騒音チェック:Web Audio APIでリアルタイム解析
騒音チェックではマイクを使って周囲の音量の目安を表示します。
getUserMedia
↓
AudioContext
↓
GainNode
↓
AnalyserNode
↓
Spectrum
↓
Current / Average / Peak
という構成です。
ただし重要なのは、
校正済み騒音計のdB SPLではない
ことです。
端末によってマイク性能が異なるため、表示値はあくまで周囲の音量を比較するための目安として扱っています。
マイク音声自体は保存しない
保存する場合も、
Average
Peak
Position
などの計測結果だけです。
Microphone Audio
↓
Browserで解析
↓
破棄
です。
iOS Safariでは権限要求を順番にする
マイクと位置情報を同時に要求すると、モバイルSafariで扱いづらいケースがあるため、
マイク許可
↓
計測開始
↓
位置情報取得
の順にしています。
さらに古い非同期位置情報取得が次の測定へ混ざらないよう、測定IDも持っています。
9. コンディションチェック:rPPGをTypeScriptで処理
実験的な機能として、カメラ映像から体調の参考値を見るコンディションチェックもあります。
rPPG、
Remote Photoplethysmography
を使っています。
測定値としては、
- 心拍
- 呼吸
- 心拍間隔の変動
- 同期傾向
- リラックス傾向
- 信号品質
などを扱っています。
※医療機器ではなく、診断目的ではありません。
RGBの微細な変化を見る
フレームごとに、
type RppgSample = {
red: number;
green: number;
blue: number;
brightness: number;
motion: number;
timestamp: number;
};
のような値を取ります。
Webカメラは完全な一定間隔でフレームが来るわけではないため、まず等間隔へresamplingします。
その後RGBを組み合わせてpulse signalを作ります。
数値を出すことより「信用してよいか」の方が重要
rPPGでは、
Heart Rate = 72
という数字自体は作れます。
ただし、
照明が暗い
顔が動いている
カメラ性能が低い
場合、その72に意味がない可能性があります。
そのためsignal qualityやpeak confidenceを計算し、
数値は出せる
≠
表示してよい
と考えています。
品質が足りない指標は無理に表示しないようにしています。
カメラ映像もサーバーへ送り続けない
rPPGも、
Camera
↓
Browser
↓
RGB Sample
↓
Signal Processing
↓
Result
です。
映像ストリーム自体を外部サーバーへ送り続ける必要はありません。
ログインは「サーバーを使うとき」に寄せる
neglegereでは、なるべく最初からログインを要求しません。
例えば、
画像変換
背景除去
PDF
騒音測定
コンディションチェック
など、ブラウザだけで完結するものはログイン不要にしています。
一方、
データを保存する
AI APIを使う
URLをServer側で取得する
生成結果を後から参照する
場合はログインを使います。
考え方としては、
Browserだけ
→ Login不要
Server Resourceを使う
→ Login
です。
AI APIにはGPT系を採用
アプリ内のAI処理にはOpenAIのGPT系APIを使っています。
主な用途は、
- 長文要約
- 投稿文生成
- テキスト処理
- 音声文字起こし
です。
Claude APIも候補として比較しましたが、現在はGPT系へ寄せています。
文字起こしまで含めるとOpenAIにまとめやすかった
neglegereは音声系の機能が多いため、
LLMだけ
ではなく、
Text Generation
+
Transcription
まで含めて考えました。
構成としては、
Audio
↓
OpenAI Transcription
↓
Text
↓
必要ならGPTで要約・整形
とできます。
文章生成と文字起こしで複数ベンダーを使い分けるより、APIキーや課金管理もシンプルになります。
コスト面でもGPT系のメリットが大きかった
個人開発では、モデルの性能だけでなく1回あたりのコストも重要です。
neglegereでは、
長文
→ 要約
短い入力
→ 投稿文生成
音声
→ 文字起こし
のような比較的小さな処理を何度も行います。
そのため、
必要十分な性能
+
低いAPIコスト
のバランスを重視しました。
Claude APIも比較しましたが、自分の用途では、
文字起こしを含めたAPI構成とコスト面でGPT系のメリットが大きい
と判断しました。
全部をGPTにはしない
AI関連だけ見ても、
文章生成・要約
→ GPT
文字起こし
→ OpenAI Transcription
ボイス生成
→ Qwen3-TTS
背景除去
→ IS-Net系モデル
音声加工
→ Web Audio API
騒音
→ Signal Processing
rPPG
→ TypeScript
と分けています。
「AIでできるからAIを使う」ではなく、普通のブラウザAPIやアルゴリズムで済むならそちらを優先します。
デザインは極力「AIっぽく」しない
開発にはAIをかなり使っていますが、デザインについては逆に、
AIっぽく見えないこと
を意識しています。
生成AIにWebサービスの見た目をそのまま任せると、
強いGradient
Glow
Glassmorphism
大量のCard
抽象的な3D Object
作り込まれすぎた背景
のようなデザインになりやすいと感じています。
特にAI生成画像は背景がAIっぽさを出しやすい
人物だけなら自然でも、
人物
+
部屋
+
小物
+
照明
+
背景装飾
まで全部AIに生成させると、細部まで作り込まれすぎて逆にAIっぽく見えることがあります。
そのため、
実写画像
または
生成した被写体
↓
背景透過
↓
人物・物だけ使う
↓
背景はWeb側で作る
という使い方をよくしています。
AIには「完成画像」ではなく素材を作らせる
AIに一枚の完成したWebデザインを作らせるより、
人物
物
アイコン的な素材
を作らせて、
AI
↓
素材
Web
↓
余白
文字
背景
配置
Responsive Layout
と分けています。
これなら、
- レスポンシブにしやすい
- 文言変更に強い
- 背景色を変えられる
- AI画像特有の情報量を減らせる
というメリットがあります。
実写も積極的に使う
例えば、
スマートフォンで音を測る
カメラで状態を確認する
ような機能では、イラストより実写の方が使い方を一瞬で理解できます。
画像は単なる飾りというより、
その機能をどう使うかを説明するUIの一部
として使うことが多いです。
AIコーディングはCodexとClaude Code Proを併用
開発には、
- Codex
- Claude Code Pro
を使っています。
Codex:
Claude Code:
2つを使っている理由の1つは、
片方へ利用量が集中してレート制限に当たりすぎないようにするため
です。
Git worktreeで並行開発する
並列化そのものはCodexだけでもできます。
重要なのはAIの種類ではなく、作業環境を分けることです。
git worktree add ../project-a feature/a
git worktree add ../project-b feature/b
とすれば、
Worktree A
↓
Codex
↓
Feature A
Worktree B
↓
Claude Code Pro
↓
Feature B
のように独立タスクを並行できます。
Codexを複数session動かす場合もあります。
同じ大きなファイルはなるべく同時に触らせない
並行開発するときは、
Codex
→ Feature A
Claude Code
→ Feature B
のように責務を分けます。
同じ巨大ファイルを両方に触らせると、最後に人間がmerge conflictと戦うことになります。
AIを何個起動するかより、
タスク境界をきれいに切る
方が重要だと感じています。
AI向けルールやSkillsは作り込みすぎない
AI向けのルールを大量に作る考え方もありますが、個人的にはそこへあまり力を入れていません。
モデル自体がかなり速い速度で賢くなっているため、
現在のモデル向けに細かい手順や制約を大量に固定すると、将来モデルの判断能力が上がったときに逆に邪魔になる可能性がある
と考えているためです。
そのため、
Project固有で絶対に守る必要がある制約
は残しますが、
実装手順を細かく全部決める
ことはなるべく避けています。
AIに制約を増やし続けるより、
AIが賢くなることを前提に、必要最低限だけ制約する
という考え方です。
AIが「できました」と言っても完了ではない
AIの出力をそのままmainへ入れることはしません。
基本的には、
Codex / Claude Code
↓
Implementation
↓
Diff確認
↓
Unit Test
↓
E2E
↓
VRT
↓
Merge
です。
特に、
- Canvas
- Camera
- Microphone
- WASM
- Cloudflare Workers
などは、実ブラウザで動かさないと分からないことも多いです。
CI/CDも「賢くサボる」
CI/CDにはGitHub Actionsを使っています。
大きな流れは、
Pull Request
│
┌────┼────┐
│ │ │
▼ ▼ ▼
Test E2E VRT
│ │ │
└────┼────┘
│
All Green
です。
Unit Test
Vitestを使っています。
CIでは、
Format
↓
Lint
↓
Unit Test
↓
Coverage
まで確認します。
rPPGや騒音計算、PDF座標計算など、pureなロジックはなるべくUnit Testへ寄せています。
E2Eはnext devではなくOpenNext buildへ当てる
本番環境は、
Next.js
↓
OpenNext
↓
Cloudflare Workers
です。
そのためE2Eも、
pnpm exec opennextjs-cloudflare build
pnpm e2e
として、本番に近いbuildへPlaywrightを当てています。
同じPRの古いCIはキャンセルする
PRへ連続pushした場合、
Commit A
↓
E2E実行中
Commit B push
↓
Aは不要
になります。
そこでcancel-in-progressを使い、最新commitだけを残しています。
GitHub Actionsの実行時間節約にもなります。
失敗したときだけArtifactを残す
成功したCI結果を毎回詳しく見ることはありません。
そのため、
Success
→ 何もしない
Failure
→ Report / Artifactを残す
という考え方にしています。
VRTで見た目も検証する
Playwrightを使ってVisual Regression Testも行っています。
Unit TestとE2Eが通っていても、
文字が見切れた
Buttonサイズが変わった
Mobileだけ崩れた
Dialogがずれた
といった問題は残ります。
そこで、
Unit Test
→ Logic
E2E
→ Behavior
VRT
→ Visual
と役割を分けています。
VRTのbaselineはPR実行時にmainから作る
snapshotをずっとGitに保存しておくのではなく、
main
↓
Build
↓
Screenshot
PR
↓
Build
↓
Screenshot
Compare
という形にしています。
さらにmainとPRを同じGitHub Actions runnerで撮影することで、
OS差
Font差
Chromium差
によるfalse positiveを減らしています。
VRTの差分をPR上ですぐ確認できるようにする
VRTが失敗した場合に、
Actions
↓
Artifact
↓
ZIP
↓
画像を探す
のは面倒です。
そのため差分画像を保存し、PRコメントから、
diff
expected
actual
を確認できるようにしています。
成功後は古い「差分あり」のコメントも更新します。
Renovateで依存更新も自動化
依存更新にはRenovateを使っています。
minor / patchなどをまとめて更新し、リリース直後のバージョンにはすぐ飛びつかないよう少し期間を空けています。
依存更新PRも、
Renovate
↓
PR
↓
Test
↓
E2E
↓
VRT
↓
全部OK
↓
Auto Merge
としています。
人間が見るのは失敗したときだけです。
依存更新を「毎回確認する仕事」から「例外処理」へ
理想的には、
Renovate
↓
CI
↓
Green
↓
Auto Merge
で終わります。
人が見るのは、
Unit Test ❌
E2E ❌
VRT ❌
のときだけです。
この、
正常系を人間が確認し続けない
という考え方は、サービスの「賢くサボる」というコンセプトにも近いです。
本番エラーはSentryで拾う
本番のアプリケーションエラー監視にはSentryを使っています。
ローカルやCIですべての状況を再現できるわけではありません。
特に、
- 特定ブラウザ
- 特定端末
- 特定データ
- 特定操作順
でしか発生しない問題があります。
そこで、
Production
↓
Error
↓
Sentry
↓
Stack Trace確認
↓
Codex / Claude Codeで調査
↓
Fix
↓
PR
↓
Test / E2E / VRT
↓
Merge
という流れで修正できます。
AI開発とSentryの相性はかなりいい
Sentryで、
どのエラーか
どのコードで起きたか
どのくらい発生しているか
が分かれば、その情報をもとにAIへコード調査を任せやすくなります。
Sentry
→ 問題の手掛かり
Codex / Claude Code
→ コード横断調査・修正
CI/CD
→ 修正で他を壊していないか確認
という役割分担です。
死活監視にはBetter Stack
Sentryはアプリ内部のerror監視ですが、
そもそもWebサイトが開かない
HTTP Requestが返らない
5xxになっている
場合は別の監視が必要です。
そのため外形監視にはBetter Stackを使っています。
役割としては、
Sentry
→ Application Error
Better Stack
→ Uptime / External Monitoring
です。
GitHub Actionsで死活監視する案も考えた
死活監視だけなら、
on:
schedule:
- cron: "*/5 * * * *"
jobs:
check:
runs-on: ubuntu-latest
steps:
- run: curl --fail https://example.com/
でも作れます。
ただ、最終的にはBetter Stackを使いました。
理由は、
GitHub ActionsはCI/CDの仕組みであって、監視専用サービスではない
からです。
専用監視サービスの方が考えなくていいことが多い
自作すると、
HTTP Check
↓
Retry
↓
何回失敗したらIncident?
↓
通知
↓
通知重複防止
↓
Recovery通知
などを考える必要があります。
Better Stackなら、この部分を監視サービス側へ任せられます。
また、
GitHub Actions
→ Build / Test / Deploy / Batch
Sentry
→ Error
Better Stack
→ Uptime
と役割が分かれていた方が構成も理解しやすいです。
HeartbeatならCronやBatchも監視できる
外形監視だけでなく、
Cloudflare Cron
↓
Job Complete
↓
Heartbeat
という構成にすれば、
サイトは開く
でもBatchだけ止まっている
ケースも検知できます。
これは今後の定期処理監視でも使いやすいと思っています。
開発から運用までを一つのループにする
最終的な開発フローはこんな形です。
Codex / Claude Code
│
▼
Implementation
│
▼
Pull Request
│
┌──────────┼──────────┐
▼ ▼ ▼
Test E2E VRT
│ │ │
└──────────┼──────────┘
│
Merge
│
▼
Production
│ │
Sentry Better Stack
│ │
└──┬───┘
│
Problem
│
▼
Codex / Claude Code
依存更新だけは別ルートで、
Renovate
↓
PR
↓
Test / E2E / VRT
↓
Auto Merge
です。
ブラウザ処理を増やすメリット
ブラウザ側へ処理を寄せるメリットはいくつかあります。
サーバーコスト
例えば画像変換なら、
Upload
↓
Server Convert
↓
Download
が不要です。
転送量
5MB画像ならUpload + Downloadだけでも転送量が増えます。
Browser内なら0です。
プライバシー
特に、
- 顔写真
- カメラ
- マイク
などは、送らなくてよいなら送らない方がシンプルです。
ただし何でもBrowserにはしない
Browserには、
- 端末性能差
- メモリ制限
- WASM初期ロード
- ブラウザ差
- 長時間処理
- 大規模モデルが厳しい
という弱点があります。
そのため、
Browser
→ 軽い処理・プライバシー
Cloudflare
→ 認証・保存・API
GPU
→ 大規模AI
という分け方に落ち着きました。
「AIを使う」より「どこまでAIを使わないか」を考える
作っていて面白かったのは、
AIサービスなのに、
AIを使わない部分をかなり考えるようになったこと
です。
例えば、
音声加工
→ Web Audio API
画像変換
→ WASM
PDF
→ PDF.js
OCR
→ Tesseract.js
騒音
→ Signal Processing
rPPG
→ TypeScript
Voice Generation
→ Qwen3-TTS
文章・要約
→ GPT
です。
全部APIへ投げる方が実装は簡単な場合もあります。
ただ、
Latency
Cost
Privacy
Offline処理
Vendor dependency
を考えると、Browserで十分なものはBrowserに残した方が使いやすいことも多いです。
個人開発でもかなり並行開発できるようになった
以前なら、
設計
↓
実装
↓
テスト
↓
次
と完全に直列でした。
今は、
┌─ Codex → Feature A
│
自分 → Task分割 ───┼─ Claude Code → Feature B
│
└─ 自分 → Review / Design
と進めることがあります。
さらに、
依存更新
→ Renovate
Test
→ GitHub Actions
本番Error
→ Sentry
死活監視
→ Better Stack
へ任せています。
AIにコードを書かせること以上に、
自分が毎回やらなくてよい作業を増やす
方が、個人開発では大きいと感じています。
最終的なアーキテクチャ
全体をまとめると、
┌────────────────────┐
│ Browser │
│ │
│ Image / PDF │
│ Audio / Camera │
│ OCR / rPPG │
│ WebAssembly │
│ Speech Synthesis │
└─────────┬──────────┘
│
必要なデータだけ
│
▼
┌────────────────────┐
│ Cloudflare │
│ │
│ Workers / Next.js │
│ D1 / R2 │
│ Durable Objects │
│ Cron / Email │
│ Auth / API │
└─────────┬──────────┘
│
Heavy AI Only
│
▼
┌────────────────────┐
│ GPU │
│ │
│ Qwen3-TTS │
└────────────────────┘
開発側は、
Codex / Claude Code Pro
│
▼
Git worktree
│
▼
Pull Request
│
┌────┼────┐
▼ ▼ ▼
Test E2E VRT
│ │ │
└────┼────┘
│
Merge
│
▼
Production
│ │
Sentry Better Stack
依存更新は、
Renovate
↓
PR
↓
Test / E2E / VRT
↓
Auto Merge
まで自動化しています。
SEOはコンテンツだけでなく技術面もかなり意識している
neglegereでは機能を作るだけでなく、検索エンジンやAI検索から内容を理解してもらいやすくするための技術SEOも意識しています。
Next.jsのMetadata APIを使い、
title
description
Open Graph
Twitter Card
などの基本的なメタデータを設定しています。
さらに、
sitemap.xml
robots.txt
もNext.js側から生成しています。
sitemapには公開ページだけを載せる
sitemap.tsでは、単純にすべてのRouteを出すのではなく、
公開ツール
LP
FAQ
お問い合わせ
利用規約
プライバシーポリシー
など、検索対象にしたい公開ページだけを列挙しています。
逆に、
ログイン後のページ
ユーザー固有ページ
設定画面
などはsitemapへ含めません。
また、
トップページ
→ priority 1
主要ツール
→ priority 0.8
各LP
→ priority 0.7
のようにページの重要度も分けています。
robots.txtでもクロール範囲を制御する
robots.tsでは、
/api/
/me/
/post-assist/settings
/settings/
/swagger
など、検索結果へ出す必要がないページをクロール対象から外しています。
そして、
robots.txt
↓
sitemap.xml
への導線も設定しています。
つまり、
公開したいページ
→ sitemap
検索させたくないページ
→ robots
と明示的に分けています。
構造化データも意識している
今後さらに強化したいのがJSON-LDなどの構造化データです。
例えばサービス全体なら、
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "neglegere",
"url": "https://neglegere.com/"
}
のように、
これは何というサービスなのか
Webアプリなのか
何ができるのか
どのURLが正規URLなのか
を機械が理解しやすい形で明示できます。
機能ごとのLPについても、
WebApplication
SoftwareApplication
FAQPage
BreadcrumbList
など、そのページの役割に合った構造化データを使えないか検討しています。
単純に文章中へキーワードを増やすSEOより、
ページそのものの意味を機械が読み取りやすくする
方を重視しています。
AI検索も意識して「意味を構造化する」
最近はGoogle検索だけでなく、ChatGPTなどのAI経由でサービスを探すことも増えてきました。
そのため、
HTML
Metadata
JSON-LD
Sitemap
Robots
のように、
サービスの情報を機械が解釈しやすい形で持たせる
ことは、従来のSEOだけでなくAI検索への対応としても重要になると考えています。
ただし、
AI専用の謎テクニック
を大量に入れるのではなく、
人間にも分かりやすい文章
正しいHTML構造
適切なMetadata
Sitemap / Robots
必要に応じたStructured Data
というWebの基本をしっかりやる方針です。
ツール本体とLPを分けるのもSEOを考えた結果
neglegereでは、
/word-lookup
/noise-meter
/condition-check
のような実際に使うツール画面とは別に、
/lp/word-lookup
/lp/noise-meter
/lp/condition-check
のような説明用LPも持っています。
ツール本体は、
すぐ使える
操作しやすい
余計な説明を置かない
ことを優先します。
一方LPでは、
何ができるか
どういう用途か
どんな特徴があるか
を文章として持たせます。
Tool
→ UX優先
LP
→ 説明・SEO優先
と役割を分けることで、
操作画面をSEO用テキストだらけにせず、検索側にも十分な情報を渡せる
ようにしています。
まとめ
neglegereを作っていて、最近よく考えるのはこの2つです。
この処理、本当にサーバーでやる必要ある?
そして、
この作業、本当に毎回人間がやる必要ある?
画像、PDF、音声、カメラなどはBrowserへ寄せる。
認証、保存、APIはCloudflareへ任せる。
大きなAIモデルだけGPUへ逃がす。
実装はCodexとClaude Code Proで並行する。
文章生成や文字起こしは、用途とコストを見てGPT系APIを使う。
依存更新はRenovate。
品質確認はUnit Test / E2E / VRT。
本番エラーはSentry。
死活監視はBetter Stack。
デザインについては逆に、AIを使っているからこそ、AIっぽくなりすぎないよう実写や背景透過素材を使う。
そんな感じで、
サービス自体だけでなく、開発や運用も含めて「賢くサボる」
ことを目指しています。
まだ実験的な機能も多いですが、Webブラウザだけでできることは想像以上に多いです。
今後も、
BrowserでできることはBrowserへ。人間がやらなくていいことは自動化へ。
という方向で作っていこうと思います。