Skip to content

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, 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 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는 정적 사이트를 생성하는 주요 함수로, 애플리케이션과 파일 시스템 모듈을 인자로 받는다. 이 함수는 다음과 같은 기반을 두고 작동한다:

Output

toSSG 함수의 인자는 ToSSGInterface에 정의되어 있다.

ts
export interface ToSSGInterface {
  (
    app: Hono,
    fsModule: FileSystemModule,
    options?: ToSSGOptions
  ): Promise<ToSSGResult>
}
  • app은 등록된 라우트를 가진 new Hono()를 지정한다.
  • fsnode: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를 사용하려면 각 파일 시스템에 맞는 toSSG 함수를 제공한다.

Deno의 경우:

ts
import { toSSG } from 'hono/deno'

toSSG(app) // 두 번째 인자는 `ToSSGOptions` 타입의 옵션이다.

Bun의 경우:

ts
import { toSSG } from 'hono/bun'

toSSG(app) // 두 번째 인자는 `ToSSGOptions` 타입의 옵션이다.

옵션

옵션은 ToSSGOptions 인터페이스에 정의된다.

ts
export interface ToSSGOptions {
  dir?: string
  concurrency?: number
  beforeRequestHook?: BeforeRequestHook
  afterResponseHook?: AfterResponseHook
  afterGenerateHook?: AfterGenerateHook
  extensionMap?: Record<string, string>
}
  • dir은 정적 파일의 출력 경로를 지정한다. 기본값은 ./static이다.
  • concurrency는 동시에 생성할 파일의 수를 나타낸다. 기본값은 2이다.
  • extensionMapContent-Type을 키로, 확장자 문자열을 값으로 가지는 맵이다. 이는 출력 파일의 확장자를 결정하는 데 사용된다.

각 Hook에 대한 설명은 이후에 다룬다.

출력

toSSG는 다음과 같은 Result 타입으로 결과를 반환한다.

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

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/AfterResponseHook

toSSG는 앱에 등록된 모든 라우트를 대상으로 한다. 하지만 특정 라우트를 제외하고 싶다면 Hook을 지정해 필터링할 수 있다.

예를 들어, GET 요청만 출력하고 싶다면 beforeRequestHook에서 req.method를 필터링한다.

ts
toSSG(app, fs, {
  beforeRequestHook: (req) => {
    if (req.method === 'GET') {
      return req
    }
    return false
  },
})

또한, 상태 코드가 200 또는 500인 경우만 출력하고 싶다면 afterResponseHook에서 res.status를 필터링한다.

ts
toSSG(app, fs, {
  afterResponseHook: (res) => {
    if (res.status === 200 || res.status === 500) {
      return res
    }
    return false
  },
})

AfterGenerateHook

toSSG의 결과를 활용하려면 afterGenerateHook을 사용한다.

ts
toSSG(app, fs, {
  afterGenerateHook: (result) => {
    if (result.files) {
      result.files.forEach((file) => console.log(file))
    }
  })
})

파일 생성

라우트와 파일명

등록된 라우트 정보와 생성된 파일 이름에 적용되는 규칙은 다음과 같다. 기본값인 ./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'

// `application/x-html` 콘텐츠를 `.html`로 저장
toSSG(app, fs, {
  extensionMap: {
    'application/x-html': 'html',
    ...defaultExtensionMap,
  },
})

슬래시로 끝나는 경로는 확장자와 관계없이 index.ext로 저장된다는 점을 주의한다.

ts
// ./static/html/index.html로 저장
app.get('/html/', (c) => c.html('html'))

// ./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>
    )
  }
)

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

Released under the MIT License.