Skip to main content
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',
})
dir
string
default:"./static"
Output directory for generated files

Concurrency

Control how many routes are processed in parallel:
await toSSG(app, fs, {
  concurrency: 4,
})
concurrency
number
default:"2"
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

defaultPlugin
function
Default plugin with standard behavior
redirectPlugin
function
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

Build docs developers (and LLMs) love