本文へ移動

SSG ヘルパー ​

SSG ヘルパーは、Hono アプリケーションから静的サイトを生成します。登録されたルートの内容を取得し、静的ファイルとして保存します。

使い方 ​

手動での生成 ​

次のような簡単な Hono アプリケーションがあるとします。

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(
      <html>
        <head />
        <body>
          <p>{content}</p>
        </body>
      </html>
    )
  })
  await next()
})

app.get('/about', (c) => {
  return c.render(
    <>
      <title>Hono SSG Page</title>Hello!
    </>
  )
})

export default app

Node.js では、次のようなビルドスクリプトを作成します。

ts
// build.ts
import app from './index'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'

toSSG(app, fs)

スクリプトを実行すると、次のようにファイルが出力されます。

bash
ls ./static
about.html  index.html

Vite プラグイン ​

@hono/vite-ssg Vite プラグインを使うと、この処理を簡単に行えます。

詳しくは、次を参照してください。

https://github.com/honojs/vite-plugins/tree/main/packages/ssg

toSSG ​

toSSG は静的サイト生成の中心となる関数で、アプリケーションとファイルシステムモジュールを引数に取ります。詳細は次のとおりです。

入力 ​

toSSG の引数は ToSSGInterface で定義されています。

ts
export interface ToSSGInterface {
  (
    app: Hono,
    fsModule: FileSystemModule,
    options?: ToSSGOptions
  ): Promise<ToSSGResult>
}
  • app は、ルートを登録した new Hono() インスタンスを指定します。
  • fs は次のオブジェクトを指定します。node:fs/promise を想定しています。
ts
export interface FileSystemModule {
  writeFile(path: string, data: string | Uint8Array): Promise<void>
  mkdir(
    path: string,
    options: { recursive: boolean }
  ): Promise<void | string>
}

Deno と Bun 用アダプターの使用 ​

Deno や Bun で SSG を使う場合、@hono/deno と @hono/bun パッケージに toSSG 関数が用意されています。

Deno の場合:

ts
import { toSSG } from '@hono/deno'

toSSG(app) // The second argument is an option typed `ToSSGOptions`.

Bun の場合:

ts
import { toSSG } from '@hono/bun'

toSSG(app) // The second argument is an option typed `ToSSGOptions`.

オプション ​

オプションは ToSSGOptions インターフェースで定義されています。

ts
export interface ToSSGOptions {
  dir?: string
  concurrency?: number
  extensionMap?: Record<string, string>
  plugins?: SSGPlugin[]
}
  • dir は静的ファイルの出力先です。デフォルトは ./static です。
  • concurrency は同時に生成するファイル数です。デフォルトは 2 です。
  • extensionMap は、Content-Type をキー、拡張子の文字列を値とするマップです。出力ファイルの拡張子の決定に使います。
  • plugins は、静的サイト生成処理の機能を拡張する SSG プラグインの配列です。

出力 ​

toSSG は、次の Result 型で結果を返します。

ts
export interface ToSSGResult {
  success: boolean
  files: string[]
  error?: Error
}

ファイルの生成 ​

ルートとファイル名 ​

登録されたルート情報と生成されるファイル名には、次の規則が適用されます。デフォルトの ./static では次のようになります。

  • / -> ./static/index.html
  • /path -> ./static/path.html
  • /path/ -> ./static/path/index.html

ファイルの拡張子 ​

ファイルの拡張子は、各ルートが返す Content-Type によって決まります。たとえば、c.html のレスポンスは .html として保存されます。

ファイルの拡張子をカスタマイズするには、extensionMap オプションを設定します。

ts
import { toSSG, defaultExtensionMap } from 'hono/ssg'

// Save `application/x-html` content with `.html`
toSSG(app, fs, {
  extensionMap: {
    'application/x-html': 'html',
    ...defaultExtensionMap,
  },
})

スラッシュで終わるパスは、拡張子にかかわらず index.ext として保存されます。

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'))

ミドルウェア ​

SSG をサポートする組み込みミドルウェアを紹介します。

ssgParams ​

Next.js の generateStaticParams に似た API を使用できます。

例:

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(
      <div>
        <h1>{shop.name}</h1>
      </div>
    )
  }
)

isSSGContext ​

isSSGContext は、現在のアプリケーションが toSSG によって開始された SSG コンテキスト内で動いている場合に true を返すヘルパー関数です。

ts
app.get('/page', (c) => {
  if (isSSGContext(c)) {
    return c.text('This is generated by SSG')
  }
  return c.text('This is served dynamically')
})

disableSSG ​

disableSSG ミドルウェアを設定したルートは、toSSG による静的ファイルの生成から除外されます。

ts
app.get('/api', disableSSG(), (c) => c.text('an-api'))

onlySSG ​

onlySSG ミドルウェアを設定したルートは、toSSG の実行後に c.notFound() で上書きされます。

ts
app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))

プラグイン ​

プラグインを使うと、静的サイト生成処理の機能を拡張できます。フックにより、生成処理の各段階をカスタマイズします。

デフォルトプラグイン ​

デフォルトでは、toSSG は defaultPlugin を使い、200 以外のステータスのレスポンス(リダイレクト、エラー、404 など)をスキップします。成功しなかったレスポンスのファイル生成を防ぎます。

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] })

カスタムプラグインを指定すると、defaultPlugin は自動では含まれません。カスタムプラグインを追加しつつデフォルト動作を維持するには、明示的に含めてください。

ts
toSSG(app, fs, {
  plugins: [defaultPlugin, myCustomPlugin],
})

リダイレクトプラグイン ​

redirectPlugin は、HTTP リダイレクトレスポンス(301、302、303、307、308)を返すルートの HTML リダイレクトページを生成します。生成される HTML には、<meta http-equiv="refresh"> タグと canonical リンクが含まれます。

ts
import { toSSG, redirectPlugin, defaultPlugin } from 'hono/ssg'

toSSG(app, fs, {
  plugins: [redirectPlugin(), defaultPlugin()],
})

たとえば、アプリケーションに次のルートがある場合:

ts
app.get('/old', (c) => c.redirect('/new'))

redirectPlugin は /old.html に HTML ファイルを生成し、meta refresh で /new にリダイレクトします。

NOTE

defaultPlugin と併用する場合は、redirectPlugin を defaultPlugin より前に配置してください。defaultPlugin は 200 以外のレスポンスをスキップするため、先に配置すると redirectPlugin がリダイレクトレスポンスを処理できなくなります。

フックの種類 ​

プラグインは、次のフックで toSSG の処理をカスタマイズできます。

ts
export type BeforeRequestHook = (req: Request) => Request | false
export type AfterResponseHook = (res: Response) => Response | false
export type AfterGenerateHook = (
  result: ToSSGResult
) => void | Promise<void>
  • BeforeRequestHook:各リクエストの処理前に呼び出されます。false を返すと、そのルートをスキップします。
  • AfterResponseHook:各レスポンスを受け取った後に呼び出されます。false を返すと、ファイル生成をスキップします。
  • AfterGenerateHook:生成処理全体の完了後に呼び出されます。

プラグインインターフェース ​

ts
export interface SSGPlugin {
  beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
  afterResponseHook?: AfterResponseHook | AfterResponseHook[]
  afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
}

基本的なプラグインの例 ​

GET リクエストだけを対象にします。

ts
const getOnlyPlugin: SSGPlugin = {
  beforeRequestHook: (req) => {
    if (req.method === 'GET') {
      return req
    }
    return false
  },
}

ステータスコードで絞り込みます。

ts
const statusFilterPlugin: SSGPlugin = {
  afterResponseHook: (res) => {
    if (res.status === 200 || res.status === 500) {
      return res
    }
    return false
  },
}

生成したファイルを記録します。

ts
const logFilesPlugin: SSGPlugin = {
  afterGenerateHook: (result) => {
    if (result.files) {
      result.files.forEach((file) => console.log(file))
    }
  },
}

高度なプラグインの例 ​

次は、sitemap.xml ファイルを生成するサイトマッププラグインの作成例です。

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 = `<?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)
    },
  }
}

プラグインを適用します。

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'),
  ],
})

MIT ライセンスで公開されています。