Specifications for a local anonymous message board
浜松市文化深耕材団========================================
地方都市型匿名掲示板 システム仕様書
========================================
この仕様書の目的:
地域コミュニティ向け匿名掲示板を、生成AIへの指示だけで構築できるよう、
実装に必要な仕様をすべて記述する。数値・名称はすべて末尾の「設定項目一覧」
で一元管理し、自由に変更できる。
----------------------------------------
1. サービス概要
----------------------------------------
[コンセプト]
特定の地域に根ざした、短文投稿型の匿名掲示板。ユーザー登録・ログイン不要で、
誰でも即座に投稿できる。投稿はリアルタイムで全員に共有される。
地域の日常・文化・表現活動に特化し、炎上や商業利用を構造的に排除する設計とする。
[対象ユーザー]
- 地域在住・在勤・在学の一般市民
- 地域の文化・アート・日常に関心を持つ人
- 匿名性を前提とした緩やかなコミュニティ参加者
[技術スタック(推奨)]
サーバーサイド : PHP 7.4以上
データ保存 : JSONファイル(データベース不要)
フロントエンド : HTML / CSS / Vanilla JavaScript
ホスティング : 共有レンタルサーバー(Lolipop等)
画像形式 : WebP(GDライブラリによる変換)
----------------------------------------
2. ファイル構成
----------------------------------------
/(ルートディレクトリ)
├── index.php メインページ(最新投稿一覧)
├── pages.php ページ送り付き全投稿一覧
├── search.php 検索ページ
├── fetch.php 投稿データ取得API(JSON返却)
├── post.php 投稿受付・保存処理
├── random_past.php ランダム過去投稿表示
├── sitemap.php サイトマップ生成
├── .htaccess URLリライト設定
│
├── data/
│ ├── data.json 全投稿データ(アーカイブ込み)
│ ├── data_latest.json 最新N件キャッシュ
│ ├── meta_count.json 総投稿数カウンター
│ └── blacklist.json 期限切れ投稿IDリスト(自動生成)
│
├── uploads/
│ └── latest/ 投稿画像の保存先
│ └── archive/ ローテーション後の旧画像
│
└── cache/
└── view_*.json ページ表示キャッシュ
----------------------------------------
3. データ構造
----------------------------------------
[投稿オブジェクト(data.json の各要素)]
{
"id": "abc123def456",
"text": "投稿本文テキスト",
"image": "uploads/latest/abc123.webp",
"reply_to": "xyz789",
"reply_text": "返信先の冒頭16文字",
"timestamp": 1700000000,
"date": "2024-11-15 10:30:00"
}
id : ランダム生成の12文字英数字(投稿時に生成)
text : 投稿本文(最大文字数は設定値に従う)
image : 画像ファイルパス。画像なしの場合は null
reply_to : 返信先投稿ID。返信でない場合は null
reply_text : 返信先投稿の冒頭テキスト(表示用)。返信でない場合は null
timestamp : UNIXタイムスタンプ
date : 表示用日時文字列(Y-m-d H:i:s 形式)
[meta_count.json]
{ "count": 1234 }
総投稿数のカウンター。投稿のたびにインクリメントする。
[blacklist.json]
["abc123def456", "xyz789uvw012"]
期限切れ投稿のIDリスト。post.php の投稿処理時に自動生成・更新される。
----------------------------------------
4. 投稿機能(post.php)
----------------------------------------
[投稿フロー]
クライアント(フォーム送信)
↓ multipart/form-data POST
post.php
├─ 入力バリデーション
├─ 画像処理(アップロードがある場合)
├─ 投稿オブジェクト生成
├─ ファイルロック取得
│ ├─ data.json 読込
│ ├─ 新規投稿を先頭に追加
│ ├─ data.json 書込
│ ├─ data_latest.json 更新(最新N件)
│ └─ meta_count.json インクリメント
├─ ファイルロック解放
├─ 画像ローテーション(archive_rotate)
└─ レスポンス返却(JSON)
[バリデーションルール]
本文 : 1文字以上、最大文字数以内(全角換算)。空白のみ不可
文字数カウント: 全角1文字=1、半角2文字=1として換算
画像 : アップロードは任意。JPEG / PNG / GIF / WebP のみ受付
画像サイズ : 最大ファイルサイズ以内(設定値)
返信先ID : 任意。存在チェックは省略可(表示時にフォールバック)
[画像処理]
1. アップロードされた画像をGDライブラリで読み込む
2. 長辺が最大画像サイズ(設定値)を超える場合はリサイズ(アスペクト比維持)
3. グレースケール変換(imagefilter(IMG_FILTER_GRAYSCALE))
4. コントラスト強調(imagefilter(IMG_FILTER_CONTRAST, -15))
5. WebP形式で保存(uploads/latest/ 以下)
6. ファイル名はランダム生成(IDと同一文字列 + .webp)
設計意図: グレースケール化により投稿者が特定されにくくなり、
また掲示板全体の視覚的統一感が生まれる。カラー画像が必要な場合は
このステップを省略する。
[画像ファイルのローテーション(archive_rotate)]
uploads/latest/ に保存されている画像ファイル数が上限(設定値)を超えたとき、
古いものから uploads/latest/archive/ に移動する。
----------------------------------------
5. データ取得API(fetch.php)
----------------------------------------
エンドポイント: GET fetch.php?etag=<前回ETag値>
[レスポンス(更新あり 200 OK)]
{
"posts": [ 投稿オブジェクトの配列(最新N件) ],
"count": 1234,
"etag": "新しいETag値"
}
[レスポンス(更新なし 304 Not Modified)]
レスポンスボディなし。ETag ヘッダーのみ返却。
[ETag生成]
$etag = md5($count . '_' . $latest_timestamp);
----------------------------------------
6. フロントエンド(index.php)
----------------------------------------
[初期表示]
ページ読み込み時に fetch.php を叩き、最新投稿を取得して描画する。
サーバーサイドでの初期HTML生成は行わず、すべてJavaScriptで動的にDOMを構築する。
[自動更新(ポーリング)]
再帰的 setTimeout により一定間隔で fetch.php を叩く。
ETag が一致すれば何もしない。
経過時間 → 更新間隔
起動〜アイドル閾値1まで → 短い間隔(設定値)
アイドル閾値1〜閾値2まで → 中間隔(設定値)
アイドル閾値2以降 → 長い間隔(設定値)
アイドル時間は mousemove / touchstart / keydown イベントでリセットする。
[投稿カードのHTML構造]
<div class="card" data-id="abc123">
<div class="word">
<div class="text" dir="auto">投稿本文</div>
<div class="meta">
<span class="date">2024-11-15 10:30</span>
</div>
</div>
<!-- 画像がある場合 -->
<div class="pic-wrap">
<img src="uploads/latest/abc123.webp" loading="lazy" alt="">
</div>
<!-- 返信がある場合 -->
<div class="reply-line">
<a href="#xyz789">↩ 返信先テキスト冒頭…</a>
</div>
</div>
dir="auto" により、アラビア語・ヘブライ語など右から左に書く言語を
自動判定して正しく表示する。
[URLの自動リンク化]
本文中のURLを <a> タグに変換する。ただし、URLの末尾が日本語文字
(Unicode範囲 \u3000-\u9FFF 等)の場合はURLとして認識しない
(正規表現で除外)。
[プルダウンによるハッシュ(URL末尾 #id)の除去]
モバイルでの操作性向上のため、画面上部からの下方向スワイプでURLハッシュを除去する。
- 縦方向の移動量が閾値(設定値)以上
- 縦移動が横移動の2倍以上(横スワイプを誤検知しない)
- URLに # が含まれている場合のみ発動
[投稿フォーム]
本文入力欄 : <textarea>。文字数カウンターをリアルタイム表示
画像選択 : <input type="file">。選択後にプレビュー表示
返信先ID : 隠しフィールド。カードの返信ボタン押下で自動セット
送信ボタン : 送信中はdisable化してダブル送信を防止
----------------------------------------
7. ページ一覧(pages.php)
----------------------------------------
全投稿を時系列逆順でページ送り表示する。data.json を読み込み、
1ページあたりの表示件数(設定値)で分割する。
URLパラメータ: pages.php?page=2
前ページ・次ページのリンクを表示する。現在ページ番号と総ページ数も表示する。
[返信リンクのクロスページ対応]
返信先の投稿が別ページにある場合、pages.php?page=N#id 形式でリンクする。
ページ番号はID一覧から逆算する。
----------------------------------------
8. 検索機能(search.php)
----------------------------------------
- data.json の全投稿に対して本文の部分一致検索
- URLパラメータ ?q=キーワード で受け取る
- ヒット件数を表示する
- 1ページあたりの表示件数(設定値)でページ送り
- pages.php と同じカードUIで表示する。ヒット部分のハイライトは任意実装
----------------------------------------
9. ランダム過去投稿(random_past.php)
----------------------------------------
data.json の全投稿からランダムに選んで表示する。
懐古的・発見的な閲覧体験を提供する。
[フィルタリング]
以下をすべて除外してからランダム選択する:
1. 最新N件(data_latest.json と重複する投稿)— 任意
フィルタ後のプールが空の場合は全投稿からランダム選択する(フォールバック)。
1リクエストあたり設定件数(設定値)を表示する。
----------------------------------------
10. 返信スレッド機能
----------------------------------------
[返信の仕組み]
- カードに「返信」ボタンを表示する
- 押下するとフォームに reply_to フィールドがセットされ、返信先IDが記録される
- 投稿時に reply_text(返信先の冒頭16文字)を
data_latest.json → data.json の順に検索して取得・保存する
- 古い投稿で reply_text が存在しない場合は、
ID→テキストのマップをJavaScript側で保持してフォールバック表示する
[返信カードの表示]
返信カードには返信先へのアンカーリンクを表示する(↩ アイコン + 冒頭テキスト)。
----------------------------------------
11. SEO・メタ情報
----------------------------------------
任意
[ページ別タイトル・説明文]
pages.php の各ページで表示範囲の投稿内容から動的にdescriptionを生成する。
----------------------------------------
12. 多言語対応(任意)
----------------------------------------
サイトの利用案内を複数言語で提供する。
推奨言語: 日本語、英語、ポルトガル語(ブラジル)、繁体字中国語、ベトナム語、マレー語/インドネシア語。
地域の外国人居住者構成に合わせて選択する。
----------------------------------------
13. セキュリティ・運用
----------------------------------------
[入力のサニタイジング]
サーバーサイド: htmlspecialchars() を全出力に適用
JavaScript : textContent を使用し innerHTML を避ける
[ファイルロック]
data.json への書き込みは必ず flock() による排他ロックを使用する。
同時投稿による競合を防ぐ。
[レートリミット(任意実装)]
IPアドレスベースで一定時間内の投稿回数を制限する。
セッションや data/rate_limit.json で管理する。
[スパム対策]
- 本文が空・空白のみの投稿を拒否
- 同一テキストの連続投稿を拒否(任意)
- 管理者用の削除スクリプトを別途用意する(任意)
----------------------------------------
14. パフォーマンス
----------------------------------------
data_latest.json : 全件読み込みを避けるため最新N件を別ファイルにキャッシュ
ETag / 304 : 変更がない場合はボディを返さない
画像 lazy loading: <img loading="lazy"> を全画像に付与
ページキャッシュ : pages.php のHTML出力を cache/view_N.json にキャッシュ(任意)
========================================
15. 設定項目一覧
========================================
使い方: 以下の値を変更することでサイトの動作を調整できる。
コード内では定数または設定ファイルで一元定義すること。
---- 投稿・データ関連 ----
定数名(例) デフォルト値 説明
MAX_CHARS 80 投稿の最大文字数(全角換算)
LATEST_KEEP 110 data_latest.json に保持する件数
RETURN_KEEP 100 fetch.php が返す最大件数
IMAGE_MAX_SIZE 420 画像の長辺最大サイズ(ピクセル)
IMAGE_MAX_BYTES 5242880 画像の最大ファイルサイズ(バイト、デフォルト5MB)
IMG_ROTATE_LIMIT 200 uploads/latest/ の最大ファイル数
REPLY_TEXT_LEN 16 返信先の冒頭何文字を保存・表示するか
---- 表示・ページング関連 ----
定数名(例) デフォルト値 説明
PER_PAGE 100 1ページの表示件数(pages.php / search.php)
PER_PAGE_RANDOM 20 random_past.php の1回表示件数
REPLY_PREVIEW_LEN 16 カード上に表示する返信先テキスト長
---- 自動更新(ポーリング)関連 ----
定数名(例) デフォルト値 説明
POLL_NORMAL_MS 10000 通常時の更新間隔(ミリ秒、10秒)
IDLE_THRESHOLD_1_MS 900000 アイドル閾値1(ミリ秒、15分)
POLL_IDLE_MS 30000 アイドル時の更新間隔(ミリ秒、30秒)
IDLE_THRESHOLD_2_MS 3600000 アイドル閾値2(ミリ秒、1時間)
POLL_LONG_IDLE_MS 60000 長期アイドル時の更新間隔(ミリ秒、1分)
---- UI操作関連 ----
定数名(例) デフォルト値 説明
PULLDOWN_THRESHOLD_PX 150 プルダウン検知閾値(ピクセル)
---- サイト情報 ----
定数名(例) デフォルト値 説明
SITE_NAME (任意) サイト名。タイトルタグ・OGPに使用
SITE_URL (任意) サイトURL(末尾スラッシュなし)
SITE_REGION (任意) 地域名。メタdescriptionや案内文に使用
ADMIN_EMAIL (任意) 管理者メールアドレス
========================================