<?xml version="1.0" encoding="utf-8" ?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ja">
<title>学ぶ、考える、書き出す。</title>
<subtitle>学習し、自分なりに噛み砕いて、書き出すブログ。</subtitle>
<id>tag:blog.kubosho.com,2014:feed</id>
<author>
  <name>kubosho</name>
</author>
<updated>2026-04-19T21:00:26.862Z</updated>
<link rel="alternate" href="https://blog.kubosho.com"/>
<link rel="self" type="application/atom+xml" href="https://blog.kubosho.com/feed"/>
<entry>
<title>Web Speed Hackathon 2026参加記</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/web-speed-hackathon-2026"/>
<id>tag:blog.kubosho.com,2026-03-30:entry:web-speed-hackathon-2026</id>
<summary>3月20日から21日にかけて開催されたWeb Speed Hackathon 2026に参加しました。</summary>
<content type="html">&lt;p&gt;3月20日から21日にかけて開催された&lt;a href=&quot;https://cyberagent.connpass.com/event/371488/&quot;&gt;Web Speed Hackathon 2026&lt;/a&gt;に参加しました。&lt;/p&gt;
&lt;p&gt;結果は &lt;strong&gt;764.80 / 1150点&lt;/strong&gt; で、&lt;a href=&quot;https://github.com/CyberAgentHack/web-speed-hackathon-2026-scoring/issues/8#issuecomment-4102937253&quot;&gt;issueに記録された時点&lt;/a&gt;では暫定32位（132人中）になりました。最終計測で自己最高スコアを出せました。&lt;/p&gt;
&lt;p&gt;ただし、レギュレーション違反によりランキングから除外されてしまいました。レギュレーション違反していなければ5位になれていた可能性があったんですけどね。トホホ〜。&lt;/p&gt;
&lt;h2 id=&quot;どこでレギュレーション違反になったか&quot;&gt;どこでレギュレーション違反になったか&lt;/h2&gt;
&lt;p&gt;違反の原因は、ユーザープロフィールのヘッダー背景色が初期状態と異なっていたことです。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UserProfileHeader.tsx&lt;/code&gt; でプロフィール画像の平均色をTailwindの動的クラス &lt;code&gt;bg-[${averageColor}]&lt;/code&gt; として適用していましたが、&lt;code&gt;@tailwindcss/browser&lt;/code&gt;（ランタイム）を &lt;code&gt;@tailwindcss/postcss&lt;/code&gt;（ビルド時コンパイル）に置き換えたタイミングで、動的に組み立てたクラスが検出できなくなっていたことに気づけませんでした。&lt;/p&gt;
&lt;p&gt;ビルド時コンパイルに置き換えるなら &lt;code&gt;style={{ backgroundColor: averageColor }}&lt;/code&gt; のようにインラインスタイルに切り替える必要がありました（&lt;code&gt;averageColor&lt;/code&gt; 自体は &lt;code&gt;useState()&lt;/code&gt; で管理されています）。&lt;/p&gt;
&lt;p&gt;とはいえレギュレーションに即しているかどうかを判定してもらえる点数を超えることはできたので、そこに至るまでの道のりを書きます。&lt;/p&gt;
&lt;h2 id=&quot;スコアを伸ばした段階&quot;&gt;スコアを伸ばした段階&lt;/h2&gt;
&lt;p&gt;今回スコアを伸ばした段階は大きく3つあります。&lt;/p&gt;
&lt;h3 id=&quot;1日目-1808時点219-49875点&quot;&gt;1日目 18:08時点「219 → 498.75点」&lt;/h3&gt;
&lt;p&gt;初期ロードに関わる改善をまとめてデプロイした結果です。積み上げた改善によってユーザーフローをテストする閾値（300点）を超えました。&lt;/p&gt;
&lt;h3 id=&quot;1日目-1857時点49875-5766点&quot;&gt;1日目 18:57時点「498.75 → 576.6点」&lt;/h3&gt;
&lt;p&gt;FFmpeg/ImageMagick WASMのdynamic importを実施し、数十MB単位でバンドルファイルを初期読み込みから除外したのと、ReDoSの修正を入れました。&lt;/p&gt;
&lt;h3 id=&quot;1日目-2226時点5766-75085点&quot;&gt;1日目 22:26時点「576.6 → 750.85点」&lt;/h3&gt;
&lt;p&gt;GIF→MP4変換（TBT 6.7s→2.5s）、波形データの事前計算によるAudioContextデコード排除（TBT 2.5s→49ms）、JPEG→AVIF変換+リサイズ（画像容量93%削減、LCP 42s→7.3s）が中心です。&lt;/p&gt;
&lt;p&gt;とりあえずこの3つをやった上で、より改善を積み重ねつつ、テストも継続的に実行して、レギュレーション違反しないかが重要です。それは毎年変わらないですね。&lt;/p&gt;
&lt;h2 id=&quot;リポジトリのクローン&quot;&gt;リポジトリのクローン&lt;/h2&gt;
&lt;p&gt;まずリポジトリをクローンしてきたときに、リポジトリのサイズがおかしそうなことに気づきました。412個のオブジェクトに対して407.12 MiBは大きく感じます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;remote: Enumerating objects: 412, done.
remote: Total 412 (delta 0), reused 0 (delta 0), pack-reused 412 (from 1)
Receiving objects: 100% (412/412), 407.12 MiB | 9.07 MiB/s, done.
Resolving deltas: 100% (39/39), done.
Updating files: 100% (354/354), done.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これは後述しますが、シードデータのメディアファイルが合計334MBあり、これがリポジトリサイズの大半を占めていました。内訳は以下の通りで、GIFとJPEG形式のファイルが入っていて、まずここが最適化できそうということが分かります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;動画（GIF）179MB&lt;/li&gt;
&lt;li&gt;画像（JPEG）89MB&lt;/li&gt;
&lt;li&gt;音声（MP3）66MB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;初期状態のアプリケーションをデプロイする&quot;&gt;初期状態のアプリケーションをデプロイする&lt;/h2&gt;
&lt;p&gt;スコア計測はアプリケーションをデプロイしないことには始まりません。&lt;/p&gt;
&lt;p&gt;しかし、運営が用意したfly.ioの環境は競技開始直後に認証周りで問題があって、ちょっとの間デプロイすることができなかったのと、自前の環境のほうが問題は少なくなりそうだったので、早々にfly.ioアカウントを作ってそこにデプロイすることを決めました。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fly apps create {アプリ名}&lt;/code&gt; でアプリを作成し、&lt;code&gt;fly deploy --app {アプリ名}&lt;/code&gt; でデプロイしました。特に詰まることなくデプロイできたので良かったです。&lt;/p&gt;
&lt;h2 id=&quot;lighthouse-ciによる継続的パフォーマンス観測をする&quot;&gt;Lighthouse CIによる継続的パフォーマンス観測をする&lt;/h2&gt;
&lt;p&gt;最適化の効果を定量的に追跡するため、&lt;a href=&quot;https://github.com/GoogleChrome/lighthouse-ci&quot;&gt;Lighthouse CI&lt;/a&gt;を導入しました。&lt;/p&gt;
&lt;p&gt;ローカルで実行するために &lt;code&gt;@lhci/cli&lt;/code&gt; をdevDependencyに追加し、&lt;code&gt;pnpm lighthouse&lt;/code&gt; でローカルサーバーに対してLighthouse CIを実行できるようにしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# application/ でサーバー起動後
pnpm lighthouse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改善施策の実装前後でこれを実行して、ターゲットのメトリクスが実際に改善したか確認していました。&lt;/p&gt;
&lt;p&gt;また、特定のページだけ計測したい場合に備えて &lt;code&gt;lighthouse:page&lt;/code&gt; スクリプトを用意し、&lt;code&gt;--collect.url&lt;/code&gt; でURLを渡して個別に計測できるようにしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ホームページだけ計測（1回実行）
pnpm lighthouse:page -- --collect.url=&quot;http://localhost:3000/&quot;

# 検索ページだけ計測
pnpm lighthouse:page -- --collect.url=&quot;http://localhost:3000/search&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ただ今回に関しては&lt;a href=&quot;https://github.com/CyberAgentHack/web-speed-hackathon-2026/tree/main/scoring-tool&quot;&gt;公式のscoring-tool&lt;/a&gt;が提供されていたため、これを実行すれば良かったです。ここはなぜか見落とした私のミスです。&lt;/p&gt;
&lt;h3 id=&quot;パフォーマンス改善サイクルを自動で回す&quot;&gt;パフォーマンス改善サイクルを自動で回す&lt;/h3&gt;
&lt;p&gt;パフォーマンス改善の進め方として「仮説→計測→変更→計測」のサイクルを&lt;a href=&quot;https://github.com/kubosho/web-speed-hackathon-2026/blob/main/.claude/rules/perf-optimization.md&quot;&gt;Claude Codeのルール&lt;/a&gt;にして、このサイクルが自動で回せるようにしていました。&lt;/p&gt;
&lt;p&gt;ルール化したことによって、Claude Code上で「この部分が遅くなってそう」とプロンプトを投げて、それに基づきClaude Codeが「何が遅いか、なぜ遅いか、どのメトリクスに反映されるか」といった仮説を立て、Lighthouse CIによる計測で裏付けてから変更を入れた後にもう一度計測をして、ターゲットのメトリクスが改善しなければrevertするフローが割と自動化できました。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;ここまでやったところで、パフォーマンス改善をやっていきます。&lt;/p&gt;
&lt;h2 id=&quot;バンドルサイズ分析&quot;&gt;バンドルサイズ分析&lt;/h2&gt;
&lt;p&gt;webpack-bundle-analyzerでバンドル構成を可視化して &lt;code&gt;main.js&lt;/code&gt; が108MBという異常なサイズであることが分かりました。&lt;/p&gt;
&lt;p&gt;サイズの大きい順に以下の通りです。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FFmpeg WASM (&lt;code&gt;ffmpeg-core.wasm?binary&lt;/code&gt;)：動画処理用&lt;/li&gt;
&lt;li&gt;ImageMagick WASM (&lt;code&gt;magick.wasm?binary&lt;/code&gt;)：画像処理用&lt;/li&gt;
&lt;li&gt;@mlc-ai/web-llm：AI推論エンジン&lt;/li&gt;
&lt;li&gt;negaposi-analyzer-ja (&lt;code&gt;pn_ja.dic.json&lt;/code&gt;）： 感情分析辞書&lt;/li&gt;
&lt;li&gt;highlight.js / refractor：シンタックスハイライト（全言語入り）&lt;/li&gt;
&lt;li&gt;kuromoji / katex：重量級ライブラリ群&lt;/li&gt;
&lt;li&gt;moment / lodash / jquery / core-js / bluebird：現代基準でいらないライブラリ&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;webpackrspackへ移行し複数の問題をいっぺんに解消する&quot;&gt;Webpack→Rspackへ移行し、複数の問題をいっぺんに解消する&lt;/h2&gt;
&lt;p&gt;Webpackの設定も問題が多かったです。一部を紹介すると以下の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mode: &quot;none&quot;&lt;/code&gt;：minification無効&lt;/li&gt;
&lt;li&gt;&lt;code&gt;optimization.minimize: false&lt;/code&gt;：圧縮無効&lt;/li&gt;
&lt;li&gt;&lt;code&gt;optimization.splitChunks: false&lt;/code&gt;：コード分割無効&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;問題は他にもありますが、こういった問題を1つずつ直すより、Rspackに移行して設定をいい感じにするのが手っ取り早いと判断し、Rspackに移行したのち設定をいい感じにしました。&lt;/p&gt;
&lt;p&gt;Rspack移行後に変えた設定は以下の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;builtin:swc-loader&lt;/code&gt; の使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode: &quot;production&quot;&lt;/code&gt; への移行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;splitChunks: { chunks: &quot;all&quot; }&lt;/code&gt; でコード分割&lt;/li&gt;
&lt;li&gt;&lt;code&gt;devtool: false&lt;/code&gt; でソースマップをバンドルから除外&lt;/li&gt;
&lt;li&gt;エントリーポイントから &lt;code&gt;core-js&lt;/code&gt;, &lt;code&gt;regenerator-runtime&lt;/code&gt; を除去&lt;/li&gt;
&lt;li&gt;CSSは &lt;code&gt;CssExtractRspackPlugin&lt;/code&gt; + &lt;code&gt;css-loader&lt;/code&gt; + &lt;code&gt;postcss-loader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HtmlRspackPlugin&lt;/code&gt; で &lt;code&gt;inject: true&lt;/code&gt; にしてJS/CSSを自動的に注入&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これによる結果は以下の通りになりました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ビルド時間：15秒以上 → 0.9秒&lt;/li&gt;
&lt;li&gt;エントリーポイント合計：108MB → 12.3MB
&lt;ul&gt;
&lt;li&gt;FFmpegやImageMagickは別バンドルとして出力されるようになった&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;初回スコア計測&quot;&gt;初回スコア計測&lt;/h2&gt;
&lt;p&gt;ここまでやった段階でスコア計測をして、以下の結果になりました。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;テスト項目&lt;/th&gt;
&lt;th&gt;CLS (25)&lt;/th&gt;
&lt;th&gt;FCP (10)&lt;/th&gt;
&lt;th&gt;LCP (25)&lt;/th&gt;
&lt;th&gt;SI (10)&lt;/th&gt;
&lt;th&gt;TBT (30)&lt;/th&gt;
&lt;th&gt;合計 (100)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ホームを開く&lt;/td&gt;
&lt;td&gt;20.75&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;20.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写真つき投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;24.75&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;24.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;動画つき投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;23.50&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;23.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;音声つき投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;検索ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM一覧ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;利用規約ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;合わせて219点でした。ユーザーフローテストは通常テストのスコアが300点未満だと計測されない仕様で、まずは300点を超えるのが最初の目標という感じです。&lt;/p&gt;
&lt;h2 id=&quot;phase-1-219498点&quot;&gt;Phase 1: 219→498点&lt;/h2&gt;
&lt;h3 id=&quot;サーバーのgzip圧縮&quot;&gt;サーバーのgzip圧縮&lt;/h3&gt;
&lt;p&gt;サーバー側でレスポンスの圧縮が一切入っていなかったので、Expressに &lt;code&gt;compression&lt;/code&gt; ミドルウェアを追加してgzipによる圧縮を有効化しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;+import compression from &quot;compression&quot;;
 import Express from &quot;express&quot;;

 export const app = Express();

 app.set(&quot;trust proxy&quot;, true);

+app.use(compression());
 app.use(sessionMiddleware);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ついでにレスポンスヘッダーを確認したところ、全レスポンスに付与されていたヘッダーにも罠がありました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;-app.use((_req, res, next) =&gt; {
-  res.header({
-    &quot;Cache-Control&quot;: &quot;max-age=0, no-transform&quot;,
-    Connection: &quot;close&quot;,
-  });
-  return next();
-});
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;no-transform&lt;/code&gt;: プロキシやCDNによる変換の無効化&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max-age=0&lt;/code&gt;: ブラウザーキャッシュの無効化&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Connection: close&lt;/code&gt;: HTTP Keep-Aliveの無効化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3つとも意図的な妨害コードで、レスポンスデータの容量を増やしたりリクエストを無駄に飛ばしたりする効果があるので丸ごと消しました。&lt;/p&gt;
&lt;h3 id=&quot;静的ファイルのcontenthashと長期キャッシュ&quot;&gt;静的ファイルのcontenthashと長期キャッシュ&lt;/h3&gt;
&lt;p&gt;アセット類をキャッシュしようとして、Rspackの出力ファイル名にハッシュが付いていないことに気づきました。内容が変わったときにキャッシュが無効化されるよう &lt;code&gt;[contenthash]&lt;/code&gt; を追加して長期キャッシュを使えるようにしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// rspack.config.js
output: {
  filename: &quot;scripts/[name]-[contenthash].js&quot;,
  chunkFilename: &quot;scripts/chunk-[contenthash].js&quot;,
},
// CssExtractRspackPlugin
{ filename: &quot;styles/[name]-[contenthash].css&quot; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そしてルーター側で &lt;code&gt;/scripts&lt;/code&gt;, &lt;code&gt;/styles&lt;/code&gt;, &lt;code&gt;/assets&lt;/code&gt; に対し &lt;code&gt;Cache-Control: public, max-age=31536000, immutable&lt;/code&gt; を設定しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// routes/static.ts
staticRouter.use(&#39;/scripts&#39;, (_req, res, next) =&gt; {
  res.setHeader(&#39;Cache-Control&#39;, &#39;public, max-age=31536000, immutable&#39;);
  next();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;認証が必要なAPIレスポンスと、ハッシュを付けなかった静的ファイル（index.htmlなど）にはキャッシュヘッダーを付けていません。&lt;/p&gt;
&lt;h3 id=&quot;infinitescrollの不要な繰り返し判定の除去&quot;&gt;InfiniteScrollの不要な繰り返し判定の除去&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;InfiniteScroll&lt;/code&gt; コンポーネントで、スクロール位置が最下部に到達したかの判定を2 ** 18回繰り返していました。&lt;/p&gt;
&lt;p&gt;「念の為」というコメントが付いていましたが、&lt;code&gt;window.innerHeight + Math.ceil(window.scrollY) &gt;= document.body.offsetHeight&lt;/code&gt; は同一イベントハンドラ内では毎回同じ値を返す純粋な比較式で繰り返す意味がありません。&lt;/p&gt;
&lt;p&gt;この判定は &lt;code&gt;scroll&lt;/code&gt;, &lt;code&gt;wheel&lt;/code&gt;, &lt;code&gt;touchmove&lt;/code&gt;, &lt;code&gt;resize&lt;/code&gt; の4イベントすべてで発火するため、スクロールするたびに約26万回のDOM参照と配列生成が走り、TBTを悪化させていました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;- // 念の為 2の18乗 回、最下部かどうかを確認する
- const hasReached = Array.from(Array(2 ** 18), () =&gt; {
-   return window.innerHeight + Math.ceil(window.scrollY) &gt;= document.body.offsetHeight;
- }).every(Boolean);
+ const hasReached = window.innerHeight + Math.ceil(window.scrollY) &gt;= document.body.offsetHeight;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;この実装は以前のWeb Speed Hackathonでもあったので、早めに「念の」でgrepしていました。こんなキーワードでgrepするのはWeb Speed Hackathonくらいな気がします。&lt;/p&gt;
&lt;h3 id=&quot;reactマウントのloadイベント待ち除去&quot;&gt;Reactマウントのloadイベント待ち除去&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;index.tsx&lt;/code&gt; を見ると、&lt;code&gt;window.addEventListener(&quot;load&quot;, ...)&lt;/code&gt; の中でReactをマウントしていました。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;load&lt;/code&gt;イベントは全リソースの読み込み完了後に発火するため、バンドルファイルのダウンロード＋パースが終わるまで描画が一切始まりません。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;-window.addEventListener(&quot;load&quot;, () =&gt; {
-  createRoot(document.getElementById(&quot;app&quot;)!).render(
-    &amp;#x3C;Provider store={store}&gt;
-      &amp;#x3C;BrowserRouter&gt;
-        &amp;#x3C;AppContainer /&gt;
-      &amp;#x3C;/BrowserRouter&gt;
-    &amp;#x3C;/Provider&gt;,
-  );
-});
+createRoot(document.getElementById(&quot;app&quot;)!).render(
+  &amp;#x3C;Provider store={store}&gt;
+    &amp;#x3C;BrowserRouter&gt;
+      &amp;#x3C;AppContainer /&gt;
+    &amp;#x3C;/BrowserRouter&gt;
+  &amp;#x3C;/Provider&gt;,
+);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tailwind-cssのビルド時コンパイル&quot;&gt;Tailwind CSSのビルド時コンパイル&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt; で &lt;code&gt;@tailwindcss/browser@4.2.1&lt;/code&gt; をCDNから同期スクリプトとして読み込み、ブラウザー内でCSSをコンパイルしている構成でした。&lt;/p&gt;
&lt;p&gt;外部スクリプトのダウンロード＋パース＋CSSコンパイルで描画をブロックするので、&lt;code&gt;@tailwindcss/postcss&lt;/code&gt; を導入して、ビルド時にCSSを生成するように変更しました。&lt;/p&gt;
&lt;p&gt;そしてここまで変更をおこなったところで、ようやくパフォーマンス計測を行いました。結果は以下の通りです。&lt;/p&gt;
&lt;p&gt;ビルド時コンパイル化後の計測結果（ホーム画面）:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.17&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;0.36&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0.53&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;render-blocking-resources&lt;/td&gt;
&lt;td&gt;2件&lt;/td&gt;
&lt;td&gt;1件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unused-javascript&lt;/td&gt;
&lt;td&gt;2件&lt;/td&gt;
&lt;td&gt;1件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;FCPが0.36→0.47に改善しました。Performanceスコア自体は0.15に下がっていますが、個別のメトリクスが改善（FCP +0.11、CLS +0.13）していたので、改善を適用した状態にしました。&lt;/p&gt;
&lt;p&gt;結果的にこの変更がレギュレーション違反の原因になるわけですが、当時の自分は気づいていませんでした。&lt;/p&gt;
&lt;h3 id=&quot;ルートベースのコード分割&quot;&gt;ルートベースのコード分割&lt;/h3&gt;
&lt;p&gt;全ルートのコンテナコンポーネントが &lt;code&gt;AppContainer.tsx&lt;/code&gt; に静的importされており、巨大な単一バンドル（なんと12MB！）になっていました。&lt;/p&gt;
&lt;p&gt;ホーム画面の表示に不要な &lt;code&gt;/crok&lt;/code&gt;（web-llm, katex, react-syntax-highlighter）やNewPostModal（含むFFmpegとImageMagick）まで全て初期ロードに含まれている状態だったので、&lt;code&gt;React.lazy&lt;/code&gt; + &lt;code&gt;Suspense&lt;/code&gt; で全ルートコンテナとモーダルを遅延読み込みに変更しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const TimelineContainer = lazy(() =&gt; import(&#39;@web-speed-hackathon-2026/client/src/containers/TimelineContainer&#39;));
// ... 他のルートも同様

&amp;#x3C;Suspense&gt;
  &amp;#x3C;Routes&gt;
    &amp;#x3C;Route element={&amp;#x3C;TimelineContainer /&gt;} path=&quot;/&quot; /&gt;
    {/* ... */}
  &amp;#x3C;/Routes&gt;
&amp;#x3C;/Suspense&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;コード分割後の計測結果（ホーム画面）:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;td&gt;≥0.9 (warning消失)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;render-blocking-resources&lt;/td&gt;
&lt;td&gt;1件&lt;/td&gt;
&lt;td&gt;0件 (warning消失)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;main.jsが12.3MiB→462KiB（96%削減）となり、FCPが大幅改善してwarningの閾値以下になりました。&lt;/p&gt;
&lt;h3 id=&quot;coveredimageをネイティブimgに置換&quot;&gt;CoveredImageをネイティブ&lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt;に置換&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CoveredImage&lt;/code&gt; コンポーネントを調べると、画像表示のために毎回 &lt;code&gt;jQuery.ajax&lt;/code&gt; でバイナリをフェッチし、&lt;code&gt;image-size&lt;/code&gt;でサイズ計算、&lt;code&gt;piexifjs&lt;/code&gt;でEXIF抽出、Blob URL生成という重い処理を踏んでいました。これらは &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; + &lt;code&gt;object-fit: cover&lt;/code&gt; に置き換えれば全部不要になります。&lt;/p&gt;
&lt;p&gt;EXIFからのalt取得は「ALTを表示する」ボタン押下時のみ動的にインポートして実行するように変更し、&lt;code&gt;loading&lt;/code&gt; 属性をpropsに追加した上で、ファーストビューに入る画像のみ &lt;code&gt;eager&lt;/code&gt;、それ以外は &lt;code&gt;lazy&lt;/code&gt; に設定しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;#x3C;img alt=&quot;&quot; className=&quot;h-full w-full object-cover&quot; loading={loading} src={src} /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;置換後の計測結果（ホーム画面）:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;offscreen-images&lt;/td&gt;
&lt;td&gt;28件&lt;/td&gt;
&lt;td&gt;1件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uses-responsive-images&lt;/td&gt;
&lt;td&gt;47件&lt;/td&gt;
&lt;td&gt;34件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lcp-lazy-loaded&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;解消&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Performanceスコアは変わっていませんが、offscreen-imagesが28→1件に減りました。画像がJS経由ではなくブラウザーネイティブで読み込まれるようになった効果です。&lt;/p&gt;
&lt;h3 id=&quot;jquery-native-fetchへの置換&quot;&gt;jQuery → native fetchへの置換&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fetchers.ts&lt;/code&gt; を見ると、全HTTPリクエストを &lt;code&gt;$.ajax({ async: false })&lt;/code&gt; で実行していました。&lt;code&gt;async: false&lt;/code&gt; は同期XHRなのでリクエスト中メインスレッドがブロックされます。これをネイティブの &lt;code&gt;fetch&lt;/code&gt; APIに置き換えました。&lt;/p&gt;
&lt;p&gt;置き換え後に気づいたこととして、&lt;code&gt;fetchers.ts&lt;/code&gt; 内のFetch APIラッパーである &lt;code&gt;sendJSON&lt;/code&gt; がリクエストボディをgzip圧縮した上で &lt;code&gt;Content-Encoding: gzip&lt;/code&gt; ヘッダーを付けてリクエストしようとしたときに、Fetch APIを使うとDM送信と投稿が失敗していました。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/whatwg/fetch/issues/589&quot;&gt;Constructing a Response with Content-Encoding? · Issue #589 · whatwg/fetch&lt;/a&gt;で書かれていますが、ブラウザー（Chrome）側でレスポンスデータは自動解凍してくれるにも関わらず、リクエスト時には自動圧縮してくれないという一貫性のなさがあるようです。&lt;/p&gt;
&lt;p&gt;とはいえRequest時に &lt;code&gt;Content-Encoding&lt;/code&gt; を付けることは比較的まれという記述もissueにあって、実際に私もRspack設定内の &lt;code&gt;ProvidePlugin&lt;/code&gt; から &lt;code&gt;$&lt;/code&gt; と &lt;code&gt;window.jQuery&lt;/code&gt; を削除することで問題がなくなりました。&lt;/p&gt;
&lt;p&gt;Fetch API置換後の計測結果（ホーム画面）:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;エントリーポイント&lt;/td&gt;
&lt;td&gt;462 KiB&lt;/td&gt;
&lt;td&gt;377 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deprecations failure&lt;/td&gt;
&lt;td&gt;あり&lt;/td&gt;
&lt;td&gt;解消&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;charset failure&lt;/td&gt;
&lt;td&gt;あり&lt;/td&gt;
&lt;td&gt;解消&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;エントリポイントが85KiB縮小し、jQuery由来の非推奨API warningも解消されました。&lt;/p&gt;
&lt;h3 id=&quot;web-llmのdynamic-import化&quot;&gt;web-llmのdynamic import化&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;TranslatableText&lt;/code&gt; コンポーネントが &lt;code&gt;createTranslator&lt;/code&gt; をstatic importしていた影響で、依存していた &lt;code&gt;@mlc-ai/web-llm&lt;/code&gt;がタイムラインのチャンクに存在していました。翻訳機能はユーザーが「Show Translation」ボタンを押した時のみ使われるため、クリック時にdynamic importするよう変更しました。&lt;/p&gt;
&lt;p&gt;dynamic import後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;0.24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;web-llmがタイムラインのチャンクから分離され、ホーム画面の初期ロードに含まれなくなったことでパフォーマンスの値が上がりました。&lt;/p&gt;
&lt;h3 id=&quot;momentの除去&quot;&gt;momentの除去&lt;/h3&gt;
&lt;p&gt;複数のコンポーネントで日付フォーマットに &lt;code&gt;moment&lt;/code&gt;が使われていましたが、使い方を調べたところ3パターンしかありませんでした。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;moment(date).locale(&quot;ja&quot;).format(&quot;LL&quot;)&lt;/code&gt; →「2026年3月20日」&lt;/li&gt;
&lt;li&gt;&lt;code&gt;moment(date).locale(&quot;ja&quot;).fromNow()&lt;/code&gt; →「3時間前」&lt;/li&gt;
&lt;li&gt;&lt;code&gt;moment(date).locale(&quot;ja&quot;).format(&quot;HH:mm&quot;)&lt;/code&gt; →「17:30」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;なので &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; と &lt;code&gt;Intl.RelativeTimeFormat&lt;/code&gt; で置き換えて、momentの依存をなくしました。&lt;/p&gt;
&lt;p&gt;ただ相対的に他と比較してパッケージサイズが小さいため、パフォーマンス改善にはつながりませんでした。&lt;/p&gt;
&lt;h2 id=&quot;phase-2-498576点&quot;&gt;Phase 2: 498→576点&lt;/h2&gt;
&lt;h3 id=&quot;redos脆弱性の修正&quot;&gt;ReDoS脆弱性の修正&lt;/h3&gt;
&lt;p&gt;クライアント側の正規表現を調べたところ、3箇所にReDoS（Regular Expression Denial of Service）パターンが仕込まれていました。いずれもネスト量指定子によって指数関数的にバックトラッキングが増えていました。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;auth/validation.ts&lt;/code&gt;：&lt;code&gt;/^(?:[^\P{Letter}&amp;#x26;&amp;#x26;\P{Number}]*){16,}$/v&lt;/code&gt; → &lt;code&gt;/^[\p{Letter}\p{Number}]*$/v&lt;/code&gt;（パスワードの記号チェック）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search/services.ts&lt;/code&gt;：&lt;code&gt;/since:((\d|\d\d|\d\d\d\d-\d\d-\d\d)+)+$/&lt;/code&gt; → &lt;code&gt;/since:(\d{4}-\d{2}-\d{2})$/&lt;/code&gt;（日付抽出）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;search/services.ts&lt;/code&gt;：&lt;code&gt;/^(\d+)+-(\d+)+-(\d+)+$/&lt;/code&gt; → &lt;code&gt;/^\d+-\d+-\d+$/&lt;/code&gt;（日付形式判定。変数名が &lt;code&gt;slowDateLike&lt;/code&gt; だった）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ReDoSはユーザー入力時（フォームバリデーション）で発火するため、ユーザーフローテストのINP/TBTで効果が出ました。&lt;/p&gt;
&lt;h2 id=&quot;phase-3-576750点&quot;&gt;Phase 3: 576→750点&lt;/h2&gt;
&lt;h3 id=&quot;動画のgifmp4変換&quot;&gt;動画のGIF→MP4変換&lt;/h3&gt;
&lt;p&gt;メディアファイルの最適化に着手しました。最初にリポジトリを取得したときにデータ容量が大きいことには気づいていたので、ここを改善することで各種指標が上がりそうという肌感がありました。&lt;/p&gt;
&lt;p&gt;まずは動画です。動画は全てGIF形式で保存されていて、15ファイルで合計179MBとなっていました。&lt;/p&gt;
&lt;p&gt;GIFは &lt;code&gt;PausableMovie&lt;/code&gt; コンポーネントでfetchした後 &lt;code&gt;gifler&lt;/code&gt; + &lt;code&gt;omggif&lt;/code&gt; でフレーム単位にデコードしてcanvasへ描画する実装になっていて、メインスレッドでのデコード処理がTBTを悪化させていました。&lt;/p&gt;
&lt;p&gt;そのため、まずはシードGIFをFFmpeg CLIでMP4 (H.264) に事前変換するスクリプトを作成して、動画をMP4形式にしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# `-movflags +faststart` オプションを追加して、moov atomをファイル先頭に配置し、ダウンロード完了前からストリーミング再生できるようにした
ffmpeg -i &quot;$gif&quot; \
  -c:v libx264 \
  -pix_fmt yuv420p \
  -movflags +faststart \
  -an \
  -loglevel warning \
  &quot;$mp4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;また &lt;code&gt;PausableMovie&lt;/code&gt; コンポーネントをネイティブのvideo要素に置き換えて &lt;code&gt;gifler&lt;/code&gt;, &lt;code&gt;omggif&lt;/code&gt; の依存を削除しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;#x3C;video ref={videoRef} autoPlay className=&quot;w-full&quot; loop muted playsInline src={src} /&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;その他にソースコード内で点在した拡張子を変えることで、GIFからMP4へ置き換えられました。&lt;/p&gt;
&lt;p&gt;GIF→MP4変換後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.22&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;147s&lt;/td&gt;
&lt;td&gt;43s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;6.7s&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SI&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;td&gt;0.55s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;TBTが6.7s→2.5sに大幅改善しました。gifler/omggifによるフレーム単位のデコードがなくなり、指標改善に寄与しました。LCPも147s→43sに短縮しましたが、とはいえまだまだパフォーマンス改善の余地がありすぎる状態です。&lt;/p&gt;
&lt;p&gt;また動画のファイルサイズも元のGIFが合わせて179MBあったところ、73MBまで減りました。&lt;/p&gt;
&lt;h3 id=&quot;音声の波形事前計算とビットレート削減&quot;&gt;音声の波形事前計算とビットレート削減&lt;/h3&gt;
&lt;p&gt;次に音声の最適化です。&lt;code&gt;SoundPlayer&lt;/code&gt; コンポーネントがMP3ファイル全体を取得した後に、&lt;code&gt;SoundWaveSVG&lt;/code&gt; がArrayBufferをAudioContextを使ってデコードし波形データを作成するための計算をしていました。この一連の処理がメインスレッドをブロックしていて、TBTの悪化原因になっていました。&lt;/p&gt;
&lt;p&gt;これを解消するため、FFmpegでMP3をPCM（signed 16-bit LE, stereo）へデコードした上で、左右チャンネルの平均を取って100チャンクへ分割し、各チャンクの平均値を事前にJSONへ保存するようにしました。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SoundWaveSVG&lt;/code&gt; はAudioContextの &lt;code&gt;decodeAudioData&lt;/code&gt; と &lt;code&gt;lodash&lt;/code&gt; による波形計算を丸ごと消して、事前計算済みの波形JSONを &lt;code&gt;fetchJSON&lt;/code&gt; で取得する形に変更しました。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SoundPlayer&lt;/code&gt; もArrayBufferへの読み込みとBlobURL生成（&lt;code&gt;useFetch&lt;/code&gt; + &lt;code&gt;fetchBinary&lt;/code&gt;）をやめて &lt;code&gt;&amp;#x3C;audio src&gt;&lt;/code&gt; に音声ファイルのパスを直接渡す形にして、&lt;code&gt;lodash&lt;/code&gt; と &lt;code&gt;standardized-audio-context&lt;/code&gt; の依存を無くしました。&lt;/p&gt;
&lt;p&gt;MP3のビットレートもSNS用途としては高かったため、128kbpsに再エンコードし直すことで総容量を66MBから38MBに削減しました。&lt;/p&gt;
&lt;p&gt;波形事前計算後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;2.5s&lt;/td&gt;
&lt;td&gt;49ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SI&lt;/td&gt;
&lt;td&gt;550ms&lt;/td&gt;
&lt;td&gt;501ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;43s&lt;/td&gt;
&lt;td&gt;42s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total-byte-weight&lt;/td&gt;
&lt;td&gt;106MB&lt;/td&gt;
&lt;td&gt;79MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;TBTが2.5s→49msに改善しました。&lt;/p&gt;
&lt;p&gt;AudioContextによるデコードを排除したことで、メインスレッドのブロックがほぼなくなっています。Performanceスコアも0.25→0.56に上がり、いくつかやった改善の中でも特に大きな変化となりました。&lt;/p&gt;
&lt;p&gt;音声投稿の個別ページでもTBTが448ms→0msになりました。&lt;/p&gt;
&lt;h3 id=&quot;画像のavif変換とリサイズ&quot;&gt;画像のAVIF変換とリサイズ&lt;/h3&gt;
&lt;p&gt;最後に画像です。画像は全てJPEGで配信されているかつ、ページ内で使われるサイズに対して解像度が高すぎたことから、なんらかの最適化をする必要があることは自明でした。&lt;/p&gt;
&lt;p&gt;ここではAVIFを選択しました。WebPより圧縮効率が高い点と2024年1月時点でBaselineのNewly availableに達している点が判断材料です。&lt;/p&gt;
&lt;p&gt;sharpを使って、投稿画像をメインコンテンツの最大幅である1280px、プロフィール画像を256pxへリサイズしつつ、AVIF (quality 75) へ変換するスクリプトを作り、ソースコード内の拡張子を &lt;code&gt;.jpg&lt;/code&gt; → &lt;code&gt;.avif&lt;/code&gt; へ変更して、元のJPEGファイルを削除しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 投稿画像は最大幅1280px、プロフィール画像は256px（128px × 2 for Retina）
await sharp(inputPath).resize({ width: maxWidth, withoutEnlargement: true }).avif({ quality: 75 }).toFile(outputPath);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AVIF変換後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;42s&lt;/td&gt;
&lt;td&gt;7.3s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total-byte-weight&lt;/td&gt;
&lt;td&gt;79MB&lt;/td&gt;
&lt;td&gt;35MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;49ms&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;AVIF変換後の計測結果（画像投稿ページ）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.73&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;12.7s&lt;/td&gt;
&lt;td&gt;1.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;投稿画像は86MB→6.1MBで93%の削減、プロフィール画像は3.2MB→416KBで87%の削減となりました。また、画像投稿ページのLCPが12.7s→1.0sになり、Performanceスコアが0.96に到達しました。&lt;/p&gt;
&lt;h3 id=&quot;メディアのキャッシュヘッダーとcompression除外&quot;&gt;メディアのキャッシュヘッダーとcompression除外&lt;/h3&gt;
&lt;p&gt;メディアファイル（&lt;code&gt;/images&lt;/code&gt;, &lt;code&gt;/movies&lt;/code&gt;, &lt;code&gt;/sounds&lt;/code&gt;）に &lt;code&gt;Cache-Control&lt;/code&gt; ヘッダーが付いておらず、&lt;code&gt;serve-static&lt;/code&gt; のデフォルト &lt;code&gt;max-age=0&lt;/code&gt; が適用されていました。さらにAVIF/MP4/MP3のような圧縮済みバイナリにも &lt;code&gt;compression&lt;/code&gt; がgzipをかけようとしていたので、キャッシュ設定の追加とcompression除外の両方を対応しました。&lt;/p&gt;
&lt;p&gt;メディアパスに &lt;code&gt;Cache-Control: public, max-age=86400&lt;/code&gt; を設定し、&lt;code&gt;compression&lt;/code&gt; ミドルウェアのfilterでメディアパスを除外しました。&lt;code&gt;serve-static&lt;/code&gt; のデフォルト &lt;code&gt;max-age=0&lt;/code&gt; がミドルウェアで設定したヘッダーを上書きしていたので、&lt;code&gt;cacheControl: false&lt;/code&gt; も必要でした。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// static.ts
for (const path of [&#39;/images&#39;, &#39;/movies&#39;, &#39;/sounds&#39;]) {
  staticRouter.use(path, (_req, res, next) =&gt; {
    res.setHeader(&#39;Cache-Control&#39;, &#39;public, max-age=86400&#39;);
    next();
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;キャッシュヘッダー設定後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;7.3s&lt;/td&gt;
&lt;td&gt;7.3s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Lighthouseの単発計測ではスコアが変わりません。キャッシュは2回目以降のナビゲーションで効果が出るため、初回アクセスのみを計測するLighthouseでは差が出にくいです。&lt;/p&gt;
&lt;h3 id=&quot;画像altテキストのexif往復を廃止&quot;&gt;画像ALTテキストのEXIF往復を廃止&lt;/h3&gt;
&lt;p&gt;手動テスト項目に「画像のEXIFに埋め込まれたImage DescriptionがALTとして表示されること」という要件がありました。調査の結果、既存の実装は以下の流れでした。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;クライアント：WASM版ImageMagickで画像を変換し、&lt;code&gt;img.comment&lt;/code&gt; からEXIFのImageDescriptionを取得&lt;/li&gt;
&lt;li&gt;クライアント：piexifjsでJPEGのEXIFにImageDescriptionを書き戻し&lt;/li&gt;
&lt;li&gt;サーバー：JPEGバイナリをファイルとして保存&lt;/li&gt;
&lt;li&gt;表示時：&lt;code&gt;CoveredImage&lt;/code&gt; が画像バイナリ全体を再取得し、piexifjsでEXIFを読み取り&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;alt用のテキストをバイナリメタデータとして往復させている流れです。&lt;code&gt;convertImage&lt;/code&gt; の中で &lt;code&gt;img.comment&lt;/code&gt; からalt用テキストは取得済みですが、EXIFに書き込んで、アップロードして、また取り出すということをしていました。&lt;/p&gt;
&lt;p&gt;なので、altをデータベースに保存した上で文字列として取得する方式に変更しました。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;元のJPEGからpiexifjsで全ImageDescriptionを抽出し、&lt;code&gt;images.jsonl&lt;/code&gt; に書き込んで &lt;code&gt;seed:insert&lt;/code&gt; でSQLiteに反映&lt;/li&gt;
&lt;li&gt;&lt;code&gt;convertImage&lt;/code&gt; の戻り値を &lt;code&gt;Blob&lt;/code&gt; → &lt;code&gt;{ blob, alt }&lt;/code&gt; に変更。&lt;code&gt;img.comment&lt;/code&gt; からaltを取得し、piexifjsによるEXIF書き込みを削除&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NewPostModalPage&lt;/code&gt; で変換結果のaltを保持し、&lt;code&gt;NewPostModalContainer&lt;/code&gt; の &lt;code&gt;sendNewPost&lt;/code&gt; で &lt;code&gt;images: [{ id, alt }]&lt;/code&gt; として &lt;code&gt;POST /api/v1/posts&lt;/code&gt; に送信&lt;/li&gt;
&lt;li&gt;サーバー側は &lt;code&gt;Post.create&lt;/code&gt; の &lt;code&gt;include: images&lt;/code&gt; でImageレコードを作成して、altをDBに保存&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CoveredImage&lt;/code&gt; はpropsで &lt;code&gt;alt&lt;/code&gt; を受け取り、APIレスポンスの値を直接表示。バイナリfetchとEXIFパースを削除&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;phase-4-750点以降&quot;&gt;Phase 4: 750点以降&lt;/h2&gt;
&lt;h3 id=&quot;aspectratiobox-を-css-aspect-ratio-に置き換え&quot;&gt;AspectRatioBox を CSS aspect-ratio に置き換え&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AspectRatioBox&lt;/code&gt; コンポーネントは、JSで &lt;code&gt;clientWidth&lt;/code&gt; を読み取り &lt;code&gt;setTimeout(500ms)&lt;/code&gt; 後に高さを計算して &lt;code&gt;setState&lt;/code&gt; する実装でした。これにより初回レンダリングから500ミリ秒の間、コンテンツが非表示になるとともに、高さが0から計算値に変わるタイミングでLayout Shiftが発生していました。&lt;/p&gt;
&lt;p&gt;これをCSSの &lt;code&gt;aspect-ratio&lt;/code&gt; プロパティに置き換えることで、CLSを解消するとともに無駄な遅延をなくしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// Before: JS で高さを計算、500ms 遅延、resize リスナー付き
&amp;#x3C;AspectRatioBox aspectHeight={9} aspectWidth={16}&gt;
  &amp;#x3C;div&gt;...&amp;#x3C;/div&gt;
&amp;#x3C;/AspectRatioBox&gt;

// After: CSS aspect-ratio で即座にサイズ確定
&amp;#x3C;div className=&quot;w-full&quot; style={{ aspectRatio: &quot;16 / 9&quot; }}&gt;
  &amp;#x3C;div&gt;...&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;aspect-ratio&lt;/code&gt; 置き換え後の計測結果：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ページ&lt;/th&gt;
&lt;th&gt;CLS before&lt;/th&gt;
&lt;th&gt;CLS after&lt;/th&gt;
&lt;th&gt;Score before&lt;/th&gt;
&lt;th&gt;Score after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.433&lt;/td&gt;
&lt;td&gt;0.029&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/posts/fe6712a1...&lt;/code&gt; (画像投稿)&lt;/td&gt;
&lt;td&gt;0.098&lt;/td&gt;
&lt;td&gt;0.004&lt;/td&gt;
&lt;td&gt;0.94&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/posts/fff790f5...&lt;/code&gt; (動画投稿)&lt;/td&gt;
&lt;td&gt;0.199&lt;/td&gt;
&lt;td&gt;0.099&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;0.73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/posts/fefe75bd...&lt;/code&gt; (音声投稿)&lt;/td&gt;
&lt;td&gt;0.006&lt;/td&gt;
&lt;td&gt;0.000&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;フォントの最適化otf-サブセット-woff2-font-display-swap&quot;&gt;フォントの最適化（OTF → サブセット woff2 + font-display: swap）&lt;/h3&gt;
&lt;p&gt;利用規約ページで使用されるカスタムフォント「Rei no Are Mincho」がOTF形式（各6.3MB）で配信され、&lt;code&gt;font-display: block&lt;/code&gt; が指定されていました。フォント読み込み完了までテキストが非表示になるのでLCPに直接影響しています。&lt;/p&gt;
&lt;p&gt;これは以下の修正をすることでフォントファイルのサイズを100KB前後にしました。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TermPageで実際に使われている471文字だけに絞ったサブセットフォントを生成（&lt;code&gt;pyftsubset&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;OTF → woff2に変換&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;またフォントの表示方法を &lt;code&gt;font-display: block&lt;/code&gt; → &lt;code&gt;font-display: swap&lt;/code&gt; に変更しています。&lt;/p&gt;
&lt;p&gt;なお、最初はフォントをwoff2形式に変換しただけで、サブセット化まではやっていなかったですが、これによりLCPが645ms → 3,645msに悪化していました。&lt;/p&gt;
&lt;p&gt;原因は &lt;code&gt;font-display: swap&lt;/code&gt; によりフォールバックフォントで即座にテキストが描画され（FCP）、カスタムフォントがダウンロードされた後の再描画がLCPとして計測されたためで、これによりサブセット化も実施することを決めました。&lt;/p&gt;
&lt;p&gt;利用規約ページの計測結果：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FCP&lt;/td&gt;
&lt;td&gt;204ms&lt;/td&gt;
&lt;td&gt;206ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;645ms&lt;/td&gt;
&lt;td&gt;730ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SI&lt;/td&gt;
&lt;td&gt;204ms&lt;/td&gt;
&lt;td&gt;206ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0.018&lt;/td&gt;
&lt;td&gt;0.019&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;タイマー系サボタージュコードの除去&quot;&gt;タイマー系サボタージュコードの除去&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; の使用箇所を調査したところ、パフォーマンスに悪影響を与えている箇所が3つ見つかりました。&lt;/p&gt;
&lt;h4 id=&quot;directmessagepage-の-setinterval-resizeobserver&quot;&gt;DirectMessagePage の setInterval → ResizeObserver&lt;/h4&gt;
&lt;p&gt;DMページで &lt;code&gt;setInterval(..., 1)&lt;/code&gt; を実行し、その中で &lt;code&gt;getComputedStyle&lt;/code&gt; を1ms間隔で呼び続けている箇所がありました。強制リフローが連続発生するのでTBT/INPに悪影響があります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&gt; {
  const id = setInterval(() =&gt; {
    const height = Number(window.getComputedStyle(document.body).height.replace(&#39;px&#39;, &#39;&#39;));
    if (height !== scrollHeightRef.current) {
      scrollHeightRef.current = height;
      window.scrollTo(0, height);
    }
  }, 1);
  return () =&gt; clearInterval(id);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これを &lt;code&gt;ResizeObserver&lt;/code&gt; に置き換えました。&lt;code&gt;ResizeObserver&lt;/code&gt; はブラウザーのレイアウト処理完了後にコールバックが呼ばれるため &lt;code&gt;getComputedStyle&lt;/code&gt; のような強制reflowを引き起こしません。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;useEffect(() =&gt; {
  const observer = new ResizeObserver(() =&gt; {
    const height = document.body.scrollHeight;
    if (height !== scrollHeightRef.current) {
      scrollHeightRef.current = height;
      window.scrollTo(0, height);
    }
  });
  observer.observe(document.body);
  return () =&gt; observer.disconnect();
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;crok-sse-の-3秒-sleep-削除&quot;&gt;crok SSE の 3秒 sleep 削除&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;/api/v1/crok&lt;/code&gt; のSSEレスポンス開始前に &lt;code&gt;await sleep(3000)&lt;/code&gt; がありました。AIチャットの応答開始を3秒遅延させる意図的なサボタージュコードだったので削除しました。&lt;/p&gt;
&lt;h4 id=&quot;crok-sse-の-sleep-削除&quot;&gt;crok SSE の sleep 削除&lt;/h4&gt;
&lt;p&gt;さらに1文字ずつSSE送信する際にも &lt;code&gt;await sleep(10)&lt;/code&gt; が入っていました。&lt;code&gt;sleep(10)&lt;/code&gt; を削除し、&lt;code&gt;sleep&lt;/code&gt; 関数自体も除去しました。&lt;/p&gt;
&lt;p&gt;1文字ずつイベントとして送信する構造は維持しており、Node.jsのI/Oバッファリングによりクライアント側ではストリーミング表示として見えると判断しました。&lt;/p&gt;
&lt;h3 id=&quot;不要ポリフィルレガシーコードの除去&quot;&gt;不要ポリフィル・レガシーコードの除去&lt;/h3&gt;
&lt;p&gt;bluebird、lodash、buffer polyfillの3つがクライアントバンドルに残っていたので除去しました。&lt;/p&gt;
&lt;p&gt;ポリフィル除去後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;0.76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SI&lt;/td&gt;
&lt;td&gt;497ms&lt;/td&gt;
&lt;td&gt;325ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;ホーム画面におけるlcpの画像をpreloadで読み込む&quot;&gt;ホーム画面におけるLCPの画像をpreloadで読み込む&lt;/h3&gt;
&lt;p&gt;ホーム画面のLCPが3.8秒で、内訳を見るとLoad Delay（ブラウザーが画像を発見するまでの遅延）が3.308sかかっていました。&lt;/p&gt;
&lt;p&gt;SPAアーキテクチャでJS実行→API→Reactによる描画という直列のチェーンを経て初めて &lt;code&gt;&amp;#x3C;img&gt;&lt;/code&gt; タグがDOMに挿入されるのが原因でした。&lt;/p&gt;
&lt;p&gt;なので、サーバー側でHTMLを返す際にDBから最初の画像付き投稿を取得し、&lt;code&gt;&amp;#x3C;link rel=&quot;preload&quot; as=&quot;image&quot;&gt;&lt;/code&gt; を &lt;code&gt;&amp;#x3C;head&gt;&lt;/code&gt; へ注入するようにしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// application/server/src/routes/static.ts
staticRouter.get(&#39;/&#39;, async (_req, res, next) =&gt; {
  const posts = await Post.findAll({ limit: 10, offset: 0 });
  const firstPostWithImage = posts.find((p) =&gt; p.images?.length &gt; 0);
  const firstImageId = firstPostWithImage?.images?.[0]?.id;
  // index.htmlの&amp;#x3C;head&gt;に &amp;#x3C;link rel=&quot;preload&quot; as=&quot;image&quot; href=&quot;/images/{id}.avif&quot;&gt; を注入
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改善後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;3.8s&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Delay&lt;/td&gt;
&lt;td&gt;3,308ms (87%)&lt;/td&gt;
&lt;td&gt;0ms (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render Delay&lt;/td&gt;
&lt;td&gt;339ms (9%)&lt;/td&gt;
&lt;td&gt;3,326ms (96%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Load Delayは解消し、LCPも改善はされましたが、Render Delayにボトルネックが移動するだけになって、あまり効果はなかったです。&lt;/p&gt;
&lt;h3 id=&quot;useinfinitefetchのサーバー側pagination対応&quot;&gt;useInfiniteFetchのサーバー側pagination対応&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useInfiniteFetch&lt;/code&gt; フックを調べると、API呼び出しのたびに全件取得してクライアント側で &lt;code&gt;slice&lt;/code&gt; していました。&lt;/p&gt;
&lt;p&gt;APIは &lt;code&gt;limit&lt;/code&gt;/&lt;code&gt;offset&lt;/code&gt; パラメータに対応しているのにクライアントが使っていなかったので、サーバー側paginationに変更しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Before: 毎回全件取得してクライアントでslice
void fetcher(apiPath).then((allData) =&gt; {
  data: [...cur.data, ...allData.slice(offset, offset + LIMIT)],
});

// After: サーバー側paginationで必要分だけ取得
const paginatedPath = `${apiPath}?limit=${LIMIT}&amp;#x26;offset=${offset}`;
void fetcher(paginatedPath).then((pageData) =&gt; {
  data: [...cur.data, ...pageData],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改善後の計測結果（ホーム画面）：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指標&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;td&gt;3.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render Delay&lt;/td&gt;
&lt;td&gt;3,326ms&lt;/td&gt;
&lt;td&gt;3,205ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performanceスコア&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;思ったより効果は小さいですが、全ページで不要なデータ転送を削減する修正になるはずだったのでそのままにしました。&lt;/p&gt;
&lt;h3 id=&quot;font-awesome-svg-sprite-の軽量化&quot;&gt;Font Awesome SVG sprite の軽量化&lt;/h3&gt;
&lt;p&gt;Font AwesomeのSVGスプライトファイルにすべてのアイコンが入っていて無駄だったので削減しました。合計で約1.2MBから8.6KBになりましたが、この変更がパフォーマンス改善に直接は効きませんでした。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ファイル&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;備考&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;solid.svg&lt;/td&gt;
&lt;td&gt;639KB (1002アイコン)&lt;/td&gt;
&lt;td&gt;7.4KB (17アイコン)&lt;/td&gt;
&lt;td&gt;使用アイコンのみ抽出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regular.svg&lt;/td&gt;
&lt;td&gt;107KB (458アイコン)&lt;/td&gt;
&lt;td&gt;1.2KB (1アイコン)&lt;/td&gt;
&lt;td&gt;calendar-altのみ使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;brands.svg&lt;/td&gt;
&lt;td&gt;458KB (458アイコン)&lt;/td&gt;
&lt;td&gt;削除&lt;/td&gt;
&lt;td&gt;コード中で未参照&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;最終計測&quot;&gt;最終計測&lt;/h2&gt;
&lt;p&gt;ここまで書いたことや、書いていない細かい修正もやった上で、競技終了8分前の18時22分に計測をしてスコアが &lt;strong&gt;764.80点&lt;/strong&gt; となりました。詳細は以下の通りです。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;テスト項目&lt;/th&gt;
&lt;th&gt;CLS (25)&lt;/th&gt;
&lt;th&gt;FCP (10)&lt;/th&gt;
&lt;th&gt;LCP (25)&lt;/th&gt;
&lt;th&gt;SI (10)&lt;/th&gt;
&lt;th&gt;TBT (30)&lt;/th&gt;
&lt;th&gt;合計 (100)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ホームを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;8.00&lt;/td&gt;
&lt;td&gt;8.75&lt;/td&gt;
&lt;td&gt;3.20&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;44.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;8.80&lt;/td&gt;
&lt;td&gt;17.00&lt;/td&gt;
&lt;td&gt;8.40&lt;/td&gt;
&lt;td&gt;29.40&lt;/td&gt;
&lt;td&gt;88.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写真つき投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;9.20&lt;/td&gt;
&lt;td&gt;17.75&lt;/td&gt;
&lt;td&gt;9.20&lt;/td&gt;
&lt;td&gt;29.70&lt;/td&gt;
&lt;td&gt;90.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;動画つき投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;9.20&lt;/td&gt;
&lt;td&gt;17.75&lt;/td&gt;
&lt;td&gt;8.70&lt;/td&gt;
&lt;td&gt;23.10&lt;/td&gt;
&lt;td&gt;83.75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;音声つき投稿詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;9.30&lt;/td&gt;
&lt;td&gt;17.75&lt;/td&gt;
&lt;td&gt;9.80&lt;/td&gt;
&lt;td&gt;30.00&lt;/td&gt;
&lt;td&gt;91.85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;検索ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;8.90&lt;/td&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;9.40&lt;/td&gt;
&lt;td&gt;8.10&lt;/td&gt;
&lt;td&gt;52.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM一覧ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;8.80&lt;/td&gt;
&lt;td&gt;16.00&lt;/td&gt;
&lt;td&gt;6.90&lt;/td&gt;
&lt;td&gt;23.70&lt;/td&gt;
&lt;td&gt;80.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM詳細ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;9.10&lt;/td&gt;
&lt;td&gt;13.00&lt;/td&gt;
&lt;td&gt;5.40&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;52.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;利用規約ページを開く&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;9.20&lt;/td&gt;
&lt;td&gt;19.25&lt;/td&gt;
&lt;td&gt;9.20&lt;/td&gt;
&lt;td&gt;8.10&lt;/td&gt;
&lt;td&gt;70.75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ユーザーフローテスト&lt;/th&gt;
&lt;th&gt;INP (25)&lt;/th&gt;
&lt;th&gt;TBT (25)&lt;/th&gt;
&lt;th&gt;合計 (50)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ユーザー登録→サインアウト→サインイン&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;9.00&lt;/td&gt;
&lt;td&gt;34.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DM送信&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;計測できません&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;検索→結果表示&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;50.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crok AIチャット&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;投稿&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;計測できません&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;計測できませんと出た部分に関しては運頼みとなりました。&lt;/p&gt;
&lt;h2 id=&quot;振り返り&quot;&gt;振り返り&lt;/h2&gt;
&lt;p&gt;最初に書いた通り、レギュレーション違反によりランキングから除外されました。レギュレーション違反したことは純粋に悔しいです。&lt;/p&gt;
&lt;p&gt;違反の原因を振り返ると、VRTの実行頻度がそこまで頻繁ではなかったこと、Tailwind CSSの置き換え時に元の挙動が維持されているか検証しなかったことに帰結します。&lt;/p&gt;
&lt;p&gt;表示・動作を壊していないことを確認する仕組みもワークフローに組み込む必要がありました。たとえばClaude Codeのhookを使ってVRTを実行できていればよかったかなと感じています。&lt;/p&gt;
&lt;p&gt;あとはテストケースを事前に読んでおき、修正前後で動作を担保する仕組みを作れれば良かったです。&lt;/p&gt;
&lt;h3 id=&quot;aiエージェントを使う前提になった結果&quot;&gt;AIエージェントを使う前提になった結果&lt;/h3&gt;
&lt;p&gt;今までのWeb Speed Hackathonと比較して変更を多く入れることができて、コミット数は初めて100を超えました。これはAIエージェントによる高速な実装ができたためだと考えています。Lighthouseを逐次回していた影響で並列化はあまりできなかったですが、改善を並列でやっていればより多くの変更を入れられたはずです。&lt;/p&gt;
&lt;p&gt;ただ、パフォーマンスチューニングをする・しないといった判断はまだ人間側に委ねられていると感じました。手当り次第に改善をしても指標の向上につながらないことはあります。どの指標が改善につながるか推測するのは、まだ人間の仕事だと感じます。&lt;/p&gt;
&lt;p&gt;個人的にはどうしてもデータベース周りが弱くてインデックスを貼る思考に至らなかった反省があります。&lt;/p&gt;
&lt;p&gt;一方で来年には景色が変わっていそうです。2025年3月時点ではCursorなどからAIを使うことが一般的だったところから、Claude CodeやCodexを中心としたCLI中心の世界になって、さまざまなツールとの連携もできるようになってきたという変化もありました。&lt;/p&gt;
&lt;p&gt;2027年にもWeb Speed Hackathonは開催されるでしょうが、その頃には、AIエージェントがより安全に自律して動けるようになって、提案を積極的にしてくるようになり、人間はAIエージェントが自律的に動く間に次の手を打つということが当たり前になっていそうです。&lt;/p&gt;</content>
<published>2026-03-30T12:00:00.000Z</published>
<updated>2026-03-30T12:00:00.000Z</updated>
</entry>
<entry>
<title>Claude CodeのSkillsでWeb機能のBaseline対応状況を確認できるようにした</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/claude-code-baseline-skill"/>
<id>tag:blog.kubosho.com,2026-03-02:entry:claude-code-baseline-skill</id>
<summary>Claude Codeで使えるSkillsの仕組みを使って、Web Platform BaselineをClaude Codeから参照できるようにしました。</summary>
<content type="html">&lt;p&gt;Claude Codeで使える&lt;a href=&quot;https://code.claude.com/docs/ja/skills&quot;&gt;Skills&lt;/a&gt;の仕組みを使って、&lt;a href=&quot;https://web.dev/baseline&quot;&gt;Web Platform Baseline&lt;/a&gt;をClaude Codeから参照できるようにしました。&lt;/p&gt;
&lt;p&gt;「XX機能の対応状況はどんな感じ？」と聞くだけで、Baselineの状態やサポートブラウザーのバージョンが出ます。&lt;/p&gt;
&lt;p&gt;スキルは以下のリポジトリで公開しています。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubosho/my-skills&quot;&gt;https://github.com/kubosho/my-skills&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;導入方法はClaude Code上で完結するものとスタンドアロンの2種類あります。&lt;/p&gt;
&lt;p&gt;Claude Code上で完結する方法では、以下のコマンドを実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;/plugin marketplace add kubosho/my-skills
/plugin install kubosho-skills@my-skills
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;スタンドアロンの方法では、リポジトリをcloneして &lt;code&gt;~/.claude/skills/&lt;/code&gt; へシンボリックリンクを張ります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git clone https://github.com/kubosho/my-skills.git
ln -s /path/to/my-skills/skills/baseline ~/.claude/skills/baseline
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;なぜ作ったのか&quot;&gt;なぜ作ったのか&lt;/h2&gt;
&lt;p&gt;開発する中で「この機能のブラウザー対応状況はどんな感じだっけ」と気になった場合、今までは以下の手段で確認していました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://caniuse.com/&quot;&gt;Can I use&lt;/a&gt;を開いて検索する&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/&quot;&gt;MDN&lt;/a&gt;の互換性テーブルを確認する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;しかし、どちらもブラウザーに切り替えて検索する必要があって、コーディング中の流れが途切れるので嫌でした。&lt;/p&gt;
&lt;h2 id=&quot;技術的な話&quot;&gt;技術的な話&lt;/h2&gt;
&lt;p&gt;Baselineの情報を取得するために&lt;a href=&quot;https://api.webstatus.dev/v1/features&quot;&gt;Web Platform Status API&lt;/a&gt;へリクエストしています。このAPIは &lt;code&gt;GET https://api.webstatus.dev/v1/features?q={query}&lt;/code&gt; というエンドポイントで、ある機能のBaseline状態やブラウザーのサポート情報を返します。&lt;/p&gt;
&lt;p&gt;リクエスト時に使えるクエリを一部紹介すると、以下のような演算子が使えます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name:grid&lt;/code&gt; のようにキーワードで機能名を検索&lt;/li&gt;
&lt;li&gt;&lt;code&gt;baseline_status:widely&lt;/code&gt; で広くサポートされている機能を絞り込み&lt;/li&gt;
&lt;li&gt;&lt;code&gt;available_on:chrome&lt;/code&gt; や &lt;code&gt;-available_on:firefox&lt;/code&gt; でブラウザーごとのサポート有無でフィルタ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AND条件はスペース区切り、OR条件は &lt;code&gt;+OR+&lt;/code&gt; という記法を使うことで指定できます。例えば &lt;code&gt;name:grid baseline_status:widely&lt;/code&gt; で「Widely availableになっているgrid関連の機能」を探せます。&lt;/p&gt;
&lt;h3 id=&quot;skillの構造&quot;&gt;Skillの構造&lt;/h3&gt;
&lt;p&gt;今回作成したSkillでは、APIのエンドポイント・演算子の一覧・入出力・制約だけを定義しています。ツールの使用や、機能の詳細情報をどこまで表示するかはAIエージェントに任せています。&lt;/p&gt;
&lt;p&gt;出力フォーマットは情報量に応じて変更しています。結果が少なければBaselineの状態・サポート日時・ブラウザーバージョンなどを詳細に表示し、結果が多ければサマリーテーブルに切り替えています。&lt;/p&gt;
&lt;p&gt;また、10秒経過後にタイムアウトさせるのと、同時リクエスト数の上限を最大3リクエストにするといった制約も設けています。&lt;/p&gt;
&lt;h2 id=&quot;使ってみて&quot;&gt;使ってみて&lt;/h2&gt;
&lt;p&gt;新しめの機能を使いたいときに「これ大丈夫だっけ」とすぐ聞けて良いです。&lt;/p&gt;
&lt;p&gt;特に「主要ブラウザーでまだ使えない機能は？」と自然言語で質問した場合に、AIエージェント側でよしなにクエリを構築して検索できるのが良いです。&lt;/p&gt;
&lt;p&gt;あとは最新の状況を知る目的でも使えます。Claude Code上で「2026年にwidelyまたはnewlyへ達した、CSSやJSの機能はある？」という質問した時の応答は以下の通りです。情報は2026年2月26日時点の情報です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;⏺ Skill(baseline)
  ⎿  Successfully loaded skill

⏺ 2026年のbaseline到達状況を調べるね。CSSとJavaScriptそれぞれ、widelyとnewlyで検索する。

⏺ Fetch(https://api.webstatus.dev/v1/features?q=baseline_date:2026-01-01..2026-12-31%20baseline_status:widely%20group:css)
  ⎿  Received 35 bytes (200 OK)

⏺ Fetch(https://api.webstatus.dev/v1/features?q=baseline_date:2026-01-01..2026-12-31%20baseline_status:newly%20group:css)
  ⎿  Received 5.8KB (200 OK)

⏺ Fetch(https://api.webstatus.dev/v1/features?q=baseline_date:2026-01-01..2026-12-31%20baseline_status:widely%20group:javascript)
  ⎿  Received 35 bytes (200 OK)

⏺ Fetch(https://api.webstatus.dev/v1/features?q=baseline_date:2026-01-01..2026-12-31%20baseline_status:newly%20group:javascript)
  ⎿  Received 2KB (200 OK)

⏺ 2026年（現時点まで）にBaseline到達した機能をまとめたよ。

使ったクエリ:
- baseline_date:2026-01-01..2026-12-31 baseline_status:newly group:css
- baseline_date:2026-01-01..2026-12-31 baseline_status:newly group:javascript
- widely は CSS・JS ともに 0件だった

---
CSS — Newly available (5件)

すべて 2026-01-13 に newly 到達。最後のブラウザが Firefox 147。

┌────────────────────────┬───────────────┬────────────────┬──────────────────┐
│        Feature         │    Chrome     │     Safari     │  Firefox (最後)  │
├────────────────────────┼───────────────┼────────────────┼──────────────────┤
│ Active view transition │ 125 (2024-05) │ 18.2 (2024-12) │ 147 (2026-01-13) │
├────────────────────────┼───────────────┼────────────────┼──────────────────┤
│ rcap unit              │ 118 (2023-10) │ 17.2 (2023-12) │ 147 (2026-01-13) │
├────────────────────────┼───────────────┼────────────────┼──────────────────┤
│ rch unit               │ 111 (2023-03) │ 17.2 (2023-12) │ 147 (2026-01-13) │
├────────────────────────┼───────────────┼────────────────┼──────────────────┤
│ rex unit               │ 111 (2023-03) │ 17.2 (2023-12) │ 147 (2026-01-13) │
├────────────────────────┼───────────────┼────────────────┼──────────────────┤
│ ric unit               │ 111 (2023-03) │ 17.2 (2023-12) │ 147 (2026-01-13) │
└────────────────────────┴───────────────┴────────────────┴──────────────────┘

rcap/rch/rex/ric はルートフォントに基づく相対単位。Firefox 147 での実装で全ブラウザ対応になった。

---
JavaScript — Newly available (2件)

┌───────────────────────────────┬────────────┬───────────────┬────────────────┬──────────────────┐
│            Feature            │ Baseline日 │    Chrome     │     Safari     │  Firefox (最後)  │
├───────────────────────────────┼────────────┼───────────────┼────────────────┼──────────────────┤
│ Map.prototype.getOrInsert()   │ 2026-02-14 │ 145 (2026-02) │ 26.2 (2025-12) │ 144 (2025-10)    │
├───────────────────────────────┼────────────┼───────────────┼────────────────┼──────────────────┤
│ JS modules in service workers │ 2026-01-13 │ 91 (2021-05)  │ 15 (2021-09)   │ 147 (2026-01-13) │
└───────────────────────────────┴────────────┴───────────────┴────────────────┴──────────────────┘

Map.prototype.getOrInsert() は TC39 提案の機能で、キーが存在しなければ値を挿入して返す。Chrome 145 が最後の実装で newly に到達。

---
まとめ

- Widely 到達: CSS・JS ともに 0件（2026年はまだ2ヶ月しか経っていないので、これから増える可能性がある）
- Newly 到達: CSS 5件 + JS 2件 = 計7件
- Firefox 147 のリリース（2026-01-13）が多くの機能の baseline 到達トリガーになっている
&lt;/code&gt;&lt;/pre&gt;</content>
<published>2026-03-02T00:00:00.000Z</published>
<updated>2026-03-04T13:26:06.000Z</updated>
</entry>
<entry>
<title>ブログにダークモードを実装した</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/implement-dark-mode"/>
<id>tag:blog.kubosho.com,2026-02-02:entry:implement-dark-mode</id>
<summary>ようやくダークモードをブログに実装した話。</summary>
<content type="html">&lt;p&gt;このブログには元々ダークモードがありませんでした。&lt;/p&gt;
&lt;p&gt;ブログを&lt;a href=&quot;https://blog.kubosho.com/entries/migrating-from-contentful-to-markdown-file&quot;&gt;はてなブログから独自のシステムに移行した2019年&lt;/a&gt;は、Android 10やiOS 13でダークモードへの切り替えがリリースされて、ようやくダークモードが一般層にも広がる素地ができた年でした。&lt;/p&gt;
&lt;p&gt;このブログにも2020年5月の段階でダークモードを導入したいと&lt;a href=&quot;https://github.com/kubosho/blog.kubosho.com/issues/298&quot;&gt;issue&lt;/a&gt;は作っていました。そこから構想5年を経て、重い腰を上げダークモードを実装しました。嘘です。ずっと構想していたわけではありません。&lt;/p&gt;
&lt;p&gt;今回の実装で考慮した点や、今後の課題についてまとめます。&lt;/p&gt;
&lt;h2 id=&quot;モードの切り替え実装&quot;&gt;モードの切り替え実装&lt;/h2&gt;
&lt;p&gt;今回は &lt;code&gt;data-theme&lt;/code&gt; 属性をhtml要素に付与し、CSSでライトモードとダークモードのスタイルを定義する方法を採用しました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 実装サンプル */
[data-theme=&#39;light&#39;] {
  --color-neutral-background: oklch(0.97 0 0);
  --color-neutral-text: oklch(0.21 0 0);
}

[data-theme=&#39;dark&#39;] {
  --color-neutral-background: oklch(0.28 0 0);
  --color-neutral-text: oklch(0.97 0 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;読者がブログのテーマを選べるようにしたかったため、&lt;code&gt;data-theme&lt;/code&gt; 属性でモードを切り替える実装を採用しました。OSの設定に従う「システム」と、明示的な「ライト」「ダーク」の計3つから表示モードを選択できます。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;テーマを切り替えるUIではPopover APIを使っています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;button type=&quot;button&quot; popovertarget=&quot;theme-popover&quot;&gt;
  &amp;#x3C;!-- アイコン --&gt;
&amp;#x3C;/button&gt;

&amp;#x3C;div id=&quot;theme-popover&quot; popover&gt;
  &amp;#x3C;button data-theme-value=&quot;system&quot;&gt;システム&amp;#x3C;/button&gt;
  &amp;#x3C;button data-theme-value=&quot;light&quot;&gt;ライト&amp;#x3C;/button&gt;
  &amp;#x3C;button data-theme-value=&quot;dark&quot;&gt;ダーク&amp;#x3C;/button&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Popover APIはJavaScriptを使わなくともポップオーバーの表示・非表示を切り替えられます。他とえば前述のコードだと &lt;code&gt;popover=&quot;auto&quot;&lt;/code&gt; を指定した状態になり、ポップオーバー領域の外側やEscキーを押すことでポップオーバーを非表示にできます。&lt;/p&gt;
&lt;p&gt;またポップオーバーの位置決めにCSS Anchor Positioningを使っています。Baseline 2026なのでようやく主要ブラウザーの安定版で動作するようになった機能です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.TriggerButton {
  anchor-name: --theme-trigger;
}

.Popover {
  position-anchor: --theme-trigger;
  top: anchor(bottom);
  left: anchor(left);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ポップオーバーの位置は今までJavaScriptで位置計算をして実装するしかなかったですが、Anchor Positioningが出てきたことでCSSだけを使ってポップオーバーの配置を決められるようになったのは便利だと感じました。&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Elements/dialog&quot;&gt;dialog要素&lt;/a&gt;が登場した時と同じ感覚です。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;モードの設定はCookieに保存しています。ページ読み込み時にCookieからテーマを読み取り、html要素の &lt;code&gt;data-theme&lt;/code&gt; 属性に反映しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;html lang=&quot;ja&quot; data-theme=&quot;system&quot;&gt;
  &amp;#x3C;script&gt;
    (() =&gt; {
      const match = document.cookie.match(/(?:^|; )theme=([^;]*)/);
      const theme = match ? match[1] : &#39;system&#39;;
      document.documentElement.setAttribute(&#39;data-theme&#39;, theme);
    })();
  &amp;#x3C;/script&gt;
  &amp;#x3C;!-- ... --&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;oklch色空間の採用&quot;&gt;OKLCH色空間の採用&lt;/h2&gt;
&lt;p&gt;色の定義にはOKLCHを採用しました。従来のHEX値やHSLではなく、OKLCHを選んだ理由は知覚的な均一性を得られるためです。&lt;/p&gt;
&lt;p&gt;OKLCHの色空間を使うことで、色を比較したときに一方の色が沈んで見えたり、色が違って見えるといった問題が起きにくくなります。&lt;a href=&quot;https://oklch.com/&quot;&gt;OKLCH Color Picker &amp;#x26; Converter&lt;/a&gt;といった直感的な色調整ツールがあるのも良いです。&lt;/p&gt;
&lt;p&gt;ただ、色の選定には想定以上に時間がかかりました。ライトモードの色をそのまま反転させても違和感のある配色になります。結局、ライトモードとダークモードの両方とも一から色を選び直しました。&lt;/p&gt;
&lt;h2 id=&quot;コントラストの調整&quot;&gt;コントラストの調整&lt;/h2&gt;
&lt;p&gt;コントラストの基準はWCAG 3で採用予定の&lt;a href=&quot;https://gihyo.jp/article/2023/08/apca-02&quot;&gt;APCA&lt;/a&gt;を満たすようにしました。測定には&lt;a href=&quot;https://apcacontrast.com/&quot;&gt;APCA Contrast Calculator&lt;/a&gt;を使いました。&lt;/p&gt;
&lt;p&gt;今回の調整で一番苦労したのはメインの色です。今までは &lt;code&gt;#003760&lt;/code&gt; ——OKLCHでは &lt;code&gt;oklch(0.329 0.0888 248.18)&lt;/code&gt; を使っていましたが、ダークモードの背景 &lt;code&gt;oklch(0.28 0 0)&lt;/code&gt; と合わせたときにコントラストがLc 0となってしまうため色を変えなくてはいけませんでした。&lt;/p&gt;
&lt;p&gt;個人的には深みのある青色の象徴として &lt;code&gt;#003760&lt;/code&gt; という値を暗唱できるくらいには気に入っていましたが、コントラストの関係で色を調整せざるを得なくなり、ライトモードでは &lt;code&gt;oklch(0.43 0.119 253)&lt;/code&gt; を使い、ダークモードでは &lt;code&gt;oklch(0.51 0.141 253)&lt;/code&gt; を使うようにしました。とはいえ、新しい色もすでに慣れました。&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;ダークモード対応は思った以上に奥が深いものでした。結果的に配色を一から考え直すことになりました。その分、個人的には気にいった見た目になりました。もしダークモードを使っている人がいれば、見え方のフィードバックをもらえると嬉しいです。&lt;/p&gt;
&lt;h2 id=&quot;参考リンク&quot;&gt;参考リンク&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/API/Popover_API&quot;&gt;Popover API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Global_attributes/popover&quot;&gt;HTML popover グローバル属性 - HTML | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/CSS/Guides/Anchor_positioning&quot;&gt;CSS アンカー位置指定 - CSS | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zenn.dev/ubie_dev/articles/anchor-positioning-popover&quot;&gt;Anchor Positioningが全対応。HTML・CSSだけのポップオーバーが完全体に&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/CSS/Reference/Values/color_value/oklch&quot;&gt;oklch() - CSS | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oklch.com/#0.7,0.1,61,100&quot;&gt;OKLCH Color Picker &amp;#x26; Converter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://speakerdeck.com/godlingkogami/charcoal-2-dot-0-tesainsisutemunoji-pan-wozai-gou-zhu&quot;&gt;Charcoal 2.0: デザインシステムの基盤を再構築 - Speaker Deck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gihyo.jp/article/2023/08/apca-02&quot;&gt;第2回　WCAG3のコントラスト基準APCAの考え方と実例 | gihyo.jp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.apcacontrast.com/documentation/APCA_in_a_Nutshell&quot;&gt;APCA in a Nutshell | APCA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2026-02-02T03:00:00.000Z</published>
<updated>2026-02-02T03:00:00.000Z</updated>
</entry>
<entry>
<title>ブログにいいねボタンを実装した</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/implement-like-button"/>
<id>tag:blog.kubosho.com,2026-02-01:entry:implement-like-button</id>
<summary>いいねボタンをフロントエンド・バックエンドの両面で設計・実装した記録です。</summary>
<content type="html">&lt;p&gt;このブログにいいねボタンを実装しました。以下の構成で実装しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;LikeButton (UI)
↓
React custom hooks (SWRフック、楽観的更新、デバウンス + バッファリング)
↓
POST /api/likes/[id] (Astro APIエンドポイント)
↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;フロントエンドはReactコンポーネントとカスタムフックを組み合わせて、APIリクエストとUI更新を担い、バックエンドはAstroのAPI Routesを使っていいね周りのAPIを実装し、PostgreSQLを使ってデータを永続化しています。&lt;/p&gt;
&lt;h2 id=&quot;なぜ実装したのか&quot;&gt;なぜ実装したのか&lt;/h2&gt;
&lt;p&gt;ブログ記事を読んだ人から反応が欲しかったためです。&lt;/p&gt;
&lt;p&gt;いいね機能は、コメント機能と比較して、読者が手軽に反応を示せる手段だと考えています。読者は「参考になった」「面白かった」という気持ちをワンクリックで伝えられます。また、書き手である自分にとっても、どの記事が読者に響いているかを把握する指標になる可能性があると考えました。&lt;/p&gt;
&lt;h3 id=&quot;いいねボタンを連打可能にした理由&quot;&gt;いいねボタンを連打可能にした理由&lt;/h3&gt;
&lt;p&gt;実装にあたっては、ログイン不要で読者が何度でもいいねできる仕様にしました。気軽に連打できる体験をさせたかったのと、個人ブログにわざわざログインをする人はいないと考えたためです。&lt;/p&gt;
&lt;h3 id=&quot;完璧を求めない判断&quot;&gt;完璧を求めない判断&lt;/h3&gt;
&lt;p&gt;また「いいね数が多少失われても問題ない」という前提で設計しました。&lt;/p&gt;
&lt;p&gt;たとえば、いいねのリクエストのリトライキューの実装にlocalStorageではなくsessionStorageを使っています。いいねの送信や反映が失敗して無かったことになったとしても、不要なリトライキューがブラウザーに残り続け、リクエストが繰り返されてしまうことを防ぐようにしました。&lt;/p&gt;
&lt;h2 id=&quot;フロントエンドの実装&quot;&gt;フロントエンドの実装&lt;/h2&gt;
&lt;p&gt;まずはフロントエンドの実装から見ていきましょう。&lt;/p&gt;
&lt;h3 id=&quot;likebuttonコンポーネント&quot;&gt;LikeButtonコンポーネント&lt;/h3&gt;
&lt;p&gt;いいねボタン用のコンポーネントは、React組み込みのフックやカスタムフックを組み合わせて、拍手アイコンといいね数を表示している構成です。&lt;/p&gt;
&lt;p&gt;Astroのコンポーネントでコンポーネントを作らなかった理由は、&lt;a href=&quot;https://docs.astro.build/en/recipes/sharing-state/&quot;&gt;Share state between Astro components | Docs&lt;/a&gt;というドキュメントを見て、Astroで状態を管理するためにNano Storesというライブラリを導入しないといけないことを知り、それが若干面倒に感じたためです。&lt;/p&gt;
&lt;p&gt;では実装を見ていきましょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function LikeButton({ entryId, likeLabel, onClick }: Props): React.JSX.Element {
  // 状態・カスタムフック定義
  const [clapping, setClapping] = useState(false);
  const { counts, handleLikes, isLoading } = useLikes({ entryId });
  // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;clapping&lt;/code&gt; はクリック時に表示するアニメーション用の状態です。&lt;code&gt;useLikes&lt;/code&gt; フックではいいねの数と操作用のハンドラーを定義しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// ...
const handleClick = useCallback(() =&gt; {
  handleLikes();

  setClapping(false);
  requestAnimationFrame(() =&gt; {
    setClapping(true);
  });

  onClick?.();
}, [handleLikes, onClick]);
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;クリック時に &lt;code&gt;requestAnimationFrame&lt;/code&gt; を使い、アニメーションが毎回実行されるようにしています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;  // ...
  return (
    &amp;#x3C;div className={styles.container}&gt;
      &amp;#x3C;button type=&quot;button&quot; className={styles.button} aria-label={likeLabel} onClick={handleClick}&gt;
        &amp;#x3C;span className={clsx(styles.clap, clapping &amp;#x26;&amp;#x26; styles.clapping)}&gt;
          {/* アイコン */}
        &amp;#x3C;/span&gt;
      &amp;#x3C;/button&gt;
      &amp;#x3C;span className={styles.count} aria-live=&quot;polite&quot;&gt;
        {counts}
      &amp;#x3C;/span&gt;
    &amp;#x3C;/div&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;いいね数を表示する要素に対して &lt;code&gt;aria-live=&quot;polite&quot;&lt;/code&gt; を指定することで、いいね数をスクリーンリーダーで読まれるようにしています。&lt;/p&gt;
&lt;h3 id=&quot;楽観的ui更新&quot;&gt;楽観的UI更新&lt;/h3&gt;
&lt;p&gt;ユーザーがいいねボタンをクリックした瞬間に、サーバーからのレスポンスを待たずにUIを更新しています。&lt;/p&gt;
&lt;p&gt;データ取得にはSWRを使っています。TanStack Queryを使うことも考えましたが、今回はいいね数の取得とUIの更新に使うだけなので、APIがシンプルなSWRで十分と判断しました。&lt;/p&gt;
&lt;p&gt;まずSWRを使っていいね数を取得し、バッファリング用のカスタムフックを読み込みます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useLikes({ entryId }: UseLikeParams): UseLikeReturn {
  const { data, isLoading, mutate } = useSWR&amp;#x3C;LikesOnGetResponse | null&gt;(
    `/api/likes/${entryId}`,
    fetcher,
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
    }
  );
  const { updateLikeCounts } = useLikesBuffer();
  // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;タブがフォーカスされた時やネットワーク再接続時の自動再検証は必要ないので、&lt;code&gt;useSWR&lt;/code&gt; のオプションに &lt;code&gt;revalidateOnFocus: false&lt;/code&gt; と &lt;code&gt;revalidateOnReconnect: false&lt;/code&gt; を指定しています。&lt;/p&gt;
&lt;p&gt;いいねボタンがクリックされた時のハンドラー定義は以下の通りです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;  // ...
  const handleLikes = useCallback(() =&gt; {
    // 即座にUI更新
    void mutate({
      id: entryId,
      counts: countsRef.current + 1
    }, { revalidate: false });
    // バッファに追加
    updateLikeCounts(entryId, 1);
  }, [entryId, mutate, updateLikeCounts]);

  return { counts: countsRef.current, handleLikes, isLoading };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SWRの &lt;code&gt;mutate&lt;/code&gt; 関数を使ってローカルのキャッシュを即座に更新しています。&lt;code&gt;revalidate: false&lt;/code&gt; でサーバーへの再検証リクエストを抑制しています。同時に &lt;code&gt;updateLikeCounts&lt;/code&gt; でバッファにいいねの回数を追加し、後でまとめてリクエストをするようにしています。&lt;/p&gt;
&lt;h3 id=&quot;バッファリングによるリクエスト最適化&quot;&gt;バッファリングによるリクエスト最適化&lt;/h3&gt;
&lt;p&gt;いいねボタンを連打した場合、ボタンを押した回数分そのままAPIリクエストした場合に、無駄なリクエストが増え、サーバーとユーザーどちらにも嬉しくない事態になります。そこで一定時間内はいいねボタンの押した回数をバッファリングしておき、いいねを押した回数だけいいね数を加算してリクエストを送信するようにしています。&lt;/p&gt;
&lt;p&gt;以下のコードでは、バッファリング用の定数とrefを定義しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const FLUSH_TIMER = 1_000;

export function useLikesBuffer(): UseLikeBufferReturn {
  const bufferedIncrementsRef = useRef&amp;#x3C;Map&amp;#x3C;string, number&gt;&gt;(new Map());
  const debounceTimerRef = useRef&amp;#x3C;ReturnType&amp;#x3C;typeof setTimeout&gt; | null&gt;(null);
  // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bufferedIncrementsRef&lt;/code&gt; は記事IDごとのいいね数の増分を保持し、&lt;code&gt;debounceTimerRef&lt;/code&gt; はデバウンス用のタイマーを管理しています。&lt;/p&gt;
&lt;p&gt;いいね数を更新する関数は以下の通り定義します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;  // ...
  const updateLikeCounts = useCallback((entryId: string, increment: number) =&gt; {
    // 現在のバッファに増分を加算
    const currentIncrement = bufferedIncrementsRef.current.get(entryId) ?? 0;
    bufferedIncrementsRef.current.set(entryId, currentIncrement + increment);

    // 既存のタイマーをクリア
    if (debounceTimerRef.current != null) {
      clearTimeout(debounceTimerRef.current);
    }
    // ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;いいねボタンが押されるたびにタイマーをリセットすることで、連打中はリクエストを送信しないようにして、連打が止まってから送信されるようにしています。&lt;/p&gt;
&lt;p&gt;最後に、タイマーを発火してバッファに溜めたいいねの数をサーバーに送信します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;    // ...
    debounceTimerRef.current = setTimeout(() =&gt; {
      const totalIncrement = bufferedIncrementsRef.current.get(entryId) ?? 0;
      bufferedIncrementsRef.current.delete(entryId);
      void sendLikes(entryId, totalIncrement);
    }, FLUSH_TIMER);
  }, []);

  return { updateLikeCounts };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ここまでの実装によって、1秒以内に5回いいねボタンを押された場合に5回リクエストするのではなく、payloadに &lt;code&gt;increment: 5&lt;/code&gt; というobjectが入った状態のリクエストにまとめられます。デバウンスの間隔は実際に連打してみて、1秒くらいが「連打が止まった」と判断するのにちょうど良い体感でした。&lt;/p&gt;
&lt;h3 id=&quot;リトライキューの実装&quot;&gt;リトライキューの実装&lt;/h3&gt;
&lt;p&gt;ネットワークエラーでリクエストが失敗した場合を考慮して、sessionStorageにリトライキューを保存しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function saveToRetryQueue(entryId: string, increment: number): void {
  const storage = getStorage();
  if (storage == null) {
    return;
  }

  const queue = loadRetryQueue();
  queue.push({ entryId, increment, timestamp: Date.now() });
  storage.setItem(LIKE_SEND_RETRY_QUEUE_KEY, JSON.stringify(queue));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ページを再読み込みした時にリトライキューを読み込み、失敗したリクエストを再送信しています。&lt;/p&gt;
&lt;h2 id=&quot;バックエンドの実装&quot;&gt;バックエンドの実装&lt;/h2&gt;
&lt;p&gt;次にバックエンドの実装を見ていきます。&lt;/p&gt;
&lt;h3 id=&quot;いいね数を保存するためのテーブル定義&quot;&gt;いいね数を保存するためのテーブル定義&lt;/h3&gt;
&lt;p&gt;Drizzleを使って以下のように定義しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const likes = pgTable(&#39;likes&#39;, {
  id: serial(&#39;id&#39;).primaryKey(),
  entryId: varchar(&#39;entry_id&#39;, { length: 255 }).notNull().unique(),
  counts: integer(&#39;counts&#39;).default(0).notNull(),
  createdAt: timestamp(&#39;created_at&#39;).defaultNow(),
  updatedAt: timestamp(&#39;updated_at&#39;).defaultNow(),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;記事IDにはユニーク制約を付けていて、記事ごとに1レコードとなります。PostgreSQLのホスティングは&lt;a href=&quot;https://neon.com/&quot;&gt;Neon&lt;/a&gt;を使っています。無料枠の存在が決め手でしたが、無料枠がなくなった場合は料金次第で移行するかもしれません。&lt;/p&gt;
&lt;h3 id=&quot;apiエンドポイント&quot;&gt;APIエンドポイント&lt;/h3&gt;
&lt;p&gt;AstroのAPI Routesを使って、GETとPOSTのエンドポイントを実装しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// GET: いいね数の取得
export async function GET({ locals, params, request }: APIContext): Promise&amp;#x3C;Response&gt; {
  const { id } = params;
  if (!isValidEntryIdFormat(id)) {
    return createClientErrorResponse({ type: &#39;invalidEntryId&#39; });
  }

  const exists = await entryExists(id);
  if (!exists) {
    return createClientErrorResponse({ type: &#39;entryNotFound&#39; });
  }

  const counts = await getLikeCounts({ context: locals, entryId: id });
  return new Response(JSON.stringify({ id, counts }), { status: 200 });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;いいね数の登録更新&quot;&gt;いいね数の登録・更新&lt;/h3&gt;
&lt;p&gt;いいね数の登録・更新には、Drizzleで定義されている &lt;code&gt;onConflictDoUpdate&lt;/code&gt;（いわゆるUpsert）を使っています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export async function incrementLikeCounts({ context, entryId, increment }: IncrementParams): Promise&amp;#x3C;number&gt; {
  const db = getDbClient(context);

  const result = await db
    .insert(likes)
    .values({ counts: increment, entryId })
    .onConflictDoUpdate({
      target: [likes.entryId],
      set: {
        counts: sql`${likes.counts} + ${increment}`,
        updatedAt: new Date(),
      },
    })
    .returning({ counts: likes.counts });

  return result[0]?.counts ?? 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;cloudflare-cache-apiによるキャッシュ&quot;&gt;Cloudflare Cache APIによるキャッシュ&lt;/h3&gt;
&lt;p&gt;GETリクエストの結果はCloudflare Cache APIを使ってエッジでキャッシュしています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const cache = locals.runtime?.caches?.default ?? null;
const cacheKey = createNormalizedCacheKey(request);

const cachedResponse = await cache?.match(cacheKey);
if (cachedResponse != null) {
  return new Response(await cachedResponse.text(), {
    status: cachedResponse.status,
    headers: cachedResponse.headers,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;POSTリクエストでいいね数が更新された際には、該当するキャッシュを削除して、次回のGETで最新の値が取得されるようにしています。&lt;/p&gt;
&lt;h2 id=&quot;セキュリティ対策&quot;&gt;セキュリティ対策&lt;/h2&gt;
&lt;p&gt;セキュリティ対策の一部は、AIエージェントに指摘されて追加しました。自分だけでは見落としていた観点です。&lt;/p&gt;
&lt;p&gt;入力値のバリデーションと記事IDの検証は、AIエージェントからの指摘で「確かにDevToolsやcURLによるリクエストでゴミデータが増えたり、不正にいいね数が増えたりする」と認識しました。&lt;/p&gt;
&lt;h3 id=&quot;レート制限&quot;&gt;レート制限&lt;/h3&gt;
&lt;p&gt;Cloudflare WorkersのRate Limiting APIを使って、短時間での大量リクエストを制限しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export async function checkRateLimit({ clientIp, entryId, rateLimiter }: Params): Promise&amp;#x3C;boolean&gt; {
  try {
    const rateLimitKey = JSON.stringify({ clientIp, entryId });
    const { success } = await rateLimiter.limit({ key: rateLimitKey });
    return !success;
  } catch (error) {
    console.error(&#39;Rate limit check failed:&#39;, error);
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;入力値のバリデーション&quot;&gt;入力値のバリデーション&lt;/h3&gt;
&lt;p&gt;リクエストボディのバリデーションにはValibotを使用しています。バリデーションを実装する過程で&lt;a href=&quot;https://docs.astro.build/en/reference/modules/astro-zod/&quot;&gt;AstroではZodをデフォルトで使える&lt;/a&gt;ことに気付きましたが、見ないふりをしました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const likesOnPostRequestSchema = object({
  increment: pipe(
    number(),
    integer(),
    minValue(1, &#39;Increment must be at least 1&#39;),
    maxValue(MAX_INCREMENT_VALUE, `Increment must be at most ${MAX_INCREMENT_VALUE}`),
  ),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;記事idの検証&quot;&gt;記事IDの検証&lt;/h3&gt;
&lt;p&gt;存在しない記事IDに対するリクエストを弾くため、記事IDの形式チェックと存在確認を行っています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const MAX_ENTRY_ID_LENGTH = 100;
const ENTRY_ID_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;

// 形式チェック：100文字以下の英小文字、数字、ハイフンのみ許可
export function isValidEntryIdFormat(id: string | undefined): id is string {
  if (id == null || id === &#39;&#39; || id.length &gt; MAX_ENTRY_ID_LENGTH) {
    return false;
  }
  return ENTRY_ID_PATTERN.test(id);
}

// 記事の存在確認
export async function entryExists(entryId: string, getEntryFn: GetEntryFn): Promise&amp;#x3C;boolean&gt; {
  const entry = await getEntryFn(&#39;entries&#39;, entryId);
  return entry !== undefined;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;今回のいいねボタンの実装では個人ブログという規模に合わせた判断をしました。「いいねボタンを連打可能にする」のはその1つです。&lt;/p&gt;
&lt;p&gt;一方で実装するにあたってAIエージェントも使ってさまざまな点を考慮しました。たとえば以下の点です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;楽観的UI更新による即時フィードバック&lt;/li&gt;
&lt;li&gt;バッファリングによるAPIリクエストの最適化&lt;/li&gt;
&lt;li&gt;レート制限と入力値バリデーションによるセキュリティ対策&lt;/li&gt;
&lt;li&gt;sessionStorageを使うことで、不要なデータの残留を防ぐ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;こういったどのように実装するか自分ですべて判断し実装できるという点で、個人ブログの開発は面白いです。&lt;/p&gt;</content>
<published>2026-02-01T10:30:00.000Z</published>
<updated>2026-02-01T10:30:00.000Z</updated>
</entry>
<entry>
<title>個人の技術ブログをリニューアルした</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/blog-renewal-2025"/>
<id>tag:blog.kubosho.com,2025-12-28:entry:blog-renewal-2025</id>
<summary>個人の技術ブログ（今あなたが見ているブログ）をリニューアルしました。</summary>
<content type="html">&lt;p&gt;個人の技術ブログ（今あなたが見ているブログ）をリニューアルしました。&lt;/p&gt;
&lt;p&gt;見た目はそこまで変わっていないですが、内部はいろいろと構成を見直したり機能を追加したりしました。&lt;/p&gt;
&lt;h2 id=&quot;システム構成の変更&quot;&gt;システム構成の変更&lt;/h2&gt;
&lt;p&gt;以前は、Next.jsベースでブログを作り、microCMSから記事を入稿し、Vercelでホスティングしていました。その時のシステム構成は&lt;a href=&quot;https://blog.kubosho.com/entries/my-blog-architecture&quot;&gt;Next.js + Vercel + microCMSなどを使ってほぼ無料でブログを運用する&lt;/a&gt;で書いています。&lt;/p&gt;
&lt;p&gt;しかし、いくつか問題がありました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静的コンテンツが多いブログに対して、Next.jsがオーバースペック&lt;/li&gt;
&lt;li&gt;CMSで管理したくなるほどコンテンツ管理が複雑化していない。やはりオーバースペック&lt;/li&gt;
&lt;li&gt;Vercelにロックインされるのが、なんとなく嫌だった&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;そのため、現在はAstroで大部分のコンテンツをSSGするようにして、&lt;a href=&quot;https://github.com/kubosho/articles&quot;&gt;専用のリポジトリ&lt;/a&gt;で記事を管理し、Cloudflare Workersでホスティングしています。&lt;/p&gt;
&lt;p&gt;なおロックインという観点では、Cloudflareの機能にだいぶ依存しているので、依存先が変わっただけと言えます。ロックイン解消という観点では移行した意味がないですね。&lt;/p&gt;
&lt;h2 id=&quot;いいねボタンの設置&quot;&gt;いいねボタンの設置&lt;/h2&gt;
&lt;p&gt;noteのいいねボタンや、&lt;a href=&quot;https://catnose.me/notes&quot;&gt;catnose&#39;s notes&lt;/a&gt;の拍手ボタンに触発されて、読者が簡易的なフィードバックを送れる手段として設置しました。&lt;/p&gt;
&lt;p&gt;最近はnoteで&lt;a href=&quot;https://note.com/kubosho_/m/mef4952821660&quot;&gt;日々の心がけ&lt;/a&gt;といった記事を書くことが増えています。noteはいいねの数がPV以外の指標として分かりやすく、記事が読まれている実感を得られる点が良いです。下衆な話をすれば、承認欲求が満たされるとも言います。&lt;/p&gt;
&lt;p&gt;そういった体験を自分のブログでも得たくなったのが開発のきっかけでした。&lt;/p&gt;
&lt;h3 id=&quot;いいねボタンの技術的な話&quot;&gt;いいねボタンの技術的な話&lt;/h3&gt;
&lt;p&gt;APIの実装にはAstroの&lt;a href=&quot;https://docs.astro.build/en/guides/endpoints/#server-endpoints-api-routes&quot;&gt;Server Endpoints&lt;/a&gt;を使用しています。&lt;/p&gt;
&lt;p&gt;GETリクエストは&lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/cache/&quot;&gt;Cache API&lt;/a&gt;でキャッシュして、Cloudflareの&lt;a href=&quot;https://developers.cloudflare.com/cache/how-to/edge-browser-cache-ttl/&quot;&gt;Edge and Browser Cache TTL&lt;/a&gt;のデフォルト値を適用しています。&lt;/p&gt;
&lt;p&gt;キャッシュのinvalidationはPOSTリクエスト時の &lt;code&gt;cache.delete()&lt;/code&gt; で実現していて、キャッシュヒット率向上のためにクエリパラメータを削除したURLをキーにしています。&lt;/p&gt;
&lt;p&gt;POSTリクエストに対しては、&lt;a href=&quot;https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/&quot;&gt;Rate Limiting API&lt;/a&gt;で一定のレートリミットもかけています。アクセス数は多くないので不要そうですが、攻撃対策として一応入れています。&lt;/p&gt;
&lt;p&gt;いいね数は&lt;a href=&quot;https://neon.com/&quot;&gt;Neon&lt;/a&gt;上のデータベースに保存しています。Neonは無料枠の存在が嬉しいですが、もし無料枠がなくなったら移行するかもしれません。&lt;/p&gt;
&lt;p&gt;またリクエストのバリデーションに&lt;a href=&quot;https://valibot.dev/&quot;&gt;Valibot&lt;/a&gt;を使っています。Astroでは &lt;code&gt;astro/z&lt;/code&gt; を介して&lt;a href=&quot;https://docs.astro.build/en/reference/modules/astro-zod/#z&quot;&gt;Zod&lt;/a&gt;が使えることをAstro v5へ更新したときに知りましたが、開発を一段落させることを優先してValibotをひとまず使うことにしました。&lt;/p&gt;
&lt;h2 id=&quot;シェアボタンの設置&quot;&gt;シェアボタンの設置&lt;/h2&gt;
&lt;p&gt;以前は、SNSの喧噪と距離を置くため意図的に設置していませんでした。&lt;/p&gt;
&lt;p&gt;しかし、記事をより多くの人に見てほしいという欲が出てきたのでシェアボタンを設置しました。シェアボタンを設置することによって、すぐにシェアが増加して承認欲求が満たされてウハウハ！とはならないですが、まずは設置することが重要と考えました。&lt;/p&gt;
&lt;h2 id=&quot;いろいろと試せる場があると楽しい&quot;&gt;いろいろと試せる場があると楽しい&lt;/h2&gt;
&lt;p&gt;個人の技術ブログは自分の好きなようにできますし、やりたいことをやる気になったらすぐ試せます。いろいろあーでもないこーでもないと格闘できるのが楽しいです。&lt;/p&gt;
&lt;p&gt;そういった場の存在を保つためにも、この個人の技術ブログを無くすことはないでしょう。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;なお、実装がどうなっているのか見たい方は、ソースコードを&lt;a href=&quot;https://github.com/kubosho/blog.kubosho.com&quot;&gt;kubosho/blog.kubosho.com&lt;/a&gt;に置いているので見てください。&lt;/p&gt;</content>
<published>2025-12-28T12:00:00.000Z</published>
<updated>2025-12-30T14:00:00.000Z</updated>
</entry>
<entry>
<title>eslint-plugin-importによってVitestの設定ファイル上でエラーが発生する場合がある</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/eslint-plugin-import-error-on-vitest-configuration-file"/>
<id>tag:blog.kubosho.com,2022-11-23:entry:eslint-plugin-import-error-on-vitest-configuration-file</id>
<summary>vitest と eslint-plugin-import に依存している環境では、vitest.config.ts内で vitest/config をインポートすると Unable to resolve path to module 'vitest/config'. eslint(import/no-unresolved) と表示されることがあります。</summary>
<content type="html">&lt;p&gt;&lt;code&gt;vitest&lt;/code&gt; と &lt;code&gt;eslint-plugin-import&lt;/code&gt; に依存している環境では、vitest.config.ts内で &lt;code&gt;vitest/config&lt;/code&gt; をインポートすると &lt;code&gt;Unable to resolve path to module &#39;vitest/config&#39;. eslint(import/no-unresolved)&lt;/code&gt; と表示されることがあります。&lt;/p&gt;
&lt;p&gt;これはnode_modules/vitest以下にconfig.d.tsのみが存在し、config.jsの実体はdist/config.jsにあるのが原因です。ディレクトリ構造を示すと次のようになります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/node_modules/vitest
|-- dist
|   |-- config.cjs
|   |-- config.d.ts
|   |-- config.js
`-- config.d.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vitest/config.d.tsの内容はvitest/dist/config.jsでexportしているものをそのままexportしています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export * from &#39;./dist/config.js&#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;解決方法&quot;&gt;解決方法&lt;/h2&gt;
&lt;p&gt;ESLintの設定ファイルで &lt;code&gt;import/resolver&lt;/code&gt; 内の &lt;code&gt;node.extensions&lt;/code&gt; の値に &lt;code&gt;d.ts&lt;/code&gt; を追加すると &lt;code&gt;d.ts&lt;/code&gt; 内のモジュール読み込みが解決できるようになり、ESLintの &lt;code&gt;import/no-unresolved&lt;/code&gt; のエラーが無くなります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;settings: {
  &#39;import/resolver&#39;: {
    node: {
      extensions: [&#39;.js&#39;, &#39;.jsx&#39;, &#39;.ts&#39;, &#39;.tsx&#39;, &#39;.d.ts&#39;],
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;</content>
<published>2022-11-23T05:16:24.348Z</published>
<updated>2022-11-23T05:50:53.625Z</updated>
</entry>
<entry>
<title>Next.js + Vercel + microCMSなどを使ってほぼ無料でブログを運用する</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/my-blog-architecture"/>
<id>tag:blog.kubosho.com,2022-08-24:entry:my-blog-architecture</id>
<summary>当ブログのシステム構成について紹介します。構成を真似することでほぼ無料（後述）でブログを運営できます。</summary>
<content type="html">&lt;p&gt;当ブログのシステム構成について紹介します。構成を真似することでほぼ無料（後述）でブログを運営できます。&lt;/p&gt;
&lt;h2 id=&quot;当ブログの構成概要&quot;&gt;当ブログの構成概要&lt;/h2&gt;
&lt;p&gt;いわゆるJamstack構成です。構成を画像で示すと次の通りです。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/blog_system_diagram.png&quot; alt=&quot;ブログの構成を表した図&quot;&gt;&lt;/p&gt;
&lt;p&gt;microCMSまたは&lt;a href=&quot;https://github.com/kubosho/blog.kubosho.com&quot;&gt;ブログのリポジトリ&lt;/a&gt;に変更がある場合、Vercelの&lt;a href=&quot;https://vercel.com/docs/concepts/git/deploy-hooks&quot;&gt;Deploy Hooks&lt;/a&gt;を使ってVercel上の環境を更新するようにしています。&lt;/p&gt;
&lt;p&gt;Next.js上でビルドするときに、ヘッドレスCMSのAPIを使ってブログの記事などを取得し、&lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt;の&lt;a href=&quot;https://nextjs.org/docs/basic-features/data-fetching/get-static-props&quot;&gt;getStaticProps&lt;/a&gt;を使ってStatic Site Generateをできるようにしています。&lt;/p&gt;
&lt;p&gt;ブログのコンテンツは&lt;a href=&quot;https://vercel.com/docs/concepts/edge-network/overview&quot;&gt;Vercelが提供しているCDN&lt;/a&gt;によってキャッシュされます。&lt;/p&gt;
&lt;p&gt;また画像などはAmazon S3上にアップロードしたものを、CloudFrontでキャッシュして配信するようにしています。&lt;/p&gt;
&lt;h2 id=&quot;どうしてブログをjamstack構成で作ろうと思ったか&quot;&gt;どうしてブログをJamstack構成で作ろうと思ったか&lt;/h2&gt;
&lt;p&gt;もともとこのブログは&lt;a href=&quot;https://hatenablog.com/&quot;&gt;はてなブログ&lt;/a&gt;上で書かれていました。&lt;/p&gt;
&lt;p&gt;はてなブログを使っていた理由としては、WordPressなどのCMSを使った場合に、本体やプラグインの更新管理が面倒だったのと、投稿画面が使いやすかったためでした。&lt;/p&gt;
&lt;h3 id=&quot;素振り環境が欲しくなった&quot;&gt;素振り環境が欲しくなった&lt;/h3&gt;
&lt;p&gt;はてなブログでブログを運用するうちに、Webフロントエンドやサーバーサイドも含めた素振り環境が欲しくなりました。&lt;/p&gt;
&lt;p&gt;そうした思いと、はてなブログProで提供されている機能のうち独自ドメインくらいしか恩恵を受けていないことに気づき、ブログを自前で作ろうと思い立ちました。&lt;/p&gt;
&lt;p&gt;そこでブログを作り出した当初は、サーバーサイドも含めて作っていました。&lt;/p&gt;
&lt;p&gt;ただ作業する中でいつまで経っても完成しない未来が見えたので、サーバーサイド側を作りこむのは諦め、Next.jsを使ってさくっと作って公開することにしました。&lt;/p&gt;
&lt;h2 id=&quot;どのような技術スタックになっているか&quot;&gt;どのような技術スタックになっているか&lt;/h2&gt;
&lt;p&gt;使っているサービスやフレームワーク・ライブラリーを表にしました。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;技術&lt;/th&gt;
&lt;th&gt;リンク&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;フレームワーク&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ヘッドレスCMS&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://microcms.io/&quot;&gt;microCMS&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ホスティング&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://vercel.com/&quot;&gt;Vercel&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;画像ストレージ&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://aws.amazon.com/jp/s3/&quot;&gt;Amazon S3&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;画像CDN&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://aws.amazon.com/jp/cloudfront/&quot;&gt;CloudFront&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/lukeed/rosetta&quot;&gt;Rosetta&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;マークダウンプロセッサー&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://unifiedjs.com/&quot;&gt;unified&lt;/a&gt;, &lt;a href=&quot;https://remark.js.org/&quot;&gt;remark&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTMLコンバーター&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/rehypejs/rehype&quot;&gt;rehype&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;テスト&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://vitest.dev/&quot;&gt;Vitest&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;この中からヘッドレスCMSの変遷について話します。&lt;/p&gt;
&lt;h3 id=&quot;最初はヘッドレスcmsにcontentfulを使った&quot;&gt;最初はヘッドレスCMSにContentfulを使った&lt;/h3&gt;
&lt;p&gt;はてなブログからNext.jsベースのブログに置き換えたとき、&lt;a href=&quot;https://www.contentful.com/&quot;&gt;Contentful&lt;/a&gt;を使っていました。&lt;/p&gt;
&lt;p&gt;しかし&lt;a href=&quot;https://blog.kubosho.com/entry/migrating-from-contentful-to-markdown-file&quot;&gt;自動保存のタイミングでIMEが確定されて書いた記事の文章がめちゃくちゃになる現象&lt;/a&gt;が出た結果、ContentfulからGitリポジトリにMarkdownファイルをコミットして、事前に記事の情報をまとめたJSONファイルを作る形式に変更しました。&lt;/p&gt;
&lt;h3 id=&quot;markdownファイルをコミットする形式からmicrocmsへの移行&quot;&gt;Markdownファイルをコミットする形式からmicroCMSへの移行&lt;/h3&gt;
&lt;p&gt;なんで移行しようと思ったかは覚えていないですが、6時間くらいかけてmicroCMSに記事を移行できたようです。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;だいたい 6 時間くらいでブログのシステムを Markdown を直接更新する形式から microCMS で記事を入稿する形式にできた&lt;/p&gt;&amp;mdash; kubosho (@kubosho_) &lt;a href=&quot;https://twitter.com/kubosho_/status/1452620689240780802?ref_src=twsrc%5Etfw&quot;&gt;October 25, 2021&lt;/a&gt;&lt;/blockquote&gt;
&lt;h2 id=&quot;どれほどの料金がかかるか&quot;&gt;どれほどの料金がかかるか&lt;/h2&gt;
&lt;p&gt;今年実際にかかった料金を表にしました。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名前&lt;/th&gt;
&lt;th&gt;プラン&lt;/th&gt;
&lt;th&gt;料金&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;microCMS&lt;/td&gt;
&lt;td&gt;Hobbyプラン&lt;/td&gt;
&lt;td&gt;￥0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Hobbyプラン&lt;/td&gt;
&lt;td&gt;￥0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3&lt;/td&gt;
&lt;td&gt;従量課金&lt;/td&gt;
&lt;td&gt;￥0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudFront&lt;/td&gt;
&lt;td&gt;従量課金&lt;/td&gt;
&lt;td&gt;$0～$0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;今後どうしていきたいか&quot;&gt;今後どうしていきたいか&lt;/h2&gt;
&lt;p&gt;最後に当ブログの展望を示します。&lt;/p&gt;
&lt;h3 id=&quot;ogp-imageの自動生成&quot;&gt;OGP imageの自動生成&lt;/h3&gt;
&lt;p&gt;現在は記事がSNSなどにシェアされた場合、固定の画像が表示されます。&lt;/p&gt;
&lt;p&gt;これを多くのCGMサービスで導入されているように、ブログのテーマに沿った画像を自動生成して使うようにしたいと考えています。&lt;/p&gt;
&lt;h3 id=&quot;デバフの導入&quot;&gt;デバフの導入&lt;/h3&gt;
&lt;p&gt;今はサードパーティースクリプトなどが少ないので表示速度が早いですが、広告のスクリプトやWebフォントなどを導入してそれでもなお表示速度が早いというブログにしたいと考えています。&lt;/p&gt;
&lt;p&gt;ただVercelのhobbyプランは広告を扱うと商用利用扱いになって&lt;a href=&quot;https://vercel.com/docs/concepts/limits/fair-use-policy#commercial-usage&quot;&gt;Proプラン以上の契約が必要&lt;/a&gt;になります。そのため広告のスクリプトを導入するなら、VercelのProプラン以上を契約するか、商用利用可のホスティングサービスへ移動します。&lt;/p&gt;
&lt;h3 id=&quot;ブログのパフォーマンス指標の継続的な測定&quot;&gt;ブログのパフォーマンス指標の継続的な測定&lt;/h3&gt;
&lt;p&gt;広告のスクリプトやWebフォントの導入に伴って、パフォーマンス指標の継続的な測定を何かしらでして、パフォーマンス向上施策がどのくらい効果あるのか見られるようにしたいと考えています。&lt;/p&gt;
&lt;h3 id=&quot;googleアナリティクスの活用&quot;&gt;Googleアナリティクスの活用&lt;/h3&gt;
&lt;p&gt;今はただGoogleアナリティクスを導入しているだけですが、コンバージョンの設定なども入れてPV数などを上げるための参考指標を作りたいと考えています。&lt;/p&gt;
&lt;h3 id=&quot;継続的にブログ更新できる仕組みづくり&quot;&gt;継続的にブログ更新できる仕組みづくり&lt;/h3&gt;
&lt;p&gt;なるべくブログの記事投稿を楽にしようと思い、ブログの投稿フォームや画像アップローダーを作っています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ブログの投稿フォーム - &lt;a href=&quot;https://github.com/kubosho/instant-blog-post-form&quot;&gt;https://github.com/kubosho/instant-blog-post-form&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;画像アップローダー - &lt;a href=&quot;https://github.com/kubosho/s3-image-uploader&quot;&gt;https://github.com/kubosho/s3-image-uploader&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;最後に&quot;&gt;最後に&lt;/h2&gt;
&lt;p&gt;この記事が参考になったらブログのリポジトリにスターお願いします！&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubosho/blog.kubosho.com&quot;&gt;kubosho/blog.kubosho.com: My personal blog. Built with Jamstack technology stack.&lt;/a&gt;&lt;/p&gt;</content>
<published>2022-08-24T10:26:59.603Z</published>
<updated>2022-11-28T15:39:57.855Z</updated>
</entry>
<entry>
<title>ブログ記事を投稿するためのフォームを作った</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/created-blog-post-form"/>
<id>tag:blog.kubosho.com,2022-07-16:entry:created-blog-post-form</id>
<summary>はてなブログのMarkdownによる記事編集画面やesa - 自律的なチームのための情報共有サービスのように、本文の編集画面とリアルタイムプレビューが横並びになるような投稿フォームが個人的に好みです。</summary>
<content type="html">&lt;p&gt;&lt;a href=&quot;https://hatenablog.com/&quot;&gt;はてなブログ&lt;/a&gt;のMarkdownによる記事編集画面や&lt;a href=&quot;https://esa.io/&quot;&gt;esa - 自律的なチームのための情報共有サービス&lt;/a&gt;のように、本文の編集画面とリアルタイムプレビューが横並びになるような投稿フォームが個人的に好みです。&lt;/p&gt;
&lt;p&gt;ですが、ブログで使っている&lt;a href=&quot;https://microcms.io/&quot;&gt;microCMS&lt;/a&gt;の投稿画面や、普段使っているマークダウンエディターの&lt;a href=&quot;https://obsidian.md/&quot;&gt;Obsidian&lt;/a&gt;にはプレビューを横並びにするようなオプションが無かったため、自分で記事を投稿するためのフォームを作ってみました。&lt;/p&gt;
&lt;p&gt;技術スタックとしては次の通りです。仕事では使っていないものばかりですが、いろいろ使ってみたかったものを揃えてみました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lexical.dev/&quot;&gt;Lexical · An extensible text editor framework that does things differently&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://headlessui.com/&quot;&gt;Headless UI - Unstyled, fully accessible UI components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ja.vitejs.dev/&quot;&gt;Vite | 次世代フロントエンドツール&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vitest.dev/&quot;&gt;Vitest | A blazing fast unit test framework powered by Vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zustand-demo.pmnd.rs/&quot;&gt;Zustand&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;実際の記事投稿画面は次の通りです。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/instant-blog-post-form.png&quot; alt=&quot;記事の投稿フォームのスクリーンショット&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;備えている機能&quot;&gt;備えている機能&lt;/h2&gt;
&lt;p&gt;今回作った投稿フォームの特徴は次の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ページの左側に本文編集フォームがある&lt;/li&gt;
&lt;li&gt;ページの右側にリアルタイムプレビューがある&lt;/li&gt;
&lt;li&gt;下書き状態・公開状態の切り替えが可能
&lt;ul&gt;
&lt;li&gt;ただmicroCMSはPOST APIにクエリーを付けたい場合は&lt;a href=&quot;https://microcms.io/pricing&quot;&gt;Teamプラン&lt;/a&gt;以上の契約が必要になる……（作った後の動作確認で気づいた）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ページを再読み込みしてもタイトル・URLのパス・本文の状態が保持される&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;これから作りたい機能&quot;&gt;これから作りたい機能&lt;/h2&gt;
&lt;p&gt;いま考えていることとして、次に挙げる機能を作りたいと思っています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;記事を投稿するときに確認画面を表示する&lt;/li&gt;
&lt;li&gt;記事の投稿が完了したら完了と分かるような表示を追加する&lt;/li&gt;
&lt;li&gt;記事の投稿に失敗したら失敗と分かるような表示を追加する&lt;/li&gt;
&lt;li&gt;タグの追加ができる&lt;/li&gt;
&lt;li&gt;カテゴリーの追加ができる&lt;/li&gt;
&lt;li&gt;プレビュー画面のスタイルを&lt;a href=&quot;https://blog.kubosho.com/&quot;&gt;https://blog.kubosho.com/&lt;/a&gt;と同じものを適用する&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/fukayatsu/esarea&quot;&gt;fukayatsu/esarea&lt;/a&gt;やesaの本文編集フォームのように、tabキーやenterキーを押したときにマークダウンの記法が自動で挿入される&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://textlint.github.io/&quot;&gt;textlint · The pluggable linting tool for text and markdown&lt;/a&gt;を導入して、記事を書いているときにエラーを出す&lt;/li&gt;
&lt;li&gt;何かしらのホスティングサービスにアップロードして、いろんなデバイスからアクセスできるようにする
&lt;ul&gt;
&lt;li&gt;認証機能を作る必要がある&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;本文編集部分とプレビュー部分のスクロール同期をする&lt;/li&gt;
&lt;li&gt;Vimモード&lt;/li&gt;
&lt;li&gt;画像アップロード&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;もしこの記事を見て良いなと思ったら、リポジトリへのstarとwatch登録お願いいたします。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubosho/instant-blog-post-form&quot;&gt;kubosho/instant-blog-post-form: Blog post form for https://blog.kubosho.com.&lt;/a&gt;&lt;/p&gt;</content>
<published>2022-07-16T14:36:56.557Z</published>
<updated>2022-11-28T13:54:02.295Z</updated>
</entry>
<entry>
<title>ファミリーマート公式X（Twitter）の投稿からより良い画像の代替テキスト(alt)を考える</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/familymart-tweet-alt-text"/>
<id>tag:blog.kubosho.com,2022-07-07:entry:familymart-tweet-alt-text</id>
<summary>ファミリーマートの公式X（Twitter）の投稿に、画像の代替テキストを改善できそうな投稿がありました。</summary>
<content type="html">&lt;p&gt;ファミリーマートの公式X（Twitter）の投稿に、画像の代替テキストを改善できそうな投稿がありました。&lt;/p&gt;
&lt;p&gt;この記事では画像の代替テキストのどういった点が惜しいのか、またどのように改善すればいいのか、私見を書きます。&lt;/p&gt;
&lt;p&gt;※貶すつもりは全くなく、あくまで投稿を元にどういった代替テキストを書けば良いのかを考える記事です。&lt;/p&gt;
&lt;h2 id=&quot;どのような投稿か&quot;&gt;どのような投稿か&lt;/h2&gt;
&lt;p&gt;ファミリーマート公式X（Twitter）の該当投稿を引用します。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;これが、からあげの正解。&lt;br&gt;ファミから。&lt;br&gt;&lt;br&gt;そのこだわりのすべてをALTでチェック。 &lt;a href=&quot;https://t.co/TalY56q5PX&quot;&gt;pic.twitter.com/TalY56q5PX&lt;/a&gt;&lt;/p&gt;&amp;mdash; ファミリーマート (@famima_now) &lt;a href=&quot;https://twitter.com/famima_now/status/1534686713787711491?ref_src=twsrc%5Etfw&quot;&gt;June 9, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;※投稿が削除されたので&lt;a href=&quot;https://web.archive.org/web/20220609000048/https://twitter.com/famima_now/status/1534686713787711491&quot;&gt;インターネットアーカイブに残っている投稿&lt;/a&gt;を貼っておきます。&lt;/p&gt;
&lt;p&gt;投稿を一目見ただけだと、ファミからというファミリーマートで売られている唐揚げの写真が4枚並んだ投稿です。&lt;/p&gt;
&lt;p&gt;ただし投稿にもある通り、ファミからのこだわりが代替テキストに書かれていて、投稿の画像上にあるaltボタンを押すとこだわりが見られる投稿になっています。&lt;/p&gt;
&lt;h2 id=&quot;なぜ代替テキストの使い方が惜しいのか&quot;&gt;なぜ代替テキストの使い方が惜しいのか&lt;/h2&gt;
&lt;p&gt;4枚目のお皿の上に唐揚げがたくさん乗っている画像の代替テキストとして「食べてください」を連続して書くことは、画像と代替テキストの内容が一致せず、特定の状況・状態では正しい情報が伝わらなくなります。&lt;/p&gt;
&lt;p&gt;また、1枚目の湯気が立ち込めた唐揚げを切った断面のドアップ画像・2枚目のファミからのパッケージ画像・3枚目の唐揚げが中央にあってドアップになっている画像は、画像で示している内容も代替テキストに書かれていますが、画像にはない情報や宣伝文句のような文章も書かれています。&lt;/p&gt;
&lt;p&gt;画像と代替テキストの内容が一致していないのは、テキストが画像の代替という役目を果たせないことになります。&lt;/p&gt;
&lt;p&gt;また画像にない情報や宣伝文句も代替テキストに含んでしまうと、TweetDeckや非公式アプリを使っている晴眼者は代替テキストに書かれた文章を見れず、伝えたい情報を伝えられないことになります。&lt;/p&gt;
&lt;h2 id=&quot;そもそも画像の代替テキストとは&quot;&gt;そもそも画像の代替テキストとは？&lt;/h2&gt;
&lt;p&gt;画像の代替テキストはなんらかの状況・状態で画像が使えないときに、設定した代替テキストが使われます。&lt;/p&gt;
&lt;p&gt;どういった状況・状態で代替テキストが使われるか一例を挙げます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;スクリーンリーダーを使っている場合に、画像の代替テキストが読まれる&lt;/li&gt;
&lt;li&gt;通信速度が遅く画像表示まで時間がかかる場合に、画像の代替テキストを表示する&lt;/li&gt;
&lt;li&gt;CDNに障害が起きて画像が表示できない場合に、画像の代替テキストを表示する&lt;/li&gt;
&lt;li&gt;Webブラウザーで特定の画像形式に対応していない場合に、画像の代替テキストを表示する&lt;/li&gt;
&lt;li&gt;w3mやLynxなどのテキストベースのブラウザーで、画像表示に対応していない場合に画像の代替テキストを表示する&lt;/li&gt;
&lt;li&gt;検索エンジンのクローラーが、画像の代替テキストを元に画像の情報を得る&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HTMLにおいては、img要素のalt属性の値に画像の代替テキストを設定します。&lt;/p&gt;
&lt;p&gt;画像の代替テキストの書き方として&lt;a href=&quot;https://html.spec.whatwg.org/multipage/images.html#alt&quot;&gt;HTMLの仕様&lt;/a&gt;には次のように書かれています。&lt;/p&gt;
&lt;blockquote lang=&quot;en&quot;&gt;
One way to think of alternative text is to think about how you would read the page containing the image to someone over the phone, without mentioning that there is an image present. Whatever you say instead of the image is typically a good start for writing the alternative text.
&lt;/blockquote&gt;
&lt;p&gt;同じ部分を&lt;a href=&quot;https://momdo.github.io/html/images.html#alt&quot;&gt;日本語訳されたHTMLの仕様&lt;/a&gt;を見てみると次のように書かれています。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;代替テキストを考える1つの方法は、画像の存在があることを言及することなく、電話で誰かに画像を含むページをどのように読むかを考えることである。通常、画像の代わりにいうものは何でも、代替テキストを記述するための良いスタートである。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;さてX（Twitter）の話に戻ると、画像の代替テキストのことを「画像の説明」という言葉で説明しています。&lt;/p&gt;
&lt;p&gt;X（Twitter）のヘルプにある&lt;a href=&quot;https://help.twitter.com/ja/using-twitter/write-image-descriptions&quot;&gt;良い画像の説明を作成する方法&lt;/a&gt;を引用すると、「画像の説明は、目の不自由な方や弱視の方、ユーザー補助テクノロジーをご利用の方、低帯域幅の地域にお住まいの方、さらなる背景情報を求めている方の一助」向けの機能と書かれています。&lt;/p&gt;
&lt;p&gt;X（Twitter）の画像の説明も、HTMLのimg要素のalt属性に指定するような文言を書くことを意図していそうです。&lt;/p&gt;
&lt;h2 id=&quot;どのように代替テキストを改善するか&quot;&gt;どのように代替テキストを改善するか&lt;/h2&gt;
&lt;p&gt;ここまで書いたことを踏まえて、ファミリーマートの投稿の画像の代替テキストを考えてみます。&lt;/p&gt;
&lt;h3 id=&quot;投稿で何を伝えたかったのかを考える&quot;&gt;投稿で何を伝えたかったのかを考える&lt;/h3&gt;
&lt;p&gt;ファミリーマートの投稿ですが、製品の美味しさやシズル感を伝えたかったと推測します。&lt;/p&gt;
&lt;p&gt;製品のストーリーを伝えることで製品開発者の熱い思いがこの投稿を見た人達に伝わって、好意的な印象を持つことで実際の購入に至ってほしい考えがあったと考えます。&lt;/p&gt;
&lt;p&gt;こういったストーリーをX（Twitter）で書くには、X（Twitter）に投稿できる文字数では足りないでしょう。ただストーリーを別途書くための特設ページを用意できなかった可能性があります。&lt;/p&gt;
&lt;p&gt;そのため、altボタンを押すと見られるようにすればいいというX（Twitter）運用担当者の工夫があったと考えられます。&lt;/p&gt;
&lt;p&gt;また4枚目のお皿の上に唐揚げがたくさん乗っている画像の説明では「食べてください」を連続して書くことで、altボタンを押した人達を驚かせたり、クスっと笑わせたりという公式アカウントがおちゃめなことをしているというギャップを感じさせたかった気持ちがあったのでしょう。&lt;/p&gt;
&lt;p&gt;実際リプライでは&lt;q&gt;4枚目ホラーで草&lt;/q&gt;という投稿があり、これはX（Twitter）運用担当者が意図した反応だと考えられます。&lt;/p&gt;
&lt;h3 id=&quot;代替テキストの改善例&quot;&gt;代替テキストの改善例&lt;/h3&gt;
&lt;p&gt;というわけで、自分が考える代替テキストの改善例を書きます。まずもう一度投稿を引用します。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;これが、からあげの正解。&lt;br&gt;ファミから。&lt;br&gt;&lt;br&gt;そのこだわりのすべてをALTでチェック。 &lt;a href=&quot;https://t.co/TalY56q5PX&quot;&gt;pic.twitter.com/TalY56q5PX&lt;/a&gt;&lt;/p&gt;&amp;mdash; ファミリーマート (@famima_now) &lt;a href=&quot;https://twitter.com/famima_now/status/1534686713787711491?ref_src=twsrc%5Etfw&quot;&gt;June 9, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;※投稿が削除されたので&lt;a href=&quot;https://web.archive.org/web/20220609000048/https://twitter.com/famima_now/status/1534686713787711491&quot;&gt;インターネットアーカイブに残っている投稿&lt;/a&gt;を貼っておきます。&lt;/p&gt;
&lt;p&gt;この投稿にある画像の代替テキストを書くのであれば次のようにするかもしれません。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;画像1枚目の代替テキストは「唐揚げの断面がドアップになっていて、断面から湯気が出ている様子」&lt;/li&gt;
&lt;li&gt;画像2枚目の代替テキストは「ファミから醤油味とファミから塩味のパッケージ」&lt;/li&gt;
&lt;li&gt;画像3枚目の代替テキストは「唐揚げが中央にあり、その唐揚げから湯気が出ている様子」&lt;/li&gt;
&lt;li&gt;画像4枚目の代替テキストは「白いお皿の上に唐揚げがたくさん乗っていて、その後ろに付け合わせの野菜がある」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;電話口の相手に伝える感じだと、こんな感じで書けば伝わりそうです。&lt;/p&gt;
&lt;p&gt;また4枚目の「食べてください」を連呼する形式を活かすのであれば、スマートフォンのメモアプリに「食べてください」とひたすら書いたものをスクリーンショットという形式で投稿して、代替テキストに「食べてください」をひたすら書く形式がありそうです。&lt;/p&gt;
&lt;h2 id=&quot;おまけ-自分が良いと感じた代替テキスト&quot;&gt;おまけ: 自分が良いと感じた代替テキスト&lt;/h2&gt;
&lt;p&gt;まず&lt;a href=&quot;https://twitter.com/zennoh_food&quot;&gt;全農広報部【公式】日本の食を味わう&lt;/a&gt;の投稿が挙げられます。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;フライパンでできるサラダチキンの作り方、ご紹介しています。今月の広報誌エプロンです。サラダチキンは鍋で茹でるイメージでしたが、フライパンでできるんですね。夏場、お湯を沸かすと思うと一気に腰が重くなりますがフライパンなら…なんとか…！(フライパン大好き民)&lt;a href=&quot;https://t.co/5EWrikEpd2&quot;&gt;https://t.co/5EWrikEpd2&lt;/a&gt; &lt;a href=&quot;https://t.co/7vtZqbhX2z&quot;&gt;pic.twitter.com/7vtZqbhX2z&lt;/a&gt;&lt;/p&gt;&amp;mdash; 全農広報部【公式】日本の食を味わう (@zennoh_food) &lt;a href=&quot;https://twitter.com/zennoh_food/status/1544115957634723840?ref_src=twsrc%5Etfw&quot;&gt;July 5, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;画像の内容が代替テキストでも過不足無く表現されていると感じます。&lt;/p&gt;
&lt;p&gt;他の投稿だと、にじさんじ所属のVTuberである壱百満天原サロメの投稿が挙がります。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;きょうのおまんが、ホラー回と言われていてにっこりしてしまったので、おまけですわ😊 &lt;a href=&quot;https://t.co/nfShKjKWw7&quot;&gt;pic.twitter.com/nfShKjKWw7&lt;/a&gt;&lt;/p&gt;&amp;mdash; 壱百満天原サロメ💯🦂 (@1000000lome) &lt;a href=&quot;https://twitter.com/1000000lome/status/1543884318765891584?ref_src=twsrc%5Etfw&quot;&gt;July 4, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;画像の内容を全て書いているわけではないですが、漫画で伝えたいことを表現できている代替テキストで百満点ですわ～！&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;代替テキストは人によって考え方にばらつきがありますし、状況によっても代替テキストの内容が変わるので、一概にこれが正解とは言いづらいものです。&lt;/p&gt;
&lt;p&gt;しかし何かしらの代替テキストを設定することで、より多くの状況・状態にある人へ伝えたいことを伝えられるようになります。&lt;/p&gt;
&lt;p&gt;代替テキストを設定したことがない人は、とりあえず代替テキストを一度設定してみましょう。&lt;/p&gt;
&lt;h2 id=&quot;参考リンク&quot;&gt;参考リンク&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.twitter.com/ja/using-twitter/write-image-descriptions&quot;&gt;良い画像の説明を作成する方法 | Xヘルプ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yuugop.com/articles/practicalaccessiblehtml/pah01.html&quot;&gt;altはつけるだけじゃなくて - 実践アクセシブルHTML 第一回&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.slideshare.net/nozomisawada969/ss-122034985&quot;&gt;代替テキストの基本から応用まで&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2022-07-07T02:01:43.978Z</published>
<updated>2022-11-28T14:30:00.685Z</updated>
</entry>
<entry>
<title>ブログを国際化対応した</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/introduce-i18n-in-blog"/>
<id>tag:blog.kubosho.com,2022-06-16:entry:introduce-i18n-in-blog</id>
<summary>当ブログのUIで使う文言を国際化対応しました。</summary>
<content type="html">&lt;p&gt;当ブログのUIで使う文言を国際化対応しました。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubosho/blog.kubosho.com/pull/933&quot;&gt;Introduce i18n by kubosho · Pull Request #933 · kubosho/blog.kubosho.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;国際化対応には&lt;a href=&quot;https://github.com/lukeed/rosetta&quot;&gt;rosetta&lt;/a&gt;という軽量の国際化対応ライブラリーを使いました。&lt;/p&gt;
&lt;p&gt;文言だけを各言語に対応させる分には必要なAPIが揃っていたのと、bundle sizeが426バイトだったのでJavaScriptのサイズが大きくならないのも良いと思い、rosettaを選択しました。&lt;/p&gt;
&lt;p&gt;きっかけとしては、Androidアプリでの開発ではLintで文字列に都度IDを振って文字列リソースとして記載するよう指摘される旨の投稿を見て、Webでも同じ対応を入れていいはずと感じたためやりました。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;Androidはこの辺しっかりしてて、「文字列は都度IDを振ってres/values/strings.xmlに文字列リソースとして記載しなさい」というのがAndroid Lintの指摘事項になってるんだよね。おかげで日本語をUIの中にベタ書きしない習慣を身につけやすい。&lt;br&gt;（まあ僕もWebだとベタ書きしてしまうのだけど） &lt;a href=&quot;https://t.co/ZSONvK7u1d&quot;&gt;https://t.co/ZSONvK7u1d&lt;/a&gt;&lt;/p&gt;&amp;mdash; なかざん💉💉💉 (@Nkzn) &lt;a href=&quot;https://twitter.com/Nkzn/status/1531110969073537024?ref_src=twsrc%5Etfw&quot;&gt;May 30, 2022&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;正直あまり深く考えずにやれそうだからやりますわ～という感じでやりました。&lt;/p&gt;
&lt;p&gt;感想としては、文言が特定のディレクトリに集約されたため、文言を変えるときに作業が楽になるなと感じました。&lt;/p&gt;
&lt;p&gt;多言語対応という意味では現状日本語しか対応していないため、国際化対応によって何か得られたというのは無いです。&lt;/p&gt;
&lt;p&gt;今後はブログの記事も多言語で書けるようになっていきたいです。&lt;/p&gt;</content>
<published>2022-06-16T00:00:03.004Z</published>
<updated>2022-11-28T13:57:24.766Z</updated>
</entry>
<entry>
<title>AVA上でsinon.useFakeTimers()を複数のテスト内で実行するとエラーが出る</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/run-sinon-usefaketimers-for-each-test-on-ava-throw-error"/>
<id>tag:blog.kubosho.com,2022-05-20:entry:run-sinon-usefaketimers-for-each-test-on-ava-throw-error</id>
<summary>Sinon.JSのバージョンをv14.0.0に上げた際、エラーが出てテスト実行が失敗するようになりました。</summary>
<content type="html">&lt;p&gt;&lt;a href=&quot;https://sinonjs.org/&quot;&gt;Sinon.JS&lt;/a&gt;のバージョンをv14.0.0に上げた際、エラーが出てテスト実行が失敗するようになりました。&lt;/p&gt;
&lt;p&gt;この記事では対処方法を書いていきます。&lt;/p&gt;
&lt;h2 id=&quot;事象&quot;&gt;事象&lt;/h2&gt;
&lt;p&gt;たとえば次のようなテストがあったとします。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sinon.useFakeTimers&lt;/code&gt; は&lt;a href=&quot;https://sinonjs.org/releases/latest/fake-timers/&quot;&gt;ドキュメント&lt;/a&gt;にある通り、&lt;code&gt;setTimeout&lt;/code&gt; や &lt;code&gt;clearTimeout&lt;/code&gt;などを置き換える関数です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import test from &#39;ava&#39;;
import sinon from &#39;sinon&#39;;

test(&#39;test&#39;, (t) =&gt; {
  const fakeTimer = sinon.useFakeTimers();
  t.pass();
  fakeTimer.restore();
});

test(&#39;test2&#39;, (t) =&gt; {
  const fakeTimer = sinon.useFakeTimers();
  t.pass();
  fakeTimer.restore();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2回目の &lt;code&gt;sinon.useFakeTimers()&lt;/code&gt; を実行するとき、先ほどのテストファイルで言うと &lt;code&gt;test2&lt;/code&gt; を実行するときに次のようなメッセージが表示されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;TypeError {
  message: &#39;Can\&#39;t install fake timers twice on the same global object.&#39;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;原因&quot;&gt;原因&lt;/h2&gt;
&lt;p&gt;書き換えられたグローバルオブジェクトを戻すために &lt;code&gt;sinon.useFakeTimers&lt;/code&gt; と &lt;code&gt;fakeTimer.restore()&lt;/code&gt; を対になる形で実行しないといけません。&lt;/p&gt;
&lt;p&gt;うっかり &lt;code&gt;fakeTimer.restore()&lt;/code&gt; を実行しないまま &lt;code&gt;sinon.useFakeTimers&lt;/code&gt; を実行すると、元の日時に復元することが難しくなります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// サンプル
const sinon = require(&#39;sinon@12.0.0&#39;);

console.log(&#39;Original time: &#39; + new Date().getTime()); // &quot;Original time: 1653007080412&quot;
let fakeTimer = sinon.useFakeTimers(Date.parse(&#39;2014-06-05T12:07:07.662Z&#39;));
fakeTimer = sinon.useFakeTimers(Date.parse(&#39;2018-04-11T14:08:00Z&#39;));
fakeTimer.restore();
console.log(&#39;Restored time: &#39; + new Date().getTime()); // &quot;Restored time: 1401970027662&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;今回のサンプルコードの場合はまだ復元できますが、これがより回数を重ねて &lt;code&gt;sinon.useFakeTimers&lt;/code&gt; を実行してしまうとより復元が難しくなります。&lt;/p&gt;
&lt;p&gt;この問題が、&lt;a href=&quot;https://github.com/sinonjs/sinon/issues/2449&quot;&gt;Impossible to restore fake timers in certain situations. · Issue #2449 · sinonjs/sinon&lt;/a&gt;で報告されて、対応として &lt;code&gt;@sinonjs/fake-timers&lt;/code&gt; 側で&lt;a href=&quot;https://github.com/sinonjs/fake-timers/pull/426&quot;&gt;Prohibit faking of faked timers by cjbarth · Pull Request #426 · sinonjs/fake-timers&lt;/a&gt;というPull Requestがマージされました。&lt;/p&gt;
&lt;p&gt;Pull RequestのSolutionに「If an attempt is make to fake a timer that is already faked, an exception will be thrown.」と書いてある通り、timerがすでにfakeだった場合に再度 &lt;code&gt;sinon.useFakeTimers&lt;/code&gt; を実行した場合に例外が投げられるという変更がされました。&lt;/p&gt;
&lt;p&gt;この変更により、複数のテストで &lt;code&gt;sinon.useFakeTimers&lt;/code&gt; と &lt;code&gt;fakeTimer.restore&lt;/code&gt; を実行していた場合に、AVA上でテストが並列で実行されることもあって &lt;code&gt;fakeTimer.restore&lt;/code&gt; が実行される前に &lt;code&gt;sinon.useFakeTimers&lt;/code&gt; が実行される場合が出てきました。&lt;/p&gt;
&lt;p&gt;その結果として「Can&#39;t install fake timers twice on the same global object.」というエラーが出力されるようになりました。&lt;/p&gt;
&lt;h2 id=&quot;解決策&quot;&gt;解決策&lt;/h2&gt;
&lt;p&gt;解決方法は2つあります。&lt;/p&gt;
&lt;p&gt;まず1つは、テストコード側で並列実行をやめて直列実行にすることです。具体的には次の通り書くとテストが成功します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import test from &#39;ava&#39;;
import sinon from &#39;sinon&#39;;

test.serial(&#39;test&#39;, (t) =&gt; {
  const fakeTimer = sinon.useFakeTimers();
  t.pass();
  fakeTimer.restore();
});

test.serial(&#39;test2&#39;, (t) =&gt; {
  const fakeTimer = sinon.useFakeTimers();
  t.pass();
  fakeTimer.restore();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;または &lt;code&gt;test.before&lt;/code&gt; や &lt;code&gt;test.after&lt;/code&gt; といったテストファイル内の最初と最後のテスト前後で実行されるフックを使って、fakeTimerを使うのも良いです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import test from &#39;ava&#39;;
import sinon from &#39;sinon&#39;;

let fakeTimer = null;

test.before(() =&gt; {
  fakeTimer = sinon.useFakeTimers();
});

test.after(() =&gt; {
  if (!fakeTimer) {
    return;
  }
  fakeTimer.restore();
});

test(&#39;test&#39;, (t) =&gt; {
  t.pass();
});

test(&#39;test2&#39;, (t) =&gt; {
  t.pass();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AVAのように並列実行がデフォルトのテストフレームワークだと同じ問題が起きそうですが、他のJestやVitestなどはどうしているのか気になります。&lt;/p&gt;</content>
<published>2022-05-20T00:57:40.873Z</published>
<updated>2022-05-20T05:23:39.303Z</updated>
</entry>
<entry>
<title>自分がアクセシビリティ向上に力を入れる理由</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/why-focus-on-improving-accessibility"/>
<id>tag:blog.kubosho.com,2021-11-02:entry:why-focus-on-improving-accessibility</id>
<summary>いま自分は、担当サービスのアクセシビリティ向上を推進する「アクセシビリティタスクフォース」を率いる立場になっています（アクセシビリティタスクフォースについては AbemaTV ABEMA iOS版アプリのアクセシビリティ向上支援 ｜ 事例紹介 ｜ 株式会社コンセント で少し触れられています）。</summary>
<content type="html">&lt;p&gt;いま自分は、担当サービスのアクセシビリティ向上を推進する「アクセシビリティタスクフォース」を率いる立場になっています（アクセシビリティタスクフォースについては &lt;a href=&quot;https://www.concentinc.jp/works/abematv_202101/&quot;&gt;AbemaTV ABEMA iOS版アプリのアクセシビリティ向上支援 ｜ 事例紹介 ｜ 株式会社コンセント&lt;/a&gt; で少し触れられています）。&lt;/p&gt;
&lt;p&gt;現在、アクセシビリティ向上の文化やプロセスを社内に浸透させていくため、さまざまな施策を考え実行しています。&lt;/p&gt;
&lt;p&gt;ただそうやって活動していくうちに、ふと気づいたことがありました。自分はなぜアクセシビリティ向上に力を込めているかという言語化をしていないことです。&lt;/p&gt;
&lt;p&gt;アクセシビリティ向上の推進は、個人でやるには大変で、組織としてやっていくのが一番良いと思っています。&lt;/p&gt;
&lt;p&gt;ただ、組織としてアクセシビリティ向上をやっていくように伝えていく中で、個人の思いというのは活動の原動力として大事だと思っています。ここをあやふやにしてしまうと、ふとしたときにくじけてしまい折れてしまう気がします。&lt;/p&gt;
&lt;p&gt;自分がそうやってくじけて折れてしまわぬよう、ここで自分のアクセシビリティ向上に対する思いを言語化してみます。&lt;/p&gt;
&lt;h2 id=&quot;元々の自分の考え&quot;&gt;元々の自分の考え&lt;/h2&gt;
&lt;p&gt;自分は元々「かけがえのないモノをつくる」というビジョンを持っています。&lt;/p&gt;
&lt;p&gt;「かけがえのないモノ」の定義は自分の中でも定まってはいないですが、現時点だと「社会インフラになるもの」を作りたいという考えです。&lt;/p&gt;
&lt;p&gt;また高校生のときからHTMLとCSSを書いてきた経験もあり「インターネットを基礎とした社会インフラをつくる」ことを自分の理想として掲げています。&lt;/p&gt;
&lt;p&gt;その「インターネットを基礎とした社会インフラ」の中で、自分の基盤がHTMLとCSSというのもあって、Webフロントエンドで貢献したいという考えを持っていました。&lt;/p&gt;
&lt;h2 id=&quot;聴覚障害者からのサービスに対するツッコミ&quot;&gt;聴覚障害者からのサービスに対するツッコミ&lt;/h2&gt;
&lt;p&gt;さてここで、2年前の &lt;a href=&quot;https://japan-a11y-conf.com/vol2/&quot;&gt;Japan Accessibility Conference digital information vol.2&lt;/a&gt;（以下、JA11YC）での体験を話します。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://abema.tv/video/title/89-66&quot;&gt;ABEMA Prime&lt;/a&gt; という番組内で、かつて &lt;a href=&quot;https://abema.tv/channels/abema-news/slots/CTEDYRdBBoo76w&quot;&gt;補聴器ユーザーの苦悩と葛藤&lt;/a&gt;（リンク先では期限切れで動画視聴できず）というテーマを取り上げたことがありました。&lt;/p&gt;
&lt;p&gt;この番組を放送したあと、聴覚障害者の方から「字幕をつけないのはなぜか。聴覚障害者を排除することに自覚がないのか、自覚があってもやるべきことをしていないのはなぜか（要約）」といった意見をもらいました。&lt;/p&gt;
&lt;p&gt;そしてこの意見を書いた人が、JA11YCの場にいてそこで話をしました。&lt;/p&gt;
&lt;p&gt;話した内容自体は2年前ということもあり忘れてしまっています（これに関しては申し訳ないです）。&lt;/p&gt;
&lt;p&gt;ただ、ここで自分は「サービスを楽しめない人を出してしまうなんて自分で自分が恥ずかしい」という気持ちになり、サービスのアクセシビリティ向上をやっていくという思いを強くしました。&lt;/p&gt;
&lt;h2 id=&quot;視覚障害者を誘導する体験&quot;&gt;視覚障害者を誘導する体験&lt;/h2&gt;
&lt;p&gt;JA11YCに参加したあと、視覚障害者の人を渋谷駅まで誘導する機会がありました。どういう流れで誘導することになったか詳細は思い出せないですが、自分から手を挙げた気がします。&lt;/p&gt;
&lt;p&gt;ただ誘導するのは初めてということもあり、果たしてちゃんと誘導できるのか少し緊張しました。&lt;/p&gt;
&lt;p&gt;渋谷の道玄坂のほうは点字ブロックがほとんどありませんでした。そのため、点字ブロックに沿って歩けず、ほぼ自分に頼って歩かないといけない状況でした。なので自分は口頭でいまどの辺りにいるかを、場所の名前や道路の形状なども交えつつ案内しました。&lt;/p&gt;
&lt;p&gt;結果的にはなんとか渋谷駅まで誘導することができて、電車に乗ったところまで確認しました。&lt;/p&gt;
&lt;p&gt;誘導し終えたあとは、正直いつもは感じない疲れを感じましたが、「視覚障害者を誘導して気づいた点」で後述するように自分の中では良い経験でした。&lt;/p&gt;
&lt;p&gt;なお道中で前から来た人がすれ違おうとしたときに、あまり避けずに軽く肩を当てた後こちらが悪いかのように軽い舌打ちをしたときは、何様だと感じました。相手側ももしかしたらイライラしていた、もしくは気持ちが良くなかったと推測はできます。しかしその気持ちを自分達に向けるなとこちらも相手にイラつきました。&lt;/p&gt;
&lt;h3 id=&quot;視覚障害者を誘導して気づいた点&quot;&gt;視覚障害者を誘導して気づいた点&lt;/h3&gt;
&lt;p&gt;この経験は自分があまり意識してこなかった点に気付かされたという意味で、良い経験でした。気づいたことを挙げると次の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;渋谷駅周辺は点字ブロックがほぼ無い。駅内は割とあるが、路上は皆無&lt;/li&gt;
&lt;li&gt;渋谷駅周辺の道路は人の多さに対して、歩道が狭かったり分かれ道があったりで、視覚障害者に限らず視覚にはほぼ問題ない自分も歩きづらい&lt;/li&gt;
&lt;li&gt;ざわざわしているところを歩くのは、歩くのに必要な手がかりが聞きづらくなるという意味で大変そう&lt;/li&gt;
&lt;li&gt;歩いているときに前から来た人が避けてくれるとは限らないのが、大変そう&lt;/li&gt;
&lt;li&gt;前述のカンファレンスに参加するため、行きはタクシーで来たということで、タクシー代が余計にかかりそう&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自分が気づいていなかっただけで、不自由な部分は多く転がっているのを感じました。この経験が元となり、せめて自分が関わるところはアクセスしやすくしたいと考えるようになりました。&lt;/p&gt;
&lt;h2 id=&quot;なぜいまの会社サービスでアクセシビリティ向上をやるのか&quot;&gt;なぜ、いまの会社・サービスでアクセシビリティ向上をやるのか？&lt;/h2&gt;
&lt;p&gt;自分の考えとしてはこれまで書いた通りですが、ではなぜ、いまの会社・サービスでアクセシビリティ向上を推進するタスクフォースを作ってまでアクセシビリティ向上を推進しているのでしょうか？&lt;/p&gt;
&lt;p&gt;その理由としては、最近&lt;a href=&quot;https://www.cyberagent.co.jp/corporate/purpose/&quot;&gt;会社でパーパスが発表された&lt;/a&gt;ことにあります。&lt;/p&gt;
&lt;p&gt;パーパスは存在意義を示すものです。発表されたパーパスは「新しい力とインターネットで日本の閉塞感を打破する」という内容で、アクション的な項目の中に「新しい未来のテレビABEMAを、いつでもどこでも繋がる社会インフラに」があります。&lt;/p&gt;
&lt;p&gt;そうです、会社としてABEMAを社会インフラにすることを明言しています。これによって、自分のビジョンと会社の向いている方向が合致していると感じるようになりました。また、アクセシビリティタスクフォースを立ち上げたときに「いつでもどこでも繋がる」というのは考えていて、それが会社としても明言されるようになり嬉しくなりました。&lt;/p&gt;
&lt;p&gt;これらを踏まえて、いまの会社・サービスでアクセシビリティ向上を推進していき、やがては担当サービス外にも影響を及ぼせる様になりたいと思いました。&lt;/p&gt;
&lt;h2 id=&quot;アクセシビリティ向上を推進する上で気をつけていること&quot;&gt;アクセシビリティ向上を推進する上で気をつけていること&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://speakerdeck.com/ymrl/webenziniatosite-imazhi-tuteokitai-webakusesibiritei?slide=11&quot;&gt;Webエンジニアとして いま知っておきたい Webアクセシビリティ&lt;/a&gt;というスライドにも書かれているように、特定の個性・状況・環境だけを考えて倫理観に訴えるのではなく、自分も含めたみんながもっと便利に使えて、結果ユーザーが増えていくようなベストな方法を考えていくことを意識しています。&lt;/p&gt;
&lt;p&gt;アクセシビリティ &quot;対応&quot; と書くと後ろ向きな感じになりがちなのと、特定のユーザー向けに特別対応をすると捉えがちというのもあるので、アクセシビリティ &quot;向上&quot; を一貫して使うのも気をつけている点です。&lt;/p&gt;
&lt;p&gt;また自分たちだけでアクセシビリティ向上をやれるところからやっています。例を出すとWCAGの達成基準を満たしているかチェックしたり、アプリをVoiceOverやTalkBackを有効にした状態で触ってみたりなどといったことはやってきました。&lt;/p&gt;
&lt;p&gt;ただ自分たちだけでは留まらない領域もあります。例を出すと字幕対応などが最たる例でしょう。&lt;/p&gt;
&lt;p&gt;そういった部分では自分たち以外の社内の人ともっと対話して、お互いにとってベストな物を出していきたいと考えています。&lt;/p&gt;
&lt;p&gt;なお、いまも自分たちだけで話を閉じているわけではなく、Slackのチャンネルでアクセシビリティについて話したり考えたりするチャンネルはあり、そこである機能のアクセシビリティをより良くするための相談などがされます。&lt;/p&gt;
&lt;p&gt;しかし相談されるなどといった受動的なコミュニケーションだけでなく、能動的なコミュニケーションをより取っていこうと考えています。&lt;/p&gt;</content>
<published>2021-11-02T00:00:03.024Z</published>
<updated>2021-11-02T03:26:47.295Z</updated>
</entry>
<entry>
<title>X（Twitter）の右側サイドバーを消す</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/remove-twitter-trend"/>
<id>tag:blog.kubosho.com,2021-03-06:entry:remove-twitter-trend</id>
<summary>X（Twitter） のトレンドを見ると、イラつきを覚えるようになりました。そんなにイラつきを覚えるようなら見なければいいし、そもそも X（Twitter） やめろという話はあります。</summary>
<content type="html">&lt;p&gt;X（Twitter）のトレンドは見るたびにイラつきを覚えます。そんなにイラつきを覚えるようなら見なければいいし、そもそもX（Twitter）をやめろという話でしょう。&lt;/p&gt;
&lt;p&gt;ただ次の投稿にある通り、ある意味で期待通りの報酬を得られる行為となります。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;インターネットでQOLを1番下げる行為は、間違いなく「嫌いな人間/集団/コンテンツを自分から見に行って、嫌なものを見つけて、嫌なのを確認する」事なんだけど、これはある意味で「期待通りの報酬を得られる」行為なので、常習化を招きやすいんだよな。人生には得てはいけない成功体験があるというな。&lt;/p&gt;&amp;mdash; rei@生きてるだけで疲労困憊発売延期 (@rei10830349) &lt;a href=&quot;https://twitter.com/rei10830349/status/1178553747770753025?ref_src=twsrc%5Etfw&quot;&gt;September 30, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;そのためX（Twitter）のトレンドを見てしまうことが多くトレンドを見るたびに後悔していました。ただやめられなくなっていて正直なところ常習化していました。&lt;/p&gt;
&lt;h2 id=&quot;なぜxtwitterのトレンドを見てしまうのか&quot;&gt;なぜX（Twitter）のトレンドを見てしまうのか&lt;/h2&gt;
&lt;p&gt;X（Twitter）のページをWeb上で見ると自然とトレンドが目に入ってきます。普段見ているX（Twitter）の画面は画像の通りです。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/twitter_home_before.png&quot; alt=&quot;CSSを調整する前のX（Twitter）は3カラムになっている&quot;&gt;&lt;/p&gt;
&lt;p&gt;左サイドバーにナビゲーション、中央に投稿一覧、そして右サイドバーにX（Twitter）内の回遊リンク一覧がある構成です。&lt;/p&gt;
&lt;p&gt;この構成だと右サイドバーのX（Twitter）内の回遊リンク一覧が目に入ります。特に「いまどうしてる？」というタイトルが付けられたX（Twitter）内のトレンド一覧が目に入ります。&lt;/p&gt;
&lt;p&gt;これはX（Twitter）側でトレンドを見てもらうことでX（Twitter）内を回遊してくれて、結果としてX（Twitter）内で追っているKPIを達成できるというデータがあるのでしょう。なのでトレンドが目に入りやすい位置へ配置されていますし、トレンドの先頭は広告枠になっているのでしょう。&lt;/p&gt;
&lt;p&gt;ただX（Twitter）のトレンドを目に入れたくない立場からすればとにかく消えてほしいものです。あとついでにおすすめユーザーも邪魔な気がしてきました。&lt;/p&gt;
&lt;h2 id=&quot;xtwitterのトレンドを視界から消すついでにおすすめユーザーも&quot;&gt;X（Twitter）のトレンドを視界から消す(ついでにおすすめユーザーも)&lt;/h2&gt;
&lt;p&gt;ということでX（Twitter）のトレンドを視界から消します。&lt;/p&gt;
&lt;p&gt;今回は&lt;a href=&quot;https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne?hl=ja&quot;&gt;Stylus&lt;/a&gt;を使って、X（Twitter）側で定義されているCSSを上書きしてX（Twitter）のトレンドのみならず右サイドバーごと非表示にしました。&lt;/p&gt;
&lt;p&gt;なおX（Twitter）のトレンドを消す方法として、Chrome向けのX（Twitter）拡張機能である&lt;a href=&quot;https://chrome.google.com/webstore/detail/calm-twitter/cknklikacoaeledfaldmhabmldkldocj?hl=ja&quot;&gt;おだやかX（Twitter）&lt;/a&gt;や&lt;a href=&quot;https://chrome.google.com/webstore/detail/simplified-twitter/kfopmjhmejbgomgeajemgpgpbckpoopg/related&quot;&gt;Simplified Twitter/X&lt;/a&gt;を入れる方法や、&lt;a href=&quot;https://www.tsukutarou.net/entry/X%EF%BC%88Twitter%EF%BC%89-Trend-Shutout&quot;&gt;地域を変更してトレンドのみ表示できないようにする&lt;/a&gt;方法もあります。&lt;/p&gt;
&lt;h3 id=&quot;xtwitterの右サイドバーを削除するときの課題と解決方法&quot;&gt;X（Twitter）の右サイドバーを削除するときの課題と解決方法&lt;/h3&gt;
&lt;p&gt;X（Twitter）はReact Native for Webを使っています。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;If you use X（Twitter） Lite you&amp;#39;re now using a web app rendered by React Native for Web&lt;/p&gt;&amp;mdash; Nicolas (@necolas) &lt;a href=&quot;https://twitter.com/necolas/status/913877194199359488?ref_src=twsrc%5Etfw&quot;&gt;September 29, 2017&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;これは今も変わってなさそうでHTMLに定義されているクラス名はランダムな文字列です。&lt;/p&gt;
&lt;p&gt;このためクラス名を元にスタイル定義をすると、将来の変更でスタイルが適用されなくなりそうです。&lt;/p&gt;
&lt;p&gt;そのためStylus上で &lt;code&gt;data-*&lt;/code&gt; 属性のセレクタに対してスタイル定義をすれば将来的に壊れにくくなりそうという考えを持ちました。これに従って書いたCSSは次の通りです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;[data-testid=&#39;primaryColumn&#39;] {
  max-width: 600px;
}

[data-testid=&#39;sidebarColumn&#39;] {
  display: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このCSSを適用したX（Twitter）のスクリーンショットを見てみましょう。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/twitter_home_after.png&quot; alt=&quot;CSSを調整した後のX（Twitter）は2カラム表示になっている。右サイドバーは非表示になっている&quot;&gt;&lt;/p&gt;
&lt;p&gt;見事にX（Twitter）の右サイドバーを非表示にできています。今回の目的を達成しました。&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;正直X（Twitter）を見ないようにしたりアカウントを削除して何もかも見れなくしたりしたほうが手っ取り早いです。&lt;/p&gt;
&lt;p&gt;しかし他サービスのログインに使っていたり、DMでやり取りしている人がいたり、拡散される役目をX（Twitter）に依存しがちだったり、いろいろやめられない状況でした。なので今回は妥協案として右サイドバーを非表示にしました。&lt;/p&gt;
&lt;p&gt;プレミアム会員向けにトレンドなどを削除する機能を作ってほしいですが、広告枠の扱いなども考えると難しそうです。&lt;/p&gt;</content>
<published>2021-03-06T00:00:00.000Z</published>
<updated>2022-11-28T16:44:31.940Z</updated>
</entry>
<entry>
<title>PreactとTypeScriptを使った環境でStorybookを使う方法</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/preact-typescript-storybook"/>
<id>tag:blog.kubosho.com,2021-01-14:entry:preact-typescript-storybook</id>
<summary>Preact と TypeScript を使った環境で Storybook を使おうとしたら以下のエラーが出ました。</summary>
<content type="html">&lt;p&gt;PreactとTypeScriptを使った環境でStorybookを使おうとしたら以下のエラーが出ました。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/storybook_error.png&quot; alt=&quot;Storybook上で出たエラー。ReferenceError: h is not definedと出ている。&quot;&gt;&lt;/p&gt;
&lt;p&gt;原因はStorybookを使うために必要なパッケージとBabelの設定が足りなかったことでした。&lt;/p&gt;
&lt;p&gt;なので、&lt;a href=&quot;https://github.com/storybookjs/storybook/tree/master/lib/cli#manually-specify-project-type&quot;&gt;storybook/lib/cli#Manually specify project type&lt;/a&gt;を参考に、次のコマンドを実行します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx -p @storybook/cli sb init --type preact
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そして &lt;code&gt;.babelrc.js&lt;/code&gt; を次のような設定にします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const presets = [&#39;@babel/preset-env&#39;, &#39;@babel/preset-typescript&#39;];
const plugins = [
  [
    &#39;@babel/plugin-transform-react-jsx&#39;,
    {
      runtime: &#39;automatic&#39;,
      importSource: &#39;preact&#39;,
    },
  ],
];

module.exports = { presets, plugins };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これでYarnを使っている環境であれば &lt;code&gt;yarn storybook&lt;/code&gt; をターミナル上で実行することで、Storybookが表示されるようになります。&lt;/p&gt;
&lt;h2 id=&quot;参考にしたページ&quot;&gt;参考にしたページ&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/preactjs/preact-compat/issues/541&quot;&gt;Uncaught ReferenceError: h is not defined · Issue #541 · preactjs/preact-compat&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2021-01-14T00:00:00.000Z</published>
<updated>2022-11-28T14:57:55.078Z</updated>
</entry>
<entry>
<title>WindowsでもmacOSのようなキー操作を実現する</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/use-autohotkey-to-macos-like-keymapping"/>
<id>tag:blog.kubosho.com,2020-08-30:entry:use-autohotkey-to-macos-like-keymapping</id>
<summary>前から macOS のように Windows でも Ctrl キーを使ったカーソル移動をおこないたいと思っていました。</summary>
<content type="html">&lt;p&gt;前からmacOSのようにWindowsでもCtrlキーを使ったカーソル移動をおこないたいと思っていました。&lt;br&gt;
またmacOSのショートカットはCommandキーを使うことが多いため、Windowsでも各種ショートカットにWindowsキーを使うようにしたいと思っていました。&lt;/p&gt;
&lt;p&gt;今回これらを実現するべく、レジストリエディターと&lt;a href=&quot;https://www.autohotkey.com/&quot;&gt;AutoHotkey&lt;/a&gt;を使って、キーボードショートカットを望む形にしました。&lt;/p&gt;
&lt;h2 id=&quot;レジストリエディターを使って左の-windows-キーを-f13-キーに変更する&quot;&gt;レジストリエディターを使って、左の Windows キーを F13 キーに変更する&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://www.grismar.net/ventrilocapsfix/&quot;&gt;Push To Talk Fix - Remapping keys to F13&lt;/a&gt;内にある「Remap Left Windows Key to F13」を適用します。&lt;br&gt;
次のレジストリ情報を任意の場所に &lt;code&gt;.reg&lt;/code&gt; 形式で保存して実行したあと、PCを再起動すれば左のWindowsキーがF13キーに変わります。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
&quot;Scancode Map&quot;=hex:00,00,00,00,00,00,00,00,02,00,00,00,64,00,5b,e0,00,00,00,00
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;autohotkey-を使ってキーボードショートカットを実装する&quot;&gt;AutoHotkey を使ってキーボードショートカットを実装する&lt;/h2&gt;
&lt;p&gt;キーボードショートカットの設定は次の通りになりました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ahk&quot;&gt;#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
#UseHook, On
#SingleInstance, force
; F13 &amp;#x26; Warn  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.

; macOS like keymap
F13 &amp;#x26; /::^/
F13 &amp;#x26; ,::^,
F13 &amp;#x26; 1::^1
F13 &amp;#x26; 2::^2
F13 &amp;#x26; 3::^3
F13 &amp;#x26; 4::^4
F13 &amp;#x26; 5::^5
F13 &amp;#x26; 6::^6
F13 &amp;#x26; 7::^7
F13 &amp;#x26; 8::^8
F13 &amp;#x26; 9::^9
F13 &amp;#x26; 0::^0
F13 &amp;#x26; a::^a
F13 &amp;#x26; b::^b
F13 &amp;#x26; c::^c
F13 &amp;#x26; f::^f
F13 &amp;#x26; h::^h
F13 &amp;#x26; i::^i
F13 &amp;#x26; k::^k
F13 &amp;#x26; l::^l
F13 &amp;#x26; n::^n
F13 &amp;#x26; p::^p
F13 &amp;#x26; q::!F4
F13 &amp;#x26; r::^r
F13 &amp;#x26; s::^s
F13 &amp;#x26; t::^t
F13 &amp;#x26; v::^v
F13 &amp;#x26; w::^w
F13 &amp;#x26; x::^x
F13 &amp;#x26; z::^z
F13 &amp;#x26; Enter::^Enter
F13 &amp;#x26; Space::#s
; https://superuser.com/questions/1246946/autohotkey-remapping-altshifttab-to-lwinshifttab
F13 &amp;#x26; Tab::
    AltTabMenu := true
    If GetKeyState(&quot;Shift&quot;,&quot;P&quot;)
        Send {Alt Down}{Shift Down}{Tab}
    else
        Send {Alt Down}{Tab}
return

#If (AltTabMenu)
    ~*F13 Up::
        Send {Shift Up}{Alt Up}
        AltTabMenu := false
    return
#If

; Windows keymap
F13 &amp;#x26; e::#e
F13 &amp;#x26; Up::#Up
F13 &amp;#x26; Right::#Right
F13 &amp;#x26; Down::#Down
F13 &amp;#x26; Left::#Left
#IfWinNotActive, ahk_exe WindowsTerminal.exe
Return
#IfWinNotActive

; Emacs like keymap
#IfWinNotActive, ahk_exe WindowsTerminal.exe
^p::Send,  {Up}
^f::Send,  {Right}
^n::Send,  {Down}
^b::Send,  {Left}
^+p::Send, {Shift}+{Up}
^+f::Send, {Shift}+{Right}
^+n::Send, {Shift}+{Down}
^+b::Send, {Shift}+{Left}
^a::Send,  {Home}
^e::Send,  {End}
^d::Send,  {Delete}
^h::Send,  {BackSpace}
^m::Send,  {Enter}
^k::Send,  {Shift}+{End}{BackSpace}
Return
#IfWinNotActive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先ほど左のWindowsキーをリマップして作り出したF13キーをふんだんに使って、WindowsのControlキーを使ったキーボードショートカットをF13キーを使うように変更しています。&lt;br&gt;
ControlキーについてはEmacsっぽくカーソル移動のショートカットに割り当てています。&lt;br&gt;
Windows Terminalを使っているときはショートカットを無効化したかったため、&lt;a href=&quot;https://pouhon.net/ahk-win-active/2812/&quot;&gt;[AutoHotKey]#IfWinActive で対象ウインドウを指定する | Output 0.1&lt;/a&gt;を参考にしてショートカットを無効化しています。&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;Windowsをメインで使い始めてだいぶ経ちますが、ようやく思い通りの操作ができるようになって嬉しいです。&lt;/p&gt;</content>
<published>2020-08-30T00:00:00.000Z</published>
<updated>2022-11-28T14:56:53.256Z</updated>
</entry>
<entry>
<title>ブログの記事管理をContentfulからGitリポジトリに変更した</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/migrating-from-contentful-to-markdown-file"/>
<id>tag:blog.kubosho.com,2020-05-18:entry:migrating-from-contentful-to-markdown-file</id>
<summary>このブログは最初はてなブログで書いていて、1 年前にこっそり Next.js + Contentful という仕組みに変えて運用していた。</summary>
<content type="html">&lt;p&gt;このブログは最初はてなブログで書いていて、1年前にこっそりNext.js + Contentfulという仕組みに変えて運用していた。今回仕組みをふたたび変えて、MarkdownファイルをGitリポジトリにコミットしていく形式にした。&lt;/p&gt;
&lt;p&gt;コミットしたMarkdownファイルはGitHub上へpushするとVercelのGitHub integrationによってVercel上にデプロイされる仕組みとなっている。その辺の仕組みについてコードを読みたい人は &lt;a href=&quot;https://github.com/kubosho/kubosho.com/blob/master/src/entry/entryConverter.ts&quot;&gt;entryConverter.ts&lt;/a&gt; と &lt;a href=&quot;https://github.com/kubosho/kubosho.com/blob/master/tools/entriesJsonBuilder.ts&quot;&gt;entriesJsonBuilder.ts&lt;/a&gt; を読むと良い。&lt;/p&gt;
&lt;h2 id=&quot;なぜ-contentful-から-markdown-ファイルを編集する形式に変えたのか&quot;&gt;なぜ Contentful から Markdown ファイルを編集する形式に変えたのか&lt;/h2&gt;
&lt;p&gt;理由は3つある。&lt;/p&gt;
&lt;h3 id=&quot;1-contentful-の-markdown-エディタの挙動が信用できなくなった&quot;&gt;1. Contentful の Markdown エディタの挙動が信用できなくなった&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.kubosho.com/entry/working-style-after-covid-19&quot;&gt;リモートワーク体制になって変わったこと・変わらなかったこと&lt;/a&gt;の記事を書いていたときに、下記の事象に遭遇した。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;Contentfulのエディタで記事を書いていたら自動保存のタイミングでIMEが確定されていたようで、文章がめちゃくちゃになっていた。そしてその状態がエディタに表示されてなかったので、何も対策できなかった…&lt;a href=&quot;https://t.co/IDBnTnzZf6&quot;&gt;https://t.co/IDBnTnzZf6&lt;/a&gt;&lt;/p&gt;&amp;mdash; kubosho (@kubosho_) &lt;a href=&quot;https://twitter.com/kubosho_/status/1253014495930118146?ref_src=twsrc%5Etfw&quot;&gt;April 22, 2020&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.contentfulcommunity.com/t/can-i-disable-auto-save-in-the-content-posting-editor/2626&quot;&gt;Contentful Community&lt;/a&gt; にも同様の問題が報告されていた。Markdownエディタを使ってフルスクリーン状態で記事を書いていたらこの問題が発生した。後日問題が再現するか試したが、再現しなかったので複合的な要因でバグる可能性が考えられる。&lt;/p&gt;
&lt;p&gt;とはいえ、記事を書くときに「記事ちゃんとした形で保存されているのかな…」といった余計な思考が混ざることで記事を書くことを億劫にさせるし、安定した環境で書きたくなった。&lt;/p&gt;
&lt;h3 id=&quot;2-contentful-の-編集画面が気に食わない&quot;&gt;2. Contentful の 編集画面が気に食わない&lt;/h3&gt;
&lt;p&gt;ブログ記事を書くときに自分は本文部分とプレビュー部分をそれぞれ横並びに表示したい。どういうことかは次に示すはてなブログの編集画面の画像を見てもらうと分かる。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/hatena-blog-editor.png&quot; alt=&quot;はてなブログの編集画面は本文とプレビューで2カラムになっている&quot;&gt;&lt;/p&gt;
&lt;p&gt;だがContentfulの編集画面は1カラムになっている。次に示すContentfulの編集画面の画像を見てもらうと分かるように自分の理想とするレイアウトになっていない。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/contentful-editor.png&quot; alt=&quot;Contentfulの編集画面は1カラムになっていて余計な空白が空いているため使いづらい&quot;&gt;&lt;/p&gt;
&lt;p&gt;一応Contentful上のContent modelから本文のエディタをMarkdownエディタにしたうえでフルスクリーンモードにすると、編集画面とプレビュー画面が横並びに表示されるようにはなる。ただこの表示にすると、先ほど書いたように本文がとんでもないことになることがあるので詰んでしまう。&lt;/p&gt;
&lt;p&gt;またMarkdownエディタにした場合、エディタ部分のfont-familyに &lt;code&gt;font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace!important;&lt;/code&gt; が指定されているため、特にWindows環境だと文章を書く気力を失う見た目になる。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/contentful-markdown-editor.png&quot; alt=&quot;ContentfulのMarkdownエディタの画面&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-思ったよりいろんな環境でブログの記事を書かない&quot;&gt;3. 思ったよりいろんな環境でブログの記事を書かない&lt;/h3&gt;
&lt;p&gt;いつどこでもブログの記事を書けるようにしたいがWordPressは導入が面倒だなと思って、裏側をHeadless CMSひいてはContentfulにしてみたものの、スマホやタブレットでブログの記事を書くことはなかった…&lt;/p&gt;
&lt;p&gt;実際に運用してみたらほぼパソコンからしか記事を書いていなかったので、だったら使い慣れたテキストエディタ上で記事を書けたほうが筆は進むのではと思い、移行に至った。&lt;/p&gt;
&lt;p&gt;実際この記事も使い慣れたVisual Studio Code上で書いているが、使い慣れているだけあって頭のリソースを100%文章に向けられている。これで良かった。&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;ちゃんとユースケースを考えよう。&lt;/p&gt;</content>
<published>2020-05-18T00:00:00.000Z</published>
<updated>2022-11-28T15:38:42.969Z</updated>
</entry>
<entry>
<title>WSL上のChromiumでPDF出力をする</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/output-pdf-in-wsl-chromium"/>
<id>tag:blog.kubosho.com,2018-08-05:entry:output-pdf-in-wsl-chromium</id>
<summary>Windows Subsystem for Linux(WSL)上で Headless Chrome を使って、PDF 出力をしてみました。</summary>
<content type="html">&lt;p&gt;Windows Subsystem for Linux(WSL)上でHeadless Chromeを使って、PDF出力をしてみました。&lt;br&gt;
やり方としては次に示すコマンドを実行すれば、Cドライブ上のTempフォルダの中にGoogleをスクリーンショットした &lt;code&gt;output.pdf&lt;/code&gt; が出力されます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;sudo apt install chromium-browser
chromium-browser --headless --disable-gpu --print-to-pdf=/mnt/c/Temp/output.pdf https://www.google.com
&lt;/code&gt;&lt;/pre&gt;</content>
<published>2018-08-05T00:00:00.000Z</published>
<updated>2022-11-28T14:50:45.867Z</updated>
</entry>
<entry>
<title>TypeScriptでsetInterval()の型が合わない理由と解決方法</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/setinterval-trap-on-typescript"/>
<id>tag:blog.kubosho.com,2018-07-18:entry:setinterval-trap-on-typescript</id>
<summary>@types/node に依存した状態で以下のようなコードを書いたときに Type 'Timer' is not assignable to type 'number'. というエラーメッセージが出ます。</summary>
<content type="html">&lt;p&gt;&lt;code&gt;@types/node&lt;/code&gt; に依存した状態で以下のようなコードを書いたときに &lt;code&gt;Type &#39;Timer&#39; is not assignable to type &#39;number&#39;.&lt;/code&gt; というエラーメッセージが出ます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class ExampleClass {
  intervalId: number;

  constructor() {
    this.intervalId = 0;
  }

  start() {
    this.intervalId = setInterval(() =&gt; {
      // do something
    }, 1000);
  }

  stop() {
    clearInterval(this.intervalId);
    this.intervalId = 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;原因&quot;&gt;原因&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;@types/node&lt;/code&gt; に依存している場合 &lt;code&gt;setInterval()&lt;/code&gt; の返り値が &lt;code&gt;NodeJS.Timer&lt;/code&gt; となるためです。&lt;code&gt;@types/node&lt;/code&gt; に依存していなければ &lt;code&gt;setInterval()&lt;/code&gt; の返り値は &lt;code&gt;number&lt;/code&gt; になります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@types/node&lt;/code&gt; の型定義： &lt;a href=&quot;https://github.com/DefinitelyTyped/DefinitelyTyped/blob/010a8de/types/node/timers.d.ts#L73-L76&quot;&gt;https://github.com/DefinitelyTyped/DefinitelyTyped/blob/010a8de/types/node/timers.d.ts#L73-L76&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;TypeScriptの &lt;code&gt;lib.dom.d.ts&lt;/code&gt; の型定義： &lt;a href=&quot;https://github.com/microsoft/TypeScript/blob/3431912/lib/lib.dom.d.ts#L16739&quot;&gt;https://github.com/microsoft/TypeScript/blob/3431912/lib/lib.dom.d.ts#L16739&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;解決方法&quot;&gt;解決方法&lt;/h2&gt;
&lt;h3 id=&quot;1-setinterval-を使う箇所で-windowsetinterval-とする&quot;&gt;1. &lt;code&gt;setInterval()&lt;/code&gt; を使う箇所で &lt;code&gt;window.setInterval()&lt;/code&gt; とする&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;window&lt;/code&gt; オブジェクトにある &lt;code&gt;setInterval()&lt;/code&gt; の返り値の型は &lt;code&gt;number&lt;/code&gt; となっています。なので &lt;code&gt;window.setInterval()&lt;/code&gt; が使える環境であれば &lt;code&gt;window.setInterval()&lt;/code&gt; と書き換えることで解決します。&lt;/p&gt;
&lt;p&gt;参考： &lt;a href=&quot;https://github.com/TypeStrong/atom-typescript/issues/1053#issuecomment-321126192&quot;&gt;setInterval - Type &#39;Timer&#39; is not assignable to type &#39;number&#39; · Issue #1053 · TypeStrong/atom-typescript&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-setinterval-の返り値を入れるプロパティの型を変更する&quot;&gt;2. &lt;code&gt;setInterval()&lt;/code&gt; の返り値を入れるプロパティの型を変更する&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;window.setInterval()&lt;/code&gt; と書き換える方法は、SSRをしているときには使えません。たとえばNext.jsを使っている場合にはエラーが出ると思われます。&lt;/p&gt;
&lt;p&gt;この場合は以下のように &lt;code&gt;intervalId&lt;/code&gt; の初期値に &lt;code&gt;null&lt;/code&gt; を入れておいて、&lt;code&gt;clearInterval()&lt;/code&gt; を実行するところで &lt;code&gt;setInterval()&lt;/code&gt; の返り値が &lt;code&gt;this.intervalId&lt;/code&gt; に入っているか確かめる方法があります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class ExampleClass {
  intervalId: NodeJS.Timer | null;

  constructor() {
    this.intervalId = null;
  }

  start() {
    this.intervalId = setInterval(() =&gt; {
      // do something
    }, 1000);
  }

  stop() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }

    this.intervalId = null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-setinterval-全体を-number-で囲む&quot;&gt;3. &lt;code&gt;setInterval()&lt;/code&gt; 全体を &lt;code&gt;Number()&lt;/code&gt; で囲む&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/ollin2012/article/details/88963553&quot;&gt;关于typescript的定时器setInterval()坑_ollin2012的博客-CSDN博客_ts 定时器类型&lt;/a&gt;で紹介されている手法を多少改変して、&lt;code&gt;setInterval()&lt;/code&gt; 全体を &lt;code&gt;Number()&lt;/code&gt; で囲んで &lt;code&gt;number&lt;/code&gt; 型にtype assertionする手法もあります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export class ExampleClass {
  intervalId: number;

  constructor() {
    this.intervalId = 0;
  }

  start() {
    this.intervalId = Number(
      setInterval(() =&gt; {
        // do something
      }, 1000),
    );
  }

  stop() {
    clearInterval(this.intervalId);
    this.intervalId = 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;手癖のように &lt;code&gt;intervalID&lt;/code&gt; が入るところで初期値として &lt;code&gt;0&lt;/code&gt; を代入していましたが、TypeScriptではハマる可能性が高いので気をつけましょう。&lt;/p&gt;</content>
<published>2018-07-18T00:00:00.000Z</published>
<updated>2022-11-28T08:18:51.814Z</updated>
</entry>
<entry>
<title>Atomic Designを実案件に導入して運用してみた結果はどうなのか</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/atomic-desigin-on-abematv"/>
<id>tag:blog.kubosho.com,2017-12-18:entry:atomic-desigin-on-abematv</id>
<summary>かつてAtomic Design の考え方と利点・欠点という記事を書きました。</summary>
<content type="html">&lt;p&gt;かつて&lt;a href=&quot;http://blog.kubosho.com/entry/using-atomic-design&quot;&gt;Atomic Design の考え方と利点・欠点&lt;/a&gt;という記事を書きました。この記事内で日本では&lt;a href=&quot;https://abema.tv/&quot;&gt;AbemaTV&lt;/a&gt;で使われていると書きました。そして今でもAbemaTVではAtomic Designの考えに基づいてコンポーネントが作られています。&lt;/p&gt;
&lt;p&gt;AbemaTVでの経験を通じて、Webアプリケーションのクライアントサイドの開発者という立場からAtomic Designはどうなのかについて書いていきます。&lt;/p&gt;
&lt;p&gt;なおAbemaTVではビューライブラリとしてReactを使っているので、React前提の話になります。&lt;/p&gt;
&lt;h2 id=&quot;atomic-design-に基づくのは実際どうなのか&quot;&gt;Atomic Design に基づくのは実際どうなのか&lt;/h2&gt;
&lt;p&gt;基本的には良いです。良い点について書いていたら&lt;a href=&quot;https://ygoto3.com/posts/atomic-design-on-actual-project/&quot;&gt;Atomic Design を実案件に導入 - UI コンポーネントの粒度を明確化した結果と副産物 | ygoto3.com&lt;/a&gt;や&lt;a href=&quot;https://www.slideshare.net/ygoto3q/atomic-desigin-powered-by-react-abematv/31&quot;&gt;Atomic Design powered by React @ AbemaTV&lt;/a&gt;に書いてある内容と似てきたので、良い点についてはこれらの資料を見てください。&lt;/p&gt;
&lt;p&gt;ただし問題点もあります。いま思いつく限りでは大きく2つ問題があると考えています。&lt;/p&gt;
&lt;h3 id=&quot;人によってコンポーネントの粒度が違う&quot;&gt;人によってコンポーネントの粒度が違う&lt;/h3&gt;
&lt;p&gt;Atomic Designにおいて &lt;code&gt;atoms&lt;/code&gt; はそれ単体で使うことはなく &lt;code&gt;molecules&lt;/code&gt; や &lt;code&gt;organisms&lt;/code&gt; 内に取り込まれた状態で使われます。&lt;/p&gt;
&lt;p&gt;また &lt;code&gt;molecules&lt;/code&gt; にあたるコンポーネントは単一責任の原則に基づくのが良いとされています。&lt;/p&gt;
&lt;p&gt;しかしコンポーネントによっては &lt;code&gt;props&lt;/code&gt; が多すぎるものもあります。特に &lt;code&gt;molecules&lt;/code&gt; の一部コンポーネントは &lt;code&gt;props&lt;/code&gt; が22個あります。実際に該当のコンポーネントは挙動を変更する理由がいくつかある状態です。&lt;/p&gt;
&lt;p&gt;この場合は責務ごとにコンポーネントを分割すれば良かったでしょう。もしくは&lt;a href=&quot;https://reactjs.org/docs/higher-order-components.html&quot;&gt;Higher-Order Components - React&lt;/a&gt;を取り入れても良さそうです。&lt;/p&gt;
&lt;h4 id=&quot;デザイナーとデベロッパーのコンポーネントの粒度も違う&quot;&gt;デザイナーとデベロッパーのコンポーネントの粒度も違う&lt;/h4&gt;
&lt;p&gt;今だとデザイナーがSketchのSymbol上に構築したコンポーネントの粒度とデベロッパーが実装したコンポーネントの粒度が違うものもあります。&lt;/p&gt;
&lt;p&gt;デザイナーとデベロッパーのどちらかの考えに粒度を合わせるのが良いでしょう。この場合、基本的にはデザイナーの考えに合わせたほうが良いです。&lt;a href=&quot;http://morishitter.hatenablog.com/entry/2016/07/29/204642&quot;&gt;morishitter の CSS の書き方（2016 年夏） - morishitter blog&lt;/a&gt;にもある「デザインの意図を正確に理解した上で書かれたCSSは破綻しない」です。&lt;/p&gt;
&lt;p&gt;とはいえ、自分もあまり余裕がなかったため「なぜこのデザインなのか」をあまり聞けていません。Slack上でどのデザインにするかやりとりしているときに「これが良いですねー」とか反応するくらいです。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://demo.patternlab.io/&quot;&gt;Pattern Lab のデモ&lt;/a&gt;に作りたいコンポーネントのカテゴリーがあったらお互いそれに沿うのがいいでしょう。たとえば &lt;code&gt;blocks&lt;/code&gt; だったら &lt;code&gt;molecules&lt;/code&gt; で &lt;code&gt;sections&lt;/code&gt; だったら &lt;code&gt;organisms&lt;/code&gt; という感じです。&lt;/p&gt;
&lt;h3 id=&quot;コンポーネントのテストが面倒&quot;&gt;コンポーネントのテストが面倒&lt;/h3&gt;
&lt;p&gt;コンポーネントのテストを書く場合は &lt;code&gt;enzyme&lt;/code&gt; を使って &lt;code&gt;find()&lt;/code&gt; などを用いて要素があるかを探し、表示されている文字が期待のものと合致しているかなどをテストします。&lt;/p&gt;
&lt;p&gt;この場合テストとは直接的に関係ないコードがテストコード内に存在してしまいます。テストを書くときにVirtual DOM内をいちいち探索しないといけなくなるため、そこまでテストコードが書かれていないコンポーネントが増えることにもなります。また各コンポーネントがどのような &lt;code&gt;props&lt;/code&gt; を持っているか調べないといけないため特に &lt;code&gt;organisms&lt;/code&gt; にあたるコンポーネントのテストは面倒になるでしょう。&lt;/p&gt;
&lt;p&gt;そのためJestの&lt;a href=&quot;https://facebook.github.io/jest/docs/en/snapshot-testing.html&quot;&gt;Snapshot Testing&lt;/a&gt;のように、スナップショットとなるコードを書いてテストコード側でそれと合致するか確かめるようにする流れができつつあります。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;この記事は&lt;a href=&quot;https://adventar.org/calendars/2216&quot;&gt;AbemaTV Advent Calendar 2017&lt;/a&gt;の17日目の記事でした。&lt;/p&gt;</content>
<published>2017-12-18T00:00:00.000Z</published>
<updated>2022-11-28T15:23:41.646Z</updated>
</entry>
<entry>
<title>CSSを破綻させない</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/not-break-css"/>
<id>tag:blog.kubosho.com,2016-12-14:entry:not-break-css</id>
<summary>12/3（土）にCSS を破綻させないという内容をbuilderscon tokyo 2016で話しました。</summary>
<content type="html">&lt;p&gt;12/3（土）に&lt;a href=&quot;https://builderscon.io/builderscon/tokyo/2016/session/720e29c6-9b11-46f3-adf4-f6f52e4fcbb9&quot;&gt;CSS を破綻させない&lt;/a&gt;という内容を&lt;a href=&quot;https://builderscon.io/builderscon/tokyo/2016&quot;&gt;builderscon tokyo 2016&lt;/a&gt;で話しました。&lt;/p&gt;
&lt;p&gt;そこで使った発表資料の内容を編集した上で、&lt;a href=&quot;http://qiita.com/advent-calendar/2016/css&quot;&gt;CSS Advent Calendar 2016&lt;/a&gt; 14日目の記事として公開します。&lt;/p&gt;
&lt;h2 id=&quot;css-は破綻しやすい&quot;&gt;CSS は破綻しやすい&lt;/h2&gt;
&lt;p&gt;OOCSSの提唱者Nicole Sulliban氏も&lt;a href=&quot;http://www.andoh.org/2009/11/web-directions-east-2009-nicole.html&quot;&gt;&quot;CSS is too fragile&quot;&lt;/a&gt;と2008年のイベントで言いました。なぜ破綻しやすいのか。それはCSSの特性が絡んでいます。&lt;/p&gt;
&lt;h2 id=&quot;css-の特性&quot;&gt;CSS の特性&lt;/h2&gt;
&lt;p&gt;CSSの特性としておもに3つあります。&lt;/p&gt;
&lt;p&gt;はじめに、記述を間違えてもエラーにならないことです。ブラウザで表示確認をおこなって初めて見た目がおかしいことに気づきます。&lt;/p&gt;
&lt;p&gt;次に、スタイルが適用される条件としてルールセットを書く順序は関係ありますが、常に関係があるわけではない点です。&lt;/p&gt;
&lt;p&gt;ちなみにルールセットはCSSのセレクタ・プロパティ・値の定義をまとめたものです。&lt;br&gt;
分かりやすい図として&lt;a href=&quot;http://terkel.jp/archives/2011/09/css-rule-structure/&quot;&gt;CSS ルールセット構造図 · terkel.jp&lt;/a&gt;内の画像があるので引用します。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//blog-assets.kubosho.com/css-rule-set.png&quot; alt=&quot;CSSのルールセットの図&quot;&gt;&lt;/p&gt;
&lt;p&gt;最後に、ルールセット間で同じプロパティが定義されている場合、順序・詳細度・重要度にもとづいて適用されるスタイルが決定されます。CSSをややこしくしているのはここですが、意識して書かないと容易に破綻します。&lt;/p&gt;
&lt;p&gt;ここからは実際にCSSが破綻した例を見ていきます。&lt;/p&gt;
&lt;h2 id=&quot;css-の破綻&quot;&gt;CSS の破綻&lt;/h2&gt;
&lt;p&gt;CSSの破綻はいくつか種類があります。これらの要素のうち2つ以上が複合して起きていると手がつけられないCSSになります。&lt;/p&gt;
&lt;h3 id=&quot;スタイルの上書きが複数ある&quot;&gt;スタイルの上書きが複数ある&lt;/h3&gt;
&lt;p&gt;スタイルの上書きはBootstrapなどCSSフレームワークを使うときに独自の見た目を実現しようとすると起こりがちな問題です。スタイルの上書きが多くなるとどんな見た目になるのか予測できなくなり破綻します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.button {
  border: 1px solid #ccc;
}

/* ...長いコードの後や別ファイルなど... */

.button {
  border: 1px solid #666;
}

/* ...長いコードの後や別ファイルなど... */

.article .button {
  border: 1px solid #00c;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;前に書いたセレクタの詳細度が高い&quot;&gt;前に書いたセレクタの詳細度が高い&lt;/h3&gt;
&lt;p&gt;前に書いたセレクタの詳細度が高くてスタイルが上書きできないことも破綻の一因になります。先ほどのスタイルの上書きを多くしていると起こりがちな問題です。これがもたらす結果としては &lt;code&gt;!important&lt;/code&gt; の濫用です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 詳細度はa=0, b=4, c=0なので0.4.0となる */
.container .form .form-group .form-submit-button {
}

/*
  詳細度はa=0, b=1, c=0なので0.1.0となる
  上書きできない。つらい
*/
.form__submit-button {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;命名規則がバラバラ&quot;&gt;命名規則がバラバラ&lt;/h3&gt;
&lt;p&gt;よくあることとして単語の区切りがケバブケース・キャメルケース・スネークケースのようにバラけていると、どの規則に合わせればいいのか分からずいつまでも命名規則が統一されません。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.account-login-button {
}

.commentSubmitButton {
}

.form_submit_button {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;css-の破綻-まとめ&quot;&gt;CSS の破綻 まとめ&lt;/h2&gt;
&lt;p&gt;ここまでCSSが破綻する理由について書きました。まとめると次のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;詳細度が管理されていない&lt;/li&gt;
&lt;li&gt;ルールセットの分割粒度が明確ではない&lt;/li&gt;
&lt;li&gt;命名規則が決まっていない&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;css-を破綻させない&quot;&gt;CSS を破綻させない&lt;/h2&gt;
&lt;p&gt;ここからはCSSを破綻させないためにはどうすればいいのかを書いていきます。&lt;/p&gt;
&lt;h3 id=&quot;詳細度を管理する&quot;&gt;詳細度を管理する&lt;/h3&gt;
&lt;p&gt;CSSなどのファイルを分割するときはファイル内で下へ行くにつれて詳細度が高くなるようにします。またセレクタ定義やIDセレクタを書きすぎないようにするのも重要です。&lt;/p&gt;
&lt;p&gt;例を挙げるとフォーム共通のスタイルを適用するときにセレクタを定義しすぎないことです。これにより少ないセレクタ定義で適用したスタイルを上書きできて秩序が保てます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/* 詳細度はa=0, b=2, c=0なので0.2.0となる */
.form .form-button {
  margin: 0;
}

/* ... */

/*
  詳細度はa=0, b=2, c=0なので0.2.0となる
  記述の順序が後なので値が上書きされる
*/
.comment-form .form-button {
  /* 上書きできる */
  margin: 10px auto;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ルールセットの分割粒度を明確にする&quot;&gt;ルールセットの分割粒度を明確にする&lt;/h3&gt;
&lt;p&gt;スタイルの上書きを減らすためにルールセットの分割粒度を明確にすることも重要です。これはさまざまな方法が提案されていて、たとえば&lt;a href=&quot;https://github.com/hiloki/flocss&quot;&gt;FLOCSS&lt;/a&gt;や&lt;a href=&quot;https://smacss.com/&quot;&gt;SMACSS&lt;/a&gt;、&lt;a href=&quot;http://ecss.io/&quot;&gt;ECSS&lt;/a&gt;などがあります。&lt;/p&gt;
&lt;p&gt;これは作るものによって適しているものは違ってきます。そのため一概にどれがいいか言えません。スタイルを適用したいときにルールセットをどこに置けばいいのかチーム内で迷わないようにするのが重要です。&lt;/p&gt;
&lt;h3 id=&quot;命名規則を決める&quot;&gt;命名規則を決める&lt;/h3&gt;
&lt;p&gt;命名規則も&lt;a href=&quot;http://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/&quot;&gt;MindBEMding&lt;/a&gt;や&lt;a href=&quot;http://ecss.io/chapter5.html#h-H2_1&quot;&gt;ECSS&lt;/a&gt;、&lt;a href=&quot;https://smacss.com/book/categorizing&quot;&gt;SMACSS&lt;/a&gt;などさまざまなものがあります。これもチーム内で使う命名規則を一致させることが重要です。&lt;/p&gt;
&lt;h2 id=&quot;css-を破綻させない-まとめ&quot;&gt;CSS を破綻させない まとめ&lt;/h2&gt;
&lt;p&gt;ここまでCSSの破綻を起こさないためにどうしたらいいか書きました。まとめると次のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;詳細度を管理する&lt;/li&gt;
&lt;li&gt;チーム内でルールセットの分割粒度を明確にする&lt;/li&gt;
&lt;li&gt;チーム内で命名規則を決める&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;しかしこれらを実践する前により大事なことが1つあります。それは&lt;strong&gt;デザイナーとの認識合わせ&lt;/strong&gt;です。&lt;/p&gt;
&lt;p&gt;これは&lt;a href=&quot;http://morishitter.hatenablog.com/entry/2016/07/29/204642&quot;&gt;デザインの意図を正確に理解した上で書かれた CSS は破綻しない&lt;/a&gt;という言葉があります。&lt;/p&gt;
&lt;p&gt;デザイナーが作ったSketchやPhotoshopのファイルを見て質問や提案をおこない、デザイナーとUIを実装する人で意図の認識を合わせることが重要です。もっと言うならSketchやPhotoshopなどでUIデザインを作る前のプロトタイピング段階から関わるとお互い意図の認識がしやすくなります。&lt;/p&gt;
&lt;p&gt;これを踏まえてまとめると、&lt;strong&gt;チームで議論して良いCSS設計を考えよう&lt;/strong&gt;になります。&lt;/p&gt;
&lt;h2 id=&quot;css-がすでに破綻している場合は&quot;&gt;CSS がすでに破綻している場合は？&lt;/h2&gt;
&lt;p&gt;buildersconのQ&amp;#x26;Aで「途中から入ったプロジェクトのCSSが破綻していた場合どう改善したらいいのか？」というのがあったのですが、今のところチームで話し合って設計指針を決めて1から書き直すしかないと考えています。&lt;/p&gt;
&lt;h2 id=&quot;参考資料&quot;&gt;参考資料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://dskd.jp/archives/54.html&quot;&gt;僕は CSS を見殺しにした - dskd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://app.codegrid.net/entry/2016-ecss-1&quot;&gt;Enduring CSS の設計思想 - ECSS が目指す設計 | CodeGrid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://app.codegrid.net/entry/smacss-1&quot;&gt;SMACSS による CSS の設計 - ベースとレイアウト | CodeGrid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://qiita.com/BYODKM/items/b8f545453f656270212a&quot;&gt;破綻しにくい CSS 設計の法則 15 - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://qiita.com/BYODKM/items/8c777db2d89f4e830c93&quot;&gt;CSS が破綻する 4 つの理由 - Qiita&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2016-12-14T00:00:00.000Z</published>
<updated>2022-11-28T15:18:37.222Z</updated>
</entry>
<entry>
<title>Atomic Designの考え方と利点・欠点</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/using-atomic-design"/>
<id>tag:blog.kubosho.com,2016-07-19:entry:using-atomic-design</id>
<summary>Atomic Design はデザインシステムを作る方法論となります。</summary>
<content type="html">&lt;p&gt;Atomic Designはデザインシステムを作る方法論となります。デザインシステムというのはスタイルガイドやブランドのガイドラインなどを指すようです。&lt;/p&gt;
&lt;p&gt;日本だと&lt;a href=&quot;https://abema.tv/&quot;&gt;AbemaTV（アベマ TV）&lt;/a&gt;で使われています。（&lt;a href=&quot;http://ygoto3.com/posts/atomic-design-on-actual-project/&quot;&gt;Atomic Design を実案件に導入 - UI コンポーネントの粒度を明確化した結果と副産物 | ygoto3.com&lt;/a&gt;より）&lt;/p&gt;
&lt;p&gt;Atomic Designは今までのページ単位と違いコンポーネント単位でデザインカンプを作る考え方です。作ったコンポーネント同士の組み合わせでページを作ります。&lt;/p&gt;
&lt;p&gt;Atomic Designはコンポーネントの単位を5つに分けています。その5つの単位はAtoms（原子）・Molecules（分子）・Organisms（有機体）・Templates（テンプレート）・Pages（ページ）です。各コンポーネントの詳細は次のとおりです。&lt;/p&gt;
&lt;h2 id=&quot;atoms原子&quot;&gt;Atoms（原子）&lt;/h2&gt;
&lt;p&gt;Atoms（原子）は、UIを構成する基礎的な要素が該当します。&lt;/p&gt;
&lt;p&gt;フォームでいうと、画像で示すようにラベル・入力部分・ボタンの各要素がAtomsとなります。他の要素では、カラーパレットやフォント、アニメーションがAtomsに入ります。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//blog-assets.kubosho.com/atoms.png&quot; alt=&quot;Atomsを指した図。フォームのラベル・入力フォーム・ボタンがAtomsとなる&quot;&gt;&lt;/p&gt;
&lt;p&gt;Atomsに振り分ける基準としては、&lt;strong&gt;対象の要素が機能的にそれ以上分割できない&lt;/strong&gt; 場合、Atomsへ振り分けます。フォームで例えると、ラベルはそれ以上機能的に分割できません。また入力フォームやボタンもそれ以上機能的に分割できません。&lt;/p&gt;
&lt;p&gt;Atoms単体だと抽象的でどういう意味を持つかは分からないです。入力フォームだけ見ても、それがアカウント登録フォームもしくはコメント入力フォームという情報は読み取れません。&lt;/p&gt;
&lt;p&gt;Atomsはコンポーネントの基礎部分になります。それは、Atomsを組み合わせてより大きなコンポーネントを構成するという点から言えることです。&lt;/p&gt;
&lt;p&gt;またAtomsを俯瞰できるページを用意しておくことで、そのページがどのようにデザインされたかという雰囲気を感じ取ることができます。それによりページデザインに一貫性を持たせることができます。&lt;/p&gt;
&lt;h2 id=&quot;molecules分子&quot;&gt;Molecules（分子）&lt;/h2&gt;
&lt;p&gt;Molecules（分子）は、Atomsを組み合わせて作る要素です。このAtomsを組み合わせてMoleculesを作るというのは「単一責任の原則」やUNIX哲学の「1つのプログラムは1つのことをうまくやる」に基づいているようです。&lt;/p&gt;
&lt;p&gt;Moleculesになることで意味を持つ要素となります。たとえば、Atomsであるラベル・入力フォーム・登録ボタンという3つのコンポーネントがあってもそれら単体は意味をなしません。しかし、これらの要素を組み合わせることにより「ラベルで示したことに応じて、入力フォームに何かを書いて、登録ボタンを押す」という意味が示せるようになります。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//blog-assets.kubosho.com/molecules.png&quot; alt=&quot;Moleculesを指した図。フォームのラベル・入力フォーム・ボタンをまとめたものがMoleculesとなる&quot;&gt;&lt;/p&gt;
&lt;p&gt;Moleculesはできるだけ単純にして、再利用性やUIの一貫性を高めます。&lt;/p&gt;
&lt;h2 id=&quot;organisms有機体&quot;&gt;Organisms（有機体）&lt;/h2&gt;
&lt;p&gt;Organisms（有機体）は、AtomsやMolecules、また他のOrganismsを組み合わせて作る要素です。今までのAtomsやMoleculesとは違い複雑な要素になります。ヘッダーやフッターと呼ばれる要素はこのOrganismsになります。&lt;/p&gt;
&lt;p&gt;たとえば画像で示すようなヘッダーは「タイトル」というAtomsと、「ナビゲーション」「SNSのボタン群」というMoleculesが組み合わさって、ヘッダーというOrganismsになっています。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//blog-assets.kubosho.com/organisms.png&quot; alt=&quot;Organismsを指した図。WebページのヘッダーやフッターなどがOrganismsとなる&quot;&gt;&lt;/p&gt;
&lt;p&gt;Organismsからそのページの特色が出やすくなります。&lt;/p&gt;
&lt;h2 id=&quot;templatesテンプレート&quot;&gt;Templates（テンプレート）&lt;/h2&gt;
&lt;p&gt;ここからAtoms（原子）・Molecules（分子）・Organisms（有機体）といった化学的なものに例えることをしなくなります。これは仕事の依頼元や上司・同僚に見せるものということを明確にするため、より一般的な言葉を使います。&lt;/p&gt;
&lt;p&gt;Templates（テンプレート）の説明に移ると、Templatesはページ構造を説明するものです。TemplatesはMoleculesやOrganismsを組み合わせて作ります。&lt;/p&gt;
&lt;p&gt;Templatesの段階ではページ内容がまだ仮となります。Templatesを言い換えるなら「&lt;a href=&quot;https://www.google.co.jp/search?q=%E3%83%AF%E3%82%A4%E3%83%A4%E3%83%BC%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0&amp;#x26;tbm=isch&quot;&gt;ワイヤーフレーム&lt;/a&gt;」になります。&lt;/p&gt;
&lt;h2 id=&quot;pagesページ&quot;&gt;Pages（ページ）&lt;/h2&gt;
&lt;p&gt;Pages（ページ）は、Template内へ実際の文章や画像などが入ったものとなります。&lt;/p&gt;
&lt;p&gt;ここまで5つの要素について概要を書きました。要素の振り分けかたについて、どのように考えたらいいかは&lt;a href=&quot;http://atomicdesign.bradfrost.com/chapter-2/#atomic-design-is-for-user-interfaces&quot;&gt;Atomic design is for user interfaces&lt;/a&gt;内のInstagramで例えたものが分かりやすいです。&lt;/p&gt;
&lt;h2 id=&quot;atomic-design-の利点&quot;&gt;Atomic Design の利点&lt;/h2&gt;
&lt;p&gt;さて、Atomic Designを実際に適用した結果、次に挙げる3つの利点があると感じました。&lt;/p&gt;
&lt;h3 id=&quot;名前がついている&quot;&gt;名前がついている&lt;/h3&gt;
&lt;p&gt;Atomic Designはコンポーネントの大きさによって、それぞれAtoms・Molecules・Organisms・Templates・Pagesという名前がついています。この名前がついていることで、Atomsは「それ以上分割できないコンポーネント」ということや、Moleculesの「Atomsを組み合わせた意味があるコンポーネント」という特徴が共有されやすくなります。&lt;/p&gt;
&lt;h3 id=&quot;デザインの変更に対応しやすくなる&quot;&gt;デザインの変更に対応しやすくなる&lt;/h3&gt;
&lt;p&gt;Atomic Designの考え方でコンポーネントを作ると、デザイン変更に対応しやすくなり再利用性も高くなります。特にAtomsやMoleculesへ振り分けられるような細かいコンポーネントはデザイン変更にも強いです。&lt;/p&gt;
&lt;p&gt;今回適用したページはそこまでデザイン変更が起こりませんでした。それでもデザイン変更があったときはいつもと比べて他コンポーネントへの影響を考えずに対応できました。&lt;/p&gt;
&lt;h3 id=&quot;セレクタの詳細度が平坦に近づく&quot;&gt;セレクタの詳細度が平坦に近づく&lt;/h3&gt;
&lt;p&gt;Atomic Designを適用するとセレクタの詳細度が平坦になるようです。&lt;/p&gt;
&lt;p&gt;次の画像は今回Atomic Designの考え方を使って作ったCSSの詳細度を示したグラフですが、割と平坦なグラフになっています。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//blog-assets.kubosho.com/css-valhalla.png&quot; alt=&quot;神獄のヴァルハラゲートのイベントページのCSSの詳細度を指した図。飛び出すところがあまりない平坦なグラフとなっている&quot;&gt;&lt;/p&gt;
&lt;p&gt;またAtomic Designを採用しているAbemaTVのCSSも突然詳細度が上がることなく平坦なグラフです。ただこれは中の人になって分かったことですが、CSS Modulesの仕組み（css-loader）を取り入れているためCSSは各コンポーネントごとにスコープが閉じた状態で書くことができます。そのためセレクタの詳細度をあまり上げなくてもスタイル宣言をできるため、詳細度が抑えられています。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//blog-assets.kubosho.com/css-abematv.png&quot; alt=&quot;AbemaTVのCSSの詳細度を指した図。急に飛び出していない平坦なグラフとなっている&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;atomic-design-の欠点&quot;&gt;Atomic Design の欠点&lt;/h2&gt;
&lt;p&gt;利点ばかりではなく、Atomic Designの欠点も見えました。&lt;/p&gt;
&lt;h3 id=&quot;デザイナーがどのようにデザインしていけばいいか分からない&quot;&gt;デザイナーがどのようにデザインしていけばいいか分からない&lt;/h3&gt;
&lt;p&gt;Atomic Designは「小さい単位でコンポーネントを作り大きいコンポーネントにしていく」というデザイン手法です。そのため、フロントエンド実装では利点があります。&lt;/p&gt;
&lt;p&gt;しかしデザイナーからすると、Atomic Designの考え方でデザインすることは難しそうです。デザイナーにAtomic Designについて説明しましたが、いきなりの手順変更は難しかったため、デザインしてもらうときはコンポーネント単位ではなくページ単位でデザインしてもらう従来の方法をとりました。&lt;/p&gt;
&lt;p&gt;デザイナーによるページデザインの段階でAtomic Designを取り入れることに関しては、どうしたらいいのかまだ分かりません。&lt;/p&gt;
&lt;h3 id=&quot;欠点に対しての対応&quot;&gt;欠点に対しての対応&lt;/h3&gt;
&lt;p&gt;今回はコンポーネントリストを作りました。以下のjsfiddleではかなり簡略化したリストですが、以下のようにAtoms・Molecules・Organismsとコンポーネントを分けて見せるようにしました。&lt;/p&gt;
&lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/bed3aj1k/2/embedded/result,css/&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;デザイナーには通常通りページ単位でデザインカンプを作ってもらいました。そして、自分のほうでそのデザインカンプを見つつ、Atomic Designの各単位に要素を切り出し、コンポーネントを作りました。&lt;/p&gt;
&lt;p&gt;作ったコンポーネントリストをプランナーやデザイナーに共有しておくことで、実機でどのように表示されるか分かりやすくなることと、開発者にも共有しておくことでコンポーネントを使うことを促し、結果としてコード量や実装の工数を減らすことを目論みました。また、コンポーネントリストは作っておくと、どのようにコンポーネントを分割するか意識できます。&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;今回初めてAtomic Designの考え方でページを作ってみました。結果としては、思ったより良い感じにハマった感があります。今後もAtomic Designの考え方に照らし合わせてコンポーネントを作り、変更に強く分割されたコンポーネントを作っていきたいです。&lt;/p&gt;
&lt;h2 id=&quot;出典&quot;&gt;出典&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://bradfrost.com/blog/post/atomic-web-design/&quot;&gt;Atomic Design | Brad Frost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://atomicdesign.bradfrost.com/&quot;&gt;Atomic Design by Brad Frost&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://ygoto3.com/posts/smashing-conference-whistler-and-atomic-design/&quot;&gt;最近よくクリエイターが移住するカナダで Atomic Design を学ぶ | ygoto3.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://ygoto3.com/posts/atomic-design-on-actual-project/&quot;&gt;Atomic Design を実案件に導入 - UI コンポーネントの粒度を明確化した結果と副産物 | ygoto3.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://postd.cc/the-unicorn-workflow-design-to-code-with-atomic-design-principles-and-sketch/&quot;&gt;珍しいワークフロー：Atomic Design の原則と Sketch でデザインからプログラミングまで | デザイン | POSTD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://design.dmm.com/entry/2016/02/05/153408&quot;&gt;Atomic Design は Web 開発を救うのか - DMM.com ラボ デザイナーズブログ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2016-07-19T00:00:00.000Z</published>
<updated>2022-11-28T15:18:44.351Z</updated>
</entry>
<entry>
<title>Chromium のチャンネル間で変更された点を確認する方法</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/chromium-version-diff"/>
<id>tag:blog.kubosho.com,2016-05-13:entry:chromium-version-diff</id>
<summary>とある事情により、Chromium の stable と beta チャンネル間で変更された点から、バグが直されたコミットがどれなのか確認する必要に迫られました。</summary>
<content type="html">&lt;p&gt;とある事情により、Chromiumのstableとbetaチャンネル間で変更された点から、バグが直されたコミットはどれなのか確認する必要に迫られました。&lt;br&gt;
その時どのようにして目的のコミットにたどり着いたのか書いていきます。&lt;/p&gt;
&lt;h2 id=&quot;chromium-の各チャンネルのバージョンを確認する&quot;&gt;Chromium の各チャンネルのバージョンを確認する&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://omahaproxy.appspot.com/&quot;&gt;OmahaProxy - Google Chrome&lt;/a&gt;というページで確認します。&lt;br&gt;
これは各OSのChromeのチャンネルごとに現在のバージョンを表示しているページです。&lt;/p&gt;
&lt;h2 id=&quot;chromium-のチャンネル間で変更された点を見る&quot;&gt;Chromium のチャンネル間で変更された点を見る&lt;/h2&gt;
&lt;p&gt;実際に先ほどのページからstableとbetaチャンネル間で変更された点を見てみます。それには画像内でも矢印で指していますが、任意のチャンネルのchangelog部分にある「cr」というリンクをクリックします。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/chrome_version.png&quot; alt=&quot;Chromiumの各チャンネルの情報を表示している画面&quot;&gt;&lt;/p&gt;
&lt;p&gt;すると、選択したチャンネル（以下の画像ではdevチャンネル）の前と現在のバージョンで変更された点が見られます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/chrome_stable_dev.png&quot; alt=&quot;Chromiumのstableチャンネルとdevチャンネルの差分を表示している画面&quot;&gt;&lt;/p&gt;
&lt;p&gt;ここからstableとdevチャンネル間で変更された点を見るにはアドレスバー上で以下のようにURLを書き換えます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://chromium.googlesource.com/chromium/src/+log/:start..:end?pretty=fuller&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;たとえば2021/3/6現在のstableとdevチャンネル間で変更された点を見たい場合は &lt;code&gt;:start&lt;/code&gt; にstableチャンネルのバージョンを &lt;code&gt;:end&lt;/code&gt; にdevチャンネルのバージョンを入れます。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://chromium.googlesource.com/chromium/src/+log/50.0.2661.102..51.0.2704.47?pretty=fuller&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;すると、以下のような感じでstableとbetaチャンネル間で変更された点を見ることができます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.kubosho.com/chrome_diff.png&quot; alt=&quot;Chromiumのチャンネル間の更新差分を表示している画面&quot;&gt;&lt;/p&gt;
&lt;p&gt;あとはブラウザのページ内検索機能を使い、適当な語句で検索して目的のコミットを見つけるだけです。&lt;/p&gt;</content>
<published>2016-05-13T00:00:00.000Z</published>
<updated>2022-11-28T15:34:06.007Z</updated>
</entry>
<entry>
<title>Nicoのこれまでとこれから</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/nico"/>
<id>tag:blog.kubosho.com,2015-12-08:entry:nico</id>
<summary></summary>
<content type="html">&lt;!-- markdownlint-disable no-trailing-punctuation --&gt;
&lt;p&gt;&lt;a href=&quot;http://qiita.com/advent-calendar/2015/bootstrap&quot;&gt;Bootstrap Advent Calendar 2015&lt;/a&gt; 8日目の記事です。ここではBootstrapのテーマの1つである、&lt;a href=&quot;http://nico.kubosho.com/&quot;&gt;Nico&lt;/a&gt;のこれまでとこれからについて書きます。&lt;/p&gt;
&lt;h2 id=&quot;これまで&quot;&gt;これまで&lt;/h2&gt;
&lt;p&gt;まずはNicoができてから、現時点の最新バージョンであるv3.3.6-1.1.0までの軌跡を振り返ります。&lt;/p&gt;
&lt;p&gt;はじめにNicoを作ったきっかけとしては、現在冬コミに向けて執筆中の&lt;a href=&quot;https://github.com/o2project/start-dash-of-site-making&quot;&gt;サイト制作のSTART:DASH!!&lt;/a&gt;という同人誌です。同人誌の目的上、ラブライブ！のアニメにでてきたラブライブ参加者募集サイトを実際に作る必要がでてきました。ただ1からCSSを書くのが面倒と感じた自分は、&lt;a href=&quot;http://honokak.osaka/&quot;&gt;Honoka&lt;/a&gt;をforkし、オリジナルのBootstrapテーマを作ることにしました。&lt;/p&gt;
&lt;p&gt;このラブライブ参加者募集サイトですが、図1のアニメ画像キャプチャを見て分かるように配色がピンク系となっています。配色がピンク系ということは、にこしかいないということで、名前がNicoに決まりました。&lt;/p&gt;
&lt;p&gt;しかしHonokaをforkしてできあがったNicoですが、リポジトリーのファイルを見ているうちにいろいろと作業環境を整えたくなりました。そのためまずは作業環境を整えました。&lt;/p&gt;
&lt;h3 id=&quot;作業しやすくするためのstartdash&quot;&gt;作業しやすくするためのSTART:DASH!!&lt;/h3&gt;
&lt;p&gt;主にやったことは3つあります。&lt;/p&gt;
&lt;p&gt;1つめは、Gruntをなんとなく気分的にグローバルインストールしたくないと思ったため、&lt;a href=&quot;https://github.com/kubosho/Nico/commit/57c244d8b8c898efffb45e5e9977222f0a8f6d41&quot;&gt;npmのrun-scriptでラップしました&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;2つめは、scssファイルがちゃんとできるか自動で確認したかったので、&lt;a href=&quot;https://github.com/kubosho/Nico/blob/743aeb2a8e5e102506432dc450e8f2bc8f0efc06/.travis.yml&quot;&gt;Travis CIを導入しました&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;3つめは、Travis CI導入に関係していますが、HonokaだったりNicoの公開用ファイルをgh-pagesブランチに上げる際は手動で上げないといけませんでした。&lt;br&gt;
しかし手動は面倒だと感じたため、&lt;a href=&quot;https://github.com/kubosho/Nico/blob/743aeb2a8e5e102506432dc450e8f2bc8f0efc06/.bin/deploy-to-gh-pages.sh&quot;&gt;よしなにgh-pagesブランチへpushしてくれるシェルスクリプトを書きました&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&quot;nico襲来&quot;&gt;Nico襲来&lt;/h3&gt;
&lt;p&gt;これで作業がしやすくなったので、ようやく配色をHonokaからNico仕様にしていきます。&lt;br&gt;
配色を考える上で真姫・凛・花陽・絵里・希のイメージカラーを入れたいと思い、考えた結果次のような配色になりました。&lt;br&gt;
&lt;code&gt;danger&lt;/code&gt;が紫色になっているのは、希のイメージカラーを入れたいためだったのですが、一応根拠もあります。&lt;br&gt;
それは、天気予報で特別警報の配色が紫色というところから来ています。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名前&lt;/th&gt;
&lt;th&gt;カラーコード&lt;/th&gt;
&lt;th&gt;色&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#f3d4df&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #f3d4df;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inverse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff50ac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ff50ac;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;primary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff50ac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ff50ac;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;success&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#4caf50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #4caf50;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;info&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#5cfaf9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #5cfaf9;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warning&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff5052&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ff5052;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;danger&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ac62ff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ac62ff;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;そして&lt;a href=&quot;https://github.com/kubosho/Nico/releases/tag/v3.3.5a&quot;&gt;Nico v3.3.5a&lt;/a&gt;をリリースという流れになります。&lt;/p&gt;
&lt;h3 id=&quot;なんとかしなきゃ&quot;&gt;なんとかしなきゃ！&lt;/h3&gt;
&lt;p&gt;リリースした後は、Honokaのバージョン更新に追従していくだけだったのですが、ある時自分の中で事件が起こります。それは、&lt;a href=&quot;http://coliss.com/articles/build-websites/operation/work/best-templates-for-bootstrap-2015-autumn.html&quot; rel=&quot;nofollow&quot;&gt;商用利用無料！Bootstrap 3, Bootstrap 4をベースに最近のUIデザインのトレンドを取り入れた新作テーマのまとめ | コリス&lt;/a&gt;という記事にHonokaと&lt;a href=&quot;https://nkmr6194.github.io/Umi/&quot;&gt;Umi&lt;/a&gt;は掲載されたのですが、Nicoが掲載されなかったことです。最初は自分で使うために作ったとはいえ、せっかく作ったし他の人にも使ってほしいと思った自分にとって、ショックを受けた出来事でした。&lt;/p&gt;
&lt;p&gt;この時にはHonokaのサイトからNicoがリンクされていたので見てないということはたぶん無いはずですが、それでも掲載されなかった理由を考えて、配色的に使いづらさがあったという結論に至りました。&lt;/p&gt;
&lt;p&gt;特に&lt;code&gt;info&lt;/code&gt;の色が強すぎること、また&lt;code&gt;warning&lt;/code&gt;と&lt;code&gt;danger&lt;/code&gt;の色が元のBootstrapだったり、Honokaとかけ離れているのが原因でしょう。そのため、&lt;code&gt;v3.3.5-1.0.0&lt;/code&gt;より次のような配色にしました。基本Honokaを踏襲しつつ、モノクロで見たときに&lt;code&gt;primary&lt;/code&gt;より&lt;code&gt;success&lt;/code&gt;の色が強くなるよう調整しました。また&lt;code&gt;info&lt;/code&gt; &gt; &lt;code&gt;warning&lt;/code&gt; &gt; &lt;code&gt;danger&lt;/code&gt;の順に色が強くなっています。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名前&lt;/th&gt;
&lt;th&gt;カラーコード&lt;/th&gt;
&lt;th&gt;色&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#f3d4df&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #f3d4df;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inverse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff50ac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ff50ac;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;primary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff64b1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ff64b1;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;success&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#0faf20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #0faf20;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;info&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#a27dac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #a27dac;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warning&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#ff7302&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #ff7302;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;danger&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#f45042&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;div style=&quot;width: 1em; height: 1em; background-color: #f45042;&quot;&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;なお&lt;code&gt;default&lt;/code&gt;と&lt;code&gt;inverse&lt;/code&gt;も色を変えれば使いやすくなりそうですが、これを変えてしまうとNicoがNicoで無くなってしまうため、変える予定は今のところありません。&lt;/p&gt;
&lt;h2 id=&quot;これから&quot;&gt;これから&lt;/h2&gt;
&lt;p&gt;というわけで必要に迫られて作り、いろいろと更新したNicoですが、これからの展望としてはHonokaの変更にできる限り付いていきたいです。なんだかんだでBootstrapは便利ですし、しばらく廃れないはずです。このNicoやHonokaを通じてBootstrapがどのように進化していくのかを見ていきます。&lt;/p&gt;</content>
<published>2015-12-08T00:00:00.000Z</published>
<updated>2025-05-18T12:18:00.000Z</updated>
</entry>
<entry>
<title>なぜ CSS や Grunt はつらいと言われるのか</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/why-css-and-grunt-is-hard"/>
<id>tag:blog.kubosho.com,2015-11-12:entry:why-css-and-grunt-is-hard</id>
<summary>少し主語を大きく書いてしまいましたが、\*.css と gruntfile.js についての話です。</summary>
<content type="html">&lt;p&gt;少し主語を大きく書いてしまいましたが、&lt;code&gt;*.css&lt;/code&gt; と &lt;code&gt;gruntfile.js&lt;/code&gt; についての話です。&lt;/p&gt;
&lt;p&gt;「&lt;a href=&quot;http://0-9.sakura.ne.jp/pub/kbkz_tech/start.html&quot;&gt;CSS に死を！&lt;/a&gt;」と言われたり、「&lt;a href=&quot;http://mizchi.hatenablog.com/entry/2014/12/28/160715&quot;&gt;CSS 勉強するのはだるい&lt;/a&gt;」と言われたりしますが、これらを見ていると「Gruntがつらい」という話と共通項があると感じました。&lt;/p&gt;
&lt;h2 id=&quot;どちらも設定ファイルである&quot;&gt;どちらも設定ファイルである&lt;/h2&gt;
&lt;p&gt;自分の考えとして前提を書くと「&lt;code&gt;*.css&lt;/code&gt; と &lt;code&gt;gruntfile.js&lt;/code&gt; はそれぞれ「どのような見た目にするか」「どのようなタスクを実行するか」を定義するための設定ファイルである」という考えです。&lt;/p&gt;
&lt;p&gt;その前提のもと、設定ファイルであるという利点と欠点をこれから書いていきます。&lt;/p&gt;
&lt;h3 id=&quot;利点&quot;&gt;利点&lt;/h3&gt;
&lt;h4 id=&quot;少ない行数の時の見やすさ&quot;&gt;少ない行数の時の見やすさ&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;*.css&lt;/code&gt; や &lt;code&gt;*gruntfile.js&lt;/code&gt; は200-250行くらいまでなら、ソースコードの全体が少ない時間で把握でき、何をしているものなのかすぐに分かります。&lt;/p&gt;
&lt;h4 id=&quot;敷居の低さ&quot;&gt;敷居の低さ&lt;/h4&gt;
&lt;p&gt;CSS・Gruntともに、ブログやQiitaなどに上がっているソースコードをコピペしてくれば、なんとなくそれっぽい見た目になったり、タスクが実行できるようになります。&lt;/p&gt;
&lt;h3 id=&quot;欠点&quot;&gt;欠点&lt;/h3&gt;
&lt;h4 id=&quot;破綻しやすい&quot;&gt;破綻しやすい&lt;/h4&gt;
&lt;p&gt;CSSはすぐに、無計画にルールセットを増やしたことによるファイルサイズの肥大化、スタイルの思いもよらぬ上書き（それに伴う !important地獄）、必要ないルールセットの放置などが発生します。&lt;/p&gt;
&lt;p&gt;Gruntも、無計画にタスクを増やしたことによるファイルサイズの肥大化、必要ないタスクの放置が発生しやすいと感じます。&lt;/p&gt;
&lt;p&gt;ファイルサイズの肥大化については、CSS・Gruntともにファイルを分割すれば良いという話ですが、CSSは @importを使うとCSSファイルの読み込みが並列でおこなわれないため、全てのCSSファイルを読み込むための時間がかかってしまう問題を抱えています。&lt;/p&gt;
&lt;p&gt;また、GruntもloadTasks() を使えば別ファイルに分割したタスクを読み込むことはできます。しかし、定義されているタスクの一覧性が悪くなるということと、読み込んだタスクが何をやっているのかを見るのが若干面倒という問題を抱えています。&lt;/p&gt;
&lt;h4 id=&quot;手法が複数ある&quot;&gt;手法が複数ある&lt;/h4&gt;
&lt;p&gt;CSSはMindBEMdingだったり、SUIT CSSだったり、セレクタの設計手法が複数あります。&lt;/p&gt;
&lt;p&gt;また、画像置換の手法についても、昔ながらの &lt;code&gt;text-indent: -9999px;&lt;/code&gt; とする手法から、&lt;a href=&quot;http://www.zeldman.com/2012/03/01/replacing-the-9999px-hack-new-image-replacement/&quot;&gt;&lt;code&gt;text-indent&lt;/code&gt; &lt;code&gt;white-space&lt;/code&gt; &lt;code&gt;overflow&lt;/code&gt; を組み合わせる手法&lt;/a&gt;、&lt;a href=&quot;http://nicolasgallagher.com/css-image-replacement-with-pseudo-elements/&quot;&gt;擬似要素を使う手法&lt;/a&gt;など複数あります。&lt;/p&gt;
&lt;p&gt;Gruntも例えばSassをCSSにコンパイルしたいといった時に、&lt;a href=&quot;https://github.com/gruntjs/grunt-contrib-sass&quot;&gt;gruntjs/grunt-contrib-sass&lt;/a&gt; を使えば良いのか、&lt;a href=&quot;https://github.com/sindresorhus/grunt-sass&quot;&gt;sindresorhus/grunt-sass&lt;/a&gt; を使えば良いのか、分からなくなります。&lt;/p&gt;
&lt;p&gt;複数ある手法から最適解を求めたいところですが、そこに至るまでの時間が長くなりがちで、それがだるさに繋がります。&lt;/p&gt;
&lt;h2 id=&quot;css-が-gulp-から学べる点はなんなのか&quot;&gt;CSS が gulp から学べる点はなんなのか？&lt;/h2&gt;
&lt;p&gt;と、書いてみたものの、特に思い浮かばないので、ここは任せました。&lt;/p&gt;</content>
<published>2015-11-12T00:00:00.000Z</published>
<updated>2022-06-21T06:22:54.673Z</updated>
</entry>
<entry>
<title>fs.exists()がdeprecatedになった理由</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/why-deprecated-of-fs-exists"/>
<id>tag:blog.kubosho.com,2015-09-20:entry:why-deprecated-of-fs-exists</id>
<summary>wearefractal/vinyl-fs の dest(folder, \[opt]) が出力先のディレクトリが無い場合でも、そのディレクトリを作ってくれないということで、自分で「ディレクトリの有無を確認して、無い場合はディレクトリを作る」という処理を作る必要がでてきました。</summary>
<content type="html">&lt;p&gt;&lt;a href=&quot;https://github.com/wearefractal/vinyl-fs&quot;&gt;wearefractal/vinyl-fs&lt;/a&gt; の &lt;code&gt;dest(folder, [opt])&lt;/code&gt; が出力先のディレクトリが無い場合でも、そのディレクトリを作ってくれないということで、自分で「ディレクトリの有無を確認して、無い場合はディレクトリを作る」という処理を作る必要がでてきました。&lt;/p&gt;
&lt;p&gt;そこで &lt;a href=&quot;https://nodejs.org/api/fs.html&quot;&gt;File System Node.js v4.1.0 Manual &amp;#x26; Documentation&lt;/a&gt; を見て、ディレクトリの有無を確認するのに使えそうな &lt;a href=&quot;https://nodejs.org/api/fs.html#fs_fs_exists_path_callback&quot;&gt;fs.exists()&lt;/a&gt; というAPIを見つけたのですが、「Deprecated: Use fs.stat or fs.access instead.」ということで、他のAPIを使うように書かれていました。&lt;/p&gt;
&lt;p&gt;「ファイルの有無を確認するAPIがdeprecatedになるのはなぜ？」と疑問に思い、調べた結果をまとめます。&lt;/p&gt;
&lt;h2 id=&quot;本題&quot;&gt;本題&lt;/h2&gt;
&lt;p&gt;まず &lt;code&gt;fs.exists()&lt;/code&gt; ですが、ドキュメントを見ると下記のようなコードが載っています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;fs.exists(&#39;/etc/passwd&#39;, function (exists) {
  console.log(exists ? &quot;it&#39;s there&quot; : &#39;no passwd!&#39;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;この中のcallback関数を見ると、引数として、与えられたパスが存在しているかどうかを示す真偽値が入っていますが、この引数の形式が他のFile SystemのAPIのように &lt;code&gt;err&lt;/code&gt; が第一引数になっていないため、規則に沿っていないということで、&lt;a href=&quot;https://github.com/nodejs/node-v0.x-archive/issues/8369#issuecomment-55559828&quot;&gt;fs.exists does not follow node conventions · Issue #8369 · nodejs/node-v0.x-archive&lt;/a&gt; というissueが作られています。&lt;/p&gt;
&lt;p&gt;しかし、2つ目のコメントで事態が変化しています。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nodejs/node-v0.x-archive/issues/8369#issuecomment-55559828&quot;&gt;fs.exists does not follow node conventions · Issue #8369 · nodejs/node-v0.x-archive&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;この2つ目のコメントについてはid:yosuke_furukawaの投稿を見たほうがどういうことか分かりやすいです。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;ja&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;&lt;a href=&quot;https://twitter.com/kubosho_&quot;&gt;@kubosho_&lt;/a&gt; exists はrace condition に沿わないのでdeprecatedですね。存在チェックした後で消されたらどうするのっていう。普通に読み書きしたエラーでいいはずだし、どうしてもチェックしたかったらaccessかstatを使うというのが通例ですね。&lt;/p&gt;&amp;mdash; Yosuke FURUKAWA (@yosuke_furukawa) &lt;a href=&quot;https://twitter.com/yosuke_furukawa/status/645260394592759808&quot;&gt;2015, 9月 19&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;この投稿の「存在チェックした後で消されたらどうするの」というのを処理の流れに落としこむと、&lt;code&gt;fs.exists()&lt;/code&gt; で与えられたパスが存在しているか確認し、&lt;code&gt;fs.exists()&lt;/code&gt; のcallback関数内で &lt;code&gt;fs.unlinkSync(path)&lt;/code&gt; としてファイルを削除した後に、callback関数の &lt;code&gt;exists&lt;/code&gt; 引数を使って &lt;code&gt;exists&lt;/code&gt; がtrueだったら、ファイルを開くというソースコードを書いて実行すると &lt;code&gt;no such file or directory&lt;/code&gt; のエラーが出力されます。&lt;/p&gt;
&lt;p&gt;実際のソースコードは下記の通りとなります。これなら &lt;code&gt;fs.open(path, &quot;r&quot;, callback)&lt;/code&gt; として、ファイルの有無で処理を分けたほうが入れ子を浅くできます。&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/kubosho/c19c2267bf4715ba80d2.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;話を「Nodeの規則に沿っていないというissueが作られた」というところに戻すと、issueでやりとりがあった後、&lt;a href=&quot;https://github.com/nodejs/node-v0.x-archive/pull/8418&quot;&gt;fs: deprecate exists() and existsSync() by cjihrig · Pull Request #8418 · nodejs/node-v0.x-archive&lt;/a&gt; というPull Requestが作られます。&lt;/p&gt;
&lt;p&gt;このPull Requestは元のタイトルが「fs: add errback style support to exists()」とあるように、&lt;a href=&quot;https://github.com/cjihrig/node/commit/aeb381ccf6f72546e4ad1a3615d29f52f49dacf4&quot;&gt;互換性を保ちつつ、callback 関数の第一引数に error が入るようにしたもの&lt;/a&gt;がコミットされていました。&lt;/p&gt;
&lt;p&gt;Pull Request内でコードレビューがおこなわれ、&lt;a href=&quot;http://jsperf.com/function-length-performance/8&quot;&gt;Function.length のパフォーマンスが悪い&lt;/a&gt;という指摘があったりしたのですが、それらの指摘を踏まえてPull Requestをopenしたcjihrig氏は&lt;a href=&quot;https://github.com/nodejs/node-v0.x-archive/pull/8418#discussion_r17825801&quot;&gt;いくつか提案をしています&lt;/a&gt;（何もしない、互換性がある、または壊す形で第一引数の &lt;code&gt;err&lt;/code&gt; をサポートする)。&lt;/p&gt;
&lt;p&gt;しかし、再び事態が変わります。&lt;a href=&quot;https://github.com/nodejs/node-v0.x-archive/pull/8418#discussion_r17825997&quot;&gt;&lt;code&gt;fs.exists()&lt;/code&gt; を deprecate にして代わりに &lt;code&gt;fs.access()&lt;/code&gt; を使うようにする&lt;/a&gt;というコメントがされました。&lt;/p&gt;
&lt;p&gt;そして、そのコメントが流れを変えて、&lt;code&gt;fs.exists()&lt;/code&gt; は第一引数の &lt;code&gt;err&lt;/code&gt; がサポートされることなくdeprecatedになりました。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;ja&quot;&gt;&lt;p lang=&quot;ja&quot; dir=&quot;ltr&quot;&gt;&lt;a href=&quot;https://twitter.com/kubosho_&quot;&gt;@kubosho_&lt;/a&gt; そうですね。その例の他にもNodeと無関係な全く別なプロセスからファイルが削除される事もあるから、exists =&amp;gt; read みたいな事やろうとするのって筋悪なんですよね。突き詰めていくとexists自体の必要性が怪しくて、deprecatedっていう。&lt;/p&gt;&amp;mdash; Yosuke FURUKAWA (@yosuke_furukawa) &lt;a href=&quot;https://twitter.com/yosuke_furukawa/status/645276523625234433&quot;&gt;2015, 9月 19&lt;/a&gt;&lt;/blockquote&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;「ディレクトリの有無を確認して、無い場合はディレクトリを作る」という処理を作るために &lt;code&gt;fs.exists()&lt;/code&gt; を使おうとしたらdeprecatedになっていて、なぜだろうと思い調べたら、&lt;code&gt;fs.exists()&lt;/code&gt; がdeprecatedになった経緯が分かった話でした。結果として「ディレクトリの有無を確認して、無い場合はディレクトリを作る」という処理は「ディレクトリを作るように試み、既に存在したら（エラーになったら）作らない」という処理になりました。&lt;/p&gt;
&lt;p&gt;具体的なソースコードとしては、最初は &lt;code&gt;fs.exists()&lt;/code&gt; を使っていたため、下記のようなソースコードでした。&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/kubosho/d4052651a1c8b8153a5b.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;これを「ディレクトリを作るように試み、既に存在したら（エラーになったら）作らない」という考え方で書き換えて下記のようにしました。&lt;/p&gt;
&lt;script src=&quot;https://gist.github.com/kubosho/d26ef7da6c399c318365.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;結果的に、&lt;code&gt;fs.exists()&lt;/code&gt; というdeprecatedになったAPIを使う必要がなくなり、また入れ子は浅く、ソースコードも短くなりました。&lt;/p&gt;</content>
<published>2015-09-20T00:00:00.000Z</published>
<updated>2022-11-28T15:34:34.081Z</updated>
</entry>
<entry>
<title>MindBEMding の Modifier は元のクラス名と一緒に指定するべきか否か</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/extend-base-ruleset-vs-define-base-class-and-modifier-class-in-html"/>
<id>tag:blog.kubosho.com,2015-05-11:entry:extend-base-ruleset-vs-define-base-class-and-modifier-class-in-html</id>
<summary>MindBEMdingのModifierを定義する際、単一のクラスセレクタで書く方法と、複数のクラスセレクタで書く方法があります。</summary>
<content type="html">&lt;p&gt;MindBEMdingのModifierを定義する際、単一のクラスセレクタで書く方法と、複数のクラスセレクタで書く方法があります。&lt;/p&gt;
&lt;p&gt;実際のHTMLで示すと以下のような感じです。&lt;/p&gt;
&lt;h2 id=&quot;htmlの複数クラス定義&quot;&gt;HTMLの複数クラス定義&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;table class=&quot;mod-table-01 mod-table-01--line-color-green&quot;&gt;
  &amp;#x3C;tr&gt;
    &amp;#x3C;th&gt;foo&amp;#x3C;/th&gt;
    &amp;#x3C;th&gt;bar&amp;#x3C;/th&gt;
  &amp;#x3C;/tr&gt;
  &amp;#x3C;tr&gt;
    &amp;#x3C;td&gt;baz&amp;#x3C;/td&gt;
    &amp;#x3C;td&gt;foobar&amp;#x3C;/td&gt;
  &amp;#x3C;/tr&gt;
&amp;#x3C;/table&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;htmlの単一クラス定義&quot;&gt;HTMLの単一クラス定義&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;table class=&quot;mod-table-02--line-color-green&quot;&gt;
  &amp;#x3C;tr&gt;
    &amp;#x3C;th&gt;foo&amp;#x3C;/th&gt;
    &amp;#x3C;th&gt;bar&amp;#x3C;/th&gt;
  &amp;#x3C;/tr&gt;
  &amp;#x3C;tr&gt;
    &amp;#x3C;td&gt;baz&amp;#x3C;/td&gt;
    &amp;#x3C;td&gt;foobar&amp;#x3C;/td&gt;
  &amp;#x3C;/tr&gt;
&amp;#x3C;/table&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;multiple class&lt;/code&gt; とコメントで書かれているほうでは、クラス名は &lt;code&gt;mod-table-01&lt;/code&gt; と &lt;code&gt;mod-table-01--line-color-green&lt;/code&gt; の2つが指定されています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;single class&lt;/code&gt; とコメントで書かれているほうでは、クラス名は &lt;code&gt;mod-table-02--line-color-green&lt;/code&gt; のみ指定されています。&lt;/p&gt;
&lt;p&gt;上記のHTMLに対し、CSSでルールセットを書く場合は以下のようになります。&lt;/p&gt;
&lt;h2 id=&quot;cssの複数クラス定義&quot;&gt;CSSの複数クラス定義&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.mod-table-01 {
  border: 1px solid black;
  border-collapse: collapse;
  margin: 10px auto;
}
.mod-table-01--line-color-green {
  border: 1px solid green;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;cssの単一クラス定義&quot;&gt;CSSの単一クラス定義&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.mod-table-02,
.mod-table-02--line-color-green {
  border: 1px solid black;
  border-collapse: collapse;
  margin: 10px auto;
}
.mod-table-02--line-color-green {
  border: 1px solid green;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;この2つの指定方法のうち、果たしてどちらが良いのかというのを考えたので、以下に利点と欠点を書いていきます。&lt;/p&gt;
&lt;h2 id=&quot;htmlに単一のクラス名を指定する際の利点欠点&quot;&gt;HTMLに単一のクラス名を指定する際の利点・欠点&lt;/h2&gt;
&lt;h3 id=&quot;利点複数のクラスを指定するより若干簡潔になる&quot;&gt;利点：複数のクラスを指定するより、若干簡潔になる&lt;/h3&gt;
&lt;p&gt;MindBEMdingの考えでセレクタに名前を付けた場合は名前が長くなりがちです。複数のクラス名を指定した場合は、上記で示したHTMLの例を見ると分かるように冗長感が否めません。&lt;/p&gt;
&lt;p&gt;その一方、単一のクラス名で指定した場合は、HTML側の指定は複数のクラス名指定より、簡潔になります。&lt;/p&gt;
&lt;h3 id=&quot;欠点スタイル定義が複雑になる&quot;&gt;欠点：スタイル定義が複雑になる&lt;/h3&gt;
&lt;p&gt;下記のJSFiddleの例を見ると分かるのですが、単一のクラス名を指定するほうが、複数のクラス名を指定するより、スタイル定義が複雑になっています。&lt;/p&gt;
&lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/aempn99y/2/embedded/result,html,css&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&gt;&lt;/iframe&gt;
&lt;p&gt;これは例えば、後からプロジェクトに入ってきた人がCSSファイルで定義されている内容を見た時に、理解を妨げる原因になるのではという考えがあります。&lt;/p&gt;
&lt;h3 id=&quot;欠点css-のファイルサイズが大きくなりがち&quot;&gt;欠点：CSS のファイルサイズが大きくなりがち&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://www.luft.co.jp/cgi/str_counter.php&quot;&gt;文字数 カウント（文字バイト数チェック）&lt;/a&gt;で測ると分かるのですが、単一のクラス名をHTML側で指定してスタイル定義をしていく場合、複数のクラス名をHTML側で指定するより、CSSのファイルサイズが大きくなります。&lt;/p&gt;
&lt;h2 id=&quot;htmlに複数のクラス名を指定する際の利点&quot;&gt;HTMLに複数のクラス名を指定する際の利点&lt;/h2&gt;
&lt;h3 id=&quot;利点スタイル定義の見通しが良くなる&quot;&gt;利点：スタイル定義の見通しが良くなる&lt;/h3&gt;
&lt;p&gt;再び先ほどのJSFiddleの例を引っ張り出しますが、複数のクラス名を指定する場合、単一のクラス名を指定するよりCSSのルールセット定義が簡潔になっています。&lt;/p&gt;
&lt;iframe width=&quot;100%&quot; height=&quot;300&quot; src=&quot;//jsfiddle.net/aempn99y/2/embedded/result,html,css&quot; allowfullscreen=&quot;allowfullscreen&quot; frameborder=&quot;0&quot;&gt;&lt;/iframe&gt;
&lt;h3 id=&quot;欠点html上でのクラス名の指定が単一のクラスを指定するより若干冗長になる&quot;&gt;欠点：HTML上でのクラス名の指定が単一のクラスを指定するより若干冗長になる&lt;/h3&gt;
&lt;p&gt;単一のクラス名を定義する時はHTMLでは &lt;code&gt;prefix-table-01--line-color-green&lt;/code&gt; とだけclass属性の値に書けばよかったですが、複数のクラスを定義する場合は &lt;code&gt;prefix-table-01 prefix-table-01--line-color-green&lt;/code&gt; と若干冗長になります。&lt;/p&gt;
&lt;h2 id=&quot;結果どちらを使ったほうが良いのか&quot;&gt;結果どちらを使ったほうが良いのか&lt;/h2&gt;
&lt;p&gt;単一のクラス名を指定するほうはHTML側が少し簡潔になりますが、複数のクラス名を指定するのと比べCSS側が複雑になりがちというのは先ほど書いたとおりです。&lt;/p&gt;
&lt;p&gt;その複雑になりがちというのが自分ではあまり許容できなかったので、複数のクラス名をHTMLのclass属性の値として指定して、スタイル定義をしていくのが良いかなと思いました。&lt;/p&gt;
&lt;h2 id=&quot;他の参考記事&quot;&gt;他の参考記事&lt;/h2&gt;
&lt;p&gt;マルチクラス設計について物申している記事もあるので、よかったらこちらも参考にしてみてください。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://morishitter.hatenablog.com/entry/2014/11/28/004234&quot;&gt;CSSプリプロセッサーの&lt;code&gt;extend&lt;/code&gt;の悪いところ - morishitter blog&lt;/a&gt;&lt;/p&gt;</content>
<published>2015-05-11T00:00:00.000Z</published>
<updated>2022-06-21T06:22:12.830Z</updated>
</entry>
<entry>
<title>GitHub の Contributions を Ingress の Resistance 色にする Chrome 拡張を作った</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/change-ingress-registance-color-of-github-contributions"/>
<id>tag:blog.kubosho.com,2015-04-06:entry:change-ingress-registance-color-of-github-contributions</id>
<summary>GitHubのContributionsを眺めていて「これってEnlightenedの色だよね」と思ったので、Resistanceの色に置き換えるようなChrome拡張を作ってみました。</summary>
<content type="html">&lt;p&gt;GitHubのContributionsを眺めていて「これってEnlightenedの色だよね」と思ったので、Resistanceの色に置き換えるようなChrome拡張を作ってみました。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/resistance-contributions/codbppehieinnfafggcefkipfajcapep&quot;&gt;Resistance Contributions - Chrome ウェブストア&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ソースはこちらにあります。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubosho/resistance-contributions&quot;&gt;kubosho/resistance-contributions&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ちなみに自分はEnlightenedです。&lt;/p&gt;</content>
<published>2015-04-06T00:00:00.000Z</published>
<updated>2022-11-28T15:38:52.335Z</updated>
</entry>
<entry>
<title>編集しやすいCSSにするために自分が心がけている事</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/mindful-of-coding-css"/>
<id>tag:blog.kubosho.com,2015-02-23:entry:mindful-of-coding-css</id>
<summary>CSS は、書くのが怖いと自分は感じます。</summary>
<content type="html">&lt;p&gt;CSSは、書くのが怖いと自分は感じます。&lt;/p&gt;
&lt;p&gt;ある程度運用したサービスやアプリとなると、自分の変更した点が思いもよらぬところに反映されてしまい「しまった」と思った事はあることでしょう。&lt;/p&gt;
&lt;p&gt;とはいえ、運用していく上で何も変更しないというのはありえません。CSSを変更する恐怖からなるべく解放されるために自分が心がけている事を書いていきます。&lt;/p&gt;
&lt;h2 id=&quot;コメントを書く&quot;&gt;コメントを書く&lt;/h2&gt;
&lt;p&gt;自分の場合は、そのセレクタがどこで使われているかという情報を書く事が非常に多いです。例としては下記のような感じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sass&quot;&gt;//------------------------------------------------------------------------
//    Common: サイト上部のヘッダー
//------------------------------------------------------------------------
.prefix-global-header {
  background: #000;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このようにする事で区切りが分かりやすくなると思っています。また、ルールセットがどう書かれているかが、より頭の中に入ってくると自分は感じています。&lt;/p&gt;
&lt;p&gt;ちなみに上記の区切りは、SassやLESSでしか使えない（コメントアウトに &lt;code&gt;//&lt;/code&gt; を使っているため）ので、CSSでも区切りを付けたい場合は、&lt;a href=&quot;https://github.com/csswizardry/inuit.css&quot;&gt;inuit.css&lt;/a&gt;の下記の区切りが良いなと自分は感じています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;/*------------------------------------*\
    はうはう
\*------------------------------------*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;class-名や-id-名を意味のある名前にする&quot;&gt;class 名や id 名を意味のある名前にする&lt;/h2&gt;
&lt;p&gt;極端な例ですが、下記のように「要素名をそのまま繰り返しただけのclass名」というのは意味が無いです。下記の例では、いっそ要素型セレクタでスタイル指定したほうがいいでしょう。&lt;/p&gt;
&lt;p&gt;&lt;a class=&quot;jsbin-embed&quot; href=&quot;http://jsbin.com/vibejoqaka/4/embed?html,css,output&quot;&gt;JS Bin&lt;/a&gt;&lt;script src=&quot;http://static.jsbin.com/js/embed.js&quot;&gt;&lt;/script&gt;&lt;/p&gt;
&lt;p&gt;では、自分はどのように名付けるかという話ですが、今は&lt;a href=&quot;http://blog.kubosho.com/entry/2014/12/09/valhalla-gate-css-architecture&quot;&gt;神獄のヴァルハラゲートの CSS 設計&lt;/a&gt;でも書いたように、MindBEMdingを使った命名にするかなという感じです。&lt;/p&gt;
&lt;p&gt;例えば &lt;code&gt;.header&lt;/code&gt; としているところなら、&lt;code&gt;.prefix-global-header&lt;/code&gt; か &lt;code&gt;.prefix-top-header&lt;/code&gt; といった命名をします。&lt;/p&gt;
&lt;h2 id=&quot;important-を使う時は注意深く使う&quot;&gt;&lt;code&gt;!important&lt;/code&gt; を使う時は注意深く使う&lt;/h2&gt;
&lt;p&gt;CSSを書いていく上で、どうしても &lt;code&gt;!important&lt;/code&gt; を使いたくなる時があります。そんな時は &lt;code&gt;!important&lt;/code&gt; を使わないようにCSSを書き直すよりも &lt;code&gt;!important&lt;/code&gt; を使ったほうが、時間がかからなかったり複雑にならない可能性が高いと感じています。&lt;/p&gt;
&lt;p&gt;とはいえ &lt;code&gt;!important&lt;/code&gt; を乱用してはいけません。改修に時間がかからなそうだったり複雑にならなそうであれば、使わないほうが良いです。&lt;/p&gt;
&lt;p&gt;なお自分が &lt;code&gt;!important&lt;/code&gt; を使う時は、なぜ &lt;code&gt;!important&lt;/code&gt; を使うのかというコメントを絶対書いています。それは下記の理由です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;意味が書いてない &lt;code&gt;!important&lt;/code&gt; は消すのが怖いため
&lt;ul&gt;
&lt;li&gt;消したらどこかに悪影響が出るのではという恐怖が生まれる&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;将来CSSを書き直した時に消せる可能性があるため
&lt;ul&gt;
&lt;li&gt;上記でも書いたように、なるべく使わないようにしたい&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;style-属性を使う時は注意深く使う&quot;&gt;style 属性を使う時は注意深く使う&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;!important&lt;/code&gt; と同じく、乱用すべきものではないですが、かといって、ある機能に関するページが10ページくらいあったとして、そのうち1ページでしか使わない（かつスタイルを適用させたいところも少ない）となると、CSSファイル内でスタイル指定をするよりも、style属性でスタイル指定をしたほうが、時間がかからなかったり複雑にならない可能性が高いと感じています。&lt;/p&gt;
&lt;p&gt;それ以外では、style属性を使わないほうが良いです。&lt;/p&gt;
&lt;h2 id=&quot;最後に&quot;&gt;最後に&lt;/h2&gt;
&lt;p&gt;ここまで、CSSを書く際に心がけている事を書いてきましたが、こういう心がけは下記の思いがあるためです。&lt;/p&gt;
&lt;!-- textlint-disable ja-no-weak-phrase, no-doubled-joshi --&gt;
&lt;p&gt;&lt;a href=&quot;https://twitter.com/kubosho_/status/569730148536164352&quot;&gt;ドキュメント残しておかないと、身軽になれないし、あとは気をつけていても病気とか事故にあって長期離脱という可能性は 0 ではないので、そうなった時に引き継いだ人が積むし、自分への恨みが起こると思うので、ドキュメントは出来るだけ残しておきたいと自分は考えている。&lt;/a&gt;&lt;/p&gt;
&lt;!-- textlint-enable ja-no-weak-phrase, no-doubled-joshi --&gt;</content>
<published>2015-02-23T00:00:00.000Z</published>
<updated>2022-11-28T15:23:50.811Z</updated>
</entry>
<entry>
<title>background-imageは仕様上アニメーションを適用できない</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/background-image-is-not-animatable"/>
<id>tag:blog.kubosho.com,2015-01-15:entry:background-image-is-not-animatable</id>
<summary>CSSで @keyframes に background-image プロパティを指定してクロスフェード効果で背景画像の表示が切り替わるという動作を実装しようとしました。</summary>
<content type="html">&lt;p&gt;CSSで &lt;code&gt;@keyframes&lt;/code&gt; に &lt;code&gt;background-image&lt;/code&gt; プロパティを指定してクロスフェード効果で背景画像の表示が切り替わるという動作を実装しようとしました。&lt;/p&gt;
&lt;p&gt;実際にChromeでは期待通りの動作をしました。ただFirefoxとAndroid Browserではアニメーションどころか画像自体が表示されずなぜそうなるのか謎でした。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://jsfiddle.net/csmt3fyn/3/&quot;&gt;クロスフェードされないコードを確認するためのJSFiddle&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;そもそもbackground-imageはアニメーション不可能&quot;&gt;そもそもbackground-imageはアニメーション不可能&lt;/h2&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;ja&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://twitter.com/o_ti&quot;&gt;@o_ti&lt;/a&gt; &lt;a href=&quot;https://twitter.com/kubosho_&quot;&gt;@kubosho_&lt;/a&gt; background-image はアニメーション不可能なプロパティでだからではないですか？&lt;/p&gt;&amp;mdash; xl1blue (@xl1blue) &lt;a href=&quot;https://twitter.com/xl1blue/status/555695483037822977&quot;&gt;2015, 1月 15&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;！？&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot; lang=&quot;ja&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://twitter.com/kubosho_&quot;&gt;@kubosho_&lt;/a&gt; &lt;a href=&quot;https://twitter.com/xl1blue&quot;&gt;@xl1blue&lt;/a&gt; ホントだ。Animatable: no だった。&lt;a href=&quot;http://t.co/JopFQtOO5F&quot;&gt;http://t.co/JopFQtOO5F&lt;/a&gt;&lt;/p&gt;&amp;mdash; 越智 (@o_ti) &lt;a href=&quot;https://twitter.com/o_ti/status/555696632675594240&quot;&gt;2015, 1月 15&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;なん……だと……？&lt;/p&gt;
&lt;p&gt;background-imageは仕様上アニメーション不可能なプロパティですが、Chromeが仕様に準拠していませんでした。&lt;/p&gt;
&lt;p&gt;なので仕様に沿う形で書き直しました。&lt;/p&gt;
&lt;h2 id=&quot;各ブラウザーでクロスフェードされるように書き直したコード&quot;&gt;各ブラウザーでクロスフェードされるように書き直したコード&lt;/h2&gt;
&lt;h3 id=&quot;html&quot;&gt;HTML&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;test-01&quot;&gt;
  &amp;#x3C;img src=&quot;https://picsum.photos/id/0/400/200&quot; alt=&quot;&quot; /&gt;
  &amp;#x3C;img src=&quot;https://picsum.photos/id/1/400/200&quot; alt=&quot;&quot; /&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;css&quot;&gt;CSS&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.test-01 {
  position: relative;
}

.test-01 img:first-child {
  position: absolute;
  animation: crossfade 5s linear 0s infinite alternate;
}

@keyframes crossfade {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://jsfiddle.net/ts2qu35b/20/&quot;&gt;クロスフェードされるコードを確認するためのJSFiddle&lt;/a&gt;&lt;/p&gt;</content>
<published>2015-01-15T00:00:00.000Z</published>
<updated>2022-12-01T05:47:05.247Z</updated>
</entry>
<entry>
<title>神獄のヴァルハラゲートの CSS 設計</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/valhalla-gate-css-architecture"/>
<id>tag:blog.kubosho.com,2014-12-09:entry:valhalla-gate-css-architecture</id>
<summary>CSS Architecture Advent Calendar 2014 9 日目の記事になります。</summary>
<content type="html">&lt;p&gt;&lt;a href=&quot;http://www.adventar.org/calendars/337&quot;&gt;CSS Architecture Advent Calendar 2014&lt;/a&gt; 9日目の記事になります。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://grani.jp/product/valhalla-gate/&quot;&gt;神獄のヴァルハラゲート&lt;/a&gt;のCSS設計方法について振り返りつつ、こうしているということや、上手くいったところ、改善したいところを書きます。&lt;/p&gt;
&lt;h2 id=&quot;アプリの規模&quot;&gt;アプリの規模&lt;/h2&gt;
&lt;p&gt;ASP.NET MVCを使っていて、View側はRazorテンプレートを使っているのですが、Razorのファイルを検索してみると以下の量となります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;find ./ -name &quot;*.cshtml&quot; | wc -l
    1674
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;実際には、これにフィーチャーフォン向けのファイルや、部分的なViewファイルも含まれています。&lt;/p&gt;
&lt;p&gt;なので、実際CSSを適用しているファイルの量は、この検索結果よりは格段に減りますが、それでも約600ファイルはあります。&lt;/p&gt;
&lt;p&gt;ちなみに、今年のはじめに同僚の&lt;a href=&quot;https://twitter.com/mayuki&quot;&gt;@mayuki&lt;/a&gt;が、&lt;a href=&quot;https://speakerdeck.com/mayuki/sosiyarugemufalsehurontoendotoasp-dot-net-mvc-lite&quot;&gt;ソーシャルゲームのフロントエンドと ASP.NET MVC (Lite)&lt;/a&gt;というタイトルで、&lt;a href=&quot;http://partake.in/events/a531c0be-e8dd-46fe-a73c-e51d8ad7a69b&quot;&gt;めとべや東京#3 (Room metro Tokyo #3)&lt;/a&gt;にて発表したのですが、そこには「View数は400超」と書いてあるので、1年間の運用で約200ファイル増えています。&lt;/p&gt;
&lt;h2 id=&quot;対応端末ブラウザー&quot;&gt;対応端末、ブラウザー&lt;/h2&gt;
&lt;p&gt;スマートフォンでは、以下の環境でアクセスされることを想定して対応しています。&lt;/p&gt;
&lt;h3 id=&quot;os&quot;&gt;OS&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;iOS 6 or later&lt;/li&gt;
&lt;li&gt;Android 2.3.x
&lt;ul&gt;
&lt;li&gt;重要な機能だったら、Android 2.2.xにも対応することも…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Android 4.0 or later&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;ブラウザー&quot;&gt;ブラウザー&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;iOS
&lt;ul&gt;
&lt;li&gt;Safari&lt;/li&gt;
&lt;li&gt;WebView&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Android
&lt;ul&gt;
&lt;li&gt;Android Browser&lt;/li&gt;
&lt;li&gt;Google Chrome&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/uupaa/b25c9cf47bbeedea5a7f&quot;&gt;S Browser&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;iOS 6やAndroid 2系はそろそろ対応端末から外したいなと思っています。来年の今頃は対応しなくて良くなってたらいいですね。&lt;/p&gt;
&lt;h2 id=&quot;命名規則&quot;&gt;命名規則&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.bem.info/method/naming-convention/&quot;&gt;BEM&lt;/a&gt;を採用しています。ただし、セレクタの命名規則は以下のように変えています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;[prefix]-[block]_[element]-[[Modifier-Key]-[Modifiler-Value]] {
  // property: value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://geckotang.tumblr.com/post/104065014321/css&quot;&gt;今年のネーミングルール #CSS 設計 - &amp;#x3C; /gecko &gt;&lt;/a&gt;とだいたい同じですが、以下の点が異なります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BlockとElementの区切り文字は「_（アンダースコア）」&lt;/li&gt;
&lt;li&gt;複数の単語を連結する場合は「-（ハイフン）」を使う&lt;/li&gt;
&lt;li&gt;ModifierはCamelCaseを使う。例は以下の通り
&lt;ul&gt;
&lt;li&gt;foo-Bar-Baz ([block]-[Modifier-Key]-[Modifier-Value])&lt;/li&gt;
&lt;li&gt;foo&lt;em&gt;bar-Baz ([block]&lt;/em&gt;[element]-[Modifier-Value])&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;prefixは、共通で使うルールセットはプロジェクトのコードネーム、各機能のみで使うルールセットはその機能名を使う
&lt;ul&gt;
&lt;li&gt;ガチャならprefixは&lt;code&gt;gacha&lt;/code&gt;という感じ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;あとは、&lt;a href=&quot;https://github.com/hiloki/flocss#mindbemding&quot;&gt;hiloki/flocss&lt;/a&gt;のMindBEMdingの章にあるように、JavaScriptで操作されるような「状態」を表すようなModifierについては、SMACSSのStateパターンの命名のように、&lt;code&gt;is-*&lt;/code&gt;プレフィックスを付けて&lt;code&gt;.is-active&lt;/code&gt;という感じにしています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.is-active&lt;/code&gt;には直接スタイル指定をすることを禁止しています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sass&quot;&gt;// これはOK
.block_element.is-active {
  display: block;
}

// これはNG
.is-active {
  display: block;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;良いところ&quot;&gt;良いところ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--&lt;/code&gt;や&lt;code&gt;__&lt;/code&gt;という区切り文字より、セレクタ名が醜くならない（個人の主観です）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;あとは構造が分かりやすいだったり、class名が衝突するということが少ないという、BEMやMindBEMdingを採用した人ならだいたい感じることでしょうか。&lt;/p&gt;
&lt;h3 id=&quot;改善したいところ&quot;&gt;改善したいところ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;prefixのところで世代管理をしなかったのは間違いだった
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://geckotang.tumblr.com/post/104065014321/css&quot;&gt;今年のネーミングルール #CSS 設計 - &amp;#x3C; /gecko &gt;&lt;/a&gt;を読んで気づいた…&lt;/li&gt;
&lt;li&gt;古いセレクタ名と、上記の命名規則を適用したセレクタ名が混ざっていて、辛くなることがある&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Modifierを使っているclass名で、Modifier-Keyがないclass名というのは、分かりにくいのではと思った
&lt;ul&gt;
&lt;li&gt;この章を書いて気づいた…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ディレクトリ構成&quot;&gt;ディレクトリ構成&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hiloki/flocss&quot;&gt;hiloki/flocss&lt;/a&gt; に基本は準拠しています。「基本は」と書いたのは以下のディレクトリ名を自分の好みで変えているからです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;foundation → base&lt;/li&gt;
&lt;li&gt;object/project → object/page&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;使っている-css-プリプロセッサーやライブラリー&quot;&gt;使っている CSS プリプロセッサーやライブラリー&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;LESS&lt;/li&gt;
&lt;li&gt;Normalize.css&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LESSな理由ですが、&lt;a href=&quot;https://speakerdeck.com/mayuki/sosiyarugemufalsehurontoendotoasp-dot-net-mvc-lite?slide=7&quot;&gt;ソーシャルゲームのフロントエンドと ASP.NET MVC (Lite)&lt;/a&gt;の7ページにも書いてあるように、Visual Studioとの相性が良かった（新規ファイル作成時に*.less形式でのみファイルが作れた）ことが要因です。&lt;/p&gt;
&lt;p&gt;とはいえ、MSDN Blogsの&lt;a href=&quot;http://blogs.msdn.com/b/webdev/archive/2014/02/25/announcing-new-web-features-in-visual-studio-2013-update-2-ctp2.aspx&quot;&gt;Announcing new Web Features in Visual Studio 2013 Update 2 CTP2&lt;/a&gt;にも書かれているように、Visual Studio 2013 Update 2（ちなみに今はUpdate 4が最新）から公式にSass(*.scss形式）のファイルも新規作成できるようになったので、Visual Studioユーザーでも好みでLESSかSassかを選べるようになったと言えます。&lt;/p&gt;
&lt;p&gt;ライブラリーはNormalize.cssくらいしか使っていないです。これはやはり同僚の@mayukiが入社したての頃に、汎用classをゴリッと書いてくれたので、ライブラリーを使う理由があまり無くなったという感じです。&lt;/p&gt;
&lt;h2 id=&quot;css-プリプロセッサーの機能について&quot;&gt;CSS プリプロセッサーの機能について&lt;/h2&gt;
&lt;h3 id=&quot;extend&quot;&gt;extend&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hiloki/flocss#css%E3%83%97%E3%83%AA%E3%83%97%E3%83%AD%E3%82%BB%E3%83%83%E3%82%B5%E3%81%AEextend&quot;&gt;CSS プリプロセッサの Extend - hiloki/flocss&lt;/a&gt;に書かれているように、モジュールで完結するextendは許容し、それ以外は禁止しています。むやみにextendを多用すると、どこにそのルールセットが書かれているか分からなくなり破綻する、というのが大きな理由です。&lt;/p&gt;
&lt;h3 id=&quot;ルールセットの入れ子&quot;&gt;ルールセットの入れ子&lt;/h3&gt;
&lt;p&gt;CSSプリプロセッサーのほとんど（全て？）はルールセットの入れ子がおこなえますが、入れ子の深さは二段階までと定めています。具体例としては以下のコードです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sass&quot;&gt;.foo {
    border: 1px solid #003760;
    color: #000;
    background: rgba(0, 0, 0, 0.5);

    .nest-01 {
        font-weight: bold;

        .nest-02 {
            color: #f00;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;なぜ、二段階までに抑えているのかというと、制限がなかった場合、出力後のCSSのセレクタ部分がとても長くなってしまう可能性があるということが挙げられます。&lt;/p&gt;
&lt;p&gt;具体例を書くと、以下のようなLESSのコードがあったとします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sass&quot;&gt;// Uuuugly!
.foo {
    .nest-01 {
        .nest-02 {
            .nest-03 {
                .nest-04 {
                    .nest-05 {
                        color: #333;
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上記のコードをCSSにコンパイルした場合、以下のようになるでしょう。非常に長くて醜いです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.foo .nest-01 .nest-02 .nest-03 .nest-04 .nest-05 {
  color: #333;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;また、実際に挙動を確かめられていないですが、多くのサイトで「ブラウザーのセレクターの解釈として、右から左に解釈される」と書かれています。&lt;/p&gt;
&lt;p&gt;とすると、上記のCSSは「.nest-05 → .nest-04 → .nest-03 → .nest-02 → .nest-01 → .foo」と解釈されることになります。「.nest-01 → .foo」という構造よりも解釈に時間がかかりそうです。&lt;/p&gt;
&lt;p&gt;ちなみに、古い記事（2011年）ですが、&lt;a href=&quot;http://developers.linecorp.com/blog/?p=178&quot;&gt;CSS セレクタによる高速化、実際のところ « LINE Engineers&#39; Blog&lt;/a&gt;に実際の計測結果が掲載されており、そこには以下のように書かれています。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;スタイルを当てる要素にはできるだけclass又はIDを指定する。&lt;/li&gt;
&lt;li&gt;子孫セレクタは重いのできるだけ減らす。&lt;br&gt;
という施策に一定の効果はあるようですが、その効果が如実に現れるのはHTML、CSSのコードが非常に大きいページに限られるようです。&lt;br&gt;
そのなかでも、子孫セレクタを減らす施策よりも、スタイルを当てる要素にはできるだけclass名を指定する施策のほうが、効果があるようです。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;ということで、MindBEMdingの考え方を適用し、あるBlockの中でスタイルを当てたい要素には、.block_elementだったり、.block_element-Key-Valueという感じでclass名を指定し、スタイルを適用しています。&lt;/p&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;ソーシャルゲームというのは、最初から機能が数十種類あったり、Viewのファイル数が数百ファイルを超えるということは珍しくありません。&lt;/p&gt;
&lt;p&gt;そんな中で、CSSを書く上での指針がないと、時間が経つにつれ運用が苦痛になってきます。&lt;/p&gt;
&lt;p&gt;例えば新しい人が入ってきたり、自分に何かあって他の人が一時的にプロジェクトに入ってきた場合、CSSを書く上での指針が無いと、どのようにCSSを書いていけばいいのか、理解に時間がかかることになります。&lt;/p&gt;
&lt;p&gt;なのでCSSを書く上での指針を定め、それをGitHubのWikiなど目に入りやすい場所へ残しておくのが重要と、自分は考えています。&lt;/p&gt;
&lt;p&gt;明日は&lt;a href=&quot;http://www.adventar.org/users/9&quot;&gt;oti&lt;/a&gt;さんです。&lt;/p&gt;
&lt;h2 id=&quot;記事内で紹介した-github-のリポジトリや記事&quot;&gt;記事内で紹介した GitHub のリポジトリや記事&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hiloki/flocss&quot;&gt;hiloki/flocss&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://speakerdeck.com/mayuki/sosiyarugemufalsehurontoendotoasp-dot-net-mvc-lite&quot;&gt;ソーシャルゲームのフロントエンドと ASP.NET MVC (Lite)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/&quot;&gt;MindBEMding – getting your head ’round BEM syntax – CSS Wizardry – CSS, OOCSS, front-end architecture, performance and more, by Harry Roberts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://blogs.msdn.com/b/webdev/archive/2014/02/25/announcing-new-web-features-in-visual-studio-2013-update-2-ctp2.aspx&quot;&gt;Announcing new Web Features in Visual Studio 2013 Update 2 CTP2 - .NET Web Development and Tools Blog - Site Home - MSDN Blogs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://developers.linecorp.com/blog/?p=178&quot;&gt;CSS セレクタによる高速化、実際のところ « LINE Engineers&#39; Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2014-12-09T00:00:00.000Z</published>
<updated>2022-11-28T15:23:32.345Z</updated>
</entry>
<entry>
<title>colgroup 要素や col 要素は適用できる CSS のプロパティに制限がある</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/restrict-css-property-of-colgroup-col"/>
<id>tag:blog.kubosho.com,2014-12-01:entry:restrict-css-property-of-colgroup-col</id>
<summary>やりたかったこと</summary>
<content type="html">&lt;h2 id=&quot;やりたかったこと&quot;&gt;やりたかったこと&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;table&gt;
  &amp;#x3C;colgroup span=&quot;1&quot; style=&quot;padding:10px; background:#66c; color:#fff; text-align: center;&quot; /&gt;
  &amp;#x3C;tbody&gt;
    &amp;#x3C;tr&gt;
      &amp;#x3C;td&gt;10&amp;#x3C;/td&gt;
      &amp;#x3C;td&gt;foo&amp;#x3C;/td&gt;
    &amp;#x3C;/tr&gt;
    &amp;#x3C;tr&gt;
      &amp;#x3C;td&gt;20&amp;#x3C;/td&gt;
      &amp;#x3C;td&gt;bar&amp;#x3C;/td&gt;
    &amp;#x3C;/tr&gt;
    &amp;#x3C;tr&gt;
      &amp;#x3C;td&gt;30&amp;#x3C;/td&gt;
      &amp;#x3C;td&gt;baz&amp;#x3C;/td&gt;
    &amp;#x3C;/tr&gt;
  &amp;#x3C;/tbody&gt;
&amp;#x3C;/table&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;というHTMLを書いた際に、縦1列目にpadding, background, color, text-alignの指定が適用される想定でした。&lt;/p&gt;
&lt;h2 id=&quot;問題&quot;&gt;問題&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://jsbin.com/xaleva/4/edit?html,output&quot;&gt;上記の HTML を書いた JS Bin&lt;/a&gt;を見ると分かるように、backgroundの指定以外は適用されていません。なぜ？&lt;/p&gt;
&lt;h2 id=&quot;分かったこと&quot;&gt;分かったこと&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://www.w3.org/TR/CSS21/tables.html#columns&quot;&gt;W3C の CSS 2.1 の仕様書内にある Tables 17.3 Columns&lt;/a&gt;を見ると分かるのですが、table-columnやtable-column-groupのプロパティが指定されている要素（col要素とcolgroup要素）には、下記4つのプロパティのみ指定できるようです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;border
&lt;ul&gt;
&lt;li&gt;table要素に&lt;code&gt;border-collapse: collapse;&lt;/code&gt;が指定されている時のみ適用される&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;background&lt;/li&gt;
&lt;li&gt;width&lt;/li&gt;
&lt;li&gt;visibility&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;まとめ&quot;&gt;まとめ&lt;/h2&gt;
&lt;p&gt;自分が適用したかったスタイルはcolgroup要素やcol要素では適用できないことが分かったので、colgroup要素やcol要素は使わないことにしました。&lt;/p&gt;
&lt;p&gt;その代わりに、縦1列目にclassを指定して、そのclassに適用したかったスタイルを指定することにしました。&lt;/p&gt;</content>
<published>2014-12-01T00:00:00.000Z</published>
<updated>2022-11-28T15:17:46.739Z</updated>
</entry>
<entry>
<title>カラーコードを正規表現を使って判定する</title>
<link rel="alternate" href="https://blog.kubosho.com/entries/color-code-regexp"/>
<id>tag:blog.kubosho.com,2014-06-15:entry:color-code-regexp</id>
<summary>カラーコードを判定するための正規表現を書きました。</summary>
<content type="html">&lt;p&gt;カラーコードを判定するための正規表現を書きました。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;^#([\da-fA-F]{6}|[\da-fA-F]{3})$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;カラーコードの正規表現が合っているかどうかの検証に使った文字列は次の通りです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;// match
#339
#251
#aaa
#fff
#AAA
#cdf
#Abc3Fc
#F9012b
#afdcbf
#987345
// mismatch
#9af#f6f
339
621345
af89f8
#ggg
#fgf
#delcmi
#3r9
#-12345
#jjjjjj
#aaaaaaa
jkhfng
ff8iju
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;matchに挙げた文字列はカラーコードと判定されることを確認しています。またmismatchに挙げた文字列はカラーコードと判定されないことを確認してます。&lt;/p&gt;
&lt;h2 id=&quot;正規表現の検証に使ったページ&quot;&gt;正規表現の検証に使ったページ&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://regex101.com/&quot;&gt;regex101: build, test, and debug regex&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;参考にしたページ&quot;&gt;参考にしたページ&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions&quot;&gt;正規表現 - JavaScript | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://userweb.mnet.ne.jp/nakama/&quot;&gt;サルにもわかる正規表現入門&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content>
<published>2014-06-15T00:00:00.000Z</published>
<updated>2022-11-28T13:39:59.728Z</updated>
</entry>
</feed>
