SSG 헬퍼
SSG 헬퍼는 Hono 애플리케이션에서 정적 사이트를 생성한다. 등록된 라우트의 내용을 가져와 정적 파일로 저장한다.
사용법
수동 설정
다음과 같은 간단한 Hono 애플리케이션이 있다고 가정한다:
// 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 appNode.js를 사용한다면, 다음과 같은 빌드 스크립트를 작성한다:
// build.ts
import app from './index'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'
toSSG(app, fs)이 스크립트를 실행하면 다음과 같이 파일이 생성된다:
ls ./static
about.html index.htmlVite 플러그인
@hono/vite-ssg Vite 플러그인을 사용하면 이 과정을 쉽게 처리할 수 있다.
자세한 내용은 다음 링크를 참고한다:
https://github.com/honojs/vite-plugins/tree/main/packages/ssg
toSSG
toSSG는 정적 사이트를 생성하는 주요 함수로, 애플리케이션과 파일 시스템 모듈을 인자로 받는다. 이 함수는 다음과 같은 기반을 두고 작동한다:
Output
toSSG 함수의 인자는 ToSSGInterface에 정의되어 있다.
export interface ToSSGInterface {
(
app: Hono,
fsModule: FileSystemModule,
options?: ToSSGOptions
): Promise<ToSSGResult>
}app은 등록된 라우트를 가진new Hono()를 지정한다.fs는node:fs/promise를 가정한 다음 객체를 지정한다.
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의 경우:
import { toSSG } from 'hono/deno'
toSSG(app) // 두 번째 인자는 `ToSSGOptions` 타입의 옵션이다.Bun의 경우:
import { toSSG } from 'hono/bun'
toSSG(app) // 두 번째 인자는 `ToSSGOptions` 타입의 옵션이다.옵션
옵션은 ToSSGOptions 인터페이스에 정의된다.
export interface ToSSGOptions {
dir?: string
concurrency?: number
beforeRequestHook?: BeforeRequestHook
afterResponseHook?: AfterResponseHook
afterGenerateHook?: AfterGenerateHook
extensionMap?: Record<string, string>
}dir은 정적 파일의 출력 경로를 지정한다. 기본값은./static이다.concurrency는 동시에 생성할 파일의 수를 나타낸다. 기본값은2이다.extensionMap은Content-Type을 키로, 확장자 문자열을 값으로 가지는 맵이다. 이는 출력 파일의 확장자를 결정하는 데 사용된다.
각 Hook에 대한 설명은 이후에 다룬다.
출력
toSSG는 다음과 같은 Result 타입으로 결과를 반환한다.
export interface ToSSGResult {
success: boolean
files: string[]
error?: Error
}훅
toSSG의 동작을 커스터마이징하려면 옵션에 다음 커스텀 훅을 지정한다.
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를 필터링한다.
toSSG(app, fs, {
beforeRequestHook: (req) => {
if (req.method === 'GET') {
return req
}
return false
},
})또한, 상태 코드가 200 또는 500인 경우만 출력하고 싶다면 afterResponseHook에서 res.status를 필터링한다.
toSSG(app, fs, {
afterResponseHook: (res) => {
if (res.status === 200 || res.status === 500) {
return res
}
return false
},
})AfterGenerateHook
toSSG의 결과를 활용하려면 afterGenerateHook을 사용한다.
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 옵션을 설정한다.
import { toSSG, defaultExtensionMap } from 'hono/ssg'
// `application/x-html` 콘텐츠를 `.html`로 저장
toSSG(app, fs, {
extensionMap: {
'application/x-html': 'html',
...defaultExtensionMap,
},
})슬래시로 끝나는 경로는 확장자와 관계없이 index.ext로 저장된다는 점을 주의한다.
// ./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를 사용할 수 있다.
예제:
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에 의해 정적 파일 생성에서 제외된다.
app.get('/api', disableSSG(), (c) => c.text('an-api'))onlySSG
onlySSG 미들웨어가 설정된 라우트는 toSSG 실행 후 c.notFound()에 의해 재정의된다.
app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))