以前のブログのデザインが微妙だな、という気持ちになりブログを作り直した。
開発期間は約2週間程度で比較的満足のいく仕上がりになったと思う。
mdxを使えるようにしたことで、より自由な構成で記事を書けるようになった。
上のグリッド上に並べた画像もmdxで実現されている。
技術スタック
このブログを参考に使うフレームワークを決めた。
How I Built My Blog • Josh W. Comeau
www.joshwcomeau.com
「見た目がいい感じ」、「軽量」あたりを軸に、開発工数を削減するために得意なライブラリを活用していく方針を取っている。
- Next.jsでStatic Export
- Cloudflare Pagesにホスト
- マイクロインタラクションはSVG + react-springで実装
- コードブロックはshiki
- next-mdx-remote-clientでmd, mdxをレンダリング、足りない機能は自前でremarkプラグインを書く
Static Exportできればフレームワークは何でも良かったが書き慣れているNext.jsを使うことにした。
以前VercelにShikiを使ったWebアプリをデプロイしようとしたときに容量制限に引っかかったが、この手の細かい話を考える必要がないのでStatic Exportは楽。
ローカルでビルドが通ればデプロイできてしまうので。
CSSは基本的にはtailwindで書いて、小難しいスタイルが必要になったらCSS Modulesを使うようにした。
tailwindは賛否あるライブラリだと思うが、自分はhtmlタグとスタイルの距離の近さ(コロケーション)がメンテナンス性の観点で大事だと思っているので、他の制約条件がなければ積極的に使っている。
next-mdx-remote-clientとreact-springは初めて使うライブラリだったが非常に体験が良かったので後述。
デザイン
当初は「積み重なり」をコンセプトにfigmaでデザインを描いていた。
束ねた色紙が鳥に食べられて後ろが見えている感じにできればと考えていたが、最終的には意匠が何かよくわからなくなってしまった気がする。
catnoseさんのタイムライン形式のランディングページが非常に良かったので、ランディングページはこれにしようと決めていた。
catnose
catnose.me
タイムラインを実装するにあたって、記事やアナウンスをどのように管理するか考える必要があった。
当初yamlファイルでメタデータを管理する方針で実装したが、細かい調整が難しく取り回しが悪かった。
結局Timeline.tsxというファイルの中でコンポーネント内に直接記事へのリンクやアナウンス文を書く形式にした。
yamlのような構造化されたデータではないので、sitemapやrss.xmlを生成する際に若干手間がかかったが、許容できる範囲の工数増だったので割り切った。
イメージとしてはこんな感じ↓
export const Timeline: React.FC = () => {
return (
<>
<TimelineYear year={2025} />
<LocalArticle path="./content/blogs/20251102_new_blog.mdx" />
<TimelineAnnounce date={new Date("2025-11-02")}>
Launched the redesigned blog 🚀
</TimelineAnnounce>
</>
);
}
Markdownのレンダリング
nextの公式ドキュメントでも使われているnext-mdx-remote-clientでmdxをレンダリングすることに決めた。
GitHub - ipikuka/next-mdx-remote-client: A wrapper of `@mdx-js/mdx` for `Next.js` applications in order to load MDX content. It is a fork of `next-mdx-remote`.
github.com
このライブラリは「覚える必要があることが少ない」+「remarkのプラグインがそのまま使える」という点が非常に良い。
特にremarkのプラグインが使えることで、Reactコンポーネント側で処理しにくいメタデータを事前に自作プラグインで計算して埋め込んでおいて、各コンポーネントはただのPresentationとして実装できる。
この構成はコンポーネント側でハック的なコードを書く必要がなくなる点が良い。
具体的なところでリンクカードやコードブロックは事前にリンクだけの段落かどうかの判定や、言語と行数の検出を予め処理しておくことでシンプルに実装できた。
mdx
next-mdx-remote-clientのおかげで文中にReactコンポーネントを埋め込むことができる。
フロントエンドの機能のデモには使いやすそうで、Reactコンポーネントとして実装できれば自在に埋め込める。
Count: 0
他にもインタラクティブなグラフを表示したり用途は色々ありそう。
今からブログを作るのであればmdx対応して損することは無いと思う。
マイクロインタラクション
細かいインタラクションはsvgをreact-springで動かしている。
figmaでsvgを書いて、手動で取り回しやすいReactコンポーネントとして表現している。
アニメーションは自分で頑張ればライブラリ無しでも実装できるかもしれないが、Reactのマウント、アンマウントを考えるのが面倒なので外部のライブラリを使ったほうが絶対に良い。
今回はreact-springを使ったが、対抗馬としてGSAPやMotion(旧Framer Motion)あたりを一応検討した。結局GSAPはToo Much感がある、Motionはバンドルサイズは小さいが無料で利用できる範囲が狭そう、ということでreact-springに落ち着いた。
react-springはhtmlのタグに対応するコンポーネントが提供されていて、アニメーションで動かしたい箇所を置き換えていくスタイル。
svgをコピペして軽く書き換える + パラメータをハードコードしていくだけなので実装が楽。
"use client";
import { animated, useSpring } from "@react-spring/web";
import { useEffect } from "react";
interface ArrowAnimationProps {
className?: string;
active: boolean;
}
export const ArrowAnimation: React.FC<ArrowAnimationProps> = ({
className,
active,
}) => {
const x1Base = 12;
const x2Base = 19;
const colorBase = "rgba(153, 161, 175, 1)";
const x1Active = 21;
const x2Active = 28;
const colorActive = "#E6BC00";
const [{ x1, x2, color }, api] = useSpring(() => ({
from: {
x1: x1Base,
x2: x2Base,
color: colorBase,
},
config: {
duration: 100,
},
}));
useEffect(() => {
if (active) {
api.start({
to: {
x1: x1Active,
x2: x2Active,
color: colorActive,
},
});
} else {
api.start({
to: {
x1: x1Base,
x2: x2Base,
color: colorBase,
},
});
}
}, [active, api.start]);
return (
<div className={className}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 28 13"
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<title>Arrow</title>
<animated.line x1="0" y1="11" x2={x2} y2="11" stroke={color} />
<animated.line x1={x1} y1="6" x2={x2} y2="11" stroke={color} />
</svg>
</div>
);
};
その他
- Claude Codeに書いた記事の添削用のコマンドを用意して品質を担保できるようにした。
- ChromeのSecretモードでLightHouseを動かして品質を担保した。
Three.jsを使ってランディングページに3Dモデルを入れたかったが、面倒くさくなってやめてしまった。
気が向いたら代わりにいい感じのSVGアニメーションを追加するかもしれない。