AI開発収益化ラボ
CmdDojo

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との違い

同じ「ブラウザ仮想ターミナル」でも、設計の方向性は全く違います。

要素HackSimCmdDojo
目的エンタメ・ゲーム体験学習・スキル定着
ファイルシステムゲーム進行に合わせて変化練習用固定構成
正解判定なし(ミッションクリア)毎コマンド即時採点
画面構成フルスクリーンターミナル左:解説 / 右:ターミナル
状態管理useReducerZustand + 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カラムです。

.layoutheight: calc(100vh - 56px) を設定していたのですが、コンテナの .pagemin-height: 100vh だったため、フッターで押し出されてターミナルの入力欄が見切れていました。

修正は2点:

  • .pageheight: 100dvh; overflow: hidden に変更(min-height だと伸びてしまう)
  • .layoutheight を削除し 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で継続的に集客できる構造があります。まずはコース拡充から着手していきます。