SSG ヘルパー
SSG ヘルパーは、Hono アプリケーションから静的サイトを生成します。登録されたルートの内容を取得し、静的ファイルとして保存します。
使い方
手動での生成
次のような簡単な Hono アプリケーションがあるとします。
// index.tsx
const app = new Hono()
app.get('/', (c) => c.html('Hello, World!'))
app.use('/about', async (c, next) => {
c.setRenderer((content) => {
return c.html(
<html>
<head />
<body>
<p>{content}</p>
</body>
</html>
)
})
await next()
})
app.get('/about', (c) => {
return c.render(
<>
<title>Hono SSG Page</title>Hello!
</>
)
})
export default appNode.js では、次のようなビルドスクリプトを作成します。
// build.ts
import app from './index'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'
toSSG(app, fs)スクリプトを実行すると、次のようにファイルが出力されます。
ls ./static
about.html index.htmlVite プラグイン
@hono/vite-ssg Vite プラグインを使うと、この処理を簡単に行えます。
詳しくは、次を参照してください。
https://github.com/honojs/vite-plugins/tree/main/packages/ssg
toSSG
toSSG は静的サイト生成の中心となる関数で、アプリケーションとファイルシステムモジュールを引数に取ります。詳細は次のとおりです。
入力
toSSG の引数は ToSSGInterface で定義されています。
export interface ToSSGInterface {
(
app: Hono,
fsModule: FileSystemModule,
options?: ToSSGOptions
): Promise<ToSSGResult>
}appは、ルートを登録したnew Hono()インスタンスを指定します。fsは次のオブジェクトを指定します。node:fs/promiseを想定しています。
export interface FileSystemModule {
writeFile(path: string, data: string | Uint8Array): Promise<void>
mkdir(
path: string,
options: { recursive: boolean }
): Promise<void | string>
}Deno と Bun 用アダプターの使用
Deno や Bun で SSG を使う場合、@hono/deno と @hono/bun パッケージに toSSG 関数が用意されています。
Deno の場合:
import { toSSG } from '@hono/deno'
toSSG(app) // The second argument is an option typed `ToSSGOptions`.Bun の場合:
import { toSSG } from '@hono/bun'
toSSG(app) // The second argument is an option typed `ToSSGOptions`.オプション
オプションは ToSSGOptions インターフェースで定義されています。
export interface ToSSGOptions {
dir?: string
concurrency?: number
extensionMap?: Record<string, string>
plugins?: SSGPlugin[]
}dirは静的ファイルの出力先です。デフォルトは./staticです。concurrencyは同時に生成するファイル数です。デフォルトは2です。extensionMapは、Content-Typeをキー、拡張子の文字列を値とするマップです。出力ファイルの拡張子の決定に使います。pluginsは、静的サイト生成処理の機能を拡張する SSG プラグインの配列です。
出力
toSSG は、次の Result 型で結果を返します。
export interface ToSSGResult {
success: boolean
files: string[]
error?: Error
}ファイルの生成
ルートとファイル名
登録されたルート情報と生成されるファイル名には、次の規則が適用されます。デフォルトの ./static では次のようになります。
/->./static/index.html/path->./static/path.html/path/->./static/path/index.html
ファイルの拡張子
ファイルの拡張子は、各ルートが返す Content-Type によって決まります。たとえば、c.html のレスポンスは .html として保存されます。
ファイルの拡張子をカスタマイズするには、extensionMap オプションを設定します。
import { toSSG, defaultExtensionMap } from 'hono/ssg'
// Save `application/x-html` content with `.html`
toSSG(app, fs, {
extensionMap: {
'application/x-html': 'html',
...defaultExtensionMap,
},
})スラッシュで終わるパスは、拡張子にかかわらず index.ext として保存されます。
// save to ./static/html/index.html
app.get('/html/', (c) => c.html('html'))
// save to ./static/text/index.txt
app.get('/text/', (c) => c.text('text'))ミドルウェア
SSG をサポートする組み込みミドルウェアを紹介します。
ssgParams
Next.js の generateStaticParams に似た API を使用できます。
例:
app.get(
'/shops/:id',
ssgParams(async () => {
const shops = await getShops()
return shops.map((shop) => ({ id: shop.id }))
}),
async (c) => {
const shop = await getShop(c.req.param('id'))
if (!shop) {
return c.notFound()
}
return c.render(
<div>
<h1>{shop.name}</h1>
</div>
)
}
)isSSGContext
isSSGContext は、現在のアプリケーションが toSSG によって開始された SSG コンテキスト内で動いている場合に true を返すヘルパー関数です。
app.get('/page', (c) => {
if (isSSGContext(c)) {
return c.text('This is generated by SSG')
}
return c.text('This is served dynamically')
})disableSSG
disableSSG ミドルウェアを設定したルートは、toSSG による静的ファイルの生成から除外されます。
app.get('/api', disableSSG(), (c) => c.text('an-api'))onlySSG
onlySSG ミドルウェアを設定したルートは、toSSG の実行後に c.notFound() で上書きされます。
app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))プラグイン
プラグインを使うと、静的サイト生成処理の機能を拡張できます。フックにより、生成処理の各段階をカスタマイズします。
デフォルトプラグイン
デフォルトでは、toSSG は defaultPlugin を使い、200 以外のステータスのレスポンス(リダイレクト、エラー、404 など)をスキップします。成功しなかったレスポンスのファイル生成を防ぎます。
import { toSSG, defaultPlugin } from 'hono/ssg'
// defaultPlugin is automatically applied when no plugins specified
toSSG(app, fs)
// Equivalent to:
toSSG(app, fs, { plugins: [defaultPlugin] })カスタムプラグインを指定すると、defaultPlugin は自動では含まれません。カスタムプラグインを追加しつつデフォルト動作を維持するには、明示的に含めてください。
toSSG(app, fs, {
plugins: [defaultPlugin, myCustomPlugin],
})リダイレクトプラグイン
redirectPlugin は、HTTP リダイレクトレスポンス(301、302、303、307、308)を返すルートの HTML リダイレクトページを生成します。生成される HTML には、<meta http-equiv="refresh"> タグと canonical リンクが含まれます。
import { toSSG, redirectPlugin, defaultPlugin } from 'hono/ssg'
toSSG(app, fs, {
plugins: [redirectPlugin(), defaultPlugin()],
})たとえば、アプリケーションに次のルートがある場合:
app.get('/old', (c) => c.redirect('/new'))redirectPlugin は /old.html に HTML ファイルを生成し、meta refresh で /new にリダイレクトします。
NOTE
defaultPlugin と併用する場合は、redirectPlugin を defaultPlugin より前に配置してください。defaultPlugin は 200 以外のレスポンスをスキップするため、先に配置すると redirectPlugin がリダイレクトレスポンスを処理できなくなります。
フックの種類
プラグインは、次のフックで toSSG の処理をカスタマイズできます。
export type BeforeRequestHook = (req: Request) => Request | false
export type AfterResponseHook = (res: Response) => Response | false
export type AfterGenerateHook = (
result: ToSSGResult
) => void | Promise<void>- BeforeRequestHook:各リクエストの処理前に呼び出されます。
falseを返すと、そのルートをスキップします。 - AfterResponseHook:各レスポンスを受け取った後に呼び出されます。
falseを返すと、ファイル生成をスキップします。 - AfterGenerateHook:生成処理全体の完了後に呼び出されます。
プラグインインターフェース
export interface SSGPlugin {
beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
afterResponseHook?: AfterResponseHook | AfterResponseHook[]
afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
}基本的なプラグインの例
GET リクエストだけを対象にします。
const getOnlyPlugin: SSGPlugin = {
beforeRequestHook: (req) => {
if (req.method === 'GET') {
return req
}
return false
},
}ステータスコードで絞り込みます。
const statusFilterPlugin: SSGPlugin = {
afterResponseHook: (res) => {
if (res.status === 200 || res.status === 500) {
return res
}
return false
},
}生成したファイルを記録します。
const logFilesPlugin: SSGPlugin = {
afterGenerateHook: (result) => {
if (result.files) {
result.files.forEach((file) => console.log(file))
}
},
}高度なプラグインの例
次は、sitemap.xml ファイルを生成するサイトマッププラグインの作成例です。
// plugins.ts
import fs from 'node:fs/promises'
import path from 'node:path'
import type { SSGPlugin } from 'hono/ssg'
import { DEFAULT_OUTPUT_DIR } from 'hono/ssg'
export const sitemapPlugin = (baseURL: string): SSGPlugin => {
return {
afterGenerateHook: (result, fsModule, options) => {
const outputDir = options?.dir ?? DEFAULT_OUTPUT_DIR
const filePath = path.join(outputDir, 'sitemap.xml')
const urls = result.files.map((file) =>
new URL(file, baseURL).toString()
)
const siteMapText = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map((url) => `<url><loc>${url}</loc></url>`).join('\n')}
</urlset>`
fsModule.writeFile(filePath, siteMapText)
},
}
}プラグインを適用します。
import app from './index'
import { toSSG } from 'hono/ssg'
import { sitemapPlugin } from './plugins'
toSSG(app, fs, {
plugins: [
getOnlyPlugin,
statusFilterPlugin,
logFilesPlugin,
sitemapPlugin('https://example.com'),
],
})