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, head) => {
return c.html(
<html>
<head>
<title>{head.title ?? ''}</title>
</head>
<body>
<p>{content}</p>
</body>
</html>
)
})
await next()
})
app.get('/about', (c) => {
return c.render('Hello!', { title: 'Hono SSG Page' })
})
export default app在 Node.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.html使用 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,可通过对应的 toSSG 函数:
Deno:
import { toSSG } from 'hono/deno'
toSSG(app) // 第二个参数为可选项,类型为 `ToSSGOptions`。Bun:
import { toSSG } from 'hono/bun'
toSSG(app) // 第二个参数为可选项,类型为 `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 会返回如下结果类型:
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'
// 将 `application/x-html` 内容保存为 `.html`
toSSG(app, fs, {
extensionMap: {
'application/x-html': 'html',
...defaultExtensionMap,
},
})注意:以 / 结尾的路径会被保存为 index.ext,与具体扩展名无关。
// 保存到 ./static/html/index.html
app.get('/html/', (c) => c.html('html'))
// 保存到 ./static/text/index.txt
app.get('/text/', (c) => c.text('text'))中间件
Hono 提供了一些内置中间件来辅助 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>
)
}
)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:
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'),
],
})