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 プラグイン
Vite プラグインである @hono/vite-ssg を使うと、簡単に処理を行うことができます。
詳しくは、以下のページを御覧ください:
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 を行いたい場合、それぞれのファイルシステム API 用に 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を key に、拡張子の文字列を value に持つオブジェクトです。 出力するファイルの拡張子を決めるために使われます。pluginsは静的サイトジェネレータの機能を拡張する SSG プラグインの配列です。
出力
toSSG はこのような型の戻り値を返します。
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.拡張子 として保存されることに注意してください。
// 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 のようなことをしたい場合に使います。
例:
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 は 200 以外のステータスレスポンス (リダイレクト、エラー、404 など) をスキップする defaultPlugin を使用します。 これにより、成功しなかったレスポンスのファイル生成が防止されます。
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 は、 /new への meta refresh リダイレクトを含む HTML ファイルを /old.html として生成します。
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'),
],
})