Specifications for a local anonymous message board

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 (任意) 管理者メールアドレス


========================================

Report Page