SvelteKitでSSR/SSGのプロトを爆速構築したい

# はじめに

フロントエンドに触れる機会が多いのですが、SSR/SSGに詳しくなりたい（ならねばならない）という焦燥に駆られたので、勉強も兼ねて触りたいと思います。手を動かすのであれば、何かものづくりしたいなと思ったので、話題の SvelteKit で SSR/SSG のプロト構築をしてみようと思います。

最終的には、↓のプロトタイプを完成させます。素早く。
<img width='600' alt='result' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/abaf6533-f2d9-2025-2f23-ec1e8394fd89.png' style="display: block; margin: auto;">

## SSR / SSG とは

SSR は Server Side Rendering、SSG は Static Site Generator の略です。文字通りどちらもサーバ側でレンダリングや静的サイトを作成するもので、CSR(Client Side Rendering) と対比して考えられます。世間を賑わせている React, Vue, Angular によって普及した SPA(Single Page Application) は、CSR が必須の技術です。
この記事では深くは触れませんが、SSR/SSG は SEO対策になり、CSR と異なり読み込み速度が速いので、セッション時間やサイト直帰率など、ユーザ指標の改善にも寄与できます。


## Why SvelteKit ?

<img width="1000" alt='first' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/29c383f6-2718-b081-3dcc-602c5253d882.png'>

SvelteKit は、[Svelte](https://svelte.jp/) と [Vite](https://vitejs.dev/) で構成されたフレームワークで、「高速/楽しい/柔軟」をモットーに掲げています。Svelte はデフォルトで SSR を採用しています。仮想DOM を使用しないのです。なので設定をいじらない限り、SSR で実装を進めることができます。

Svelte の発音は「**/ˈsvɛlt/**」です。

私が SvelteKit を採用した理由は **話題のSvelteを触ってみたかった。** それにつきます。
公式は Svelte をフレームワークとしてこう謳っているので、その想い受け取るしかない。[^1]

> Svelte はコンパイラを使用する UI フレームワークで、息を呑むほど簡潔にコンポーネントを書くことができ、ブラウザで最小限の動作となるようにしてくれます。
開発者は既知の言語(HTML、CSS、JavaScript)を使うことができます。
**これは、web 開発へのラブレターです。**

https://kit.svelte.jp/

# SSR 編

ということで、今回は SvelteKit でいかに素早くお手軽にプロトを作成するかに挑戦したいと思います。せっかくなので、Svelte のお作法に倣った上で API を叩き、画像をフェッチして表示するところも含めたいと思います。

## 環境構築

公式 Docs に従って、進めていきます。Node.js がインストールされていれば、動作します。

https://kit.svelte.jp/docs/creating-a-project

言われたとおりコマンドを叩きます。

```shell
npm create svelte@latest my-app
```

すると、install するパッケージのバージョンを聞いてくれます。
今回は「create-svelte@2.0.1」のようです。（2022/12/17 時点）

```shell
Need to install the following packages:
  create-svelte@2.0.1
Ok to proceed? (y) y
```

「y」 を入力しエンターを押すと、インストールが進みます。インストールが完了すると、プロジェクトの詳細設定を対話形式で行ないます。「SvelteKit demo app」を選択すると、ページ遷移なども含んだデモアプリになりますが、今回は最小限の構成にしたいので「Skelton project」にしました。

```shell
create-svelte version 2.0.1

Welcome to SvelteKit!

? Which Svelte app template? › - Use arrow-keys. Return to submit.
    SvelteKit demo app
❯   Skeleton project - Barebones scaffolding for your new SvelteKit app
    Library skeleton project
```

テンプレートを選択すると、プロジェクトに含ませるベースの言語やツールを順に聞いてくれます。lint ツールの [ESLint](https://eslint.org/) やコードフォーマットツールの [Prettier](https://prettier.io/) を標準で入れてくれるだけでなく、テストツールとして [Playwright](https://playwright.dev/) や [Vitest](https://vitest.dev/) を選ばせてくれるのは親切ですね。

```shell
? Add type checking with TypeScript? › - Use arrow-keys. Return to submit.
    Yes, using JavaScript with JSDoc comments
❯   Yes, using TypeScript syntax
    No
? Add ESLint for code linting? › No / Yes
? Add Prettier for code formatting? › No / Yes
? Add Playwright for browser testing? … No / Yes
? Add Vitest for unit testing? › No / Yes
```

一通り選択すると、「Your project is ready!」ということで SvelteKit としての準備が整ったようです。
選択した項目が「✔」になってくれるので、わかりやすいです。

```shell
✔ Which Svelte app template? › Skeleton project
✔ Add type checking with TypeScript? › Yes, using TypeScript syntax
✔ Add ESLint for code linting? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Playwright for browser testing? … No / Yes
✔ Add Vitest for unit testing? … No / Yes

Your project is ready!
✔ Typescript
  Inside Svelte components, use <script lang="ts">
✔ ESLint
  https://github.com/sveltejs/eslint-plugin-svelte3
✔ Prettier
  https://prettier.io/docs/en/options.html
  https://github.com/sveltejs/prettier-plugin-svelte#options

Install community-maintained integrations:
  https://github.com/svelte-add/svelte-adders

Next steps:
  1: cd my-app
  2: npm install (or pnpm install, etc)
  3: git init && git add -A && git commit -m "Initial commit" (optional)
  4: npm run dev -- --open
```

ここまで完了すると、以下の構成でプロジェクトが作成されます。

```shell
my-app
├── README.md
├── package.json
├── src
│   ├── app.d.ts
│   ├── app.html
│   └── routes
│       └── +page.svelte
├── static
│   └── favicon.png
├── svelte.config.js
├── tsconfig.json
└── vite.config.js
```

ということで、あっという間に実行できるアプリができてしまいました。
実行してみるとこのような画面になります。

<img alt='first' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/6f8e3776-1f31-cb62-ada6-8d22a5fbb104.png'>

ではもう少し見栄えを良くして、それっぽくしていきます。

## スタイルを当てる

Sveltekit は公式ページの記載通り、tailwindcssと相性がいいので使ってみましょう。
tailwindcss 側の Docs にも SvelteKit に対するガイドがあるのが素敵ですね。

https://tailwindcss.com/docs/guides/sveltekit

<img alt='tailwindcss_supportlineup' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/b9054d10-ac8f-f692-a491-cc621be4c85c.png'>

導入手順は、SvelteKit 同様に公式 Docs 通り行なっていきます。
詳細は記載通りなので割愛しますが、最後まで進めて tailwind のスタイルが当たっていれば成功です。

<img width="400" alt="tailwindcss_firststep" src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/bdff800d-7181-6903-b9dd-8c85dbdc8a56.png'>

さて、これで **SvelteKit + tailwindcss** を用いた実装環境 が整ったので、プロト制作に取り掛かります。

## それっぽいサイトの構築

繰り返しますが、今回はいかに素早くサイトのプロトを作成するかに重きを置いています。
なので公開されているコンポーネント集を使って、それっぽいサイトを作りたいと思います。

tailwindcss には、UI キットやコンポーネント集が多く公開されています。公式ライブラリも Tailwind UI が存在します。ただ有料のものも多いので注意が必要です。
（サブスク形式ではなく、買い切りタイプなのは嬉しいポイント[^2]）

Tail-kitを利用して実装を進めていきます。

https://www.tailwind-kit.com/

まずページの大枠を決めたいので、Header, Footer を設定したいと思います。
Svelteでは、`+layout.svelte` に記述していきます。
Tail-kit から良さそうな素材をそれぞれ探し、コードを貼っていく作業です。

### Header / Footer の設定

Header / Footer を設定したい場合は、`<slot />` の上 / 下に記述すると、それぞれ表示してくれます。

```html:+layout.svelte
<script>
  import '../app.css';
</script>

<!-- ここにHeader部分を設定 -->

<slot />

<!-- ここにFooter部分を設定 -->
```
設定後はこちらです。
HeaderとFooterが正しい位置に設定され、メインページを挟んでくれていますね。
<img width="850" alt="footer" src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/8b022ed1-89e9-b093-99ef-78d6a7f6cd0b.png'>

### Main の設定

では最後に、メインページを作っていきます。

各ページは `+page.svelte` に記述することで前述の `<slot />` 位置に差し込んで表示してくれます。同じく Tail-kit から好きな素材を持ってきます。今回は以下のようなチームメイトを一覧で表示するページにしたいと思います。

<img width="1000" alt="sample" src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/868a71a3-200e-27d5-86e3-4ba4ddd0c71a.png'>

表示する中身ですが、せっかくなので API を叩いて画像をフェッチしてみます。
肝心の API ですが、いい感じにキツネの画像を返してくれる公開 API を見つけたので、キツネで構成された big team を結成したいと思います。（~~可愛かったからです。~~）

https://randomfox.ca/

具体的な実装は、以下の通りです。ひとまず1件のみ取得して表示してみます。
Svelte 独自の記法だと、**await ブロック `{#await 式}...{:then name}...{/await}`** を用いることで Promise が成功するかどうかで、レンダリング有無を選択させることができます。Promise が取りうる 3 つの状態（pending(保留中)、fulfilled(成功)、rejected(失敗)）に分岐できるというわけですね[^3]。

```html:+page.svelte
<script>
  const numOfFox = 1;
  const foxEndPoint = 'https://randomfox.ca/api/v1/getfoxes/?count=' + numOfFox;

  let imgSrc = '';

  const fetchImage = (async () => {
    const response = await fetch(foxEndPoint);
    const data = await response.json();
    return data.images;
  })();

  const getFox = (async () => {
    let img = await fetchImage;
    imgSrc = img[0];
    return await imgSrc;
  })();
</script>

{#await getFox}
  <!-- promise is pending -->
  <p>...waiting</p>
{:then imgSrc}
  ... 省略 ...
  <!-- promise was fulfilled -->
  <div class="p-4">
    <div class="flex-col  flex justify-center items-center">
      <div class="flex-shrink-0">
        <a href="#" class="relative block">
          <img
            alt="profil"
            src="{imgSrc}"  // APIで取得した URL を指定
            class="mx-auto object-cover rounded-full h-20 w-20 "
          />
        </a>
      </div>
      <div class="mt-2 text-center flex flex-col">
        <span class="text-lg font-medium text-gray-600 dark:text-white">
          Hean Hiut
        </span>
        <span class="text-xs text-gray-400"> Designer </span>
      </div>
    </div>
  </div>
{/await}
```

これで一つ取ってこれました。一覧表示まであと一歩です。

<img width='650' alt='one' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/87d6d9b5-c407-589a-a660-84a9825d9d16.png'>

APIを複数取得できるようクエリを設定して叩いてあげましょう。
複数取得の実装は、特に Svelte らしいところはないので割愛します。なんやかんや（[氏名をランダム生成する API](https://randomuser.me/)で取得したり、役職をテキトーに設定したり[^4]）で、表示するデータを得ることができたので、`+page.svelte` に一覧表示したいと思います。

[UI キットのサンプル](https://www.tailwind-kit.com/components/team)をみると、同じ構造が繰り返されていることがわかります。
なので、Svelte の記法である **eachブロック `{#each 式 as name}...{/each}`** で囲ってあげる ことで記述を大幅に省略できるかつ、数に応じて可変表示することができます。ここも1件取得の場合と同様、awaitブロック で囲ってあげます。

```html:+page.svelte
{#await getFoxes}
  <p>...waiting</p>
{:then foxes}
  ... 省略 ...
  {#each foxes as fox}
    <div class="p-4">
      <div class="flex-col  flex justify-center items-center">
        <div class="flex-shrink-0">
          <a href="#" class="relative block">
            <img
              alt="profil"
              src="{fox.imgSrc}"
              class="mx-auto object-cover rounded-full h-20 w-20 "
            />
          </a>
        </div>
        <div class="mt-2 text-center flex flex-col">
          <span class="text-lg font-medium text-gray-600 dark:text-white">
            {fox.name}
          </span>
          <span class="text-xs text-gray-400"> {fox.pos} </span>
        </div>
      </div>
    </div>
  {/each}
{/await}
```

その結果がこちらです。見事キツネたちが並びました。壮観です。
（~~個人的には、右下のSEOが可愛い。~~）

<img width='1000' alt='result' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2017966/abaf6533-f2d9-2025-2f23-ec1e8394fd89.png'>

以上で SvelteKit を利用したプロト作成は完了です。お疲れ様でした。

最終的なプログラムはこちらです。
<details><summary>ご興味のある方はコチラ</summary><div>

```html: +page.svelte
<script lang="ts">
	const foxEndPoint = 'https://randomfox.ca/api/v1/getfoxes/';
	const nameEndPoint = 'https://randomuser.me/api/';
	const numOfFox = 18;

	/**
	 * @type {any[]}
	 */
	let foxes = [];

    /**
     * 画像取得
     */
	let fetchImage = (async () => {
		const response = await fetch(foxEndPoint + '?count=' + numOfFox);
		const data = await response.json();
		return data.images;
	})();

    /**
     * 氏名取得
     */
	let fetchName = (async () => {
		const response = await fetch(nameEndPoint + '?results=' + numOfFox);
		const data = await response.json();
		return data.results;
	})();

    /**
     * 役職設定
     */
	function randomPos() {
		const posList = ['Chef', 'SEO', 'CTO', 'Designer', 'Architect', 'Engineer'];
		const pos = [];
		for (let i = 0; i < numOfFox; i++)
			pos.push(posList[Math.floor(Math.random() * posList.length)]);
		return pos;
	}

	const getFoxes = (async () => {
		let imgs = await fetchImage;
		let names = await fetchName;
		let poses = randomPos();

		for (let i = 0; i < numOfFox; i++) {
			foxes.push({
				imgSrc: imgs[i],
				name: names[i].name.first + ' ' + names[i].name.last,
				pos: poses[i]
			});
		}
		return await foxes;
	})();
</script>

{#await getFoxes}
	<p>...waiting</p>
{:then foxes}
	<div class="p-8 bg-white shadow dark:bg-gray-800">
		<p class="text-3xl font-bold text-center text-gray-800 dark:text-white">The big team</p>
		<p class="mb-12 text-xl font-normal text-center text-gray-500 dark:text-gray-300">
			Meat the best team in world
		</p>
		<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
			{#each foxes as fox}
				<div class="p-4">
					<div class="flex-col  flex justify-center items-center">
						<div class="flex-shrink-0">
							<a href="#" class="relative block">
								<img
									alt="profil"
									src={fox.imgSrc}
									class="mx-auto object-cover rounded-full h-20 w-20 "
								/>
							</a>
						</div>
						<div class="mt-2 text-center flex flex-col">
							<span class="text-lg font-medium text-gray-600 dark:text-white"> {fox.name} </span>
							<span class="text-xs text-gray-400"> {fox.pos} </span>
						</div>
					</div>
				</div>
			{/each}
		</div>
	</div>
{/await}
```

</div></details>

今回ルーティングは行なっていませんが、 **URL対応するディレクトリ** と **`+page.svelte`** を作成すると基本的なものは実現できます。ファイル名が特徴的なので注意が必要です。

```shell
src/routes/
├── Gallery/
│   └── +page.svelte
├── Content/
│   └── +page.svelte
├── Contact/
│   └── +page.svelte
├── +page.svelte
└── +layout.svelte
```

https://kit.svelte.jp/docs/routing#layout

# SSG 編

ここまでで盛りだくさんになったので、SSG についてはサクッと書きたいと思います。
SvelteKit で SSG 構築を実現するだけであれば、3 つの手順だけでよいです。

1. パッケージ `@sveltejs/adapter-static` のインストール
2. svelte.config.js の書き換え
3. `+layout.svelte` の書き換え

詳細は、GitHubを参照ください。

https://github.com/sveltejs/kit/tree/master/packages/adapter-static

```shell
npm install -D @sveltejs/adapter-static
```

```diff_javascript:svelte.config.js
- import adapter from '@sveltejs/adapter-auto';
+ import adapter from'@sveltejs/adapter-static';
import preprocess from 'svelte-preprocess';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: [
		preprocess({
			postcss: true
		}),
	],

	kit: {
-	  adapter: adapter()
+     adapter: adapter({
+     // default options are shown. On some platforms
+     // these options are set automatically — see below
+     pages: 'build',
+     assets: 'build',
+     fallback: null,
+     precompress: false,
+     strict: true
+   }),
+     trailingSlash: 'always',  // 詳細は後述
	}
};

export default config;
```

`trailingSlash` は、URLから末尾のスラッシュ(trailing slash)を取り除くかどうかを設定できます。
デフォルトは `trailingSlash: 'never'`で、例えば `/about/`にアクセスすると、`/about`へのリダイレクトをレスポンスとして受け取ります。`trailingSlash: 'alway'` の場合、`/about` の route が `about/index.html`となり、静的Webサーバの慣習に従います。

:::note warn
`'ignore'` は、`/x` と`/x/` が別URLとして扱われ、SEO上有害となるため非推奨です。
:::

https://kit.svelte.jp/docs/page-options#trailingslash

```html:+layout.svelte
<script context="module">
  export const prerender = true;
</script>
```

# おわりに

今回は、SvelteKitでSSR/SSGのプロトタイプを素早く構築する方法 をご紹介しました。

実際に触ってみて、プロト制作ではありますが、公式が謳っているとおり「高速/楽しい/柔軟」を感じることができたと思います。Svelte が「Write less code」を提唱しているだけあって、明快にプログラムが書けるような印象でした。JavaScriptを少しでも触ったことがある方なら、すぐに手早く取り掛かれるのではないでしょうか。

先日（2022/12/14）[公式ブログ](https://svelte.jp/blog/announcing-sveltekit-1.0) にて、「SvelteKit 1.0」が発表されました。Svelte は有志によって日本語化が進んでいるのも好印象です。それだけ注目されている証ですね。FW本体の開発も活発なので、今後も注目しておきたいと思います。

最後まで読んでいただき、ありがとうございました。
皆さんのお役に立てていれば、嬉しい限りです。


#### 余談

UI キットや公開APIを選んでいる時間の方が体感で長かったように思います。（あくまでも体感）
ドキュメントの日本語化が進んでいるのは、概念的に理解していないとっかかりの時期だと、
よりありがたいですね。

[^1]: 公式ドキュメンタリ「Svelte Origins: A JavaScript Documentary」
https://www.offerzen.com/community/svelte-origins-documentary
[^2]: taiwind UI 料金プラン
https://tailwindui.com/all-access
[^3]: 今回はエラーハンドリングしていないです。
行なう場合は、`{#await 式}...{:then name}...{:catch name}...{/await}` の構成となるはずです。
https://svelte.jp/docs#template-syntax-await
[^4]:UIキットに含まれていた値をもとに設定してみました。
`const posList = ['Chef', 'SEO', 'CTO', 'Designer', 'Architect', 'Engineer'];`