npx claudepluginhub kazuph/dotfiles --plugin kazuph-dotfilesWant just this skill?
Add to a custom plugin, then install with one command.
MoonBit + Luna UI (Sol Framework) でのWebアプリ開発。MoonBitコード、Solルーティング、Island Components、Server Actions、D1データベース、Cloudflare Workersデプロイに使用
This skill uses the workspace's default tool permissions.
CLOUDFLARE-DEPLOY.mdECOSYSTEM.mdISLAND-COMPONENTS.mdMOONBIT-FFI.mdPERFORMANCE.mdSERVER-ACTIONS.mdSOL-ROUTING.mdMoonBit + Luna UI 開発ガイド
MoonBitとLuna UI (Sol Framework) を使用したCloudflare Workers向けWebアプリケーション開発のノウハウ。
開発を始める前に必読
アプローチ選択
→ ECOSYSTEM.mdを参照
Luna UI (Sol Framework) 以外にも選択肢があります:
- vite-plugin-moonbit: HMR対応、既存Viteプロジェクトへの統合向け
- mizchi/js: JS FFIバインディング(必須ライブラリ)
- mizchi/npm_typed: Hono, Playwright等50+パッケージのバインディング
FFIを自前で書く前に、必ず既存ライブラリを確認してください。
本ドキュメントの対象
このスキルは Luna UI + Sol Framework を使う場合のガイドです。 シンプルなアプリや既存Viteプロジェクトには vite-plugin-moonbit を推奨します。
技術スタック概要
| 技術 | 役割 |
|---|---|
| MoonBit | メイン言語(WASMターゲット、JSターゲット両対応) |
| Luna UI | UIフレームワーク(Island Architecture) |
| Sol Framework | ルーティング・SSR・Server Actions |
| Cloudflare Workers | ランタイム |
| D1 | SQLiteベースのデータベース |
| Hono | HTTPミドルウェア(認証等) |
プロジェクト構成
project/
├── app/
│ ├── server/
│ │ ├── routes.mbt # ルーティング・ページ・API定義
│ │ ├── db.mbt # D1 FFIバインディング
│ │ └── _using.mbt # 共通インポート
│ ├── client/
│ │ ├── *.mbt # Island Components
│ │ └── _using.mbt
│ └── __gen__/ # 自動生成(.gitignore推奨)
├── src/
│ └── worker.ts # Cloudflare Workerエントリーポイント
├── static/
│ └── loader.js # Luna UIハイドレーションローダー
├── scripts/
│ ├── patch-for-cloudflare.js # CF Workers用パッチ
│ └── bundle-client.js # クライアントバンドル
├── moon.mod.json # MoonBit設定
├── wrangler.json # Cloudflare設定
└── .sol/ # Sol生成物(.gitignore推奨)
開発コマンド(just)
プロジェクトでは just コマンドを使用して開発タスクを実行します。
# 主要コマンド
just dev # 開発サーバー起動(wrangler dev)
just build # 完全ビルド
just deploy # Cloudflare Workersにデプロイ
# ビルド関連
just generate # sol generate 実行
just moon-build # MoonBitビルド
just bundle # クライアントバンドル
just clean # ビルド成果物削除
# テスト関連
just test # MoonBit単体テスト実行
just test-e2e # E2Eテスト実行
just test-all # 全テスト実行
# SSG関連
just ssg # SSGビルド
just ssg-preview # SSGビルド + プレビュー
# 型チェック・リント
just check # moon check 実行
just fmt # moon fmt 実行
ビルドプロセス
# 完全ビルド
pnpm build
# 内部で実行される処理:
# 1. sol generate - __gen__と.solを生成
# 2. moon build --target js - MoonBitをJSにコンパイル
# 3. patch-for-cloudflare.js - CF Workers用にパッチ
# 4. bundle-client.js - Island Componentsをバンドル
wrangler.jsonでの自動ビルド設定
{
"build": {
"command": "pnpm build",
"watch_dir": ["src", "app"]
}
}
SSG/ISR機能
Sol Frameworkは静的サイト生成(SSG)と増分静的再生成(ISR)をサポート。
sol.config.json設定
{
"ssg": {
"enabled": true,
"outDir": ".sol/static",
"routes": ["/", "/about", "/posts/*"]
},
"isr": {
"enabled": true,
"revalidate": 60
},
"metaFiles": {
"sitemap": true,
"rss": true,
"llmsTxt": true
}
}
SSGビルド
# SSGビルド実行
sol build --ssg
# プレビュー
wrangler pages dev .sol/static
ルート別ISR設定
pub fn routes() -> Array[@router.SolRoutes] {
[
@router.SolRoutes::Page(
path="/posts/:slug",
handler=@router.PageHandler(post_page),
title="Post",
meta=[],
revalidate=Some(60), // 60秒ごとに再生成
cache=None,
),
]
}
ISRの動作
- 初回リクエスト: ページを生成しKVにキャッシュ
- revalidate期間内: キャッシュから即時応答
- revalidate期間後: バックグラウンドで再生成、古いキャッシュを返却
- 再生成完了後: 新しいコンテンツをキャッシュ
metaFiles機能
自動生成されるメタファイル。
sitemap.xml
// sol.config.json
{
"metaFiles": {
"sitemap": {
"enabled": true,
"hostname": "https://example.com",
"exclude": ["/admin/*", "/api/*"]
}
}
}
RSS feed (feed.xml)
// sol.config.json
{
"metaFiles": {
"rss": {
"enabled": true,
"title": "My Blog",
"description": "技術ブログ",
"feedPath": "/feed.xml"
}
}
}
llms.txt
// sol.config.json
{
"metaFiles": {
"llmsTxt": {
"enabled": true,
"include": ["/docs/*", "/blog/*"],
"exclude": ["/admin/*"]
}
}
}
生成例:
# My Site
> サイトの説明
## Docs
- /docs/getting-started: Getting Started Guide
- /docs/api: API Reference
## Blog
- /blog/post-1: 記事タイトル1
- /blog/post-2: 記事タイトル2
CSS Utilities機能
Sol FrameworkのCSS最適化機能。
自動CSS抽出
// sol.config.json
{
"css": {
"extract": true,
"minify": true,
"purge": true
}
}
ページ別CSS分割
// sol.config.json
{
"css": {
"splitting": true,
"chunks": {
"/": ["base", "home"],
"/posts/*": ["base", "posts", "markdown"]
}
}
}
インライン化閾値
// sol.config.json
{
"css": {
"inlineThreshold": 4096, // 4KB未満はインライン化
"criticalCss": true // Above-the-fold CSSを抽出
}
}
生成されるHTML:
<head>
<!-- クリティカルCSSはインライン -->
<style>/* critical styles */</style>
<!-- 非クリティカルは非同期ロード -->
<link rel="preload" href="/styles/chunk-posts.css" as="style">
</head>
テスト
MoonBit Unit テスト
テストファイルは *_test.mbt の命名規則。
// app/server/routes_test.mbt
test "parse_slug extracts correct value" {
let result = parse_slug("/posts/hello-world")
assert_eq!(result, Some("hello-world"))
}
test "home_page returns valid html" {
let ctx = mock_page_context("/")
let html = home_page(ctx)
assert_true!(html.contains("<h1>"))
}
実行:
moon test
# または
just test
Integration テスト
// app/server/integration_test.mbt
test "api_create_post with valid data" {
let ctx = mock_api_context(
method="POST",
body="{\"title\": \"Test\", \"content\": \"Hello\"}",
)
let result = api_create_post(ctx)
assert_eq!(result.status, 201)
}
E2E テスト
Playwrightを使用。
// e2e/posts.spec.ts
import { test, expect } from '@playwright/test';
test('create and view post', async ({ page }) => {
await page.goto('/posts/new');
await page.fill('[name="title"]', 'Test Post');
await page.fill('[name="content"]', 'Hello World');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/\/posts\/test-post/);
await expect(page.locator('h1')).toContainText('Test Post');
});
実行:
pnpm test:e2e
# または
just test-e2e
詳細リファレンス
- ルーティングとページ定義 → SOL-ROUTING.md
- Island Components → ISLAND-COMPONENTS.md
- Server Actions → SERVER-ACTIONS.md
- MoonBit FFIパターン → MOONBIT-FFI.md
- Cloudflare Workersデプロイ → CLOUDFLARE-DEPLOY.md
- パフォーマンス最適化 → PERFORMANCE.md
クイックリファレンス
ルート定義(routes.mbt)
pub fn routes() -> Array[@router.SolRoutes] {
[
@router.SolRoutes::Page(
path="/",
handler=@router.PageHandler(home_page),
title="Home",
meta=[], revalidate=None, cache=None,
),
@router.SolRoutes::Post(
path="/api/posts",
handler=@router.ApiHandler(api_create_post),
),
]
}
Island Component(クライアント)
pub fn my_component(props : MyProps) -> DomNode {
let count = @signal.signal(0)
div(class="container", [
button(
on=events().click(fn(_) { count.set(count.get() + 1) }),
[text_of(count)]
)
])
}
Server Action
let create_action : @action.ActionHandler = @action.ActionHandler(async fn(ctx) {
let body = ctx.body
let data = parse_json(body)
// 処理...
@action.ActionResult::ok({ message: "Success" })
})
pub fn action_registry() -> @action.ActionRegistry {
@action.ActionRegistry::new(allowed_origins=[
"http://localhost:8787",
"https://your-app.workers.dev",
]).register(
@action.ActionDef::new("create", create_action)
)
}
D1 FFI
extern "js" fn db_query(sql : String) -> @core.Promise[@core.Any] =
#| async (sql) => {
#| const db = globalThis.__D1_DB;
#| return await db.prepare(sql).all();
#| }
よくある問題と解決策
403エラー(Server Actions)
→ action_registry()のallowed_originsに本番ドメインを追加
CSSが適用されない(Island Component)
→ CSSクラス名がroutes.mbtのスタイル定義と一致しているか確認
ビルド後にモジュールエラー
→ scripts/patch-for-cloudflare.jsでCF Workers非互換コードをパッチ
デプロイ時にビルドされない
→ wrangler.jsonにbuild.commandを設定
ハイドレーションエラー(entries_json undefined等)
→ 重要: 日本語や絵文字を含むデータをIsland Componentに渡す場合、json_stringifyでASCII-safeなJSON生成が必須。Luna UIのluna:state属性エスケープ処理がUTF-16サロゲートペアを正しく処理できないため。詳細はPERFORMANCE.md参照
CLS(レイアウトシフト)が高い
→ スケルトン(フォールバック)の高さを実際のコンテンツと近似させる。計算式: エントリー数 * 21px + 30px。詳細はPERFORMANCE.md参照
SSGビルドでルートが見つからない
→ sol.config.jsonのssg.routesにワイルドカード(/posts/*)を使用している場合、動的ルートのスラッグ一覧を返す関数が必要
pub fn get_static_paths() -> Array[String] {
// DBまたはファイルシステムから全スラッグを取得
["post-1", "post-2", "post-3"]
}
SSGで動的データが古い
→ SSGはビルド時のデータを使用。頻繁に更新されるデータにはISRを使用するか、クライアントサイドフェッチを組み合わせる
ISRが再生成されない
→ 確認事項:
revalidateがルート定義で設定されているか- KVバインディングが
wrangler.jsonで設定されているか sol.config.jsonでisr.enabled: trueか
ISRキャッシュをクリアしたい
→ 手動クリア方法:
# 特定ルートのキャッシュを削除
wrangler kv:key delete --binding=SOL_CACHE "/posts/slug-name"
# 全キャッシュをクリア
wrangler kv:bulk delete --binding=SOL_CACHE keys.json
sitemap.xmlが生成されない
→ sol.config.jsonでmetaFiles.sitemap.hostnameが設定されているか確認。ホスト名がないとsitemapは生成されない
CSSが重複してロードされる
→ css.splittingが有効な場合、チャンク定義が重複していないか確認。共通スタイルはbaseチャンクにまとめる