SvelteKitでMarkdown blogを作った2023年10月1日
Contents
- 手順
- 参考した記事
手順
SvelteKit プロジェクトを作成
npm create svelte@latest blog
Tailwind CSS
インストール
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
Typograhpy
Markdown のスタイルを適用するために必要
npm install -D @tailwindcss/typography
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [require('@tailwindcss/typography')]
}
postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
/src/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
/src/routes/+layout.svelte
<script>
import '../app.css'
</script>
<slot />
Markdown(mdsvex)
インストール
npx svelte-add@latest mdsvex
mdsvex.config.js
import { defineMDSveXConfig as defineConfig } from 'mdsvex'
const config = defineConfig({
extensions: ['.svelte.md', '.md', '.svx'],
smartypants: {
dashes: 'oldschool'
},
remarkPlugins: [],
rehypePlugins: []
})
export default config
svelte.config.js
import { mdsvex } from 'mdsvex'
import mdsvexConfig from './mdsvex.config.js'
import adapter from '@sveltejs/adapter-auto'
import { vitePreprocess } from '@sveltejs/kit/vite'
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', ...mdsvexConfig.extensions],
preprocess: [vitePreprocess(), mdsvex(mdsvexConfig)],
kit: {
adapter: adapter()
}
}
export default config
ブログの設定
設定ファイルを作成しておく。
/src/lib/config.ts
import { dev } from '$app/environment'
export const title = 'Coban'
export const description = "coban's blog"
export const url = dev ? 'http://localhost:5173' : 'https://coban.jp'
全ての Markdown ファイルを json に変換して JSON で返す API を作成
Post の型を定義しておく
// /src/lib/types.ts
export type Category = string
export type Tag = string
export type Post = {
title: string
slug: string
description: string
date: string
categories: Category[]
tags: Tag[]
published: boolean
}
/src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit'
import type { Post } from '$lib/types'
async function getPosts() {
let posts: Post[] = []
const paths = import.meta.glob('/src/posts/*.md', { eager: true })
for (const path in paths) {
const file = paths[path]
const slug = path.split('/').at(-1)?.replace('.md', '')
if (file && typeof file === 'object' && 'metadata' in file && slug) {
const metadata = file.metadata as Omit<Post, 'slug'>
const post = { ...metadata, slug } satisfies Post
post.published && posts.push(post)
}
}
posts = posts.sort(
(first, second) => new Date(second.date).getTime() - new Date(first.date).getTime()
)
return posts
}
export async function GET() {
const posts = await getPosts()
return json(posts)
}
/src/posts/*.md を glob で取得して、metadata と slug を取得。
published が true のものだけを posts に追加して、日付順にソートして返す。
一覧ページ
/api/posts からデータを取得して、一覧を表示する。
/src/routes/blog/+page.ts
import type { Post } from '$lib/types'
export async function load({ fetch }) {
const response = await fetch('api/posts')
const posts: Post[] = await response.json()
return { posts }
}
/src/routes/blog/+page.svelte
+page.ts で取得したデータは data として渡されるので、data.posts で取得できる。
<script lang="ts">import { formatDate } from "$lib/utils";
import * as config from "$lib/config";
export let data;
</script>
<svelte:head>
<title>{config.title}</title>
</svelte:head>
<!-- Posts -->
<section>
<ul class="grid sm:grid-cols-1 md:grid-cols-3">
{#each data.posts as post}
<li class="post">
<h2 class="text-3xl font-bold mb-2">
<a href={`/blog/${post.slug}`} class="title">{post.title}</a>
</h2>
<p class="text-gray-600 text-sm mb-4">{formatDate(post.date)}</p>
<p class="description">{post.description}</p>
</li>
{/each}
</ul>
</section>
page.ts で取得したデータは data として渡されるので、data.posts で取得できる。
個別ページへのリンクは/blog/[slug]
とする。
個別ページ
+page.ts でデータを取得して、+page.svelte で表示する。
/src/routes/blog/[slug]/+page.ts
import { error } from '@sveltejs/kit'
export async function load({ params }) {
try {
const post = await import(`../../../posts/${params.slug}.md`)
return {
content: post.default,
meta: post.metadata
}
} catch (e) {
throw error(404, `Could not find ${params.slug}`)
}
}
個別記事の slug は params.slug で取得できるので、Markdown ファイルを import して content と metadata を返す。
Markdown ファイルが見つからない場合は 404 エラーを返す。
/src/routes/blog/[slug]/+page.svelte
<script lang="ts">import { formatDate } from "$lib/utils";
export let data;
</script>
<svelte:head>
<title>{data.meta.title}</title>
<meta property="og:type" content="article" />
<meta property="og:title" content={data.meta.title} />
</svelte:head>
<article>
<hgroup>
<h1 class="text-3xl font-bold mb-4 flex gap-4 items-baseline">
{data.meta.title}<small class="text-sm text-gray-500">{formatDate(data.meta.date)}</small>
</h1>
</hgroup>
<div class="tags flex flex-wrap gap-2 mb-4">
{#each data.meta.tags as tag}
<a
href={`/tags/${tag}`}
class="bg-white text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-white border border-gray-500"
>#{tag}</a
>
{/each}
</div>
<div class="prose max-w-full">
<svelte:component this={data.content} />
</div>
</article>
data には +page.ts で返した content と metadata が入っている。
content は Markdown を Svelte のコンポーネントに変換したものなので、<svelte:component>
で表示する。
Tailwind CSS の typography を使っているので、prose クラスを付けると Markdown のスタイルが適用される。デフォルトでは max-width が 65ch になっているので、max-w-full
で上書きする。