SvelteでMarkdownブログを作成する手順
- 公開日
はじめに
Markdownを用いてブログサイトを作成するにあたり、今回はSvelteとSvelteKitを採用してみました。単純にReactやVueに飽きてきたこともありますが、Svelteを少し深堀りしてみたかったこともその理由の一つです。
開発自体は、Viteの力もあってとても高速に進めることができました。たとえば、Gatsbyは開発環境を起動するだけで3分近くかかります(v5)が、SvelteKitだと数秒で起動します。使い方自体もシンプルでわかりやすいため、つまづくところはほとんどありませんでした。
反面、SvelteKit自体がまだ比較的新しいフレームワークであることと、1.0.0での破壊的変更も相まって、頼りになるドキュメントは公式のもの以外ほとんどありません。また、サードパーティ製のプラグインも少ないため、基本的に必要なものは自分で何とかする必要があります。
この記事では、Svelte、SvelteKit、MDsveXおよびSass(SCSS)を用いて、Markdownブログを作成し静的なサイトを生成するための基本的な手順を紹介したいと思います!
プロジェクトのセットアップ
それでは早速、コマンドラインを用いてSvelteの新しいプロジェクトを作成しましょう。my-app
の部分は適宜書き換えてください。
コマンドを実行すると、対話形式でのセットアップが始まります。
プロジェクトのテンプレートは、Skelton project
を選択し、スクラッチで開発します。選択すると、プロジェクトの環境設定が始まりますが、ここでは、TypeScript、Eslint、Prettierを有効にし、そのほかのツールは無効にしました。適宜好みに合わせて変更してください。
設定が完了したら、作成されたディレクトリに移動し、必要なパッケージをインストールします。
インストールが終わったら、早速npm run dev
で起動してみましょう。表示されるリンクを開き、以下のようなメッセージを確認できれば準備完了です!
ディレクトリによるルーティング
SvelteKitでは、すべてのルートがディレクトリによって管理され、その中に特定のファイルを入れることにより、各ページの処理がそのディレクトリ内ですべて完結するよう設計されています。
たとえば、先ほど表示したフロントページを表示しているファイルは、src/routes/+page.svelte
です。コンテンツを書き換えると、HMRによりブラウザでの表示が変わることを確認できます。
試しにAboutページを作成してみましょう。routes
にabout
ディレクトリを作成し、+page.svelte
ファイルを追加します。SvelteKitの特殊なファイルであることを示す+
記号を忘れないように注意してください。
- src
- routes
- about
- +page.svelte
about/+page.svelte
の内容は、適当に以下のようにしてみます。
この状態で/about
にアクセスすると、Aboutページが表示されます。とても簡単ですね!
Svelteのコンポーネントですので、もちろんスクリプトも使えます。
共通レイアウトの作成
たいていのサイトでは、ヘッダやフッタなど、どのページでも使われる共通のレイアウトが存在します。SvelteKitではこれを、+layout.svelte
ファイルを用いて簡単にマークアップできます。
ヘッダとフッタ用のコンポーネント
レイアウトを作成する前に、ヘッダとフッタ用のコンポーネントを準備しておきましょう。まず、src
以下にlib
ディレクトリを作成します。このディレクトリは$lib
というパスのエイリアスがききますので、ディレクトリ名は変えないでください(もちろん設定を変更することもできます)。このディレクトリ以下の構成は任意ですが、ここではcomponents
というディレクトリを作ることにします。
- src
- lib
- components
- Header.svelte
- Footer.svelte
- routes
Header.svelte
にはメインのナビゲーションを、Footer.svelte
には著作権表示をいれてみます。
レイアウトファイルの作成
次に、routes
直下に+layout.svelte
ファイルを作成します。
- src
- lib
- routes
- about
- +layout.svelte
- +page.svelte
このファイルで、先ほど作成したコンポーネントを読み込み、配置します。
<main>
タグの中にある<slot />
は、各ページ以下の+page.svelte
のコンテンツによって置き換えられます。フロントページやAboutページを開くと、どちらのページにもヘッダやフッタが表示されていることを確認できます。
スタイルの適用
このままでは見た目が味気ないので、少しだけスタイリングしてみましょう。CSSだけでは管理しにくいので、ここではSass(SCSS)を利用します。
Sassのインストール
ViteおよびSvelteKitのプリプロセッサは何もしなくてもSassに対応していますが、本体がないのでインストールしておきます。
これだけでSassが使えるようになります。
Scopedスタイルの適用
それでは、Header.svelteを開いて<style>
タグを追加し、ヘッダの見た目を調整してみましょう。SCSSを使用するため、lang="scss"
属性を追加します。
ブラウザで確認すると、次のようにスタイルが適用されているはずです。リンクにマウスを乗せて色が変われば、SCSSのコンパイルも正しく動作しています。
Svelteのコンポーネント内でスタイルを定義すると、自動的に”scoped”として扱われ、コンポーネント外のエレメントには一切影響しません。Shadow DOMによるカプセル化によく似ていますね。このようにしてSvelteは、各コンポーネントの独立性を保っています。
コンポーネント外の要素にスタイルを当てたい場合は、:global()
を用いてグローバル化することもできます。ただし、スタイルの影響範囲が見えづらくなるため、必要最低限の使用にとどめることをお勧めします。
グローバルスタイルの定義
コンポーネント単位ではなく、グローバルにスタイルを定義したい場合は専用のスタイルシートを用意して読み込みます。まず、src
にcss
ディレクトリを用意し、さらにglobal
ディレクトリを作っておきます(ディレクトリ構成は任意です)。このディレクトリに、index.scss
とglobal.scss
の2つのファイルを用意してください。
- src
- css
- global
- global.scss
- index.scss
global.scss
に、サイト全体に対して適用するスタイルを書いていきます。
これを、index.scss
で読み込みます。デフォルトのスタイルを無効にするreset.scss
や、@font-face
を定義するfont.scss
などは、将来的にここから@forward
する設計です。
スタイルを作成したら、実際に適用してみましょう。サイト全体のレイアウトを定義している+layout.svelte
で、index.scss
を読み込んでみてください。先ほど作成したスタイルがサイトに適用されるはずです。
変数の使用
一般的に、サイトで使用する色やフォントサイズなどは、一貫性・保守性などの観点から変数として管理されます。そこで、先ほど作成したcss
ディレクトリに、今度はvariables
というディレクトリを追加し、色を定義するためのcolors.scss
というファイルを作ってみましょう。
- src
- css
- global
- variables
- colors.scss
このファイルに、サイトで使用する色を記述していきます。ライト・ダークモードの切り替えを想定している場合などは、--var()
を使用することもできます。
これを各コンポーネントから参照するのですが、../../../
のようになることを防ぐため、css
ディレクトリにパスを通しておきます。slvelte.config.js
を開いて、次のようにpreprocess
にscss
のオプションを渡してください。
こうすると、Sassコンパイラはcss
ディレクトリからもscss
ファイルを探しに行ってくれるようになります。試しに、Header.svelte
から変数を参照してみましょう。
ヘッダの背景の色がcolors.$dark600
で定義した色に変わっていれば成功です。同じようにして他の変数を読み込んだり、あるいはmixins
を定義して使用したりできます。
includePaths
は古い仕様で、本来はloadPaths
を使用する必要があります。しかし、Viteが内部で古いメソッドを使ってSCSSのコンパイルを行っているため(2022年12月現在)、includePaths
で定義しないと動作しません(#7116)。
VSCodeやJetBrainsのIDEは、includePaths
あるいはloadPaths
を認識しないため、コードの補完が利きません。ただし、JetBrainsの場合はcss
ディレクトリをResource Root
に設定すると、パスを認識するようになります。
Markdownの使用
ReactプロジェクトなどではMDXがよく利用されますが、似たような機能をSvelteで実現するMDsveXというライブラリがあります。MDsveXというネーミングに愛を感じてなりません。
このライブラリはSvelteのプリプロセッサを提供し、主に以下のような特徴があります。
- remarkによるMarkdown処理(remark/rehype系のプラグインが使える)
- MarkdownファイルからSvelteコンポーネントへの変換
- Markdown内でのSvelteコンポーネントの利用
MDsveXのインストール
まずはNPMから最新版をインストールします。
次に、svelte.config.js
を開いてpreprocess
にmdsvex
を追加してください。このオプションは配列を用いることで、複数のプリプロセッサを登録することができます。また、新たにextensions
というプロパティを追加し、Svelteがsvx
拡張子を認識できるようにします。
拡張子svx
は、md
やmdx
などとしても動作させることができますが、公式の型定義を見ると*.svx
、あるいは*.svelte.md
となっていますので、このどちらかを用いるのが適切でしょう。ただし、IDEはsvx
をMarkdownファイルとして認識しませんので、「関連付け」の機能を用いてプレビューやシンタックスハイライトを有効にするか、それが難しい場合はsvelte.md
を採用するのが得策かと思います。
動作確認
MDsveXが使えるようになったかどうかを確認するため、一時的にmarkdown
ページを作ってみましょう。src/routes/markdown
に、+page.svx
を追加します。
- src
- routes
- markdown
- +page.svx
+page.svx
に、Markdownでコンテンツを書いてみてください。
ブラウザで確認し、以下のようにMarkdownが正しく解析されていれば成功です。
僕は使っていませんが、MDsveXはコードブロックに対し、PrismJSによるシンタックスハイライトを適用します(オプションで別のライブラリに変更することもできます)。ただし、色を付けるには適切なCSSを読み込む必要がありますので、適宜サイトからダウンロードしてください。
なお、このページは確認が終わったらディレクトリごと削除して構いません。
記事ページの作成
ブログページを実現するためには、「ブログ一覧」と「それぞれの記事」の2種類のページが必要になります。まずは、記事単体のページから実装していきましょう。
ページの準備
最初に、仮の記事をいくつか作成し、ディレクトリ構成を確定しておきます。このとき、各記事を「ファイルとして並べる」か「それぞれのディレクトリに分ける」かで構成が分岐します。前者の場合は
- src
- posts
- post01.svx
- post02.svx
となり、後者の場合は
- src
- posts
- post01
- index.svx
- post02
- index.svx
となります(ディレクトリ、ファイル名ともに任意です)。今回は処理を簡単にするため、「ファイルを並べる」方式で進めていきます。
- カテゴリ別・年別など、もっと複雑な構成にもできます。
src
内に記事を配置したくない場合は、Viteの設定を変更する必要があります。
各ファイルにはコンテンツ以外に、ページのタイトルや公開日などを設定するためのfrontmatter
を記載しておきます。
ルートの設定
各ページにblog/post01
のようにしてアクセスすることを想定したとき、post01
の部分は動的に処理できる必要があります。SvelteKitでは、このようなダイナミックルーティングもディレクトリ構成を用いて実現します。
まず、src/routes
にblog
ディレクトリを作成し、さらにその中に[slug]
というディレクトリを作成してください。このディレクトリに、+page.ts
と、+page.svelte
という2つのファイルを配置します。
- src
- routes
- blog
- [slug]
- +page.svelte
- +page.ts
データの準備
page.ts
(あるいは+page.js
)は、+page.svelte
がレンダリングされる前に必要となるデータを収集・処理するための特別なファイルです。ここに、[slug]
をもとにしてMarkdownファイルを読み込む処理を記述していきます。
load
関数を用意します。この関数は、SSR時あるいはブラウザでのナビゲーション時に、+page.svelte
がレンダリングされる前必ず実行され、引数のオブジェクトにリクエストのパラメータを持っています。params
からslug
を取得し([slug]
に対応しています)、Markdownファイルを読み込みます。存在しないslugにアクセスされた場合はエラーになるので、try-catch
で囲みます。最後に、
+page.svelte
で受け取るためのデータを返します。MDsveXはfrontmatter
をmetadata
としてexportしているので、展開してすべて返しておきましょう。また、default
にはSvelteコンポーネントがモジュールとして入っていますので、content
として渡しておきます。
コード全体は以下の通りです。厳密な型定義やエラーハンドリングは省略しています。
なお、./$types
はSvelteKitが動的に生成する型へのエイリアスです。詳しくはGenerated Typesを参照してください。
記事ページのレンダリング
ページをレンダリングするために必要なデータがそろったので、+page.svelte
を編集していきましょう。load
関数が返すオブジェクトは、data
をexportすることで受け取れます。先ほどfrontmatter
およびSvelteのコンポーネントを返しましたので、これらを使って以下のようにマークアップしてみてください。Svelteのコンポーネントは、<svelte:component>
にthis
として渡すことでレンダリングできます。
完成したら、/blog/post01
にアクセスしてみましょう。下の画像のように、記事がうまく表示されていれば成功です。
JetBrainsのIDEは、動的に生成される$types
を認識してくれません(2022年12月現在)。YouTrackを追っていると、v2022.3で解決しそうに見えますが、どうなるかはわかりません。なお、VSCodeは$types
を認識してくれます。
記事一覧ページの作成
それぞれの記事を表示できるようになったので、今度はすべての記事を一覧するページを作成していきましょう。
ルートの設定
先ほど作ったblog
ディレクトリに、+page.ts
と+page.svelte
の2つのファイルを配置してください。
- src
- routes
- blog
- [slug]
- +page.svelte
- +page.ts
記事一覧の取得
まず、+page.ts
に記事の一覧を取得する処理を書いていきます。
load
関数内で、Viteが提供するimport.meta.glob
を用いてすべての記事を取得します。glob
はキーにモジュールへのパスを、値にモジュールをインポートするための関数を持ったオブジェクトを返しますので、Object.entries
などを用いて一つひとつ読み込んでいきます。path
からファイル名のみを抽出し、slugを生成します。適切なslugが見つかった場合にのみ、必要となる情報を返します。記事単体のページと同様、
metadata
にはfrontmatter
の情報が格納されています。最後に、
posts
を日付順で並べなおします。
全体のコードは次の通りです。実際には適切なエラー処理や厳密な型定義を行ったほうが安全ですが、ここでは省略します。
記事一覧の表示
データの準備ができたので、+page.svelte
で記事一覧を表示してみましょう。記事のデータは配列としてposts
に格納したので、#each
ブロックで回します。
これで記事一覧ページができました。Header.svelte
コンポーネントに/blog
へのリンクを追加し、アクセスしてみてください。次のように表示され、タイトルをクリックして各記事に遷移できれば完成です。
補足:ページネーションの実装
今回の実装では、/blog
ページにすべての記事が並ぶことになります。実践ではリストを分割し、ページネーションを実装したくなるかもしれません。このとき、/blog
および/blog/page/2
などをまとめて処理することになるため、Rest Parameterを用いて実現します。
具体的には、+page.svelte
および+page.ts
を/blog
直下に配置するのではなく、[...page]
ディレクトリの中に作成します。
- src
- routes
- blog
- [...page]
- +page.svelte
- +page.ts
- [slug]
+page.ts
でパスを解析し、必要な記事のリストをページ数に応じて返していくことになりますが、長くなるのでここでは割愛します。
静的サイトの生成
ここまででサイトの骨子は出来上がったので、静的サイトとして書き出してみます。SSR前提の場合、このステップは必要ありません。
まず、静的サイトを生成するためのアダプタをインストールします。
次に、svelte.config.jsを開いて、アダプタを置き換えます。
最後に、サイト全体をプリレンダリングするようSvelteに伝えます。routes
ディレクトリに+layout.server.ts
というファイルを追加し、次の1行を加えてください。
これで設定は完了です。念のため、ビルドして動作を確認してみましょう。
ビルドに成功すると、プロジェクト内にbuild
ディレクトリが生成され、サイトに必要なすべてのデータが書き出されます。preview
コマンドを実行して、内容を確認してみてください。
正常に動作しているようなら、あとはbuild
ディレクトリのファイルをサーバにアップロードすればサイトの完成です!
そのほかの作業
……とはいえ、実際のサイトとして運用するには、まだいくつかやらなければならないことがあります。ここでは概要の紹介のみにとどめますが、機会があればいつか詳細を書きたいと思います。
SEO対応
<title>
タグやOpen Graph用の<meta>
タグなどを、ページごとに設定する必要があります。基本的には、<svelte:head>
を用いて各ページの<head>
を書き換えていくことになります。
ただし、同じような処理が分散することは望ましくないため、専用のコンポーネントを作成し、使いまわすのが得策だと思います。
サイトマップの作成
サイトマップは、routes
にsitemap.xml
というディレクトリを作り、サーバのみで実行される+server.ts
を用いて作成します。公式サイトに例が掲載されていますので、参考にしてみてください。
エラーハンドリングとエラーページの作成
無効なURLにアクセスしたときに表示されるエラーページは、+error.page
でカスタマイズすることができるようですが、staticアダプタの場合は動作しないようです(僕が何か設定を見落としているだけかもしれませんが、サーバのデフォルトの404ページが表示されてしまう)。fallback
オプションを利用する、または専用のルートを用意してリダイレクトするなど、環境にあったやり方で実装してください。
おわりに
SvelteKitによるブログ作成は初めてでしたが、思った以上に開発がしやすい印象でした。IDEのサポートが追い付いていないこと、情報がまだ少ないことなど残念なところもありますが、しばらくは使い続けてみようかと思います。
画像の最適化やパフォーマンスの改善など、手を付けていないところもたくさんあるので、いろいろわかったらまたブログで報告しますね。