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
extensionMap?: Record<string, string>
plugins?: SSGPlugin[]
}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.pluginsis an array of SSG plugins that extend the functionality of the static site generation process.
Output
toSSG returns the result in the following Result type.
export interface ToSSGResult {
success: boolean
files: string[]
error?: Error
}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 generateStaticParams 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>))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
By default, toSSG uses defaultPlugin which skips non-200 status responses (like redirects, errors, or 404s). This prevents generating files for unsuccessful responses.
import { toSSG, defaultPlugin } from 'hono/ssg'
// defaultPlugin is automatically applied when no plugins specified
toSSG(app, fs)
// Equivalent to:
toSSG(app, fs, { plugins: [defaultPlugin] })If you specify custom plugins, defaultPlugin is not automatically included. To keep the default behavior while adding custom plugins, explicitly include it:
toSSG(app, fs, {
plugins: [defaultPlugin, myCustomPlugin],
})Hook Types
Plugins can use the following hooks to customize the toSSG process:
export type BeforeRequestHook = (req: Request) => Request | false
export type AfterResponseHook = (res: Response) => Response | false
export type AfterGenerateHook = (
result: ToSSGResult
) => void | Promise<void>- BeforeRequestHook: Called before processing each request. Return
falseto skip the route. - AfterResponseHook: Called after receiving each response. Return
falseto skip file generation. - AfterGenerateHook: Called after the entire generation process completes.
Plugin Interface
export interface SSGPlugin {
beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
afterResponseHook?: AfterResponseHook | AfterResponseHook[]
afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
}Basic Plugin Examples
Filter only GET requests:
const getOnlyPlugin: SSGPlugin = {
beforeRequestHook: (req) => {
if (req.method === 'GET') {
return req
}
return false
},
}Filter by status code:
const statusFilterPlugin: SSGPlugin = {
afterResponseHook: (res) => {
if (res.status === 200 || res.status === 500) {
return res
}
return false
},
}Log generated files:
const logFilesPlugin: SSGPlugin = {
afterGenerateHook: (result) => {
if (result.files) {
result.files.forEach((file) => console.log(file))
}
},
}Advanced Plugin Example
Here's an example of creating a sitemap plugin that generates a sitemap.xml file:
// 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:
import app from './index'
import { toSSG } from 'hono/ssg'
import { sitemapPlugin } from './plugins'
toSSG(app, fs, {
plugins: [
getOnlyPlugin,
statusFilterPlugin,
logFilesPlugin,
sitemapPlugin('https://example.com'),
],
})