HyperGen

サーバーセットアップ

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",
});

オプション

オプションデフォルト説明
sseEndpointstring(必須)iframeがSSEストリーミングに接続するURL
sseEventNamestring"message"リッスンするSSEイベント名
swapStrategystring"innerHTML"受信フラグメントに対するHTMXスワップ戦略
themeVarsRecord<string, string>{}:rootに注入される初期CSS変数
extraHeadstring""追加の<head>コンテンツ(スタイル、メタタグ)
htmxUrlstringunpkg v2HTMXのCDN URL
htmxSseUrlstringunpkg v2HTMX SSE拡張のCDN URL
hostOriginstring"*"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-Typetext/event-streamSSEに必須
Cache-Controlno-cache, no-transformキャッシュとプロキシ変換を防止
Connectionkeep-alive接続を維持
X-Accel-Bufferingnonginxのバッファリングを無効化

第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: no

CORS

エージェントサーバーがホストアプリケーションと異なるオリジンで動作する場合、ブートストラップと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-targethx-swap属性に基づいて、iframeのDOMにスワップします。

目次