SapperとContentfulでSSGを行う

この記事はSvelte Advent Calendar 2020 1日目の記事です。

早速ですが本ブログはSapperとContentfulによるSSG(Static Site Generator)にて配信されています。
今回はこのブログの作り方を簡単に解説していきたいと思います。
近いうちにソースコードもGitHubのリポジトリをpublicにして公開しようかなと思います。

Sapperとは

Svelte Advent Calender1日目にいきなりSvelteではなくSapperで、はて?と思われる方もいるかも知れません。SapperはSvelteでSSR、SPA、SSGを行うためにパッケージされたフレームワーク・ツールです。ReactやVue.jsに馴染みがある方にはNext.js,Nuxt.jsと説明するとわかりやすいかと思います。
Svelteがベースになっているのでコンポーネントの書き方はSvelteそのものとなっています。

SapperのインストールとTypeScript化

このブログではTypeScriptで記述しています。SapperのデフォルトではJavaScriptになっているのでTypeScript化する必要がありますが、SapperTemplateにTypeScript化を行うスクリプトが同梱されているので簡単に導入することができます。

npx degit "sveltejs/sapper-template#rollup" sapper-blog

cd sapper-blog

node scripts/setupTypeScript.js

Static Site Generator

Sapperではnpm run exportを行うことでSSGが可能です。
しかし、そのままではSSGではなくSPAとして機能することに注意が必要です。

Sapperでデータを取得する

Sapperにはfetch関数が用意されていて以下のようにコンポーネントでデータを取得できます。
Next.jsのgetInitialPropsやNuxt.jsのasyncDataに相当する機能です。

<script context="module">
    export async function preload() {
        const res = await this.fetch(`server-route.json`);

        // ...
    }
</script>

context="module"はコンポーネントが評価される際に実行されるわけではなく、モジュールが評価される際に一回だけ実行されます。exportの場合はこのモジュールはコンパイルされた結果を保持します。

しかしこのままでは外部のAPIを取得する際には問題があります。
こちらのQiitaの記事に紹介されていました。
[Sapper] export で 完全静的サイトを作る [Svelte]

Contentfulのデータを取得する

Contentfulのデータは公式のライブラリを使います。

例えば個別記事の取得であれば[slug].svelteと対になるようにデータ取得用のローカルエンドポイントとして[slug].json.tsを作ります。

import { client } from '~/modules/contentful'

export async function get(req: any, res: any, _: any) {
    const { slug } = req.params
    const json = await client.getEntries({
        content_type: 'article',
        'sys.id': slug
    })
    if (json.items.length === 0) {
        res.writeHead(404, {
            'Content-Type': 'application/json'
        })
        res.end(JSON.stringify({
            message: 'not found article'
        }))
        return
    }
    res.writeHead(200, {
        'Content-Type': 'application/json'
    })
    res.end(JSON.stringify(json.items[0]))
}

※SapperのRequest,Responseの型定義がnpmで配信されてないので一旦anyにしている

import { createClient } from 'contentful'

export const client = createClient({
    space: process.env.SAPPER_APP_CONTENTFUL_SPACE,
    accessToken: process.env.SAPPER_APP_CONTENTFUL_ACCESS_TOKEN,
})

[slug].svelte側ではcontent="module"で以下のように取得できます。

<script context="module" lang="ts">
export async function preload({ params }): Promise<any> {
        const res = await this.fetch(`${params.slug}.json`)
        const post = await res.json()
        if (res.status === 200) {
            return { post }
        }
        this.error(res.status, post.message)
}
</script>

.envの読み込み

SapperにはどうやらNuxt.jsで言うNUXT_ENV_でenvを自動で読み込む機能が無いようです。

require('dotenv').config()
client: {
    input: config.client.input(),
    output: config.client.output(),
    plugins: [
        replace({
            'process.browser': true,
            'process.env.NODE_ENV': JSON.stringify(mode),
            'process.env.HOGE': process.env.HOGE
        }),

このように一つ一つrollup.config.jsで定義することで読み込むことは出来ますが、毎回追加するのも手間なのでsapper-environmentを使いました。
これによってNUXT_ENV_の用にSAPPAER_APP_というプレフィックスの環境変数が自動で読み込まれます。

さいごに

要点だけかいつまんで説明しましたが同じ構成を作ろうとしてる方の参考になればと思います。
Sapper.jsというよりSvelte.jsはとても良く作られていて、コンポーネントを書くのが気持ちが良いフレームワークです(語彙が貧弱)
すでにVue.jsやReactに満足している方にも一度触ってもらいたいフレームワークです。