# 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: ```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(

{content}

) }) await next() }) app.get('/about', (c) => { return c.render( <> Hono SSG PageHello! ) }) export default app ``` 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.html ``` ### Vite 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. ```ts export interface ToSSGInterface { ( app: Hono, fsModule: FileSystemModule, options?: ToSSGOptions ): Promise } ``` - `app` specifies `new Hono()` with registered routes. - `fs` specifies the following object, assuming `node:fs/promise`. ```ts export interface FileSystemModule { writeFile(path: string, data: string | Uint8Array): Promise mkdir( path: string, options: { recursive: boolean } ): Promise } ``` ### 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: ```ts import { toSSG } from 'hono/deno' toSSG(app) // The second argument is an option typed `ToSSGOptions`. ``` For Bun: ```ts import { toSSG } from 'hono/bun' toSSG(app) // The second argument is an option typed `ToSSGOptions`. ``` ### Options Options are specified in the ToSSGOptions interface. ```ts export interface ToSSGOptions { dir?: string concurrency?: number extensionMap?: Record plugins?: SSGPlugin[] } ``` - `dir` is the output destination for Static files. The default value is `./static`. - `concurrency` is the concurrent number of files to be generated at the same time. The default value is `2`. - `extensionMap` is a map containing the `Content-Type` as a key and the string of the extension as a value. This is used to determine the file extension of the output file. - `plugins` is 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. ```ts 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. ```ts 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. ```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 Introducing built-in middleware that supports SSG. ### ssgParams 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(

{shop.name}

) } ) ``` ### disableSSG 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 Routes with the `onlySSG` middleware set will be overridden by `c.notFound()` after `toSSG` execution. ```ts app.get('/static-page', onlySSG(), (c) => c.html(

Welcome to my site

)) ``` ## 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. ```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] }) ``` 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 The `redirectPlugin` generates HTML redirect pages for routes that return HTTP redirect responses (301, 302, 303, 307, 308). The generated HTML includes a `` 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')) ``` The `redirectPlugin` will generate an HTML file at `/old.html` with a meta refresh redirect to `/new`. > [!NOTE] > When used with `defaultPlugin`, place `redirectPlugin` **before** `defaultPlugin`. Since `defaultPlugin` skips non-200 responses, placing it first would prevent `redirectPlugin` from processing redirect responses. ### Hook Types 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 ``` - **BeforeRequestHook**: Called before processing each request. Return `false` to skip the route. - **AfterResponseHook**: Called after receiving each response. Return `false` to skip file generation. - **AfterGenerateHook**: Called after the entire generation process completes. ### Plugin Interface ```ts export interface SSGPlugin { beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[] afterResponseHook?: AfterResponseHook | AfterResponseHook[] afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[] } ``` ### Basic Plugin Examples 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 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 = ` ${urls.map((url) => `${url}`).join('\n')} ` 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'), ], }) ```