The SSG (Static Site Generation) helper allows you to pre-render your Hono application routes into static HTML files, enabling deployment to static hosting services.
Import
import { toSSG , ssgParams , disableSSG , onlySSG } from 'hono/ssg'
Basic Usage
Generate Static Files
The toSSG function pre-renders your app routes:
import { Hono } from 'hono'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'
const app = new Hono ()
app . get ( '/' , ( c ) => c . html ( '<h1>Home</h1>' ))
app . get ( '/about' , ( c ) => c . html ( '<h1>About</h1>' ))
app . get ( '/contact' , ( c ) => c . html ( '<h1>Contact</h1>' ))
// Generate static files
const result = await toSSG ( app , fs , {
dir: './dist' ,
})
if ( result . success ) {
console . log ( 'Generated files:' , result . files )
} else {
console . error ( 'Generation failed:' , result . error )
}
This creates:
dist/
index.html
about.html
contact.html
File System Module
Provide a file system module for writing files:
Node.js
import fs from 'fs/promises'
import { toSSG } from 'hono/ssg'
await toSSG ( app , fs )
Deno
import { toSSG } from 'hono/ssg'
const fs = {
writeFile: Deno . writeFile ,
mkdir: Deno . mkdir ,
}
await toSSG ( app , fs )
Bun
import { toSSG } from 'hono/ssg'
const fs = {
writeFile: Bun . write ,
mkdir : async ( path : string ) => {
await Bun . write ( ` ${ path } /.keep` , '' )
},
}
await toSSG ( app , fs )
Options
Output Directory
await toSSG ( app , fs , {
dir: './public' ,
})
Output directory for generated files
Concurrency
Control how many routes are processed in parallel:
await toSSG ( app , fs , {
concurrency: 4 ,
})
Number of routes to process concurrently
Extension Mapping
Customize file extensions based on content type:
await toSSG ( app , fs , {
extensionMap: {
'text/html' : 'html' ,
'text/xml' : 'xml' ,
'application/xml' : 'xml' ,
'application/json' : 'json' ,
},
})
Dynamic Routes
For routes with parameters, use ssgParams middleware to define which parameter values to generate:
Basic Dynamic Routes
import { ssgParams } from 'hono/ssg'
app . get (
'/posts/:id' ,
ssgParams ([{ id: '1' }, { id: '2' }, { id: '3' }]),
( c ) => {
const id = c . req . param ( 'id' )
return c . html ( `<h1>Post ${ id } </h1>` )
}
)
Generates:
dist/
posts/1.html
posts/2.html
posts/3.html
Dynamic Parameters from Function
import { ssgParams } from 'hono/ssg'
const posts = [
{ id: '1' , slug: 'hello-world' },
{ id: '2' , slug: 'second-post' },
]
app . get (
'/blog/:slug' ,
ssgParams ( async ( c ) => {
// Fetch data from database or API
return posts . map (( post ) => ({ slug: post . slug }))
}),
async ( c ) => {
const slug = c . req . param ( 'slug' )
const post = posts . find (( p ) => p . slug === slug )
return c . html ( `<h1> ${ post ?. slug } </h1>` )
}
)
Multiple Parameters
import { ssgParams } from 'hono/ssg'
app . get (
'/users/:userId/posts/:postId' ,
ssgParams ([
{ userId: '1' , postId: 'a' },
{ userId: '1' , postId: 'b' },
{ userId: '2' , postId: 'c' },
]),
( c ) => {
const userId = c . req . param ( 'userId' )
const postId = c . req . param ( 'postId' )
return c . html ( `<h1>User ${ userId } - Post ${ postId } </h1>` )
}
)
Controlling SSG Behavior
Disable SSG for Specific Routes
Prevent certain routes from being statically generated:
import { disableSSG } from 'hono/ssg'
app . get ( '/api/data' , disableSSG (), ( c ) => {
// This route will not be statically generated
return c . json ({ timestamp: Date . now () })
})
Only Generate During SSG
Make routes available only during static generation:
import { onlySSG } from 'hono/ssg'
app . get ( '/preview/:id' , onlySSG (), ( c ) => {
// This route only exists during SSG
// Returns 404 when running as a server
return c . html ( '<h1>Preview</h1>' )
})
Check if in SSG Context
import { isSSGContext } from 'hono/ssg'
app . get ( '/page' , ( c ) => {
if ( isSSGContext ( c )) {
// Running during static generation
return c . html ( '<h1>Static Version</h1>' )
} else {
// Running as a server
return c . html ( '<h1>Dynamic Version</h1>' )
}
})
Hooks
Customize the generation process with hooks:
Before Request Hook
Modify requests before processing:
import { toSSG } from 'hono/ssg'
await toSSG ( app , fs , {
plugins: [
{
beforeRequestHook : async ( req ) => {
// Modify request
console . log ( 'Processing:' , req . url )
return req
},
},
],
})
Return false to skip a route:
beforeRequestHook : ( req ) => {
const url = new URL ( req . url )
if ( url . pathname . startsWith ( '/admin' )) {
return false // Skip admin routes
}
return req
}
After Response Hook
Modify responses before writing files:
await toSSG ( app , fs , {
plugins: [
{
afterResponseHook : async ( res ) => {
// Modify response
const html = await res . text ()
const modified = html . replace ( '{{timestamp}}' , Date . now (). toString ())
return new Response ( modified , res )
},
},
],
})
Return false to skip writing the file:
afterResponseHook : ( res ) => {
if ( res . status !== 200 ) {
return false // Skip non-200 responses
}
return res
}
After Generate Hook
Run logic after all files are generated:
await toSSG ( app , fs , {
plugins: [
{
afterGenerateHook : async ( result , fsModule , options ) => {
console . log ( 'Generated files:' , result . files . length )
if ( result . success ) {
// Create sitemap, copy assets, etc.
await fsModule . writeFile (
` ${ options ?. dir } /sitemap.txt` ,
result . files . join ( ' \n ' )
)
}
},
},
],
})
Plugins
Plugins bundle hooks together:
import { toSSG , defaultPlugin , redirectPlugin } from 'hono/ssg'
await toSSG ( app , fs , {
plugins: [
defaultPlugin (),
redirectPlugin (),
{
beforeRequestHook : ( req ) => req ,
afterResponseHook : ( res ) => res ,
afterGenerateHook : async ( result ) => {
console . log ( 'Done!' )
},
},
],
})
Built-in Plugins
Default plugin with standard behavior
Handles redirect responses during generation
Result Object
The toSSG function returns a result object:
interface ToSSGResult {
success : boolean
files : string []
error ?: Error
}
Success Case
const result = await toSSG ( app , fs )
if ( result . success ) {
console . log ( 'Generated files:' , result . files )
// ['./static/index.html', './static/about.html', ...]
}
Error Case
const result = await toSSG ( app , fs )
if ( ! result . success ) {
console . error ( 'Generation failed:' , result . error ?. message )
}
Build Script Example
Create a build script for your project:
// build.ts
import { Hono } from 'hono'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'
import app from './app'
async function build () {
console . log ( 'Building static site...' )
const result = await toSSG ( app , fs , {
dir: './dist' ,
concurrency: 4 ,
})
if ( result . success ) {
console . log ( `✓ Generated ${ result . files . length } files` )
process . exit ( 0 )
} else {
console . error ( '✗ Build failed:' , result . error )
process . exit ( 1 )
}
}
build ()
Add to package.json:
{
"scripts" : {
"build" : "tsx build.ts"
}
}
Deployment
Deploy generated files to static hosting:
Vercel Deploy the dist folder as a static site
Netlify Configure build command and publish directory
Cloudflare Pages Push to Git and configure Pages project
GitHub Pages Deploy from repository with GitHub Actions
Best Practices
Use ssgParams for all dynamic routes, otherwise they will be skipped during generation.
Routes that depend on request headers or real-time data may not work correctly when statically generated.
Use isSSGContext to provide different behavior during static generation vs. runtime.
Advanced: Adaptor Interface
Create runtime-specific adaptors:
import { ToSSGAdaptorInterface } from 'hono/ssg'
import fs from 'fs/promises'
const toSSGAdaptor : ToSSGAdaptorInterface = ( app , options ) => {
return toSSG ( app , fs , options )
}
export default toSSGAdaptor