SvelteKit + microCMS でブログ構築


記事の最後に向け、コードがだんだん仕上がっていく形式。ヘッドレスCMSに microCMS を採用しましたが、microCMS特化ではなく汎用性重視の実装です。

#### ＜本記事の技術的要素＞

- ルーティング, データハンドリング
- 外部APIからデータを取得 → レンダリング
- `.env`の使用
- ※ ログイン等の認証関連は一切ナシ
- ※ デザイン的な要素も一切ナシ

------

シリーズまとめ（随時追加・更新）

| | |
|:-|:-|
| [【SvelteKit 入門】はじめに](https://qiita.com/jwnr/items/430cd160e90fdfa42336) |  |
| [【SvelteKit 入門】作業の前に](https://qiita.com/jwnr/items/9bb7583b5e3d9f75289c) |  |
| [【SvelteKit 入門】アダプター設定・ホスティング・コンテナ運用](https://qiita.com/jwnr/items/740335cfb7d8e000c749) |  |
| [【SvelteKit 入門】ルーティング](https://qiita.com/jwnr/items/8932978ca2f50f102e3d) |  |
| [【SvelteKit 入門】データハンドリング(+page.js)](https://qiita.com/jwnr/items/7934196e9ba2e602e063) |  |
| SvelteKit + microCMS でブログ構築 | now reading |


------

まずは適当なプロジェクトを作って下さい。JavaScript オンリーでいきます。
（？という方は **【SvelteKit 入門】作業の前に** あたりを読むと良いかも）

```
## テンプレートは空プロジェクト
? Which Svelte app template?
    SvelteKit demo app
>   Skeleton project
    Library skeleton project

## 続く選択項目
全て `no` にしておくと以降のサンプルコードとズレがなくなると思います。
```


# microCMS側の準備

アカウントの作成や基本的な部分は良記事がたくさんあるのでそちらをご覧ください。

### ① コンテンツ用意

**テキスト系３種** + **画像** という構成。

![](https://storage.googleapis.com/zenn-user-upload/b9db31b3411b-20221011.png)
*APIスキーマ*

![](https://storage.googleapis.com/zenn-user-upload/6eab23884283-20221011.png)
*コンテンツ*

### ② API確認

microCMSのコンテンツページ（一覧・個別それぞれ）の右上にある「APIプレビュー」を押すと、以下のように情報がわかりやすく表示されます。

![](https://storage.googleapis.com/zenn-user-upload/708d980be16b-20221011.png)

- 紫枠部分 … 「サービスID」の文字列（みなさんそれぞれの値）
- 赤枠部分 … 「エンドポイント」で設定した文字列（今回は "sveltekit" にしてます）
- 青枠部分 … アクセスに必要なキー文字列（これも固有値）

アクセス手法は各種用意されており JavaScriptもありますが、今回は **cURLを参照します。** 表示されるヘッダーとURLは言語に依存しないので、当然 JavaScript でも使えます。

:::note warn
金融系のAPIなど複雑なヘッダー設定が必要な場合は用意されたSDKを利用するのが良いと思いますが、今回は非常にシンプルなAPIです。標準の`fetch`を利用するやり方で実装します。
:::

また、「取得」ボタンで実際にAPIを叩いた時のレスポンスが確認できます。
記事一覧・個別それぞれのレスポンスを確認しておくと、実装時にスムーズです。



### ③ 使用するエンドポイント

|  | エンドポイント | レスポンスの使う部分 |
|:--|:--|:--|
| 一覧 | `https://サービスID.microcms.io/api/v1/エンドポイント` | レスポンス.contents |
| 個別 | `https://サービスID.microcms.io/api/v1/エンドポイント/記事ID` | レスポンス自体 |



# SvelteKitでの実装

さて、最初に作って放置していたプロジェクトの出番ですね。
`npm run dev`で立ち上げ、ブラウザで表示されている状態（URLは`http://localhost:5173`のはず）で始めましょう。

![](https://storage.googleapis.com/zenn-user-upload/24697614ad9b-20221014.png)
*作業開始*


ページ構成は以下。ルーティングについては [こちらの記事](https://zenn.dev/wnr/articles/53cnoe5xvzmw) で解説してます。

```
http://localhost:5173/            一覧ページ
http://localhost:5173/記事ID      個別ページ
```

------

## ① 一覧ページ

### 1. microCMSからのデータ取得チェック

通信チェックなので `+page.svelte`ファイルにベタ書きです。

```js:/src/routes/+page.svelte
<script>
    // micoCMS用のAPIキーのみヘッダーにセット。GETで良いので他オプションは省略
    fetch( '一覧のAPIエンドポイント', {
        headers: { 'X-MICROCMS-API-KEY': 'APIキー' }
    }).then( x=> x.json() ).then( x=> {
        // 変数名に「x」とか使うのは私が変態だからです。変えてください
        console.log( x.contens )
    })
</script>
```

これでコンソールに記事情報の配列が出力されればok。

![](https://storage.googleapis.com/zenn-user-upload/bf70b46aa9b9-20221011.png)
*公開にしておいた記事が２つ、配列として取得できている*



### 2. データ取得を`+page.server.js`に

本来データ取得は`+page.js`や`+page.server.js`に引っ込める方が良いです。
（その理由も含め、`+ .js`ファイルによるデータハンドリングは [こちらの記事](https://zenn.dev/wnr/articles/54cnoe5xvzmw) で解説）

早速`+page.server.js`を作成し、そちらに記述を移します。

```js:/src/routes/+page.server.js
// load関数 の中でデータを return すれば +page.svelte に渡る
export function load({ fetch }){

    // JSONデータを return （ここが自動的に await となる理由は別記事参照）
    return fetch( '一覧のAPIエンドポイント',{
        headers: { 'X-MICROCMS-API-KEY': 'APIキー' }
    }).then( x=> x.json() )
}
```

```js:/src/routes/+page.svelte
<script>
    export let data  // +page.server.js からデータ受け取り
</script>


{#each data.contents as x}
    <h2> {x.title} </h2>
{:else}
    <p>記事がありません</p>
{/each}
```

`+page.svelte`のレンダリングは`+ .js`の処理後に行われるので、`+page.svelte`側で await の処理は不要です。



### 3. 表示調整

先程はタイトルのみでチェックしたので、表示項目を増やします。

```js:/src/routes/+page.svelte
<script>
    import dayjs from "dayjs"
    // 日付表示でラクしたいので。`npm i dayjs` でインストールし、ここでインポート

    export let data
</script>


{#each data.contents as x}
    <h2 data-sveltekit-prefetch><a href="/{x.id}">{x.title}</a></h2>
    <p>公開: {dayjs(x.publishedAt).format('YYYY.MM.DD')}</p>
    <img src="{x.ecimg.url}" alt="アイキャッチ画像">
    <p>{@html x.desc.replaceAll('\n','<br>')}</p>
    <hr>
{:else}
    <p>記事がありません</p>
{/each}
```

■ タイトルは個別ページへのリンクに変更
■ `data-sveltekit-prefetch`はマウスホバーでプリローディングするスグレモノ
■ microCMSでテキストエリアにした部分（descのとこ）は改行が`\n`なので`<br>`に置換。
そして Svelte は`{...}`で埋め込むとただのテキストとして扱われるので、`{@html ...}`を使いHTMLとして認識させます。


### 4. `.env`の使用

ヘッダーに設定していたAPIキーを`.env`で管理します。
（APIキーなら`.env`ではなく`conf.js`のようなファイル作ってimportで良い気もする）

```
/ ...
  ├ src/routes/
  │  ├ +page.js
  │  └ +page.svelte
  └ .env                <-  new !
```

```shell:.env
## 解説用サンプル
EXAMPLE ='VALUE'               <-  サーバー駆動ファイルからしか読み込めない
PUBLIC_EXAMPLE ='VALUE'        <-  どのファイルからでも読み込み可

## 今回はコレ
CMS_API_KEY ='xxxxxxxx-...'    <-  秘匿したいので PUBLIC_ をつけない
  # ENDPOINT もお好みで
```

SvelteKit は組み込みの機能で`.env`が読み込めるので、`dotenv`等は不要です。

```js:/src/routes/+page.server.js
// 解説用サンプル
import { EXAMPLE } from '$env/static/private'
import { PUBLIC_EXAMPLE } from '$env/static/public'
// +page.server.js からの呼び出しなので、private,public どちらも読み込み可。
// もし +page.js からだと、private の方はエラーとなる。

// 今回はコレ
import { CMS_API_KEY } from '$env/static/private'

export function load({ fetch }){
    return fetch( '一覧のAPIエンドポイント',{
        headers: { 'X-MICROCMS-API-KEY': CMS_API_KEY }
    }).then( x=> x.json() )
}
```

クライアント側から見えてかまわない値はプレフィックス`PUBLIC_`を付けます。そうでない値はサーバー駆動のファイルからしか読み込めないので、`+page.svelte` `+page.js`といったファイルで import するとエラーが出ます。


### 5. エラーハンドリング

最低限のエラーハンドリングだけ実装します。

```js:/src/routes/+page.server.js
import { CMS_API_KEY } from '$env/static/private'

export function load({ fetch }){
    return fetch( '一覧のAPIエンドポイント',{
        headers: { 'X-MICROCMS-API-KEY': CMS_API_KEY }
    })
    .then( x=>{
        if( !x.ok ) throw error()
        // 通信は成功、取得はエラー(キーの不整合など) の場合
        // ここで throw して下の catch で処理

        return x.json()  // 正常なら JSON をリターン
    })
    .catch( x=>{
        return { error: true }
        // 通信エラー・取得エラーはまとめてココ
        // +page.svelte でハンドリングする
    })
}
```

```js:/src/routes/+page.svelte
<script>
    import dayjs from "dayjs"
    export let data
</script>


{#if data?.error}
    <p>記事の取得に失敗しました</p>
{:else}
    {#each data.contents as x}
        <h2><a href="/{x.id}">{x.title}</a></h2>
        <p>公開: {dayjs(x.publishedAt).format('YYYY.MM.DD')}</p>
        <img src="{x.ecimg.url}" alt="アイキャッチ画像">
        <p>{@html x.desc.replaceAll('\n','<br>')}</p>
        <hr>
    {:else}
        <p>記事がありません</p>
    {/each}
{/if}
```

返ってきた JSON に `.error` があれば失敗として処理。
この部分、皆さんはもっとちゃんと実装してください。




ブラウザでの表示は以下のようになりました。
（記事タイトルはクリックしてもまだ`404`）

![](https://storage.googleapis.com/zenn-user-upload/fbd995eca5ab-20221014.png)
*余白や画像サイズ等、最低限のスタイルはあててます*

------


## ② 個別ページ作成

```
/src/routes/
    ├ [postid]              <- new !
    │    ├ +page.server.js  <- new !
    │    └ +page.svelte     <- new !
    ├ +page.js
    └ +page.svelte
```

### 1. パスパラメータの取得確認

URLから記事IDを取得できるかチェックします。

```js:/src/routes/[postid]/+page.server.js
export function load({ params }){  // load関数の引数で params を呼ぶ
    const { postid } =params  // postid =params.postid    と同義
    console.log(postid)
}
```

これで一覧ページと行ったり来たりして、コンソールに記事IDが出力されればOK。


### 2. 記事データ取得

ここからは一覧の時と同じですね。

```js:/src/routes/[postid]/+page.server.js
import { CMS_API_KEY } from '$env/static/private'

export function load({ params, fetch }){  // fetch 追加
    const { postid } =params
    return fetch( `個別のエンドポイント/${postid}`, {
        headers: { 'X-MICROCMS-API-KEY': CMS_API_KEY }
    })
    .then( x=>{
        if( !x.ok ) throw error()
        return x.json()
    })
    .catch( x=>{
        return { error: true }
    })
}
```

```js:/src/routes/[postid]/+page.svelte
<script>
    export let data
</script>


{#if data?.error}
    <p>データの取得に失敗しました</p>
{:else}
    <h1>{data.title}</h1>
    <hr>
    <section>{@html data.body}</section>
{/if}
```


これで記事の個別ページが表示されるようになりました。
ブラウザでの表示は以下。

![](https://storage.googleapis.com/zenn-user-upload/268a1a458ce7-20221015.png)




# まとめ

一覧・個別ページだけのブログサイトをサクっと作ってみました。
ファイル名・ディレクトリ構成にクセはありますが、コードは **非常にシンプル** かつ **少ない記述** というのが伝わったのではないでしょうか。

とはいえ、最低限の機能のみ & デザインもノータッチでしたので

- レイアウト（`+layout.xxx` / コンポーネント）
- データハンドリング（`+page.js` `+page.server.js`）
- レンダリング（SSR / CSR / SSG）
- 内部エンドポイント経由のデータ取得

このあたり、まだまだ作り込む余地があります。
むしろ **ここから掘り下げていくほど Svelte / SvelteKit の強みが出ます。**

公式ドキュメント等を参考に、色々な部分を仕上げてみてください。