サンプル

ratete.dev を支える技術と設計判断

このリポジトリで使えるすべての表示機能を、リポジトリ自身の構造とアーキテクチャを題材に網羅的に紹介する自己説明的サンプル。

PUBLISHED: 2026/06/21(日) UPDATED: 2026/06/21(日) TAGS: #sample #reference

個人ブログを作るだけなら、既存のテンプレートを使えば半日で終わる。それでもゼロから組んだのは、記事を書く体験そのものを自分好みに仕上げたかったからだ。MDXで書いた原稿がビルド時にリッチなHTMLへ変換され、クライアントJavaScriptにはほぼ依存しない。この記事ではratete.devの設計と実装を、実際の構成を見せながら解説する。

モノレポの全体像

Bunワークスペースを使ったモノレポ構成を採用している。主要な構成は以下の通り。

パス内容
apps/webAstro 6メインサイト
apps/article-preview-cliCLIプレビューツール
apps/article-preview-chromeChrome拡張プレビュー
apps/article-preview-vscodeVS Code拡張プレビュー
packages/article-renderer共有レンダリングライブラリ
モノレポ構成
workspace:*workspace:*workspace:*ratete.dev monorepoapps/packages/tools/web - Astro 6article-preview-cliarticle-preview-chromearticle-preview-vscodearticle-renderer

ここで重要なのは、apps/webは自己完結するという方針だ。

ARCHITECTURE

このモノレポではwebを共通化のために歪めないという方針をとっている。共有が必要なロジックはpackages/article-rendererに切り出し、CLI・Chrome拡張・VS Code拡張がそれを参照する。

Web本体とサブアプリ間でロジックを共有せず、二重実装を許容する。「DRY原則に反するのでは?」と思うかもしれないが、Webサイトの表示最適化とプレビューツールの汎用性はしばしば相反するため、無理に共通化すると両方が中途半端になる。

NOTE

apps/webは自己完結とし、共有資産(packages/*)を参照しない。サブアプリ群はpackages/*を正本として参照する。webとサブアプリ間でロジックを共有せず二重実装を許容する。

記事がブラウザに届くまで

MDXファイルがどのようにHTMLへ変換されるのか。rehypeプラグインチェーンが中核を担っている。

ビルドパイプライン
MDX ソースContent Collectionrehype pluginsurlCardmermaidcodeBlocktableWrapheadingLinkHTML 出力

各プラグインの責務を整理しておく。

rehypeプラグインファイルトリガー出力実行順
rehypeUrlCardurl-card.tsベアURLのみの段落OGPカード1
rehypeMermaidmermaid.ts```mermaidコードブロックインラインSVG2
rehypeCodeBlockcode-block.tsすべての<pre>要素ヘッダー・コピー・折りたたみ付き3
rehypeTableWraptable.tsすべての<table>要素div.table-wrapでラップ4
rehypeHeadingLinkheading-link.tsすべての<h2>要素リンクコピーボタン追加5

より詳しいデータフローをシーケンス図で見てみよう。

記事ビルドのデータフロー
OGP fetchPlaywrightrehype pluginsAstroOGP fetchPlaywrightrehype pluginsAstroすべてビルド時に完結MDX AST を渡す1ベア URL の OGP 取得2title / description / favicon3Mermaid ソースを送信4SVG を返却5コードブロック装飾6テーブルラップ7h2 リンクボタン8変換済み AST9

注目すべきは、すべてがビルド時に完結するという点だ。OGPの取得もMermaidのSVG描画もビルド中に行われ、クライアント側には静的HTMLだけが届く。

コンテンツスキーマの設計

AstroのContent Collectionで記事のメタデータを型安全に管理している。スキーマ定義は以下の通り。

typescript
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const articles = defineCollection({
  loader: glob({ pattern: "*.mdx", base: "./src/content/articles" }),
  schema: z.object({
    badge: z.string(),
    title: z.string(),
    lead: z.string(),
    published: z.string(),
    updated: z.string(),
    tags: z.array(z.string()).default([]),
  }),
});

エンティティ間の関係をER図で表すとこうなる。

コンテンツスキーマ
hasusesARTICLEstringslugPKstringbadgestringtitlestringleaddatepublisheddateupdatedTAGstringnamePKLAYOUTstringnamePKstringtype

Mermaidダイアグラムの事前描画

Mermaidの描画方式が、このサイトで最も技術的に面白い部分かもしれない。一般的なMermaidの利用法ではクライアント側でJavaScriptを実行して描画するが、ratete.devではビルド時にPlaywrightを起動してSVGを事前生成する。

Mermaid図はビルド時にインラインSVGへ事前描画し、実行時のCDN・クライアント描画依存を持たない。 ラベルはhtmlLabels: false(SVG <text>描画)。

— CLAUDE.md規約より

BREAKING CHANGE

htmlLabels: falsetrueに変更すると、ビルド時の文字幅計測とページ表示時のCSS適用で差異が生じ、ラベルの末尾が切れる。この設定は変更しないこと。

テーマ設定はかなり細かくカスタマイズしている。ダークモードに合わせて全ノードの色を個別指定している。

typescript
import type { MermaidConfig } from "mermaid";

export const mermaidConfig: MermaidConfig = {
  theme: "dark",
  securityLevel: "strict",
  htmlLabels: false,
  themeVariables: {
    darkMode: true,
    background: "#111",
    fontFamily: "Moralerspace, ui-monospace, Menlo, monospace",
    fontSize: "15px",
    primaryColor: "#1f1f1f",
    primaryTextColor: "#ddd",
    primaryBorderColor: "#ddd",
    secondaryColor: "#262626",
    secondaryTextColor: "#ddd",
    secondaryBorderColor: "#5a5a5a",
    tertiaryColor: "#181818",
    tertiaryTextColor: "#ddd",
    tertiaryBorderColor: "#5a5a5a",
    lineColor: "#ddd",
    textColor: "#ddd",
    mainBkg: "#1f1f1f",
    nodeBorder: "#ddd",
    clusterBkg: "#181818",
    clusterBorder: "#5a5a5a",
    titleColor: "#ddd",
    edgeLabelBackground: "#111",
  },
};
FONT DEPENDENCY

Mermaidのビルド時レンダリングではMoralerspaceフォントをbase64で埋め込んでいる。フォントファイルを差し替えた場合、Mermaidの文字幅計測がずれてノードの重なりが発生する可能性がある。

Webとpackagesの両方がMermaidを使うが、テーマ設定は共有せず各々で定義する。これも「Webを共通化のために歪めない」方針の一環だ。

レンダリングの二系統
packages/article-renderermermaid-render.tsPlaywrightHTML 出力apps/web(Astro)rehypeMermaidPlaywrightインライン SVGmermaid-theme.ts

このサイトではさまざまな種類のダイアグラムを利用できる。たとえばrehypeプラグインのクラス構造はこう表現できるし——

rehype プラグイン構成
RehypePlugin+name: string+transform(tree) : :voidUrlCard+fetchOGP(url) : : Promise+buildCard(meta) : : ElementMermaid+renderSVG(source) : : Promise+extractTitle(source) : :stringCodeBlock+addHeader(pre) : : void+addCollapse(pre) : : voidTableWrap+wrapTable(table) : :voidHeadingLink+addButton(h2) : : void

表示機能の使いどころを俯瞰する四象限チャートも描ける。

表示機能の利用頻度 vs 実装複雑度
見出しリンクURL カードMermaidCalloutテーブルコードブロック低頻度高頻度低複雑度高複雑度

タイトルを省略した場合はヘッダーにmermaidとだけ表示される。

MERMAID
タイトルなしヘッダーに mermaidと表示

開発ワークフローとCI

開発の流れはMakefile経由で統一している。package.jsonscriptsにはあえて定義せず、すべてのコマンドをMakefileに集約した。

CIパイプラインの順序

  1. make format — Biomeで整形
  2. make lint — Biomeでリント
  3. make web-typecheck — Astroの型チェック
  4. make web-test — Bunによるテスト実行
  5. make web-build — 本番ビルド

この流れを状態遷移図で表すとこうなる。

開発ワークフロー
make formatmake lintmake web-typecheckmake web-testmake web-buildcheck-version-bumppassbump missingCodingFormatLintTypeCheckTestBuildVersionCheckPush

make ciを実行するとこれらが一気に流れる。所要時間のイメージは以下の通り。

make ci 実行タイムライン
0秒5秒10秒15秒20秒25秒30秒35秒40秒format-check lint typecheck test build version-bump 静的解析apps/web検証
WARNING

packages/*src/を変更した場合、そのパッケージをworkspace:*で参照しているユニットにもversion bumpが波及する。bump漏れはmake ciで検出される。

CAUTION

force pushは避けること。CIの比較ベースが消失しfatal: bad objectでversion bumpチェックが失敗する。push前にmake ciをローカルで通して漏れを防ぐ。

CIが通ったらpushする。ブランチ戦略はシンプルにmain + featureブランチのみ。

ブランチ戦略
mainfeature/mermaidfeature/url-cardinitbase setupadd pluginadd thememerge mermaidadd OGP fetchadd card UImerge url-cardbump version

デプロイと配信

ビルドした成果物はCloudflareにデプロイされる。

デプロイ構成
BrowserCloudflare CDNWorkersStatic Assets

Wranglerの設定は最小限だ。

yaml
compatibility_date: "2026-05-24"
assets:
  binding: ASSETS
observability:
  enabled: true

スタイル設計:テーマとトークン

色やタイポグラフィはCSSカスタムプロパティ(デザイントークン)に一元化している。コンポーネント内にカラーコードを直書きすることは禁止だ。

css
:root {
  --ink: #ddd;
  --bg: #0a0a0a;
  --accent: #56b4e9;
  --border: #333;
  --code-bg: #111;
}

技術スタックの比重はこのようなイメージだ。

技術スタック構成比
29%24%18%12%6%12%Astro / MDX [5]rehype plugins [4]Mermaid [3]Shiki [2]Satori (OG) [1]Cloudflare [2]

ローカル環境のセットアップ

このリポジトリをクローンして開発を始めるには、まずNode.js(もしくはBun)が必要になる。

  1. 公式サイトからインストーラをダウンロード
  2. .msiを実行してウィザードに従う
  3. PowerShellで確認:
node --version
npm --version

Homebrewでインストール:

brew install node

バージョン確認:

node --version
npm --version

パッケージマネージャーでインストール:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

バージョン確認:

node --version
npm --version

Astroのインストール

Node.jsが入ったら、Astroをインストールする。パッケージマネージャーはお好みで。

npm install astro
yarn add astro
pnpm add astro
bun add astro

TypeScriptとJavaScriptのどちらでも書けるが、このプロジェクトではTypeScriptを使っている。

const greet = (name: string): string => {
  return `Hello, ${name}!`;
};
const greet = (name) => {
  return `Hello, ${name}!`;
};

package.jsonはこんな具合だ。

json
{
  "name": "@ratete/web",
  "version": "0.1.16",
  "private": true,
  "type": "module"
}

開発サーバーの起動はMakefile経由で行う。

bash
make ci  # format-check → lint → typecheck → test → build

言語指定なしのコードブロックも書ける。

plaintext
これは言語指定なしのコードブロック。
プレーンテキストとして表示される。

開発環境と本番環境の違い

ローカルと本番では設定が異なる。それぞれの特徴を見てみよう。

開発サーバー設定

  • ホットリロード有効
  • デバッグログ出力
  • ソースマップ生成

デプロイ設定

  • ミニファイ有効
  • ログレベル: errorのみ
  • CDN配信

参考にしたリソース

このサイトを作るにあたって特に参考になったのは、Astroの公式ドキュメントだ。

docs.astro.builddocs.astro.build

Mermaidはダイアグラム記法のデファクトスタンダードと言ってよい存在で、GitHubでも非常に活発に開発が続いている。

GitHub - mermaid-js/mermaid: Generation of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdownGeneration of diagrams like flowcharts or sequence diagrams from text in a similar manner as markdown - mermaid-js/mermaidgithub.com/mermaid-js/mermaid

SNSでの反応

リリース後、いくつかの反応をいただいた。

@astrodotbuildに返信
Astro@astrodotbuild
A beta release of v7 is out now. Try it with npx @astrojs/upgrade beta
Astro@astrodotbuild
Light mode or dark mode?
Astro@astrodotbuild
New docs drop! Learn how to use @ImageKitIO for media optimization and delivery in your Astro site. docs.astro.build/en/guides/medi…

動画付きの投稿もあった。

ChatGPT@ChatGPTapp
New in ChatGPT: a better way to schedule tasks. Scheduled tasks are faster, more reliable, and easier to manage from the new Scheduled page. The new scheduled tasks experience is rolling out to Go, Plus, Pro, Business, and Enterprise users on web and mobile.
ChatGPT@ChatGPTapp
Paperwork is better when you can just talk through it. With Images in ChatGPT and voice mode, you can upload a form, say what to fill in, and get back a completed version.

画像を複数枚含む投稿も。

@OpenAIに返信
OpenAI@OpenAI
Maria tested the idea across 10,080 reactions, and human chemists later validated representative results by hand. Under the optimized conditions, yields improved for 88% of the boronic acids and 83% of the sulfonamides tested. Human chemists then repeated 14 representative

まとめ

ratete.devはビルド時にすべてを解決するという思想で設計されている。MermaidのSVG事前描画、OGPカードのビルド時フェッチ、Shikiによるサーバーサイドシンタックスハイライト——いずれもクライアントにJavaScriptを送らないための工夫だ。

当初はクライアントサイドでMermaidを描画していたが、パフォーマンスとセキュリティの両面からビルド時描画に移行した。太字で強調すべきは、この判断によってLighthouseのパフォーマンススコアが大幅に改善したことだ。rehypeプラグインを自作する手間はかかったが、結果的には正しい選択だったと思う。

日本語テキストの中にcodeを挟んだ場合の行高への影響や、複数のインラインコードが連続する場合でも読みやすく表示できるよう、CSSを調整している。

この記事自体がAstro公式ドキュメントにあるようなMDX記法で書かれており、表示されているすべての要素——テーブル、コードブロック、Callout、Mermaidダイアグラム、URLカード、ツイートカード、CodeGroup、Tabs——がこのリポジトリの表示エンジンによって描画されている。


以下に、この記事で利用した全表示機能を一覧にしておく。

カテゴリ機能バリエーション
Markdown見出しh2 / h3 / h4
Markdownリスト順序なし / 順序付き / ネスト
Markdownテキスト装飾太字 / 斜体 / 取り消し線 / インラインコード
Markdown引用blockquote
Markdown水平線---
Markdownリンクインラインリンク
テーブル基本2列 / 多列(横スクロール)
コードブロック表示短い(通常) / 長い(折りたたみ)
コードブロック言語TypeScript / JSON / Bash / CSS / YAML / なし
Calloutタイプnote / warning / caution
Calloutラベルデフォルト / カスタム
URLカードOGPベアURL → リッチカード
ツイートカード埋め込みテキスト / 投票 / リンク付き / 動画 / 縦動画 / 複数画像
MermaidflowchartLR / TD / サブグラフ / タイトルなし
MermaidsequenceDiagramautonumber / Note
MermaiderDiagramエンティティ関連
MermaidstateDiagram-v2状態遷移
MermaidpieshowData付き
MermaidclassDiagramクラス継承
Mermaidganttタイムライン
MermaidgitGraphブランチ・マージ
Mermaidblock-betaブロック構成図
MermaidquadrantChart四象限チャート
コードグループタブ切替2タブ / 4タブ(パッケージマネージャー)
コンテンツタブタブ切替2タブ / 3タブ(OS別手順)