Appearance
SSG 助手
🌐 SSG Helper
SSG Helper 会从你的 Hono 应用生成一个静态网站。它将获取已注册路由的内容并将其保存为静态文件。
🌐 SSG Helper generates a static site from your Hono application. It will retrieve the contents of registered routes and save them as static files.
用法
🌐 Usage
手动
🌐 Manual
如果你有一个简单的 Hono 应用,如下所示:
🌐 If you have a simple Hono application like the following:
tsx
// 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 app对于 Node.js,创建如下构建脚本:
🌐 For Node.js, create a build script like this:
ts
// build.ts
import app from './index'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'
toSSG(app, fs)通过执行脚本,文件将输出如下:
🌐 By executing the script, the files will be output as follows:
bash
ls ./static
about.html index.htmlVite 插件
🌐 Vite Plugin
使用 @hono/vite-ssg Vite 插件,你可以轻松处理这个过程。
🌐 Using the @hono/vite-ssg Vite Plugin, you can easily handle the process.
有关更多详细信息,请参见此处:
🌐 For more details, see here:
https://github.com/honojs/vite-plugins/tree/main/packages/ssg
toSSG
toSSG 是用于生成静态网站的主要函数,它以一个应用和一个文件系统模块作为参数。它基于以下内容:
输入
🌐 Input
toSSG 的参数在 ToSSGInterface 中指定。
🌐 The arguments for toSSG are specified in ToSSGInterface.
ts
export interface ToSSGInterface {
(
app: Hono,
fsModule: FileSystemModule,
options?: ToSSGOptions
): Promise<ToSSGResult>
}app指定了带有注册路由的new Hono()。fs指定以下对象,假设node:fs/promise。
ts
export interface FileSystemModule {
writeFile(path: string, data: string | Uint8Array): Promise<void>
mkdir(
path: string,
options: { recursive: boolean }
): Promise<void | string>
}使用 Deno 和 Bun 的适配器
🌐 Using adapters for Deno and Bun
如果你想在 Deno 或 Bun 上使用 SSG,每个文件系统都提供了一个 toSSG 函数。
🌐 If you want to use SSG on Deno or Bun, a toSSG function is provided for each file system.
对于 Deno:
🌐 For Deno:
ts
import { toSSG } from 'hono/deno'
toSSG(app) // The second argument is an option typed `ToSSGOptions`.对于 Bun:
🌐 For Bun:
ts
import { toSSG } from 'hono/bun'
toSSG(app) // The second argument is an option typed `ToSSGOptions`.选项
🌐 Options
选项在 ToSSGOptions 接口中指定。
🌐 Options are specified in the ToSSGOptions interface.
ts
export interface ToSSGOptions {
dir?: string
concurrency?: number
extensionMap?: Record<string, string>
plugins?: SSGPlugin[]
}dir是静态文件的输出目标。默认值是./static。concurrency是同时生成的文件数量。默认值是2。extensionMap是一个包含Content-Type作为键以及扩展名字符串作为值的映射。这用于确定输出文件的文件扩展名。plugins是一个 SSG 插件数组,用于扩展静态网站生成过程的功能。
输出
🌐 Output
toSSG 返回以下 Result 类型的结果。
ts
export interface ToSSGResult {
success: boolean
files: string[]
error?: Error
}生成文件
🌐 Generate File
路由和文件名
🌐 Route and Filename
以下规则适用于注册的路线信息和生成的文件名。默认的 ./static 的行为如下:
🌐 The following rules apply to the registered route information and the generated file name. The default ./static behaves as follows:
/->./static/index.html/path->./static/path.html/path/->./static/path/index.html
文件扩展名
🌐 File Extension
文件扩展名取决于每个路由返回的 Content-Type。例如,c.html 的响应被保存为 .html。
🌐 The file extension depends on the Content-Type returned by each route. For example, responses from c.html are saved as .html.
如果你想自定义文件扩展名,请设置 extensionMap 选项。
🌐 If you want to customize the file extensions, set the extensionMap option.
ts
import { toSSG, defaultExtensionMap } from 'hono/ssg'
// Save `application/x-html` content with `.html`
toSSG(app, fs, {
extensionMap: {
'application/x-html': 'html',
...defaultExtensionMap,
},
})请注意,无论扩展名是什么,以斜杠结尾的路径都会保存为 index.ext。
🌐 Note that paths ending with a slash are saved as index.ext regardless of the extension.
ts
// 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'))中间件
🌐 Middleware
引入支持 SSG 的内置中间件。
🌐 Introducing built-in middleware that supports SSG.
ssgParams
你可以使用像 Next.js 的 generateStaticParams 这样的 API。
🌐 You can use an API like generateStaticParams of Next.js.
示例:
🌐 Example:
ts
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。
ts
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 排除在静态文件生成之外。
🌐 Routes with the disableSSG middleware set are excluded from static file generation by toSSG.
ts
app.get('/api', disableSSG(), (c) => c.text('an-api'))onlySSG
在执行 toSSG 后,设置了 onlySSG 中间件的路由将被 c.notFound() 覆盖。
🌐 Routes with the onlySSG middleware set will be overridden by c.notFound() after toSSG execution.
ts
app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))插件
🌐 Plugins
插件允许你扩展静态站点生成过程的功能。它们使用钩子在不同阶段自定义生成过程。
🌐 Plugins allow you to extend the functionality of the static site generation process. They use hooks to customize the generation process at different stages.
默认插件
🌐 Default Plugin
默认情况下,toSSG 使用 defaultPlugin,它会跳过非 200 状态的响应(如重定向、错误或 404)。这可以防止为不成功的响应生成文件。
🌐 By default, toSSG uses defaultPlugin which skips non-200 status responses (like redirects, errors, or 404s). This prevents generating files for unsuccessful responses.
ts
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 不会 自动包含。要在添加自定义插件的同时保留默认行为,请明确包含它:
🌐 If you specify custom plugins, defaultPlugin is not automatically included. To keep the default behavior while adding custom plugins, explicitly include it:
ts
toSSG(app, fs, {
plugins: [defaultPlugin, myCustomPlugin],
})重定向插件
🌐 Redirect Plugin
redirectPlugin 为返回 HTTP 重定向响应(301、302、303、307、308)的路由生成 HTML 重定向页面。生成的 HTML 包含一个 <meta http-equiv="refresh"> 标签和一个规范链接。
🌐 The redirectPlugin generates HTML redirect pages for routes that return HTTP redirect responses (301, 302, 303, 307, 308). The generated HTML includes a <meta http-equiv="refresh"> tag and a canonical link.
ts
import { toSSG, redirectPlugin, defaultPlugin } from 'hono/ssg'
toSSG(app, fs, {
plugins: [redirectPlugin(), defaultPlugin()],
})例如,如果你的应用有:
🌐 For example, if your app has:
ts
app.get('/old', (c) => c.redirect('/new'))redirectPlugin 将在 /old.html 生成一个 HTML 文件,该文件包含指向 /new 的 meta 刷新重定向。
🌐 The redirectPlugin will generate an HTML file at /old.html with a meta refresh redirect to /new.
NOTE
当与 defaultPlugin 一起使用时,将 redirectPlugin 放在 defaultPlugin 之前。由于 defaultPlugin 会跳过非 200 响应,将其放在前面会阻止 redirectPlugin 处理重定向响应。
钩子类型
🌐 Hook Types
插件可以使用以下钩子来自定义 toSSG 过程:
🌐 Plugins can use the following hooks to customize the toSSG process:
ts
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:在整个生成过程完成后调用。
插件界面
🌐 Plugin Interface
ts
export interface SSGPlugin {
beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
afterResponseHook?: AfterResponseHook | AfterResponseHook[]
afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
}基本插件示例
🌐 Basic Plugin Examples
仅过滤 GET 请求:
🌐 Filter only GET requests:
ts
const getOnlyPlugin: SSGPlugin = {
beforeRequestHook: (req) => {
if (req.method === 'GET') {
return req
}
return false
},
}按状态码过滤:
🌐 Filter by status code:
ts
const statusFilterPlugin: SSGPlugin = {
afterResponseHook: (res) => {
if (res.status === 200 || res.status === 500) {
return res
}
return false
},
}日志生成的文件:
🌐 Log generated files:
ts
const logFilesPlugin: SSGPlugin = {
afterGenerateHook: (result) => {
if (result.files) {
result.files.forEach((file) => console.log(file))
}
},
}高级插件示例
🌐 Advanced Plugin Example
这是创建一个生成 sitemap.xml 文件的网站地图插件的示例:
🌐 Here's an example of creating a sitemap plugin that generates a sitemap.xml file:
ts
// 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)
},
}
}应用插件:
🌐 Applying plugins:
ts
import app from './index'
import { toSSG } from 'hono/ssg'
import { sitemapPlugin } from './plugins'
toSSG(app, fs, {
plugins: [
getOnlyPlugin,
statusFilterPlugin,
logFilesPlugin,
sitemapPlugin('https://example.com'),
],
})