y 松本 侑真

How this works

Shorts のしくみ

この個人サイトは SSR サーバを持たない純静的サイト ですが、スマホから短い投稿(short)をその場で公開できます。バックエンドは Cloudflare Pages Functions が1つだけ。仕組みはシンプルです。

  1. 01

    スマホで書く

    ホーム画面に追加した /shorts/new を開き、本文を Markdown で書く。コードブロックやリンクの挿入ツールバー付き。

  2. 02

    GitHub に commit

    「投稿」を押すと Cloudflare Pages Functions が frontmatter を自動生成し、GitHub に .md を1件コミットする。

  3. 03

    自動ビルド

    push を検知して Cloudflare が静的サイトを再ビルド。SSR サーバは持たず、あくまで純静的サイトのまま。

  4. 04

    数分で公開

    /blog/shorts に反映される。編集・削除も同じ画面から。全部モバイルで完結する。

セキュリティ

投稿画面は誰でも開けて、フォームも試せます。ただし実際の書き込み (投稿・編集・削除)を行う API は Cloudflare Access で保護されていて、管理者(Yuuma Matsumoto)だけが成功します。未認証の書き込みは エッジで弾かれ、サーバには届きません。

Editor

書き心地(エディタ)のしくみ

長めの文章も気軽に書けるように、投稿画面はエディタのように振る舞います。 下書きの自動保存、書いた分だけ伸びる本文欄、全画面の集中モード、Tab インデントや ⌘Enter 投稿などのキー操作。ライブラリは使わず、ブラウザ標準の <textarea> に少しずつ手を足しています。

下書きの自動保存

入力が 600ms 止まったら localStorage に保存し、再訪時に復元する。加えて、タブを閉じる・アプリを切り替える瞬間(pagehide / visibilitychange)にも待たずに保存する。モバイルのブラウザは裏に回ったタブを予告なく破棄することがあり、beforeunload は発火しないことが多い。確実に呼ばれるのは visibilitychange だけなので、そこを保存の主経路にしている。

content.addEventListener("input", () => {
  clearTimeout(timer);
  timer = setTimeout(flushDraft, 600);
});
window.addEventListener("pagehide", flushDraft);
document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") flushDraft();
});

新規と編集で下書きを混ぜない

保存するのは新規投稿の書きかけだけ。過去の short の編集に入るときは、書きかけを先に保存してからフォームを上書きし、編集をやめたら戻す。投稿に成功したら破棄する。編集中に未保存のまま離脱しようとした場合だけ確認ダイアログを出す(新規はもう自動保存済みなので確認しない)。

書いた分だけ伸びる本文欄

高さを一度 auto に戻してから scrollHeight を入れ直すと、内容ぴったりに伸び縮みする。ただし auto に戻した瞬間にページが縮んでスクロール位置が飛ぶので、前後で scrollY を保存・復元している。

const y = window.scrollY;
el.style.height = "auto";
el.style.height = el.scrollHeight + 2 + "px";
window.scrollTo(0, y);

集中モード

別ページや別の入力欄は作らず、同じ textarea を囲む要素にクラスを1つ付けて position: fixed で全画面にしているだけ。入力中の内容・カーソル・undo 履歴がそのまま引き継がれる。高さには 100vh ではなく 100dvh を使う(モバイルのアドレスバーの伸縮に追従する)。z-index はサイトヘッダー(60〜90)より上にし、背面のページは html の overflow: hidden でスクロールを止める。

undo を壊さないテキスト挿入

Tab インデントやツールバーで textarea.value を直接書き換えると、ブラウザの undo 履歴が消えて ⌘Z が効かなくなる。document.execCommand("insertText") は非推奨扱いだが、今でもネイティブの undo に積める唯一の手段なので、これを優先して使い、失敗したときだけ setRangeText に落とす。

const insertText = (text) => {
  if (!document.execCommand("insertText", false, text)) {
    el.setRangeText(text, el.selectionStart, el.selectionEnd, "end");
  }
};

日本語入力(IME)を邪魔しない

Tab / ⌘Enter / ⌘S / ⌘B / ⌘K を keydown で拾っているが、変換中のキー入力は event.isComposing が true になる。これを最初に見て何もしないようにしておかないと、変換確定の Enter で誤投稿したり、変換候補の操作を横取りしたりする。

content.addEventListener("keydown", (e) => {
  if (e.isComposing) return;
  // …ショートカット処理
});

なぜエディタライブラリを入れないか

CodeMirror や Monaco を入れれば機能は増えるが、素の textarea は日本語 IME・モバイルの入力補助・音声入力・アクセシビリティをブラウザがそのまま面倒を見てくれる。書くのは短めの Markdown で、欲しかったのは「消えない・広い・キーが効く」の3点だけなので、依存ゼロ・200 行ほどの素の JS で足りると判断した。