サーバーセットアップ
HyperGenサーバーのセットアップ詳細ガイド — ブートストラップドキュメント、SSEストリーミング、レスポンスヘルパー、フレームワーク別の例。
サーバーセットアップ
HyperGenのサーバーサイドには2つの責務があります: iframeブートストラップドキュメントの提供と、SSE経由でのHTMLフラグメントのストリーミングです。必要なものは1つのファイルhypergen-server.tsにすべて含まれています。
API概要
| エクスポート | 目的 |
|---|---|
bootstrapHtml(options) | iframeのHTMLドキュメントを生成 |
createSSEStream(source, options?) | 非同期ジェネレーターからSSEイベントのReadableStreamを作成 |
streamHtml(source, eventName?) | SSEフォーマットの文字列をyieldする低レベル非同期ジェネレーター |
HyperGenResponse(stream, extraHeaders?) | ストリームを正しいSSEヘッダー付きのResponseでラップ |
formatSSEEvent(event) | 単一のSSEEventオブジェクトをSSE文字列にフォーマット |
bootstrapHtml()
サンドボックス化されたiframe内に読み込まれる完全なHTMLドキュメントを生成します。このドキュメントには、CDNからのHTMX + SSE拡張、SSE接続ルート要素(#hg-root)、ホスト-iframe通信用のpostMessageブリッジ、自動リサイズ用のResizeObserver、--hg-* CSS変数サポート付きの基本スタイルが含まれます。
import { bootstrapHtml } from "./hypergen-server";
const html = bootstrapHtml({
sseEndpoint: "/api/stream",
});オプション
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
sseEndpoint | string | (必須) | iframeがSSEストリーミングに接続するURL |
sseEventName | string | "message" | リッスンするSSEイベント名 |
swapStrategy | string | "innerHTML" | 受信フラグメントに対するHTMXスワップ戦略 |
themeVars | Record<string, string> | {} | :rootに注入される初期CSS変数 |
extraHead | string | "" | 追加の<head>コンテンツ(スタイル、メタタグ) |
htmxUrl | string | unpkg v2 | HTMXのCDN URL |
htmxSseUrl | string | unpkg v2 | HTMX SSE拡張のCDN URL |
hostOrigin | string | "*" | postMessageのターゲットを制限(本番環境では設定必須) |
スワップ戦略
swapStrategyオプションは、HTMXがストリーミングされたフラグメントを挿入する方法を制御します:
| 戦略 | 動作 |
|---|---|
"innerHTML" | #hg-rootのコンテンツを置換(デフォルト — 単一ビューUIに最適) |
"beforeend" | 最後の子要素の後に追加(チャット、ログ、シーケンシャルコンテンツに最適) |
"outerHTML" | ターゲット要素全体を置換 |
"afterend" | ターゲット要素の後に挿入 |
"beforebegin" | ターゲット要素の前に挿入 |
"afterbegin" | 最初の子要素として挿入 |
"none" | スワップしない(副作用のみのイベントに使用) |
カスタムスタイルの追加
extraHeadを使用して、iframe内にウィジェット固有のCSSを注入します:
const html = bootstrapHtml({
sseEndpoint: "/api/stream",
swapStrategy: "beforeend",
extraHead: `<style>
body { padding: 16px; }
.card {
background: var(--hg-surface-elevated);
border: 1px solid var(--hg-border);
border-radius: var(--hg-radius);
padding: var(--hg-space-4);
margin-bottom: var(--hg-space-2);
}
</style>`,
});createSSEStream()
SSEストリームを作成するためのプライマリAPI。HTML文字列をyieldする非同期ジェネレーター(または任意の非同期イテラブル)を渡すと、HTTPレスポンスボディに適したReadableStreamが返されます。
import { createSSEStream } from "./hypergen-server";
const stream = createSSEStream(async function* () {
yield "<h1>Hello</h1>";
await someAsyncWork();
yield "<p>Done!</p>";
});ソース引数
最初の引数は以下のいずれかです:
- 非同期ジェネレーター関数 (
async function* () { ... }) — 最も一般的 - 非同期イテラブル (
[Symbol.asyncIterator]を持つ任意のオブジェクト) - 同期イテラブル (テスト用の文字列配列など)
- ファクトリ関数 (上記のいずれかを返す
() => myIterable)
オプション
第2引数はSSEStreamOptionsオブジェクトまたはプレーンな文字列(eventNameとして扱われる)を受け取ります:
// オブジェクト形式
const stream = createSSEStream(source, {
eventName: "fragment", // SSEイベント名 (デフォルト: "message")
keepAliveInterval: 30_000, // キープアライブpingの間隔(ミリ秒、デフォルト: 15000)
});
// 文字列の省略形
const stream = createSSEStream(source, "fragment");キープアライブping
プロキシやロードバランサーがアイドル接続を閉じるのを防ぐため、設定された間隔でコメント行(: keepalive)が送信されます。無効にするにはkeepAliveInterval: 0を設定してください。
HyperGenResponse()
正しいSSEヘッダー付きの標準Responseを作成します。Web Response APIをサポートする任意のランタイムで動作します。
import { createSSEStream, HyperGenResponse } from "./hypergen-server";
const stream = createSSEStream(myGenerator());
return HyperGenResponse(stream);レスポンスには以下のヘッダーが含まれます:
| ヘッダー | 値 | 目的 |
|---|---|---|
Content-Type | text/event-stream | SSEに必須 |
Cache-Control | no-cache, no-transform | キャッシュとプロキシ変換を防止 |
Connection | keep-alive | 接続を維持 |
X-Accel-Buffering | no | nginxのバッファリングを無効化 |
第2引数でカスタムヘッダーを追加:
return HyperGenResponse(stream, {
"Access-Control-Allow-Origin": "https://myapp.com",
});streamHtml()
ストリームの手動制御が必要な場合のための低レベル非同期ジェネレーター。ソースからyieldされた各文字列が1つのSSEイベントになります。
import { streamHtml } from "./hypergen-server";
for await (const chunk of streamHtml(myFragments)) {
writer.write(chunk);
}これはcreateSSEStream()が内部的に使用する構成要素です。カスタムストリーミングメカニズムと統合する必要がない限り、createSSEStream()を推奨します。
formatSSEEvent()
単一のSSEイベントオブジェクトを仕様準拠の文字列にフォーマットします。SSEペイロードを手動で構築する必要がある場合に便利です。
import { formatSSEEvent } from "./hypergen-server";
const sseString = formatSSEEvent({
event: "fragment",
data: "<h1>Hello</h1>",
id: "msg-1",
});
// "event: fragment\nid: msg-1\ndata: <h1>Hello</h1>\n\n"フレームワーク別の例
Hono
import { Hono } from "hono";
import { bootstrapHtml, createSSEStream, HyperGenResponse } from "./hypergen-server";
const app = new Hono();
app.get("/bootstrap", (c) => {
return c.html(bootstrapHtml({ sseEndpoint: "/api/stream" }));
});
app.get("/api/stream", () => {
return HyperGenResponse(
createSSEStream(async function* () {
yield "<p>Hello from Hono!</p>";
}),
);
});Express
Expressは標準のWeb Response APIをネイティブサポートしていません。ストリームを直接res.write()で使用します:
import express from "express";
import { bootstrapHtml, streamHtml } from "./hypergen-server";
const app = express();
app.get("/bootstrap", (req, res) => {
res.type("html").send(bootstrapHtml({ sseEndpoint: "/api/stream" }));
});
app.get("/api/stream", async (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
});
const fragments = async function* () {
yield "<p>Hello from Express!</p>";
};
for await (const chunk of streamHtml(fragments())) {
res.write(chunk);
}
res.end();
});
app.listen(3000);Expressアダプター
ExpressでResponse APIを使いたい場合は、@hono/node-serverのようなアダプターの使用か、ストリーミングサポートが改善されたExpress 5+の利用を検討してください。
Next.js Route Handlers
// app/api/bootstrap/route.ts
import { bootstrapHtml } from "@/lib/hypergen-server";
export function GET() {
return new Response(
bootstrapHtml({ sseEndpoint: "/api/stream" }),
{ headers: { "Content-Type": "text/html" } },
);
}// app/api/stream/route.ts
import { createSSEStream, HyperGenResponse } from "@/lib/hypergen-server";
export function GET() {
return HyperGenResponse(
createSSEStream(async function* () {
yield "<p>Hello from Next.js!</p>";
}),
);
}Deno
import { bootstrapHtml, createSSEStream, HyperGenResponse } from "./hypergen-server.ts";
Deno.serve({ port: 3000 }, (req: Request) => {
const url = new URL(req.url);
if (url.pathname === "/bootstrap") {
return new Response(
bootstrapHtml({ sseEndpoint: "/api/stream" }),
{ headers: { "Content-Type": "text/html" } },
);
}
if (url.pathname === "/api/stream") {
return HyperGenResponse(
createSSEStream(async function* () {
yield "<p>Hello from Deno!</p>";
}),
);
}
return new Response("Not found", { status: 404 });
});SSEヘッダーとCORS
必須ヘッダー
HyperGenResponse()はこれらを自動的に設定します。手動でレスポンスを構築する場合は、すべて含めてください:
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: noCORS
エージェントサーバーがホストアプリケーションと異なるオリジンで動作する場合、ブートストラップとSSEの両エンドポイントにCORSヘッダーが必要です:
return HyperGenResponse(stream, {
"Access-Control-Allow-Origin": "https://myapp.com",
"Access-Control-Allow-Credentials": "true",
});セキュリティ
本番環境では、bootstrapHtml()のhostOriginを"*"ではなく実際のホストオリジンに必ず設定してください。これにより、iframeがpostMessageイベントを受け入れるオリジンが制限されます。
リバースプロキシ設定
SSE接続はリバースプロキシでの特別な処理が必要です:
Nginx:
location /api/stream {
proxy_pass http://backend;
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
}Cloudflare: SSEはそのまま動作します。X-Accel-Buffering: noヘッダー(HyperGenResponseが設定)により、早期のバッファリングが防止されます。
ユーザーインタラクションの処理
エージェントがhx-postなどのHTMX属性付きHTMLを生成すると、ユーザーインタラクションはiframeから直接サーバーに送信されます。標準HTTPエンドポイントで処理します:
app.post("/api/action", async (c) => {
const body = await c.req.parseBody();
const action = body["action"] as string;
switch (action) {
case "approve":
return c.html('<div class="success">Approved!</div>');
case "refresh":
const data = await fetchLatestData();
return c.html(renderDashboard(data));
default:
return c.html('<div class="error">Unknown action</div>');
}
});POSTエンドポイントからのレスポンスはプレーンHTMLです — HTMXはトリガー要素のhx-targetとhx-swap属性に基づいて、iframeのDOMにスワップします。