sobria
ページの一覧

デプロイ

sobria のアプリは Hono のアプリなので、Hono が対応するランタイムで動作します。

ビルドの出力

vite build は、アプリを dist/ に出力します。

パス内容
dist/client/要素のモジュール、CSS、書き出したページ
dist/server/entry.jsサーバーのエントリーをビルドしたもの
dist/server/package.jsonentry.js を ES モジュールとして読み込む設定

サーバーのエントリーは、src/server.ts があればそのファイル、無ければ sobria/app です。sobria、hono、TSX のソースのまま配布された要素のライブラリ(外部からの利用)と、src/server.ts が import した @hono/node-server は、dist/server/ に含まれます。Node と Bun では、アプリが import するほかのパッケージは dist/server/ に含まれないので、起動する環境にもインストールします。

dist/client/ の静的ファイルは、サーバーのエントリーより先に配信するように設定します。Node と Bun の例では serveStatic が、Workers では静的アセットの配信が、リクエストをサーバーのエントリーより先に処理します。

Node

sobria create で作成した src/server.ts は、そのまま Node で起動できます。

// src/server.ts
/// <reference types="node" />

import { fileURLToPath } from "node:url";

import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { Hono } from "hono";
import app from "sobria/app";

const server = new Hono();
if (import.meta.main) {
  server.use(serveStatic({ root: fileURLToPath(new URL("../client", import.meta.url)) }));
}
server.route("/", app);

if (import.meta.main) serve({ fetch: server.fetch, port: Number(process.env.PORT ?? 3000) });

export default server;
pnpm build
node dist/server/entry.js

import.meta.main は、node や bun のコマンドで直接実行したときだけ true になります。開発サーバーと vite preview は src/server.ts を import するので、サーバーを二重に起動しません。dist/client/ の場所は import.meta.url からの相対パスで指定しているので、どのディレクトリから起動しても同じファイルを配信します。ポートは、環境変数 PORT で変更できます。

Bun

Bun は、直接実行したモジュールが default で fetch を持つ値を export していれば、その値でサーバーを起動します。Bun で起動するときは、Node の src/server.ts から serve() の呼び出しと、その import を削除します。静的ファイルは、Node と同じ @hono/node-server/serve-static で配信します。

// src/server.ts
/// <reference types="node" />

import { fileURLToPath } from "node:url";

import { serveStatic } from "@hono/node-server/serve-static";
import { Hono } from "hono";
import app from "sobria/app";

const server = new Hono();
if (import.meta.main) {
  server.use(serveStatic({ root: fileURLToPath(new URL("../client", import.meta.url)) }));
}
server.route("/", app);

export default server;
pnpm build
bun dist/server/entry.js

この src/server.ts からビルドした dist/server/entry.js を node で実行しても、サーバーは起動しません。

Cloudflare Workers

Workers には、Cloudflare の Vite プラグイン @cloudflare/vite-plugin を使用します。

pnpm add -D @cloudflare/vite-plugin wrangler
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import { sobria } from "sobria/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    sobria(),
    cloudflare({
      viteEnvironment: { name: "ssr" },
      config: {
        name: "my-app",
        main: "sobria/app",
        compatibility_date: "2026-07-01",
        compatibility_flags: ["nodejs_als"],
        assets: { html_handling: "drop-trailing-slash" },
      },
    }),
  ],
});
設定内容
viteEnvironment: { name: "ssr" }sobria のサーバーのビルドを Worker として実行する
main: "sobria/app"Worker のエントリーを sobria のアプリにする
compatibility_flags: ["nodejs_als"]リクエストごとの Context を保持するために、node:async_hooks の AsyncLocalStorage を使用できるようにする
assets: { html_handling: "drop-trailing-slash" }書き出したページを /about の URL で返す。デフォルト値では、/about を /about/ にリダイレクトする

雛形の src/server.ts は Node で起動するためのファイルなので、削除します。Durable Object のクラスのように、アプリのほかにも export する値があるときは、src/server.ts を作成して main に ./src/server.ts を指定します。compatibility_date に、使用している wrangler に同梱された Workers のランタイム(workerd)のリリース日より新しい日付を指定すると、Worker が起動しません。

vite で起動する開発サーバーでは、ページとサーバー関数を workerd で実行します。ビルドした後は、wrangler で配置します。

pnpm build
pnpm exec wrangler deploy

dist/client/ は、Workers の静的アセットとして配信します。静的アセットは、書き出したページの URL への POST に 405 を返します。そのため、この設定では、書き出したページのフォームからサーバー関数に送信できません。

書き出したページからフォームを送信する

書き出したページのフォームを使用するときは、assets の run_worker_first に指定したパスのリクエストを、静的アセットより先に Worker で受信します。Worker のエントリーの src/server.ts は、GET と HEAD のリクエストを静的アセットに渡します。GET と HEAD のリクエストのうち静的アセットが 404 を返したものと、ほかのメソッドのリクエストは、sobria のアプリに渡します。

// src/server.ts
import type { ExecutionContext } from "hono";
import app from "sobria/app";

interface Env {
  readonly ASSETS?: { fetch(request: Request): Promise<Response> };
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    if (env.ASSETS && (request.method === "GET" || request.method === "HEAD")) {
      const asset = await env.ASSETS.fetch(request);
      if (asset.status !== 404) return asset;
    }
    return app.fetch(request, env, ctx);
  },
};
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";
import { sobria } from "sobria/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    sobria(),
    cloudflare({
      viteEnvironment: { name: "ssr" },
      config: {
        name: "my-app",
        main: "./src/server.ts",
        compatibility_date: "2026-07-01",
        compatibility_flags: ["nodejs_als"],
        assets: {
          binding: "ASSETS",
          html_handling: "drop-trailing-slash",
          run_worker_first: ["/*", "!/assets/*"],
        },
      },
    }),
  ],
});

この設定では、/assets/ の外のすべてのリクエストで Worker を起動します。/assets/ は、要素のモジュールと CSS を出力する Vite の build.assetsDir のデフォルト値です。build.assetsDir を変更したときは、!/assets/* のパスも変更します。vite build がページを書き出すときに src/server.ts に渡す env には ASSETS が無いので、env に ASSETS が無いときも、リクエストをアプリに渡します。

ページを書き出す

ページと route.ts が prerender を export すると、vite build は、その URL のレスポンスをファイルとして dist/client/ に書き出します。書き出したファイルは静的ファイルとして配信するので、リクエストのたびに描画しません。

// src/routes/about/page.tsx
import type { Prerender } from "sobria/router";

export const prerender: Prerender<"/about"> = true;

export default function About() {
  return <h1>about</h1>;
}

パラメータのあるページでは、書き出すパラメータの配列を返す関数を export します。関数は async 関数にもできます。

// src/routes/posts/[slug]/page.tsx
import type { PageProps, Prerender } from "sobria/router";

const posts = [
  { slug: "hello", title: "はじめての投稿" },
  { slug: "release", title: "リリースのお知らせ" },
];

export const prerender: Prerender<"/posts/[slug]"> = () =>
  posts.map((post) => ({ slug: post.slug }));

export default function Post({ params }: PageProps<"/posts/[slug]">) {
  const post = posts.find((post) => post.slug === params.slug);
  return <h1>{post?.title}</h1>;
}

ページは <URL のパス>/index.html に保存します。例えば、/posts/hello は dist/client/posts/hello/index.html になります。

route.ts は、パラメータの無いルートでだけ prerender = true を指定でき、GET のレスポンスを URL のパスのとおりに保存します。URL のパスの最後の部分には、feed.xml の .xml のような拡張子が必要です。拡張子が無いと、ビルドがエラーになります。

// src/routes/feed.xml/route.ts
export const prerender = true;

export const GET = () =>
  new Response("<feed></feed>", { headers: { "content-type": "application/xml" } });

書き出しには、次の規則があります。

  • 書き出すときは、すべての <Loading> の内側の描画が完了するまで待ってから HTML にします
  • レスポンスが 200 でない URL があると、ビルドがエラーになります。認証のミドルウェアがリダイレクトしたページも、エラーになります
  • 書き出したページを配信するときは、middleware.ts を実行しません。利用者ごとに内容が変わるページには、prerender を指定しないでください
  • 書き出したページで refreshPage() を呼び出すと、ページ全体を再読み込みします
  • 開発サーバーは、prerender を指定したページも、リクエストのたびに描画します

パスの下に配置する

Vite の設定 base に /docs/ のようなパスを指定すると、アプリをそのパスの下に配置できます。base は / で始めます。ページ、アセット、サーバー関数の URL は base から始まり、base の外のパスには 404 を返します。

// vite.config.ts
import { sobria } from "sobria/vite";
import { defineConfig } from "vite";

export default defineConfig({
  base: "/docs/",
  plugins: [sobria()],
});

dist/client/ のファイルのパスには、base を含めません。静的ファイルを配信するときは、URL のパスから base を除いてファイルを探します。Node と Bun の例では、serveStatic に rewriteRequestPath を指定します。

server.use(
  serveStatic({
    root: fileURLToPath(new URL("../client", import.meta.url)),
    rewriteRequestPath: (path) => path.replace(/^\/docs\//, "/"),
  }),
);

サーバーを置かずに配置する

Vite プラグインの設定 output を "static" にすると、すべてのページを書き出し、dist/client/ だけを静的なサイトとして配置できます。

// vite.config.ts
import { sobria } from "sobria/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [sobria({ output: "static" })],
});

output: "static" のビルドは、次のように動作します。

対象動作
パラメータの無いページprerender を export しなくても書き出す
パラメータのあるページprerender の関数が無ければ、ビルドがエラーになる
route.tsパラメータが無く、GET だけを export し、URL のパスの最後の部分に拡張子があれば書き出す。それ以外はエラーになる
サーバー関数を定義したファイルビルドがエラーになる
見つからない URL のページ描画した HTML を dist/client/404.html に保存する
dist/server/書き出した後で削除する

見つからない URL のページには、ルートのディレクトリか、URL の区切りの無いグループのディレクトリの not-found.tsx のうち、ディレクトリが最も浅いものを使用し、無ければデフォルトのページを使用します。書き出すときは、その not-found.tsx のディレクトリまでのすべての middleware.ts を実行し、レスポンスが 404 でなければ(リダイレクトなど)、ビルドがエラーになります。

public/404.html があると、書き出す 404.html と出力先のパスが重複するので、ビルドがエラーになります。404.html を見つからない URL に返すように、配置先のサービスを設定してください。vite preview は、一致するファイルが無い URL に 404.html を 404 のステータスで返します。

output: "static" の開発サーバーは、パラメータのあるページで prerender の関数を呼び出し、その戻り値に無いパラメータの URL には 404 を返します。