JoyOfCode 블로거의 SvelteKit Blog 만들기를 따라한 클론 프로젝트입니다. 소스는 깃허브 에 있습니다.
참고문서
화면 캡쳐
블로그 포스트 리스트 (메인)
블로그 포스트 페이지
1. 프로젝트 생성
If you’re seeing this, you’ve probably already done this step. Congrats!
1
2
3
4
5
6
7
8
$ pnpm create svelte@latest svltk-blog-joyofcode
- Skeleton project
- TypeScript
$ cd svltk-blog-joyofcode
$ pnpm install
$ pnpm run dev
2. Developing
라이브러리 추가
- open-props : Open Source CSS Variables
- 색상, 타이포그래피, 그림자 등의 CSS 변수를 제공합니다.
- lucide-svelte : Svelte 를 위한 lucide icon library
- fontawesome/icons 의 svelte 버전이라고 생각하면 됩니다.
- 폰트 : @fontsource/manrope @fontsource/jetbrains-mono
1
2
3
4
5
pnpm i open-props lucide-svelte @fontsource/manrope @fontsource/jetbrains-mono
pnpm i -D mdsvex # preprocessor for Svelte
pnpm i shiki # Syntax Highlighter
open-props 예시 (postcss)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@import 'https://unpkg.com/open-props';
.card {
border-radius: var(--radius-2);
padding: var(--size-fluid-3);
box-shadow: var(--shadow-2);
&:hover {
box-shadow: var(--shadow-3);
}
@media (--motionOK) {
animation: var(--animation-fade-in);
}
}
레이아웃 구성
src/routes/+layout.svelte
- Header : Title, Menu(About, Contact, RSS), Theme Toggle
- Main
- PageTransition : Page 전환 애니메이션(fade)
- key block : key 값이 변경되면 key block 의 내용이 삭제되고 다시 생성된다.
- Section : Posts 리스트 출력
- PageTransition : Page 전환 애니메이션(fade)
- Footer : CopyRight, Link
src/routes/[slug]/*
+page.svelte
: Post 페이지 (article)svelte:head
: title, seo(og 태그)- hgroup : title, date
- tags : tag list
- content :
svelte:component
로 md 파일을 렌더링
+page.ts
: slug 이름의 md 파일을 읽어와 content, meta 를 전달- meta : title, date, tags(categories)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageLoad = async ({ 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 post with slug "${params.slug}"`);
}
};
src/posts/*.md
- first-post.md, second-post.md, example.md (Counter 버튼 예시)
- counter.svelte : 예시용 Svelte 컴포넌트
src/app.html
- icon 설정
- meta 설정 : rss
- script module 설정 : color-schema(theme) 설정
- 참고: 브라우저 환경에서 module script 의 특징
- 지연 실행 (defer)
- 비동기 처리 (async)
- 외부 스크립트 (src)
- 참고: 브라우저 환경에서 module script 의 특징
src/routes/error.html
- 사용자 정의 템플릿으로 error 메시지 출력
코드 작성 (TS)
lib 폴더
$lib/index.ts
- title, description 정의
- dev 모드인지 확인하여 url 을 다르게 설정
1
2
import { dev } from '$app/environment';
export const url = dev ? 'http://localhost:5173/' : 'https://joyofcode.xyz/';
$lib/theme.ts
- writable store 를 사용하여 theme 을 저장하고 변경
- localStorage.setItem
- documentElement.setAttribute
$lib/types.ts
- Post 타입 정의
- Category 타입 정의
$lib/utils.ts
- 문자열 날짜값을 medium date format 으로 출력
- Intl.DateTimeFormat 사용 (JS 내장 객체)
API handlers
src/routes/api/posts/+server.ts
- GET handler :
/api/posts
- getPosts() 함수
- import.meta.glob() : glob 패턴으로 md 파일들을 읽어온다.
- 파일명에서 slug 를 추출
- file.metadata 에 slug 를 더해 Post 타입으로 변환
- published 가 true 인 경우만 posts 에 추가
- posts 를 date 기준으로 정렬
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { json } from '@sveltejs/kit';
import type { Post } from '$lib/types';
import type { RequestHandler } from './$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 const GET: RequestHandler = async () => {
const posts = await getPosts();
return json(posts);
};
src/routes/api/rss.xml.ts
- Prerendering : 빌드 시점에 미리 렌더링하여 정적 파일로 생성
- GET handler :
/rss.xml
api/posts
에서 posts 를 읽어와 rss.xml 을 생성- xml 이라서 json() 대신 new Response() 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const prerender = true;
import * as config from '$lib/index';
import type { Post } from '$lib/types';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ fetch }) => {
const response = await fetch('api/posts');
const posts: Post[] = await response.json();
const headers = { 'Content-Type': 'application/xml' };
const xml = `
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
...(생략)
</channel>
</rss>
`.trim();
return new Response(xml, { headers });
};
Theme Toggle
src/routes/toggle.svelte
: Theme Toggle 버튼 컴포넌트
$theme
값에 따라 if 블럭으로 Sun/Moon 아이콘 출력- Theme 에 따라 Sun/Moon 아이콘을 fly transition 으로 전환
- click 이벤트에 toggleTheme 함수를 바인딩
src/app.html
- localStorage 로부터 color-schema 를 읽어 theme 설정
localStorage.getItem('color-scheme');
document.documentElement.setAttribute('color-scheme', theme)
app.css
: CSS color-scheme 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
:root {
color-scheme: dark;
--brand: var(--brand-dark);
--text-1: var(--text-1-dark);
--text-2: var(--text-2-dark);
--surface-1: var(--surface-1-dark);
--surface-2: var(--surface-2-dark);
--surface-3: var(--surface-3-dark);
--surface-4: var(--surface-4-dark);
--background: var(--background-dark);
--border: var(--border-dark);
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light;
--brand: var(--brand-light);
--text-1: var(--text-1-light);
--text-2: var(--text-2-light);
--surface-1: var(--surface-1-light);
--surface-2: var(--surface-2-light);
--surface-3: var(--surface-3-light);
--surface-4: var(--surface-4-light);
--background: var(--background-light);
--border: var(--border-light);
}
}
Mdsvex 설정
Mdsvex 는 MarkDown 문서를 Svelte 컴포넌트로 변환해 주는 preprocessor 이기 때문에, svelte.config.js 에 preprocess 에 추가해 줍니다.
- Syntax Highlighter 로 shiki 를 설정
- Img 컴포넌트를 사용하기 위해 layout 을 설정합니다.
- img 태그를 Image 컴포넌트로 overlay 합니다.
$lib/custom/img.svelte
: Image 컴포넌트
- lazy 로딩
1
2
3
4
5
6
<script lang="ts">
export let src: string;
export let alt: string;
</script>
<img {src} {alt} loading="lazy" />
svelte.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { mdsvex, escapeSvelte } from 'mdsvex';
import shiki from 'shiki';
/** @type {import('mdsvex').MdsvexOptions} */
const mdsvexOptions = {
extensions: ['.md'],
highlight: {
highlighter: async (code, lang = 'text') => {
const highlighter = await shiki.getHighlighter({ theme: 'poimandres' });
const html = escapeSvelte(highlighter.codeToHtml(code, { lang }));
return `{@html \`${html}\`}`;
},
},
layout: {
_: './src/mdsvex.svelte',
},
remarkPlugins: [remarkUnwrapImages, [remarkToc, { tight: true }]],
rehypePlugins: [rehypeSlug],
};
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', '.md'],
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
kit: {
adapter: adapter(),
},
};
export default config;
마크다운 플러그인
잘 모르겠다. 그냥 따라했다.
1
2
pnpm i remark-unwrap-images remark-toc rehype-slug
pnpm i rehype-slug rehype-autolink-headings
- rehype-slug : 제목에 ID를 자동으로 추가
- rehype-autolink-headings : 자동으로 제목에 링크를 추가
3. Building
1
2
3
pnpm run build # 빌드
pnpm run preview # 빌드된 정적 파일을 로컬에서 확인
Vercel Adapter 적용하기
svelte.config.js
에 auto 대신에 vercel adapter 설정 추가
1
2
3
4
5
# remove the default adapter
pnpm remove @sveltejs/adapter-auto
# add Vercel adapter
pnpm i -D @sveltejs/adapter-vercel
Vercel 배포하기
- github 에 push
- Vercel 에 git 연결
9. Review
- Mdsvex 관련 플러그인들이 많은데 무슨 기능들인지 제대로 보지 못했다.
- 한글로 작성된 블로그 문서가 있는데 tailwindcss 를 적용했다는 점만 다르고 동일한 내용이다.
- 다른 블로그 프로젝트들도 찾아서 따라해 보아야겠다. 재밋다.
- Josh Collinsworth 블로그 에는 category 리스트도 있다.
blog/1/+page.md
방식으로 구성할 수도 있다.- cf. 본 글에서는
blog/post-title.md
방식으로 구성
- cf. 본 글에서는
참고문서
- Let’s learn SvelteKit by building a static Markdown blog from scratch
- 깃허브 - edde746/sveltekit-markdown-blog
- 유튜브 - Building a blog with SvelteKit, TailwindCSS, and MDsveX
끝! 읽어주셔서 감사합니다.