SSG Helper
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
If you have a simple Hono application like the following:
// 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 appFor Node.js, create a build script like this:
// 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:
ls ./static
about.html index.htmlVite Plugin
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 is the main function for generating static sites, taking an application and a filesystem module as arguments. It is based on the following:
Input
The arguments for toSSG are specified in ToSSGInterface.
export interface ToSSGInterface {
(app: Hono, fsModule: FileSystemModule, options?: ToSSGOptions): Promise<ToSSGResult>
}appspecifiesnew Hono()with registered routes.fsspecifies the following object, assumingnode:fs/promise.
export interface FileSystemModule {
writeFile(path: string, data: string | Uint8Array): Promise<void>
mkdir(path: string, options: { recursive: boolean }): Promise<void | string>
}Using adapters for Deno and Bun
If you want to use SSG on Deno or Bun, a toSSG function is provided for each file system.
For Deno:
import { toSSG } from 'hono/deno'
toSSG(app) // The second argument is an option typed `ToSSGOptions`.For Bun:
import { toSSG } from 'hono/bun'
toSSG(app) // The second argument is an option typed `ToSSGOptions`.Options
Options are specified in the ToSSGOptions interface.
export interface ToSSGOptions {
dir?: string
concurrency?: number
beforeRequestHook?: BeforeRequestHook
afterResponseHook?: AfterResponseHook
afterGenerateHook?: AfterGenerateHook
extensionMap?: Record<string, string>
}diris the output destination for Static files. The default value is./static.concurrencyis the concurrent number of files to be generated at the same time. The default value is2.extensionMapis a map containing theContent-Typeas a key and the string of the extension as a value. This is used to determine the file extension of the output file.
Each Hook will be described later.
Output
toSSG returns the result in the following Result type.
export interface ToSSGResult {
success: boolean
files: string[]
error?: Error
}Hook
You can customize the process of toSSG by specifying the following custom hooks in options.
export type BeforeRequestHook = (req: Request) => Request | false
export type AfterResponseHook = (res: Response) => Response | false
export type AfterGenerateHook = (result: ToSSGResult) => void | Promise<void>BeforeRequestHook/AfterResponseHook
toSSG targets all routes registered in app, but if there are routes you want to exclude, you can filter them by specifying a Hook.
For example, if you want to output only GET requests, filter req.method in beforeRequestHook.
toSSG(app, fs, {
beforeRequestHook: (req) => {
if (req.method === 'GET') {
return req
}
return false
},
})For example, if you want to output only when StatusCode is 200 or 500, filter res.status in afterResponseHook.
toSSG(app, fs, {
afterResponseHook: (res) => {
if (res.status === 200 || res.status === 500) {
return res
}
return false
},
})AfterGenerateHook
Use afterGenerateHook if you want to hook the result of toSSG.
toSSG(app, fs, {
afterGenerateHook: (result) => {
if (result.files) {
result.files.forEach((file) => console.log(file))
}
})
})Generate File
Route and Filename
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
The file extension depends on the Content-Type returned by each route. For example, responses from c.html are saved as .html.
If you want to customize the file extensions, set the extensionMap option.
import { toSSG, defaultExtensionMap } from 'hono/ssg'
// Save `application/x-html` content with `.html`
toSSG(app, fs, {
extensionMap: {
'application/x-html': 'html',
...defaultExtensionMap,
},
})Note that paths ending with a slash are saved as index.ext regardless of the extension.
// 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
Introducing built-in middleware that supports SSG.
ssgParams
You can use an API like generateStaticPaths of Next.js.
Example:
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
Routes with the disableSSG middleware set are excluded from static file generation by toSSG.
app.get('/api', disableSSG(), (c) => c.text('an-api'))onlySSG
Routes with the onlySSG middleware set will be overridden by c.notFound() after toSSG execution.
app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))