Web・実験実装 / Web & Experimental
Web・実験実装
ブラウザの限界に挑み、プラットフォームの制約を超越する。
フルスタックなハイブリッドDB構成から、
WebWorkerによる並列バイナリ処理まで。
boyscout-tajimi
boyscout-tajimiは、地域団体が1年以上内部運用している公式Webサイト兼CMSです。公開ページ、管理機能、DB切替、画像配信をまとめ、非エンジニアでも更新しやすい導線を作りました。
成果: SSRの公開ページ・管理画面・認証・SQLite/PostgreSQL切替をひとつの構成にまとめ、Web制作に慣れていない人でも更新を続けやすい実運用CMSとして動いています。
更新しやすさと公開速度を両立する
ボーイスカウト多治見第一団の公式ウェブサイト兼コンテンツ管理システムです。活動写真、お知らせ、各隊の紹介を見やすく整理し、外部から見たときの不安や分かりにくさを減らすことを狙いました。単にページを作るのではなく、活動を続けている人たちの温度が外にも伝わることを大事にしています。
一方で、実際に更新するのはWeb制作に慣れた人ではありません。汎用CMSは費用面でも運用面でも重くなりやすいため、管理画面に加えて Google Apps Script と Google Forms から投稿できる導線も用意し、普段の入力に近い形で更新できます。
SSR による高速ページ表示
Alpine.js SPA ライク管理 UI
環境適応型・ハイブリッドデータベース構成
開発環境では設定不要の SQLite(と
session-file-store)を使用し、本番環境(PaaS)では堅牢な
PostgreSQL(と
connect-pg-simple)へと自動で切り替わる「アダプタパターン」を実装。インフラに依存しない開発体験を提供しています。
Express v5 + EJS テンプレートエンジンでSSRしています。セマンティックなHTML構造とメタタグを適切に出力し、地域団体サイトとしてSEOとアクセシビリティを考慮しました。
Tailwind CSS + Alpine.js を採用し、jQueryを完全排除。宣言的かつ軽量なDOM操作でSPAライクな管理画面を実現。
bcryptによるパスワードのハッシュ化と、セッションベースの堅牢な管理者認証システムを自作・統合しています。
設計の意図
この案件では、きれいなWebページを作るだけでは足りませんでした。活動している人が更新を続けられなければ、サイトはすぐ古くなり、かえって団体の印象を悪くします。だから、公開速度、更新しやすさ、追跡可能性を同時に満たす設計を進めました。
ボーイスカウトの活動は、外から見ると何をしている団体なのか分かりにくい面があります。だからこそ、活動内容を継続的に発信できる仕組みを作ることは、単なるCMS制作ではなく、地域団体の信頼を支える仕事として実装しました。
現場運用に合わせて整えたこと
管理画面だけを作って終わりにせず、指導者が普段やっている入力に合わせる必要がありました。Google Forms で活動内容やお知らせを入力し、Google Apps Script 経由で投稿につなげる構成にしたのは、技術的な新規性よりも更新担当者の心理的な負担を下げるためです。
開発後半では、Google Drive 画像のローカル化、Webhook の文字コード差異やペイロード上限への対処、Google Docs から HTML への変換精度改善、CI のヘルスチェック拡充、`npm ci` 権限失敗時のデプロイ復旧など、公開後に詰まりやすい箇所を継続的に潰しました。実装力だけでなく、更新が止まらないことを優先して手当てした案件です。
運用して分かったこと
実運用では、見た目を作る段階よりも、データ流入経路の差異を吸収する部分で調整が増えました。特に GAS 側との日本語エンコード差異、Base64 画像を含む大きめのWebhook、Google Drive のリダイレクト挙動、テンプレートの小さな構文崩れによる 500 エラーは、公開系CMSでは見落とすと影響が大きいポイントでした。
次回同規模の案件を進めるなら、Webhook 契約と HTML 変換仕様をより早い段階で固定し、配信画像・認証・監査ログ・バックアップ復旧まで含めた運用試験を先に自動化します。開発中盤から安全策を厚くした経験から、初期設計の時点で「運用で折れやすい箇所」を先回りして検証しておく余地があったと考えています。
実装の深堀り:Google Drive 運用を前提にした画像配信と監査
利用者側では、写真や記事を普段の手順で更新するだけで、公開サイトが見やすく保たれ、あとから変更履歴も追えることが重要です。その価値を支えるために配信最適化と監査機能を組み合わせています。
このCMSでは、指導者が普段使っている Google Drive を更新元にしつつ、公開サイト側では読み込み速度と運用記録の両方に対応する必要がありました。なので、画像は配信用に変換してキャッシュし、管理画面の操作は追跡できます。
1. Sharp による画像変換とローカルキャッシュ
Google Apps Script 経由で取得した元画像は、そのまま配信せずに
Sharp
でリサイズとWebP変換を行っています。大きな写真を公開ページ向けのサイズへ整えたうえでキャッシュすることで、閲覧端末ごとの通信負荷を抑えました。
// imageDownloader.js: メモリ上でのストリーム最適化処理
const optimizeImage = async (buffer) => {
return await sharp(buffer)
.resize({ width: 1280, height: 720, fit: 'inside', withoutEnlargement: true })
.webp({ quality: 60 })
.toBuffer();
};
この変換をサーバー側でまとめて行うことで、公開側のHTMLは通常の画像参照だけで済みます。運用担当者に画像最適化の知識を求めず、サイト側で配信品質をそろえられる構成です。
2. 監査ログとレート制限で運用面を固める
地域団体向けの管理画面では、更新しやすさだけでなく「誰が何を変えたか」を追えることも重要でした。そこで、管理操作は
audit.middleware.js
を通して記録し、入口ではレート制限もかけています。
- レートリミット: ログインや管理系エンドポイントへのアクセス頻度を制限し、試行回数の多い攻撃を受けにくくしています。
- 監査ログ: 更新者、時刻、IPアドレス、対象記事を残し、公開後の問い合わせや差し戻し時に追跡できます。
レート制限タイムラインと監査ログの構造
Drive連携とSharpによる自動画像最適化パイプライン
DownloadEx
DownloadExは、ダウンロード先の振り分けをChrome上で自動化する拡張機能です。Manifest V3の制約下で、ルール保存、優先度、正規表現、安全な復旧導線を整理しました。
成果: ルール作成UI、URL/拡張子判定、優先度処理、コンテキストメニュー連携を実装し、Chrome Web Storeの審査を通過できる拡張機能として公開可能な品質まで仕上げました。
手作業の振り分けをMV3で自動化する
日常のブラウジングで繰り返すダウンロード整理を減らすために開発したChrome拡張機能です。Chromeウェブストアで一般公開し、一般公開を前提に、UIとルール評価の挙動を実運用向けに整理しました。
ダウンロードの振り分けルール一覧UI
細かな条件設定が可能なルール作成ダイアログ
誰でも直感的に操作できるシンプルなインターフェースを心がけて設計しました。
MV3のService Worker制約に合わせた設計にし、ブラウザ再起動後もルールが安定して動くようにしました。
設計の意図
個人用途の便利さに加えて、公開拡張機能として安定して審査・運用できる構成を重視しました。MV3の制約を前提に、ルール評価の責務を小さく保った点が核です。
公開して使える形にする難しさ
実装の難所は機能追加そのものより、MV3 特有のライフサイクルと UI
側の整合性でした。options.js
の未定義エラー、モーダルロジックの欠落、background.js
の構文エラー修正、右クリック時のドメイン名挙動変更、振り分けエラー修正などを繰り返し、イベント駆動でつながる複数画面の整合を保つ難しさに直面しました。
また、v1.3 系では日付サブフォルダ、ヘルプ整備、日本語 UI の再設計、性能最適化まで連続で手を入れており、ルール機能だけでなく「迷わず設定できるか」が使われるかどうかを決めると感じています。次回はルール評価と設定UIの結合部に対して、保存・編集・削除・コンテキストメニュー起点の E2E テストを早めに置くと、細かな回帰を減らしやすいです。
実装の深堀り:MV3 の制約に合わせたルール評価の組み立て
利用者に見える価値は、ブラウザ再起動後もルールが安定して働き、審査要件を満たしたまま整理を自動化できることです。だから、MV3前提の制約を内部設計で吸収しました。
DownloadEx は Manifest V3 の Service Worker 上で動くため、常駐プロセスを前提にした設計が使えません。そこで、ルールは常に保存済みデータから復元し、その場で評価して結果だけを返す流れにしました。
1. Service Worker 起動前提のステートレス設計
振り分けルールは
chrome.storage.local に保存し、chrome.downloads.onDeterminingFilename
発火時に読み出して評価します。メモリ上の状態を当てにしないことで、Workerが停止・再起動しても動作を変えずに保てます。
// background.js: ステートレスなダウンロードフック
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
// Service Workerが起動するたびにストレージから最新ルールを非同期でロード
chrome.storage.local.get(['rules']).then((result) => {
const rules = result.rules || [];
const matchedRule = evaluateRules(item, rules);
if (matchedRule) {
// 動的変数(日付など)を展開し、OSの禁止文字をサニタイズ
const newPath = sanitizeAndFormatPath(matchedRule.path, item);
suggest({ filename: newPath, conflictAction: 'uniquify' });
} else {
suggest(); // ルールに合致しない場合は標準の挙動
}
});
return true; // 非同期で suggest を呼び出すために必須
});
この構成にすると、ブラウザ側のライフサイクル制約をアプリ側で吸収できます。ルール数が増えても、評価入口が1か所にまとまっているため保守しやすい点も利点でした。
2. 正規表現ルールの安全性確保
公開拡張機能では、ユーザー定義の正規表現が重すぎるとダウンロード処理全体へ影響します。なので、ルール評価前にパターンを検証し、極端に負荷が高い条件を避ける仕組みを入れています。
カタストロフィックバックトラッキングの防止機構
MV3 Service
Workerのライフサイクルと非同期ルーティング
PNGtoTGA (High-speed Converter)
PNGtoTGAは、PNGからTGAへの変換をブラウザ内で完結させるWebコンバーターです。WebWorker、TGAヘッダ、Transferable Objects、ZIP生成を組み合わせました。
成果: WebWorker並列処理とZIP生成を組み合わせ、複数PNGのTGA変換をブラウザ内だけで完了できるツールにしました。
重い画像変換をブラウザ内で止めずに回す
ブラウザ上でPNG画像をTGA形式へ変換する実験的なツールです。メインスレッドを停止させず、快適なUXを保ったまま数メガバイトのバイナリ処理を扱うことを目標にしました。
Transferable Objects によるゼロコピー通信
ライブラリに頼らず、TypedArray(Uint8Array等)を用いてTGAフォーマットのヘッダーからピクセルデータまでを直接バイナリレベルで構築。
WebWorkerによるマルチスレッド処理に加え、Transferable Objectsでスレッド間のメモリコピーをなくしています。大きいファイルを複数変換するときでもメモリが膨らみにくくなりました。
設計の意図
フロントエンドで重い処理をどう逃がし、待ち時間の体感を悪化させないかを試した実験です。バイナリ操作とスレッド間通信の最適化を、体験面とセットで検証しています。
小さく見えて難しかった点
このプロジェクトでは、まずは動く実験を素早く前進させ、その後で整理する進め方をとりました。ブラウザ内バイナリ処理では、TGA ヘッダ仕様の理解、RGBA から BGRA への並べ替え、上下反転、ZIP 生成、Worker 間の受け渡しが一度に絡むため、処理自体は小さく見えても検証観点は多くなります。
反省点としては、試作速度を優先したぶん、後から変更意図を追いにくいところが残りました。次回はヘッダ構築、ピクセル変換、Worker プール、ZIP 出力をコミット単位でもう少し分け、画像破損時に切り分けやすいテストデータと計測ログを先に揃えると、実験成果を他案件へ再利用しやすくなります。
実装の深堀り:WebWorker で組んだ TGA 変換パイプライン
PNGtoTGA では、画像変換そのものよりも、ブラウザ上で重いバイナリ処理をどう扱うかを検証しました。UIを止めないことを前提に、Worker側でTGAを組み立て、結果だけをメインスレッドへ返します。
1. DataView でTGAヘッダを直接組み立てる
変換処理は外部ライブラリに任せず、DataView
を使ってTGAヘッダを書き込み、ピクセルデータも自前で並べ直しています。フォーマット仕様を直接扱うことで、どこで変換コストがかかるかを把握しやすくしました。
// converter-worker.js: TGAヘッダのバイナリ構築
const header = new DataView(new ArrayBuffer(18));
// TGA_IMAGE_TYPE_UNCOMPRESSED_TRUE_COLOR = 2
header.setUint8(2, 2);
// Width & Height (リトルエンディアンで16bit書き込み)
header.setUint16(12, width, true);
header.setUint16(14, height, true);
// Bits Per Pixel (32 bpp: RGBA)
header.setUint8(16, 32);
ヘッダ生成とピクセル配置を分けて管理できるため、画像が壊れた場合も原因を追いやすく、フォーマット変換ツールとしての見通しが良くなりました。
2. Transferable Objects でコピーを避ける
変換後のTGAバッファはサイズが大きくなりやすいため、メインスレッドへ戻す際は Transferable Objects を使って所有権ごと移しています。コピー回数を減らすことで、複数ファイル処理時のメモリ負荷を抑えました。
// WebWorker側からメインスレッドへの転送
const tgaBuffer = constructTGABinary(pixels, width, height);
// 第2引数の配列にバッファを指定することで、コピーせず所有権を移譲する(ゼロコピー)
self.postMessage({ type: 'DONE', payload: tgaBuffer }, [tgaBuffer]);
あわせて、RGBA から BGRA への並べ替えと上下反転を1パスで済ませるようにし、余分な中間バッファは持ちません。結果として、ブラウザ内処理でも複数画像をまとめて扱いやすい構成になりました。
1パスでのピクセルフォーマット変換と反転処理
OffscreenCanvasとゼロコピー通信を用いたバイナリ変換フロー