---
name: framed-post
description: Framed!（framed.dog）に投稿する HTML の作品・ミニゲームを作るときに使う。投稿は sandbox の iframe で動くので、外部スクリプト・通信・localStorage・alert が使えない。全画面のフィードでアプリの UI が重なる領域と、スコアボード・ユーザーデータのフレーム API（window.framed）の使い方をまとめている。
---

# Framed! に投稿する HTML を作る

Framed! の投稿は **1 枚の HTML 文書**です。CSS と JavaScript を含められますが、
投稿ごとに別のオリジンの `<iframe sandbox="allow-scripts">` の中で動きます。
作る前に、次の制約を守れる設計にしてください。

## 1 枚の HTML に収める

- 全体で **262,144 文字 まで**。
- **`<script>` と `<style>` はインラインで書く。** `<script src>` の外部スクリプトは読み込めない
  （CDN のライブラリも不可）。必要なら中身を貼り込むか、自分で書く。
- 画像・フォント・音声・動画は `https:` の URL と `data:` / `blob:` なら読める。
  CSS の `@import` や `<link rel="stylesheet">` の外部ファイルは読めない。
- `<!doctype html>` と `<meta name="viewport" content="width=device-width, initial-scale=1">` を付ける。

## フレームの中で使えないもの

| 使えないもの | 代わりに |
| --- | --- |
| `fetch` / XHR / WebSocket / EventSource（CSP `connect-src 'none'`） | データは HTML に埋め込む。保存は `framed.userData` |
| `localStorage` / `sessionStorage` / IndexedDB / Cookie（オリジンが `null`。触ると例外） | `framed.userData`。触るなら `try` で包む |
| `alert` / `confirm` / `prompt` | 画面の中に自前のダイアログを描く |
| `<form>` の送信 | `submit` を `preventDefault` して JavaScript で処理する |
| 上のウィンドウの移動（`top.location`） | リンクは自動で新しいタブで開く |
| カメラ・マイク・位置情報・全画面 API・ポインタロック | 使わない設計にする |

## 大きさ

- **フレームの高さは中身の高さに合わせて伸び縮みする。** タイムラインでは 24rem（全画面のフィードでは 75vh）で止まり、
  超えた分はフレームの中でスクロールする。閲覧者は「全体を見る」で画面いっぱいに広げられる。
- **`body` や一番外の箱を `100vh` / `100dvh` で組まない。** フレームを伸ばすほど中身も伸び、
  見切られて 16:9 に戻される。ゲームの画面は `width: 100%; aspect-ratio: 16 / 9;` のように幅から決める。
- 幅は 320px から横長の PC まで変わる。横スクロールが出ないよう、固定の幅を持たせない。

## 全画面のフィードで重なる領域

全画面のフィードでは投稿が画面いっぱいに出て、その上にアプリの UI が重なる。
**次の領域に、押す必要のある要素や読ませたい文字を固定しない**（`position: fixed` / `sticky` で端に貼り付けない）。

- **上端 60px**（＋ステータスバーの分 `env(safe-area-inset-top)`）: 左右に隣のタブへ移る導線。
  タッチの端末では、この帯全体が払いを受けるので、フレームには触れない。
- **右下 幅 72px × 高さ 420px**（＋`env(safe-area-inset-bottom)`）: 前 / 次の投稿、
  投稿者、いいね、コメント、共有、メニューの縦の列。
- **左右の端 16px**（タッチの端末）: 隣のタブへ払うための面。

固定の UI（スコア・ボタン）は**左上から少し下げた位置か、下端の左寄り**に置く。例:

```css
.hud {
  position: fixed;
  top: calc(60px + env(safe-area-inset-top));
  left: max(24px, env(safe-area-inset-left));
}
.controls {
  position: fixed;
  bottom: calc(16px + env(safe-area-inset-bottom));
  left: max(24px, env(safe-area-inset-left));
  right: 88px;
}
```

## 操作

- 中身が `touch-action: none` で払いを全部受けても、閲覧者は右下の列のボタンで前後の投稿へ移れる。
  それでも、**ゲームの操作面以外は `touch-action` を奪わない**（縦に払って次の投稿へ進めるように）。
- マウスとタッチの両方で操作できるようにする（Pointer Events を使う）。キーボードでも遊べるとなおよい。
- 音は利用者が操作してから鳴らす（自動再生はブラウザに止められる）。

## フレーム API（window.framed）

配信するときに `window.framed` が差し込まれる。**どの関数も Promise を返し、reject しない。**
失敗は `{ ok: false, reason }` で返るので、必ず `ok` を見て分岐する。

```js
framed.scoreboard.submit(score, { board, order }) // → { ok, best, rank, improved, order }
framed.scoreboard.top({ board, limit })            // → { ok, entries, total, order }
framed.scoreboard.me({ board })                    // → { ok, entry, total }
framed.userData.get(key)                           // → { ok, value }   無ければ value は null
framed.userData.set(key, value)                    // → { ok }
framed.userData.remove(key)                        // → { ok, removed }
framed.userData.keys()                             // → { ok, keys }
```

- **使えるのは、サインインしている人が、公開済みの投稿を本体の画面で見ているときだけ。**
  未サインインは `unauthenticated`、投稿前のプレビューは `unavailable`、本体の外では 10 秒待って `unavailable`。
  API が使えなくても遊べるように作る（順位表を隠す・進み具合をその場限りにする）。
- スコアボード: 名前（省けば `default`）ごとに 1 人 1 つの自己ベスト。1 投稿 10 個まで。
  スコアは整数。`order` は `desc`（大きいほど上。既定）か `asc`（小さいほど上。タイム）で、最初の送信で決まり変えられない。
  `top` は既定 10 人、100 人まで。行は `{ rank, score, handle, displayName, isMe }`。
- ユーザーデータ: その人がその投稿の中でだけ読み書きできる JSON。1 件 64 KB・入れ子 32 段まで、
  1 人 1 投稿 50 件・合計 1.0 MB まで。
- 名前とキーは英小文字・数字・ハイフンで 40 文字まで（先頭と末尾にハイフンは不可）。
- 回数: 書き込み（submit / set / remove）は 1 人 1 投稿で 1 分に 30 回、読み出しは 1 人 1 分に 120 回。
  同時に 4 件まで。**毎フレーム・毎操作で送らない。** ゲームオーバーやステージの区切りでまとめて送る。
- `reason` の値: `unauthenticated` / `unavailable` / `invalid_request` / `rate_limited` / `busy` / `order_mismatch` / `too_many_boards` / `too_large` / `too_deep` / `too_many_records` / `quota_exceeded` / `error`

```js
async function finish(score) {
  const result = await framed.scoreboard.submit(score)
  if (!result.ok) {
    showMessage(result.reason === 'unauthenticated' ? 'サインインすると順位表に載ります' : '')
    return
  }
  const top = await framed.scoreboard.top({ limit: 5 })
  if (top.ok) renderRanking(top.entries) // 文字は textContent で入れる
}
```

- 順位表のハンドルと表示名は他人が決めた文字列。**`innerHTML` ではなく `textContent` で入れる。**
- スコアは自己申告で、誰でも書き換えられる。賞品を賭けるような使い方はしない。

## してはいけないこと

- フレームの隔離（sandbox・CSP）を破ろうとする、他の投稿や閲覧者のセッションへ届こうとする。
- サインイン画面や入力欄を模して、パスワードなどを入力させる。
- 暗号資産の採掘、過度な計算（止まらない重いループ）、意図しない遷移やダウンロード、端末の操作を妨げる表示。
- 権利を持たない画像・音声・フォントを使う。

## 仕上げの確認

- 320px 幅と PC 幅で崩れず、横スクロールが出ない。
- 上端と右下の領域に、押す要素や大事な文字が無い。
- `localStorage` や `fetch` を使っていない（使うなら例外で止まらない）。
- フレーム API が `unauthenticated` / `unavailable` を返しても遊べる。
- 動きは `setInterval` ではなく `requestAnimationFrame` で回す（画面外のフレームでは止まるか間引かれる）。
- 詳しくは https://framed.dog/creators
