開発ドキュメント
Architecture & Developer Guide
以下は代表開発者がさまざまなプロジェクトに関わっているため、とくにこのサイトを作るのに必要になることを備忘録として書き残したものです。共著者向けに整理したものですが、サイトの改善などを目的に、広く見られてもよい内容だけ表現を簡略化するなどして以下の通り公開します。
技術スタック
フロントエンド構成
- Framework: Next.js (App Router)
- Content: MDX (
@next/mdx) - Styling: Tailwind CSS
本プロジェクトでは動的なサーバーサイドレンダリングを行わず、output: 'export' を指定して静的サイト生成(SSG)によるHTMLエクスポートを行っている。UIのスタイリングにはTailwind CSSを用い、ダークモード対応を含めたユーティリティクラスベースの設計を採用した。
数式・グラフィックス
- 数式: KaTeX (
remark-math,rehype-katex) - Graphics: React (SVG) / Three.js (
@react-three/fiber)
数式の組版処理にはKaTeXを使用し、ビルド時に静的なHTML/CSSへと変換する。MathJaxなどでよくある初回描画時のレイアウトシフト(CLS)を防止する目的だが、開発上不便なこともあるのも現状(他との兼ね合いでturboが使えないなど。改善案またはアップデート求む)。グラフや図版はできる限りラスター画像を用いず、ReactコンポーネントとしてのSVGまたはWebGL(Three.js)を用いた動的描画を行っている。
デプロイメントパイプライン
インフラはCloudflare Pages。GitHubの対象ブランチ(例: main)へコミットがプッシュされると、Cloudflareのビルド環境内でNext.jsのビルドがトリガーされ、生成された静的アセットがグローバルエッジネットワークにデプロイされるというごくごく標準的な仕様。
ローカル開発環境のセットアップ
リポジトリをクローン後、以下のコマンドで依存関係のインストールと開発サーバーの起動を行いる。なお、MDXコンパイラの諸問題を回避するためにやむなくTurbopackではなく --webpack フラグを付与している。アップデートされ次第、優先して改善するポイント。
# 依存パッケージのインストール
npm install
# 開発サーバーの起動
npm run dev --webpack起動後、ブラウザで http://localhost:3000 にアクセスして表示を確認。
プロジェクト構造と体系
ここでは複数の学問分野を扱うため、ディレクトリ構造は分野単位で整理しているほかは標準的な構造。コンパイル後のサイトマップ構成は他ページで公開中。
ディレクトリ構成(抄)
src/
├── app/ # Next.js App Router ページ
│ ├── layout.tsx # ルート レイアウト
│ ├── page.tsx # トップページ
│ ├── physics/ # 物理学の分野
│ │ ├── mechanics/ # 力学チャプター
│ │ │ ├── page.tsx # ハブページ
│ │ │ ├── kinematics/ # サブページ
│ │ │ └── ...
│ │ └── ...
│ ├── law/ # 法律分野
│ ├── kambun/ # 漢文分野
│ └── ...
├── components/ # React コンポーネント
│ ├── MyBox.tsx # 構造化ボックス
│ ├── HubLayout.tsx # 分野ハブレイアウト
│ ├── Latex.tsx # KaTeX ラッパー
│ └── ...
├── lib/ # ユーティリティ・定義
│ ├── contents_physics.ts # 物理学の構成定義
│ ├── slug.ts # URL slug 生成
│ └── main.ts # グローバル構成
└── types/ # TypeScript 型定義
└── mdx.d.ts # MDX 型
各ディレクトリの役割
src/app/: Next.js App Router を用いたページ定義。分野名ディレクトリ配下に各チャプターを配置src/components/: 再利用可能な React コンポーネント。MDXファイルから直接インポート可能src/lib/: グローバル定数、ナビゲーション構成、ユーティリティ関数を定義functions/api/: Cloudflare Workers / Functions による API ハンドラpublic/: 静的アセット(SVG、検索インデックスなど)
MDX パイプライン と コンテンツ処理
Physdocs では、MDX ファイルをビルド時に HTML に変換するための複数のプラグインを組み合わせている。これらは検索機能に使われるほか、SEOにも多少寄与している(はず)。
Remark プラグイン(入力側)
remark-frontmatter: YAML フロントマターを解析remark-mdx-frontmatter: フロントマターを MDX 変数として公開remark-math:/を数式ノードに変換remark-gfm: GitHub Flavored Markdown 拡張(テーブル、打ち消し線など)
Rehype プラグイン(出力側)
rehype-katex: 数式ノードを KaTeX による HTML/CSS に変換。SSR 最適化により CLS 防止rehype-pretty-code: コードブロックを Shiki による構文強調に変換
ビルド時の変換フロー
MDX ファイル
↓ (フロントマター抽出)
Remark プラグイン処理
↓ (数式検出 + GFM パース)
抽象構文木 (AST) 生成
↓ (HTML 要素生成)
Rehype プラグイン処理
↓ (KaTeX 変換 + コード強調)
静的 HTML/CSS 出力
↓
Cloudflare Pages へ配信フロントマターの例
---
title: ニュートンの運動方程式
description: 古典力学の基盤を成す最重要法則
tags: [mechanics, force, acceleration]
---コンポーネント体系
MyBox コンポーネント:構造化コンテンツボックス
サイトの核となるコンポーネント。学術的な内容(定義、公式、導出、図解など)を視覚的に分類・強調する。
現在は 16 種類の type に対応しており、分野別に使い分けること、あるいは統一感を出すために同じものを使い続けること、どちらも想定している。
基本書式
文章は改行しない。段落にするときは一行空ける。インライン形式の場合には $v$ のように半角スペースを空けて一つの $ ではじめて、$ でおわって半角スペースをつけると のように表示される。別行立てで中央に表示する場合は つの $$ ではじめて改行、数式を書いて改行し、$$ でおわると
のように表示される。なおこれらをテキストコピーすると数式ごとに都度改行したり二重にテキストがコピーされるので改善の余地がある。
import { MyBox } from '@/components/MyBox';
<MyBox type="D" title="速度の定義">
時間 $\Delta t$ あたりの移動距離が $\Delta x$ のとき、速度 $v$ は次のように表す。
$$
v = \frac{\Delta x}{\Delta t}
$$
</MyBox>全 BoxType 一覧と用途
以下は、 MyBox のタイプを分野別に整理した。各タイプはある程度デザインで区別化を図っている。
【基礎的な構造化】
D(Definition) — 定義・公理。背景色・太い枠線・濃い見出しで最も強調。定義、条文、公理など、議論の基盤となる要素。例:速度の定義。AX(Axiom) — 公理の明示的マーク。論理学などで使用。F(Formula/Fact) — 公式・重要な事実。やや控えめな背景色。公式、主要な法則、判例など。D より下位の重要度。例:、相対速度の公式。TH(Theorem) — 定理の宣言。緑色系で統一。数学・論理学での定理に使用。FB(Forward/Beyond) — 発展的な内容。そのセクションにしては進んだ話題、またはほかの分野に言及したことを示唆。
【論証・構造化】
P(Proof) — 導出・論証過程。軽量なスタイル。数式の導出手順、法律の解釈過程など。DP(Detailed Proof) — 詳細な説明。Pと同等の軽量スタイル。複雑な理論の丁寧な解説に使用。定義の説明にも利用できる。IR(Inference Rule) — 推論規則。論理学で使用。LE(Lemma) — 補題。定理の直前段階の小定理。次の定理を導くための準備となる事実として論理学で使用。
【分野|法律学】
H(Hanrei/判例) — 裁判例。赤色系で統一。判決文、判例の要旨、判例の立場を明示。V(View/通説) — 学説・通説。紫色系で統一。学者の有力説、通説の立場など。Q(Question/議論) — 学説上の争点。琥珀色系。複数の立場が対立している場合にはこれを使用する。J(Judgment/結論) — 主文・判断。判決文の主文や決定など。A(Applying/事実) — 事実認定・あてはめ。軽量、枠線スタイル。具体的な事実、判決での事実関係など。
【グラフィックス・その他】
G(Graph) — 図解・SVG/3D グラフ。破線枠、グレー系。SVG コンポーネント、Three.js による 3D グラフ、シミュレータを囲む。PM(Measurement Principle) — 測定原理。物理実験の測定手法、計測原理。グレー系で統一。
ネスト(入れ子)構造の推奨パターン
MyBox は入れ子にして記述することを標準とする。
<MyBox type="F" title="運動方程式">
$$
\boldsymbol{F} = m\boldsymbol{a}
$$
<MyBox type="P" title="導出">
運動量 $\boldsymbol{p} = m\boldsymbol{v}$ の時間変化率は、加わる力に等しい。
$$
\frac{d\boldsymbol{p}}{dt} = \frac{d(m\boldsymbol{v})}{dt} = m\frac{d\boldsymbol{v}}{dt} = m\boldsymbol{a} = \boldsymbol{F}
$$
</MyBox>
</MyBox>その他の主要コンポーネント
HubLayout: 分野のハブページ(例:/physics/mechanics)を構成。セクション分類、クイックリンクなどのまとめページLatex: KaTeX を React でラップ。インライン数式 / ディスプレイ モード対応TOC: 目次の自動生成RefTag: 他ページへの参照タグ。セマンティック・リンクExercise: 練習問題ボックス。タイプ、難度、解答へのリンクLiterature: 文献引用。著者、出版年、リンク情報を構造化
分野別コンテンツ管理
各分野のナビゲーション構成は、TypeScript ファイル(contents_*.ts)で定義されている。ハブページの自動生成、検索インデックスの構築を自動で行っている。純粋な教育目的でも、あるいは研究資料集めの目的ではなく、どちらかといえば高大接続や構造的に大きな矛盾のないような構成を意識して作成する。したがって学習指導要領や大学のレジュメは大きな参考にはなるが、依存はできない。
コンテンツ定義ファイルの構造
// src/lib/contents_physics.ts
export interface PhysicsItem {
title: string;
en?: string;
href: string;
isDraft?: boolean;
}
export interface PhysicsSection {
id?: string;
title: string;
en?: string;
items: PhysicsItem[];
}
export interface PhysicsChapter {
id: string;
title: string;
en: string;
color: string;
href: string;
formula?: string;
sections: PhysicsSection[];
}
export const physicsContents: PhysicsChapter[] = [
{
id: "mechanics",
title: "力学",
en: "MECHANICS",
color: "from-blue-500 to-cyan-400",
href: "/physics/mechanics",
formula: "m \\frac{d^2\\vec{r}}{dt^2} = \\vec{F}",
sections: [
{
id: "kinematics",
title: "運動の表し方",
items: [
{
title: "位置・速度・加速度",
href: "/physics/mechanics/kinematics",
},
// ...
]
}
]
}
];ナビゲーション構成の活用
HubLayoutがこの定義を読み込み、チャプターのハブページを自動生成- ビルド時インデックス生成(
scripts/gen-index.mjs)がセクション・アイテムをスキャン isDraftフラグで準備中ページを制御
インタラクティブ機能の実装
SVG グラフィックス(React)
移動、回転、スケーリング等の基本的なアニメーションには、React でインラインSVG を記述する。3Dと見やすさを考慮して使い分ける。
<MyBox type="G" title="円運動の図解">
<svg viewBox="0 0 400 400" width="400" height="400">
{/* 中心円 */}
<circle cx="200" cy="200" r="5" fill="black" />
{/* 運動軌道 */}
<circle cx="200" cy="200" r="100" fill="none" stroke="gray" strokeWidth="2" strokeDasharray="4 4" />
{/* 質点 */}
<circle cx="300" cy="200" r="8" fill="blue" />
{/* 半径ベクトル */}
<line x1="200" y1="200" x2="300" y2="200" stroke="black" strokeWidth="2" />
{/* 速度ベクトル */}
<line x1="300" y1="200" x2="300" y2="100" stroke="red" strokeWidth="2" markerEnd="url(#arrowhead)" />
</svg>
</MyBox>Three.js (WebGL)による 3D グラフィックス
複雑な 3D 図形、シミュレーション、物理演算が必要な場合、@react-three/fiber を用いた Three.js 統合を行う。端末に一定程度負担がかかることを考慮する。正確な式を用いなくとも近似式で十分な場合、範囲を決めて想定可能な範囲で実装する。
// src/components/graphics/OrbitalSimulation.tsx
import { Canvas } from '@react-three/fiber';
import { PerspectiveCamera, OrbitControls } from '@react-three/drei';
export function OrbitalSimulation() {
return (
<MyBox type="G" title="万有引力による軌道">
<Canvas style={{ height: '500px' }}>
<PerspectiveCamera position={[0, 0, 100]} makeDefault />
<OrbitControls />
{/* 星体・軌道のジオメトリ定義 */}
</Canvas>
</MyBox>
);
}アニメーション(Framer Motion)
スムーズなトランジション、遷移表現には、Framer Motion を用いる。一方でサイトが重くなったり、依存関係が壊れる原因でもあるので基本的には導入していない。
import { motion } from 'framer-motion';
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<MyBox type="D" title="定義">
...
</MyBox>
</motion.div>検索・インデックス機構
静的インデックス生成
ビルド時に、すべての MDX ファイルをスキャンし、タイトル、セクション、テキストスニペットを JSON インデックスに集約する。
# ビルド前処理
npm run build:index
# gen-index.mjs が public/search-index.json を生成
# 構造:
# [
# {
# "id": "physics-mechanics-kinematics",
# "title": "位置・速度・加速度",
# "section": "運動の表し方",
# "href": "/physics/mechanics/kinematics",
# "content": "時間に対する..."
# },
# ...
# ]クライアント側フルテキスト検索(Fuse.js)
生成されたインデックスを、ブラウザ側で Fuse.js を用いてファジー検索する。サーバーサイド処理を不要にする目的で導入しているが、検索ロジックは論理的にもいまだ構想中。
import Fuse from 'fuse.js';
const fuse = new Fuse(searchIndex, {
keys: ['title', 'section', 'content'],
threshold: 0.3, // ファジー許容度
minMatchCharLength: 2
});
const results = fuse.search('円運動');
// 結果:マッチしたアイテムを距離順でリスト検索ログの記録(API)
検索クエリは独自に条件を科したうえでサーバー側 API に送信され、特殊な処理をしてデータベースに記録している。これにより、ユーザーの情報を得ずに、ユーザー行動分析、コンテンツ改善の指標を得ている。これらは基本的に、ユーザー動きを察知しながら、検索用のページと体系的な並びを大事にするページを両立するように動いている。
// functions/api/search-log.ts
export const onRequestPost = async (context) => {
const { request, env } = context;
const { query } = await request.json();
const timestamp = Date.now();
const stmt = env.DB.prepare(
"INSERT INTO search_logs (query, timestamp) VALUES (?, ?)"
);
await stmt.bind(query, timestamp).run();
return new Response(JSON.stringify({ success: true }), { status: 200 });
};バックエンド API と管理機能
Cloudflare Functions と D1 データベース
検索ログ、メッセージ送信、管理画面のバックエンドは、Cloudflare Workers / Functions 上で実行されます。 データ永続化には、Cloudflare D1(SQLite ベース)を使用している。そのほかさまざまな手法でこれらのAPIが軽量かつ堅牢に維持できるような工夫をしている。
API エンドポイント一覧
POST /api/search-log: 検索クエリ記録GET /api/search-log: 検索ログ取得(認証済み)POST /api/contact: 問い合わせメッセージ送信
ビルド・デプロイメント パイプライン
ビルドプロセスの詳細
# 1. インデックス生成(検索用)
npm run build:index
# 2. Next.js ビルド(Webpack使用)
NODE_OPTIONS=--max-old-space-size=8192 next build --webpack
# 3. 出力
# .next/static/ → 静的アセット
# public/ → 画像・アイコン・インデックス
# 4. Cloudflare Pages へ自動デプロイ(Git トリガー)Webpack 設定と最適化
MDX コンパイラと Next.js Turbopack の競合を回避するため、開発・ビルド時に明示的に Webpack を指定している。
// next.config.mjs
const nextConfig = {
output: 'export', // 完全な静的エクスポート
images: {
unoptimized: true // 画像最適化ディセーブル(SSGのため)
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.devtool = false; // クライアント側のソースマップをオフ
}
return config;
}
};Cloudflare Pages デプロイ設定
- ビルドコマンド:
npm run build - 出力ディレクトリ:
out - トリガーブランチ:
main - 自動デプロイ: GitHub 連携により、記事認証後 push 時に自動実行
ベストプラクティスと設計パターン
コンテンツ執筆フロー
➀ 新規ページの作成
# 例:物理学 > 力学 > 新トピック を追加
mkdir -p src/app/physics/mechanics/new-topic
touch src/app/physics/mechanics/new-topic/page.mdx➁ MDX フロントマター + コンポーネントインポート
---
title: 新しいトピックの名前
description: 要約文
tags: [keyword1, keyword2]
---
import { MyBox } from '@/components/MyBox';
import { Latex } from '@/components/Latex';
import { PageSubtitle } from '@/components/PageSubtitle';
# トピックタイトル
<PageSubtitle>
English subtitle
</PageSubtitle>➂ コンテンツ
<MyBox type="D" title="主要な定義">
核心的な定義文...
</MyBox>
<MyBox type="F" title="関連する公式">
$$...$$
<MyBox type="P" title="導出">
導出手順...
</MyBox>
</MyBox>
<MyBox type="G" title="図解">
SVG または Three.js グラフ
</MyBox>➃ ナビゲーション定義を更新
必要に応じてsrc/lib/contents_physics.ts に追加
{
id: "kinematics",
title: "運動の表し方",
items: [
{
title: "新しいトピック",
href: "/physics/mechanics/new-topic", // 追加
isDraft: false
}
]
}➄ ビルドと確認&記事認証
npm run build:index
npm run dev --webpack
# http://localhost:3000/physics/mechanics/new-topic でプレビューダークモード対応
すべてのスタイルに、明色・暗色の両方の定義を含める。Tailwind CSS の dark: クラスが使える。
<div className="bg-white dark:bg-black text-gray-900 dark:text-gray-50">
自動的にダークモード対応
</div>テーマ切り替えは next-themes により、localStorage で永続化する。
セマンティック HTMLと SEO
<h1>, <h2>, <h3>等の見出しレベルを順に使う- MDX ファイルのフロントマター(title, description)を
layout.tsxで メタデータ として設定
AIの使用について
前提として、当サイトの目的を完全に満たすAIを作成するあるいは既存のAIに依存することは非常に難しい。技術ページとコンテンツページで作成方法が異なるが、まずその区別が鮮明でない。あるいは重要な点を省略し、不必要な部分を増幅することもある。一方でかなり大雑把に草稿を作成するときはよく使える。機械的に作成するものでは高い精度をほこるし、コーディングにはよく使える。
こうした特徴を理解し、今のところはすべて人間の複数回の校閲が必要であることを念頭に使用できる。用途によってセキュリティ対策をしたローカルLLMを使用する。
補遺
当サイトはさまざまな人の協力により実現したものだから、多大な感謝を述べる。
質問や改善提案があれば、リポジトリの Issues またはディスカッション を活用のこと。