SvelteKit + Newt + Github Pageでブログをデプロイするまで
前提
今回、SvelteKitの勉強も踏まえてSvelteKit + NEWT + Github Pagesで簡単なブログを作ってみました。SvelteKitはNext.jsなどと比べて、まだ情報量が少なく、躓く点がいくつかあったため、ここに残そうと思います。
構成
使用した技術
- SvelteKit : フロントエンドフレームワーク。今回は基本的に全てのページをPrerender(ビルド時に生成)する
- Github Pages:デプロイ先として使用
- Newt:Headless CMSとして記事の管理に利用
ファイル構成
概ねSvelteKitのテンプレートに沿った構成となっており、再利用するようなコンポーネントやライブラリはlib
フォルダから参照する形になっています(あまり大きなコードではないので、libの中にまとめています)
src
|- lib
---コンポーネント---
|- header
|- footer
---クライアントライブラリ---
|- newt
---型定義---
| -types
|- routes
|- articles
|- [slug].svelte
|- [slug].ts
|-pages
|- [...page].svelte
|- [...page].ts
...
実装のポイント
ソースは以下にまとめているので、ここでは簡単にポイントをピックアップしながら記載していきたいと思います(Svelteの記法や動作確認方法などはSvelte公式等を参照のこと)。
1.NewtのAppセットアップと記事作成まで
以下のクイックスタートの内容に従い、App・モデルを作成し、CDN API Tokenを発行します。非常にシンプルでテンプレートもしっかりと用意されているので、概ね迷うことなく進められるかと思います。
基本的に記事は対応するモデル(=Article)に定義したフィールド(タイトル・本文・作成日時...etc)に従ってNewt側で編集・管理し、これをJavaScript SDKで取得する形となります。2.SvelteKit 静的サイト生成の設定
SvelteKit + Github Pageの構成では、ビルド時に全てのページをレンダリングします。そのため、ビルド時にhtmlを生成するためのPrerenderの設定、およびadapter-staticのインストール・設定を行います。
- adapter-staticのインストール
npm i -D @sveltejs/adapter-static
- svelte.config.jsの設定
import static_adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
...
kit: {
adapter: static_adapter(),
...
prerender : {
default : true // デフォルトで全てのページがprerenderされる
}
}
};
3.SvelteKit + Newtのページ生成
ブログページをビルドする際にNewtから必要なデータ(記事のタイトル、本文...)を取得し、レンダリングを行います。
SvelteKitではデータを取ってくる側(Endpoint)とレンダリングする側(Page)をそれぞれ実装します。※コードは一部かつ、簡略化したものとなります
Endpointの実装
Newtから記事を取得してPage側に渡します。SvelteKitではファイル名([slug].ts
におけるslug)に対応したパラメータがparams
内に設定されますので、ここからNewtにリクエストする為のパラメータを取り出します。
import { getArticle } from '$lib/newt/client'
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async ({ params }) => {
const article = await getArticle(params.slug)
return {
body : {
article
}
};
};
import { createClient } from 'newt-client-js'
import type { Auther, Article, } from '$lib/types'
// UIDとTOKENは環境変数より取得
const client = createClient({
spaceUid: import.meta.env.VITE_SPACE_UID as string,
token: import.meta.env.VITE_CDN_API_TOKEN as string,
apiType: 'cdn' // You can specify "cdn" or "api".
});
const AppId = 'blog'
const ArticleId = 'article'
...
// Newtの記事を取得するメソッド
export const getArticle = async (slug:string)=> {
// Newtから記事データを取得する
const article = await client
.getContent<Article>({
appUid: AppId,
modelUid: ArticleId,
contentId: slug
})
return article
}
Newtの記事を取得するgetArticle
においては、スペースUIDやtokenが必要になります。開発環境では.envなどの環境変数に埋め込み、Github ActionsでビルドするにあたってはSecretsを用います(詳細は後述)。SvelteKitでは内部でViteを用いているため、環境変数にアクセスするにはimport.meta.env.VITE_SPACE_UID
といったようにViteの規則に従う必要があります(参考:
SvelteKit FAQ)。
Pageの実装
Endpointから受け取った記事データを用いてレンダリングします。Newtでは、記事の本文がリッチテキストもしくはMarkdownで書かれている場合、html
データとして取得することができるので、{@html article.body}
といった形でデータを渡してやります。
<script lang="ts">
import type { Article } from '$lib/types';
export let article: Article; // Endpointで設定されたBodyからデータを受け取る
</script>
<svelte:head>
<title>{article ? article.title : '記事が存在しません'}</title>
</svelte:head>
<div class="container">
{#if article}
<article>
<header>
<h1 bind:this={element}>{article.title}</h1>
...
</header>
<div class="content">
<!-- 記事本文のレンダリング -->
{@html article.body}
</div>
<footer>
<div class="meta">{format(new Date(article._sys.updatedAt), 'yyyy-MM-dd')}(更新)</div>
</footer>
</article>
{:else}
<h1>記事が見つかりませんでした</h1>
{/if}
</div>
<style lang="scss">
...
</style>
ここでは割愛していますが、同じ要領で、他の一覧ページなども作成していくことになります。
4.SvelteKit + Playwrightによるテスト
SvelteKitセットアップ時にPlaywrightをインストールすることができますが、ブラウザのインストール等必要なため、公式に従い以下を実施しておきます。
npm i -D @playwright/test
# ブラウザのインストール
npx playwright install
playwright.configは特にデフォルトのものから変更せずに使います。
import type { PlaywrightTestConfig } from '@playwright/test';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config : PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 3000
}
};
export default config;
テストについては1点注意があります。テストのためにビルドする際、今回はprouction想定でビルドしているので、各ページへアクセスする際に、後述(デプロイ前の準備②)のベースパスを忘れないように指定する必要があります。
import { expect, test } from '@playwright/test';
// ベースパスを忘れると page.gotoでpageが見つからなくなるので、忘れずに指定する
const BASE_PATH = '/リポジトリ名'
test('index page to article page transition', async ({ page }) => {
await page.goto(BASE_PATH);
expect(await page.textContent('h1')).toBe('My Profile');
...
});
5.Github Pagesへのデプロイ
Github Pagesへのデプロイは、いくつか設定や準備が必要のためSTEPを踏んで記載していきます。
Github側の設定
Github Pagesへデプロイするにあたって、プロジェクトのソース一式をGithubのリポジトリに登録しておきます。また、最終的にGithub Actionにてビルドするため、tokenなどの環境変数をSecretに登録しておきます(参考:リポジトリに暗号化されたシークレットを作成する)。
デプロイ前の準備①
Github Pagesのデプロイにあたって1点注意が必要となります。Github PagesではJikyllによってビルド処理を行う関係上_
や.
で始まるファイルやフォルダを無視します。
About GitHub Pages and Jekyllより:
By default, Jekyll doesn't build files or folders that:
- are located in a folder called /node_modules or /vendor
- start with _, ., or #
- end with ~
- are excluded by the exclude setting in your configuration file
一方でSvelteKitのデフォルトではbuild/_app
以下にjsやstyleファイルを出力します。このままデプロイしてしまうと、スタイルやjsが反映されなくなってしまうので、adapter-staticのREADMEに従い、Jikyllの処理を行わないように.nojekyll
という空のファイルをstatic
フォルダに生成しておきます。
touch static/.nojekyll
デプロイ前の準備②
Github Pagesでは、ルートのパスがhttps://アカウント名.github.io/リポジトリ名
となります。一方で今回作成したサイトのままだと、https://アカウント名.github.io
を起点として遷移するため、ルートページで<a href="/article/abc">
のようなリンクを踏むと、https://アカウント名.github.io/article/abc
に遷移してしまい、ページが表示されなくなります。
そこで、SvelteKitに /リポジトリ名 までがベースパスであることを認識させるために、以下の設定を行う必要があります。
import static_adapter from '@sveltejs/adapter-static';
...
+// productionの場合のみベースパスを設定する
+const production = process.env.NODE_ENV === 'production';
/** @type {import('@sveltejs/kit').Config} */
const config = {
...
kit: {
adapter: static_adapter(),
+ paths: {
+ base: production ? '/リポジトリ名' : '',
+ },
prerender : {
default : true
}
}
};
export default config;
また、これに伴って各ページ内のリンクや画像ファイルへのパスをbaseパスを伴ったものへと変更しておきます。
<script lang="ts">
import { base } from '$app/paths';
</script>
...
<div class="container">
<!-- リンクの場合 -->
<a sveltekit:prefetch href={`${base}/pages/0`}>Articles</a>
<!-- 画像の場合 -->
<img src={`${base}/my_image.svg`} width="128" alt="me" />
...
</div>
ローカルからのデプロイ
ここからはCLIからGithubにデプロイするために、gh-pages
をインストールしておきます。
npm install -D gh-pages
上記準備ができれば、最後にビルドとデプロイを行います。
npm run build && gh-pages -d build -t true
SvelteKitでビルドすると、通常build
フォルダにファイルが生成されるので、このフォルダをデプロイ対象として指定します。また、gh-pages
がGithub Pages用のブランチにビルドしたファイル群を登録する際、.
で始まるファイルが登録対象から除外されてしまわないように、オプションとして-t true
を指定しています(参考)。
ここまでで、やっとGithub Pages上でページが閲覧できるようになります🎉
Github Actionsからのデプロイ
最後にGithub Actionsでビルド・デプロイが行えるように設定しておきます。
Github Actionsの設定
プロジェクトルートに.guthub/workflows/deploy.yml
を作成し、こちらにデプロイフローの設定を記載していきます。トリガとしては、push/pull requestに加えて、Newt側で記事を更新した際に再ビルドを実行してほしいので、repository_dispatchを用いることにします
name: Svelte blog CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# webhookをトリガにする
repository_dispatch:
types:
- webhook
env:
VITE_SPACE_UID: ${{ secrets.VITE_SPACE_UID }}
VITE_CDN_API_TOKEN: ${{ secrets.VITE_CDN_API_TOKEN }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm install
# Playwrightによるテスト 記事更新(webhook)では動かないようにする
- name: install playwright
run: npx playwright install
if: github.event_name != 'repository_dispatch'
- name: run test
run: npm test
if: github.event_name != 'repository_dispatch'
- run: npm run build
- name: Deploy website
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: build
Personal access token
また、webhookを利用するにあたってはPersonal access tokenが必要になるため、存在しない場合は発行しておきます(scopeとしてはrepo
およびadmin:repo_hook
を設定します)。
Newtの設定
Newtには記事の作成・更新のタイミングでWebhookが設定できますので、以下を参考に設定しておきます(モデル単位で設定が可能)
基本的にNewtで設定が必要な項目は以下になります(参考:repository dispatchについて)。項目名 | 設定値 | 備考 |
---|---|---|
名前 | 任意の名称 | Web-Hookなど |
URL | https://api.github.com/repos/{owner}/{repository}/dispatches |
ower にはアカウント名,repository にはリポジトリ名 |
ヘッダー | Accept:application/vnd.github.everest-preview+json | |
シークレットヘッダー | Authorization: token {personal access token} | tokenにはrepo およびadmin:repo_hook のscopeが必要 |
以上までが、実装のポイントとなります。
躓いたポイント
ここからは、デプロイまでの流れにおいて、躓いた点などを簡単に紹介していきたいと思います。
SvelteKitのproductionビルドが成功しない
productionビルドをしたときに以下のようなエラーが出力されることがあります。
> 404 /特定のパス (linked from /参照元のパス)
たとえば、
> 404 /article/1 (linked from /リポジトリ名)
のようなエラーだった場合、ルート(=index.svelte
とか)で<a href="/article/1">
のような参照を行なっている可能性があります。上にも記載の通り、ベースパスを基点とするため、<a href={`${base}/article/1`}>
のように修正する必要があります。
SvelteKitのビルドがなかなか終わらない
上記にも関連しますが、次のようなコードを書くと延々とビルドが終わらなくなります。
<script lang="ts">
...
export let hasMore = false;
export let page = 0
</script>
...
<nav>
<a class={page > 0 ? 'nav-link' : 'nav-link-disabled'} href={`/pages/${page-1}`} tabindex={page > 0 ? 0 : -1}>前へ</a>
<a class={hasMore ? 'nav-link' : 'nav-link-disabled'} href={`/pages/${page+1}`} tabindex={hasMore ? 0 : -1}>次へ</a>
</nav>
このコードは、現在のページよりも次/前のページが存在しない場合、cssやtabindexの制御で遷移できないようにしています。しかし、SvelteKitはprerenderする際、href
等で参照されているページをrenderしようとするため、たとえhasMore
がfalse
であっても、href
が新たなページを指す限り、ページを生成し続けてしまいます(しかもこの場合、半永久的に...)。
なので、
<a class={page > 0 ? 'nav-link' : 'nav-link-disabled'} href={page > 0 ? `${base}/pages/${page-1}` : undefined} tabindex={page > 0 ? 0 : -1}>前へ</a>
のようにするなど、不必要なredner対象を作らない工夫が必要になります。
デプロイしようとするとA branch named 'gh-pages' already exists.
のエラーが出る
gh-pagesのキャッシュが残っている可能性があります。rm -rf node_modules/.cache/gh-pages
のようにキャッシュを削除すると解決する可能性が高いです。公式のTipsにも記載がありますね。
感想
サクッと作れると思いきや、細々と躓くポイントがありました。特にSvelteKitのビルド周りとGithub Pages...。しかし双方とも、非常に手軽・便利で開発体験の良さを実感することができました。また、Newに関してはでき手間もないのにドキュメント類が揃ってて非常に使いやすかったです。
Author And Source
この問題について(SvelteKit + Newt + Github Pageでブログをデプロイするまで), 我々は、より多くの情報をここで見つけました https://zenn.dev/mktu/articles/29eab3ac780f13著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol