CmdDojo ができるまで — ブラウザだけで動くLinuxコマンド練習サービスの設計と実装
「Linuxコマンドを覚えたいけど、環境構築がめんどくさい」
これ、初心者が一番最初にぶつかる壁です。WSL入れて、仮想マシン立てて、サーバー借りて——そこまでやってようやくターミナルを触れる。でもそこに辿り着く前に諦める人が山ほどいる。
ブラウザを開くだけでLinuxコマンドを練習できたら、その壁はなくなる。
それが CmdDojo の出発点でした。この記事では、なぜ作ったか・どう設計したか・実装で詰まったところを全部書きます。
🎯 なぜLinuxコマンド練習サービスなのか
HackSimから生まれたアイデア
CmdDojoはHackSimの副産物です。
HackSimを作るために「ブラウザ上で動く仮想ターミナル」を実装しました。その時ふと思ったのです——「これ、コマンド練習にも使えるんじゃないか?」と。
ゲームとしてのHackSimは「楽しさ」を軸にしています。一方でLinuxコマンドの学習には「正確な理解」と「反復練習」が必要です。同じインフラを使いながら、全く別の価値を提供できると判断しました。
SEO視点での勝算
AI開発収益化ラボで運営するサービスは、ターゲットがバラバラです。HackSimはゲーマー、言い訳ジェネレーターはエンタメ層、PromptStockはAIユーザー。それぞれは面白いが、SEO的なトピック集中度は低い。
CmdDojoは明確に 「Linuxコマンド」 というニッチに特化できます。
| 検索ワード | 月間検索数(概算) | 難易度 |
|---|---|---|
| Linuxコマンド 練習 | 3,000〜 | 中 |
| LPIC 問題集 | 5,000〜 | 高 |
| Linux 初心者 コマンド | 8,000〜 | 高 |
| Linuxコマンド 一覧 | 30,000〜 | 高 |
ロングテールから攻めていき、コース数が増えるにつれてトップワードも狙える。SEO的に積み上がる構造を作れると判断しました。
ターゲット設定
| 軸 | 設定 |
|---|---|
| プライマリ | Linux完全初心者・プログラミング入門者 |
| セカンダリ | LPIC/LinuC受験生・ITインフラ志望者 |
| 提供体験 | 打って・確認して・覚える 反復練習 |
🏗️ システム設計
CmdDojoもHackSim同様、バックエンドなしで動作します。
仮想ファイルシステム・コマンドエンジン・採点ロジック、すべてブラウザ上のTypeScriptです。
全体アーキテクチャ
CmdDojoApp(React + Zustand)
│
├─ 仮想ファイルシステム(不変データ構造)
│
├─ コマンドエンジン
│ ├─ CommandParser(文字列 → 構造体)
│ ├─ CommandRegistry(コマンド名 → ハンドラ)
│ └─ ExerciseJudge(入力 → 正誤判定)
│
└─ UIレイヤー
├─ DojoTerminal(入出力)
├─ ExplanationPanel(解説)
├─ ExerciseTracker(進捗)
├─ LessonComplete(完了画面)
└─ QuizSection(確認クイズ)
HackSimとの違い
同じ「ブラウザ仮想ターミナル」でも、設計の方向性は全く違います。
| 要素 | HackSim | CmdDojo |
|---|---|---|
| 目的 | エンタメ・ゲーム体験 | 学習・スキル定着 |
| ファイルシステム | ゲーム進行に合わせて変化 | 練習用固定構成 |
| 正解判定 | なし(ミッションクリア) | 毎コマンド即時採点 |
| 画面構成 | フルスクリーンターミナル | 左:解説 / 右:ターミナル |
| 状態管理 | useReducer | Zustand + localStorage永続化 |
🔧 実装の詳細
不変ファイルシステム
HackSimではファイルシステムを useReducer で管理していましたが、CmdDojoでは不変データ構造を採用しました。
書き込み操作(mkdir, touch, rm, cp, mv)はすべて新しいルートノードを返す設計です。
export function createDirectory(root: FileNode, targetPath: string): FileNode {
const parts = normalizePath(targetPath).split("/").filter(Boolean)
return insertNode(root, parts, {
name: parts[parts.length - 1],
type: "directory",
// ...
})
}
メリットは2つ。テストが簡単(副作用なし)、Zustandとの相性が良い(差し替えるだけで再レンダリングが起きる)。
コマンドエンジン
コマンドはすべてハンドラとして登録されています。
const registry: Record<string, CommandHandler> = {
ls: handleLs,
cd: handleCd,
cat: handleCat,
mkdir: handleMkdir,
// ...
}
export function dispatchCommand(parsed: ParsedCommand, state: DojoState): CommandResult {
const handler = registry[parsed.command]
if (!handler) return unknownCommandResult(parsed.command)
return handler({ flags: parsed.flags, args: parsed.args, state })
}
各ハンドラは { output: OutputLine[], stateUpdates?: Partial<DojoState> } を返すだけ。副作用なし、純粋関数。
採点ロジック(ExerciseJudge)
練習問題の正解判定は validate 関数をレッスン側に持たせています。
// lesson-01-pwd-ls.ts
{
id: "ex-01-pwd",
instruction: "現在のディレクトリを確認してみましょう。pwd を実行してください。",
validate: (input) => input.trim() === "pwd",
points: 10,
}
判定ロジックをレッスンデータに閉じ込めることで、コマンドエンジン側は採点を知る必要がない。関心の分離がきれいにできました。
Zustand + localStorage永続化
レッスン進捗はページを閉じても保持されます。persist ミドルウェアで progress だけを永続化し、セッション状態(ターミナル出力など)は永続化対象から外しています。
persist(
(set, get) => ({ ... }),
{
name: "cmd-dojo-progress",
partialize: (state) => ({ progress: state.progress }),
}
)
partialize が肝です。出力バッファやコマンド履歴まで永続化するとlocalStorageが肥大化するので、「進んだ記録」だけを保存する設計にしました。
🎨 UIデザインの考え方
HackSimとあえて差別化
HackSimのUIはCRT(ブラウン管)エフェクトが特徴です。緑のスキャンライン、ノイズ、フリッカー——「かっこいい・怖い」演出。
CmdDojoはその逆。VSCode風の落ち着いた学習ツールを目指しました。
- テーマカラー:
#4ec9b0(Teal — VSCodeのインターフェース色) - フォント: JetBrains Mono
- 左パネル: 解説・テーブル・コードブロック付きのドキュメント感
- 右パネル: シンプルな黒背景ターミナル
「勉強してる感」がある方が、学習サービスとして信頼される。エンタメとは逆の方向性です。
画面構成
┌─────────────────┬─────────────────┐
│ 解説パネル │ ターミナル │
│ │ │
│ コマンド概念 │ $ pwd │
│ オプション表 │ /home/user │
│ 使用例 │ $ ls │
│ │ documents ... │
│ ├─────────────────┤
│ │ 練習問題トラッカー│
└─────────────────┴─────────────────┘
左で理解して、右で実践。インプットとアウトプットが同一画面に収まることで「読んでる最中に試せる」体験を実現しました。
🐛 詰まったポイント
1. レッスン完了画面が出ない
一番ハマったバグです。
全問正解すると completeLesson() を呼んで progress.status = "completed" にしていました。そして画面切り替え条件が status !== "completed" だったため、完了した瞬間に条件が false になって完了画面が表示されないというバグが発生。
// NG: completeLesson を呼ぶと即 status = "completed" になる
if (allExercisesDone && progress[id]?.status !== "completed") {
return <LessonComplete />
}
修正はシンプル。ローカルの showComplete state を使い、正解した瞬間に setShowComplete(true) するだけ。
if (allDone) {
completeLesson(lesson.id, totalScore)
setShowComplete(true) // ← これだけ
}
ロジックとUIの状態を分けることで解決しました。
2. ターミナルの下部が見切れる
レッスン画面は「左:解説 / 右:ターミナル+練習トラッカー」の2カラムです。
.layout に height: calc(100vh - 56px) を設定していたのですが、コンテナの .page が min-height: 100vh だったため、フッターで押し出されてターミナルの入力欄が見切れていました。
修正は2点:
.pageをheight: 100dvh; overflow: hiddenに変更(min-heightだと伸びてしまう).layoutのheightを削除しflex: 1に委ねる
dvh(Dynamic Viewport Height)を使ったのもポイント。スマホのアドレスバーが動くと 100vh の計算がズレることがあるため、dvh で実際の表示領域に合わせています。
3. Server ComponentとClient Componentのメタデータ問題
レッスンページ(/lessons/[lessonId])を最初 "use client" で書いていたため、generateMetadata が使えずページタイトルがサイトデフォルトになっていました。
Next.js App Routerでは、"use client" なページコンポーネントにはメタデータをエクスポートできません。
解決策は「Server Componentのラッパー」:
// page.tsx — Server Component(generateMetadata あり)
export async function generateMetadata({ params }) {
const lesson = getLessonById((await params).lessonId)
return { title: `${lesson.title} — CmdDojo` }
}
export default async function LessonPage({ params }) {
const { lessonId } = await params
return <CmdDojoApp lessonId={lessonId} /> // Client Component
}
CmdDojoApp は "use client" のまま、それを包む page.tsx だけ Server Component にすればOK。インポートするだけで Next.js が自動的に境界を処理してくれます。
📊 コース設計の考え方
現時点では「Linuxコマンド入門」コース(5レッスン)のみ公開中です。
| レッスン | テーマ | 主なコマンド |
|---|---|---|
| 01 | 今どこにいる? | pwd, ls, ls -l, ls -la |
| 02 | ディレクトリ移動 | cd, cd .., cd ~ |
| 03 | ファイルを読む | cat, head, tail |
| 04 | ファイル・ディレクトリ作成 | mkdir, touch, mkdir -p |
| 05 | テキスト検索 | grep, grep -r, grep -i |
今後の追加予定コース:
- ファイル権限とオーナー(chmod, chown)
- テキスト処理(sed, awk, sort, cut)
- プロセスとジョブ管理(ps, kill, cron)
- LPIC-1 試験対策(試験範囲の網羅的カバー)
SEOとしては、コース数が増えるほどロングテールキーワードで露出が増えます。1コース追加するたびに5〜10ページの新しいコンテンツが生まれる設計です。
💰 収益化の方向性(構想)
CmdDojoの収益化モデルはまだ確立していません。現時点の構想です。
| モデル | 内容 |
|---|---|
| 無料+プレミアム | 入門コースは無料、LPIC対策コースは有料 |
| 広告 | Google AdSense(学習系は単価が高め) |
| 法人向け | 新入社員Linux研修プランとして販売 |
「ブラウザだけで練習できる」という体験価値は明確なので、プレミアムへの転換率は他サービスより高くなると想定しています。
🔮 今後やりたいこと
- コース拡充: LPIC-1の101/102試験範囲を網羅するコースを追加
- タイムアタックモード: 制限時間内にコマンドを打つゲーム的要素
- 進捗バッジ: コース完了でバッジをもらえる達成感の仕組み
- コマンドサジェスト: タブ補完の実装(体験向上)
- モバイル対応: スマホでも使えるキーボードUI
まとめ
CmdDojoはHackSimで培った「ブラウザ仮想ターミナル」技術を、全く別の方向——学習ツール——に応用したサービスです。
技術的には不変ファイルシステムと純粋関数コマンドエンジンが肝。UIはHackSimの派手さと逆に「落ち着いた学習ツール感」を目指しました。
バグで一番ハマったのは「完了画面が出ない」問題。status の二重管理という設計ミスで、ローカルstateとの役割分担を整理して解決しました。
Linuxコマンドの学習はニーズが普遍的で、検索需要も安定しています。コンテンツを積み上げていけばSEOで継続的に集客できる構造があります。まずはコース拡充から着手していきます。