SvelteKit × Newt で技術ブログを作ってみた

はじめに

最近、SvelteKitとNewtを使用して技術ブログを作成しました。この記事では、その実装過程と主要な機能について解説していきます。

※ このプロジェクトのソースコードはGitHubで公開しています。

技術スタック

主な機能

  • Markdownコンテンツのレンダリング
  • シンタックスハイライト付きコードブロック
  • コードブロックのコピーボタン
  • 目次の自動生成
  • リンクカードの表示

実装のポイント

1. Newtクライアントの設定

まず、Newtとの連携のためのクライアントを設定します。
環境変数はセキュリティ上、フロント側で露出させないように、Netlifyのデプロイ設定から Environment variables としてビルド時に挿入するようにします。svelteでは $env/static/private から型安全に取得することができます。

src/lib/server/newt.ts
import { createClient } from 'newt-client-js';
import { NEWT_SPACE_UID, NEWT_CDN_API_TOKEN } from '$env/static/private';
 
export const newtClient = createClient({
	spaceUid: NEWT_SPACE_UID,
	token: NEWT_CDN_API_TOKEN,
	apiType: 'cdn'
});

2. ヘッドレスCMS側のデータ取得

svelteでは +page.svelte と同じ階層に +page.server.ts を置いて、load関数を定義することで、$propsからサーバー側で取得したデータを参照できます。

以下の例では記事一覧を取得しています。

src/routes/(app)/articles/+page.server.ts
import { newtClient } from '$lib/server/newt';
import type { Article, Author } from '$lib/server/newt';
import { NEWT_APP_UID } from '$env/static/private';
import type { PageServerLoad } from './$types';
 
const PER_PAGE = 20;
 
export const load: PageServerLoad = async ({ url }) => {
	const page = Number(url.searchParams.get('page')) || 1;
	const skip = (page - 1) * PER_PAGE;
 
	const [articlesRes, author] = await Promise.all([
		newtClient.getContents<Article>({
			appUid: NEWT_APP_UID,
			modelUid: 'article',
			query: {
				limit: PER_PAGE,
				skip
			}
		}),
		newtClient.getFirstContent<Author>({
			appUid: NEWT_APP_UID,
			modelUid: 'author'
		})
	]);
 
	return {
		articles: articlesRes.items,
		totalPages: Math.ceil(articlesRes.total / PER_PAGE),
		currentPage: page,
		author
	};
};
src/routes/(app)/articles/+page.svelte
<script lang="ts">
	import type { PageProps } from './$types';
	import Card from '../../Card.svelte';
	import Pagination from '../../Pagination.svelte';
 
	let { data }: PageProps = $props();
</script>
 
<svelte:head>
	<title>OimOyaの技術ブログ</title>
	<meta name="description" content="フロントエンドエンジニアのOimOyaが運営する技術ブログです。" />
</svelte:head>
 
<div class="mx-auto max-w-7xl">
	<main class="flex-1">
		<h1 class="text-3xl font-bold">記事一覧</h1>
		<ul class="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
			{#each data.articles as article (article._id)}
				<Card {article} />
			{/each}
		</ul>
		<Pagination totalPages={data.totalPages} currentPage={data.currentPage} />
	</main>
</div>

3. Markdownのレンダリング

今回、Newtからは記事の内容をMarkdown形式で返すように設定します。
以下の例では記事の詳細を取得していますが、fmt: 'text' と指定することで実現できます。

src/routes/(app)/articles/[slug]/+page.server.ts
import { newtClient } from '$lib/server/newt';
import { error } from '@sveltejs/kit';
import type { Article } from '$lib/server/newt';
import type { PageServerLoad } from './$types';
import { NEWT_APP_UID } from '$env/static/private';
import { markdownToHtml } from '$lib/server/markdownToHtml';
 
export const load: PageServerLoad = async ({ params }) => {
	const article = await newtClient.getFirstContent<Article>({
		appUid: NEWT_APP_UID,
		modelUid: 'article',
		query: {
			slug: params.slug,
			select: ['_id', '_sys', 'title', 'slug', 'body', 'tags'],
			body: {
                                // Markdown形式で取得する
				fmt: 'text'
			}
		}
	});
 
	if (article === null) {
		error(404, {
			message: `記事(${params.slug})が見つかりませんでした`
		});
	}
 
	// MarkdownをHTMLに変換
	const contents = await markdownToHtml(article.body);
 
	return {
		article: {
			...article,
			body: contents
		}
	};
};
 

Markdownは、最終的にHTMLに変換して表示します。
Reactなどだと便利なライブラリが充実していますが、svelteだと対応プラグインが少なく、かなりフィジ検証に時間がかかってしまいました...。
検討の結果、remarkとrehyphプラグインを使用して変換するようにしました。
(この辺の検討内容は別の記事にまとめます)

src/lib/server/markdownToHtml.ts
import { unified } from 'unified';
import remarkLinkCard from 'remark-link-card-plus';
import markdown from 'remark-parse';
import remark2rehype from 'remark-rehype';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import html from 'rehype-stringify';
import rehypeCodeTitles from 'rehype-code-titles';
import rehypePrettyCode from 'rehype-pretty-code';
import { transformerCopyButton } from '@rehype-pretty/transformers';
import rehypeSlug from 'rehype-slug';
import rehypeToc from '@atomictech/rehype-toc';
import rehypeAutoLinkHeadings from 'rehype-autolink-headings';
 
export const markdownToHtml = async (
	input: string,
	{ toc = true }: { toc?: boolean } = {}
): Promise<string> => {
	let processor = unified()
		.use(markdown)
                // リンクカードを生成
		.use(remarkLinkCard, {
			shortenUrl: true // リンクカード内のURLのホスト名のみを表示
		})
		.use(remarkGfm)
                // 改行を有効化
		.use(remarkBreaks)
		.use(remark2rehype, { allowDangerousHtml: true })
		.use(rehypeCodeTitles);
 
	if (toc) {
		processor = processor
			.use(rehypeSlug)
                        // 目次を生成
			.use(rehypeToc, {
                                // 今回は h1, h2 タグを対象とする
				headings: ['h1', 'h2'],
				cssClasses: {
					toc: 'toc-container',
					link: 'toc-link',
					listItem: 'toc-list-item',
					list: 'toc-list'
				}
			})
                        // アンカーリンクを生成する
			.use(rehypeAutoLinkHeadings, {
				behavior: 'append',
				properties: {
					className: ['anchor'],
					ariaHidden: 'true'
				},
				content: {
					type: 'element',
					tagName: 'span',
					properties: {
						className: ['anchor-icon', 'ml-2', 'text-gray-400', 'hover:text-gray-600']
					},
					children: [{ type: 'text', value: '#' }]
				}
			});
	}
 
	processor = processor
                // コードブロックのシンタックスハイライト
		.use(rehypePrettyCode, {
			transformers: [
				transformerCopyButton({
					visibility: 'always',
					feedbackDuration: 3_000
				})
			]
		})
		.use(html, {
			allowDangerousHtml: true
		})
		.process(input);
 
	const { value } = await processor;
	return value.toString();
};

まとめ

個人的な意見になりますが、初めてSvelteKitを使ってみてよかったなぁと思った点は以下の通りです。

  • SvelteKitの直感的なルーティングとコンポーネント設計
  • Markdownベースのコンテンツ管理の柔軟性
  • 高速なHMRによる開発体験の良さ
  • $propsシンタックスを使用することでクリーンなコードを書くことができる
  • ドキュメントが見やすい

書き味はNextjsに近く、今回初めての採用だったのですが、ほとんど学習コストがかからなかったのが印象的でした。あとHMRが爆速で開発体験が良い!

一方で少しとまどった点を上げると、svelteそのものの進化が早く、開発中も記法の変更が何回かあったことでした(これは開発が盛んという意味では良いことなのかも?)

あと、初めてNewtを使ってみてよかったなと思った点は以下の通りです。

  • 他のヘッドレスCMSと比較してAPI設定が直感的で分かりやすい
  • ドキュメントが充実していて見やすい(主要なサービスとの連携方法が記載してある)
  • なんといっても無料枠が大きい...!

無料で使える上に他の主要なヘッドレスCMSサービスに比べて、ブログに特化していて、直感的でわかりやすかったです。特殊な要件がない個人開発においては、第一候補としてオススメできるサービスだと思いました!

今後の展望

以下の機能を導入したいと思っています。

  • キーワード検索(全文検索)機能の実装
  • アクセス解析の導入
  • ダークモードの導入

© 2025 OimOya's Tech Blog. All rights reserved.