個人ブログ機能を実装した

概要

以前から構想していた「テキスト中心の個人ブログ」を実装した。開発ブログ(技術記録)とは別に、随筆や日記を気軽に書ける場所が欲しかった。

固有の UI を避けてオリジナルで設計した。設計書を先に書き、それを Astro + Cloudflare D1 環境に移植する形で開発した。


デザインシステムの設計

まず CSS カラートークンを定義した。既存サイトの変数と分離するため、ブログ専用のスコープで管理している。

--paper:      #FAFAF8;   /* 目に優しいオフホワイト */
--ink:        #1F1F1D;   /* 本文・見出し */
--ink-soft:   #4F4D48;   /* サブテキスト */
--ink-mute:   #8A8780;   /* メタ情報 */
--line:       #E4E2DA;   /* 罫線 */
--accent:     #6B5B3E;   /* 暖色アクセント */

フォントは Zen Kaku Gothic New(Google Fonts)を 300/400/500/700 の 4 ウェイトで読み込んでいる。

本文サイズは 17px・line-height 2.0 と、読み物として余裕を持たせた。


データ設計

既存のブログ(開発ブログ)とテーブルを分離し、専用の 3 テーブルを作成した。

テーブルは初回アクセス時に CREATE TABLE IF NOT EXISTS で自動作成される。ローカル開発でもマイグレーションを手動で走らせる必要がない。


本文のパースとレンダリング

エディタ内部では Markdown に近い plain text を使い、保存時にブロック配列 JSON に変換する。

## 見出し     → { type: "h2", text }
> 引用        → { type: "quote", text }
---           → { type: "hr" }
![C](URL)     → { type: "image", src, caption }
@youtube(ID)  → { type: "embed", kind: "youtube", id }
@card(U|T|D|S)→ { type: "card", url, title, description, siteName }

パース済み JSON はサーバーサイドで HTML に変換して set:html で出力する。XSS 対策として、すべてのテキストフィールドはエスケープしてから出力している。外部 URL は http:// または https:// のみ許可し、それ以外は無視する。


全画面エディタ

管理画面の記事編集は、公開ページとは切り離した全画面エディタで行う。

800ms デバウンスで入力するたびに自動保存するようにした。これにより、「公開する」だけでなく書いている途中の状態も失われない。


URL 検出と埋め込みプロンプト

本文の現在行に URL が含まれる場合、画面下に黒いプロンプトを表示する。

「カードで埋め込む」を選ぶと、サーバーサイドで OGP を取得し、タイトル・説明・サイト名を自動入力する。


カテゴリ管理 UI

カテゴリ管理画面で工夫した点:


公開ページの設計

一覧ページは年ごとのセクション分けと、「新しい順 / 古い順」トグルを実装した。

記事行の構成は 日付(78px 固定)| 点線フィラー | タイトル(最大60%) のレイアウト。点線フィラーは flex: 1 で余白を埋めるシンプルな実装だが、スマートフォンでは非表示にしてタイトルを折り返す。

カテゴリフィルタータブは URL パラメータ(?category=slug)で状態を管理する。アクティブタブの下線と行罫線が同じ Y 座標で重なる仕様は、position: relative + ::beforebottom: -1px に 1px の線を引いて実現した。


リアクションボタン

「いいね」的な機能として、絵文字なしのリアクションボタンを実装した。押下で状態が反転し(背景が墨色になる)、localStorage でリアクション済み状態を保持する。カウントはサーバーへ非同期で送信して更新する楽観的更新方式。


セキュリティ上の工夫


振り返り

デザインシステムの設計書(SPEC)を先に書いた上で実装に入ったのが効率的だった。カラートークン・タイポグラフィ・レイアウト定数が固まっていると、実装中に迷いが生じない。

エディタの「自動保存 + 手動公開」の分離は書き心地を高める上で重要だと改めて感じた。