跳转到正文

SSG 辅助工具 ​

SSG 辅助工具从 Hono 应用生成静态网站。它获取已注册路由的内容,并将其保存为静态文件。

用法 ​

手动生成 ​

假设有如下简单的 Hono 应用:

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 中,创建以下构建脚本:

ts
// build.ts
import app from './index'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'

toSSG(app, fs)

执行脚本后,将输出以下文件:

bash
ls ./static
about.html  index.html

Vite 插件 ​

使用 @hono/vite-ssg Vite 插件,可以轻松完成这一过程。

更多详情请参阅:

https://github.com/honojs/vite-plugins/tree/main/packages/ssg

toSSG ​

toSSG 是生成静态网站的主要函数,接收应用和文件系统模块作为参数,具体如下:

输入 ​

toSSG 的参数由 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 适配器 ​

如果想在 Deno 或 Bun 中使用 SSG,@hono/deno 和 @hono/bun 软件包提供了 toSSG 函数。

对于 Deno:

ts
import { toSSG } from '@hono/deno'

toSSG(app) // The second argument is an option typed `ToSSGOptions`.

对于 Bun:

ts
import { toSSG } from '@hono/bun'

toSSG(app) // The second argument is an option typed `ToSSGOptions`.

选项 ​

选项由 ToSSGOptions 接口定义。

ts
export interface ToSSGOptions {
  dir?: string
  concurrency?: number
  extensionMap?: Record<string, string>
  plugins?: SSGPlugin[]
}
  • dir 是静态文件的输出目录,默认值为 ./static。
  • concurrency 是同时生成文件的并发数,默认值为 2。
  • extensionMap 是以 Content-Type 为键、扩展名字符串为值的映射,用于确定输出文件的扩展名。
  • plugins 是 SSG 插件数组,用于扩展静态网站生成过程的功能。

输出 ​

toSSG 返回以下 Result 类型的结果。

ts
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 选项。

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。

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'))

中间件 ​

以下介绍支持 SSG 的内置中间件。

ssgParams ​

可以使用类似 Next.js 中 generateStaticParams 的 API。

示例:

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 的静态文件生成之外。

ts
app.get('/api', disableSSG(), (c) => c.text('an-api'))

onlySSG ​

设置了 onlySSG 中间件的路由,在 toSSG 执行后会被 c.notFound() 覆盖。

ts
app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))

插件 ​

插件允许扩展静态网站生成过程的功能,通过钩子在不同阶段自定义生成过程。

默认插件 ​

默认情况下,toSSG 使用 defaultPlugin 跳过非 200 状态的响应(如重定向、错误或 404),避免为不成功的响应生成文件。

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。要在添加自定义插件时保留默认行为,请显式加入它:

ts
toSSG(app, fs, {
  plugins: [defaultPlugin, myCustomPlugin],
})

重定向插件 ​

redirectPlugin 为返回 HTTP 重定向响应(301、302、303、307、308)的路由生成 HTML 重定向页面。生成的 HTML 包含 <meta http-equiv="refresh"> 标签和规范链接。

ts
import { toSSG, redirectPlugin, defaultPlugin } from 'hono/ssg'

toSSG(app, fs, {
  plugins: [redirectPlugin(), defaultPlugin()],
})

例如,如果应用包含以下路由:

ts
app.get('/old', (c) => c.redirect('/new'))

redirectPlugin 会在 /old.html 生成 HTML 文件,通过 meta refresh 重定向到 /new。

NOTE

与 defaultPlugin 配合使用时,请将 redirectPlugin 放在 defaultPlugin 之前。由于 defaultPlugin 会跳过非 200 响应,将它放在前面会导致 redirectPlugin 无法处理重定向响应。

钩子类型 ​

插件可以使用以下钩子自定义 toSSG 过程:

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:整个生成过程结束后调用。

插件接口 ​

ts
export interface SSGPlugin {
  beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
  afterResponseHook?: AfterResponseHook | AfterResponseHook[]
  afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
}

基础插件示例 ​

仅保留 GET 请求:

ts
const getOnlyPlugin: SSGPlugin = {
  beforeRequestHook: (req) => {
    if (req.method === 'GET') {
      return req
    }
    return false
  },
}

按状态码过滤:

ts
const statusFilterPlugin: SSGPlugin = {
  afterResponseHook: (res) => {
    if (res.status === 200 || res.status === 500) {
      return res
    }
    return false
  },
}

记录生成的文件:

ts
const logFilesPlugin: SSGPlugin = {
  afterGenerateHook: (result) => {
    if (result.files) {
      result.files.forEach((file) => console.log(file))
    }
  },
}

高级插件示例 ​

以下示例创建一个站点地图插件,用于生成 sitemap.xml 文件:

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

应用插件:

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'),
  ],
})

基于 MIT 许可证发布。