Background
之前的博客是用 PanDoc 生成的静态网站,最近在学习 SvelteKit,花了两三天时间重构下,将所学的知识在实战中得到了练习。
本次重构功能更新:
- 实现了分页,不过只简单分了 2 页,因为没那么多文章。
- 将每周的技术周刊也加了进来,以后可以在同一个仓库写作和发布。
- 添加了全文搜索功能,方便搜索以往周刊内容。
- 使用 Skeleton 做了样式美化。
Generate Posts List
使用fast-glob
遍历 md 文件,使用gray-matter
解析 md 文件元数据,使用marked
将 md 文件转换为 html。
// src/lib/blog.js
import * as path from 'path';
import glob from 'fast-glob';
import matter from 'gray-matter';
import { marked } from 'marked';
import { readFileSync } from 'fs';
import { dateStr } from '$lib/utils/date.js';
marked.use({
mangle: false,
headerIds: false
});
const root = process.cwd();
async function importArticle(blogFilename) {
const file = path.join(root, 'blog', blogFilename);
let str = readFileSync(file, 'utf8');
const { data } = matter(str);
return {
url: blogFilename.replace(/\.md$/, ''),
data
};
}
export async function getPosts(page) {
const blogDir = path.join(root, 'blog');
let postsNames = await glob('*/*.md', {
cwd: blogDir,
onlyFiles: true
});
let posts = await Promise.all(postsNames.map(importArticle));
posts.sort((a, z) => new Date(z.data.date) - new Date(a.data.date));
const pageSize = 15;
const totalPages = Math.ceil(posts.length / pageSize);
return {
posts: posts.slice((page - 1) * pageSize, page * pageSize),
currentPage: page,
totalPages: totalPages
};
}
Skeleton
越来越喜欢 Skeleton 了,我从官方的网站仓库中学到了很多用法,有时候单看文档,写的不是很详尽,或者不知道一个组件适合的场景,官方的代码对照官网,是最好的演示。从这里开始,我喜欢上阅读源码。
Table of Contents、Modal、Drawers 这几个组件基本上不需要配置,开箱即用,非常方便。
默认主题也基本上满足需求,暂时没什么要修改的想法。
根布局+layout.svelte 如下,主要注意的是如何引入代码高亮和 Drawer、Modal 组件,还有 Ctrl+K 的按键事件。
<script>
// Dependency: Highlight JS
import hljs from 'highlight.js';
import '$lib/styles/highlight-js.css';
import { storeHighlightJs } from '@skeletonlabs/skeleton';
storeHighlightJs.set(hljs);
import '@skeletonlabs/skeleton/themes/theme-skeleton.css';
import '@skeletonlabs/skeleton/styles/skeleton.css';
import '$lib/styles/blog.css';
import '../app.postcss';
import {
AppShell,
AppBar,
LightSwitch,
Drawer,
drawerStore,
Modal,
modalStore
} from '@skeletonlabs/skeleton';
import { Menu, Search, Github } from 'lucide-svelte';
import SearchForm from '$lib/components/SearchForm.svelte';
const drawerSettings = {
id: 'drawer-menu',
width: 'w-full',
height: 'h-auto',
position: 'top',
rounded: 'rounded-none',
shadow: 'shadow-xl',
padding: 'pt-0'
};
function drawerOpen() {
drawerStore.open(drawerSettings);
}
function drawerClose() {
drawerStore.close();
}
const modalComponentRegistry = {
modalComponentSearch: { ref: SearchForm }
};
function modalOpen() {
const modal = {
type: 'component',
component: 'modalComponentSearch',
position: 'item-start'
};
modalStore.trigger(modal);
}
// Keyboard Shortcut (CTRL/⌘+K) to Focus Search
function onWindowKeydown(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
// If modal currently open, close modal (allows to open/close search with CTRL/⌘+K)
$modalStore.length ? modalStore.close() : modalOpen();
}
}
</script>
<!-- NOTE: using stopPropagation to override Chrome for Windows search shortcut -->
<svelte:window on:keydown|stopPropagation="{onWindowKeydown}" />
<Modal components="{modalComponentRegistry}" />
<Drawer>
<div class="grid grid-cols-2 gap-4 p-8">
<a class="btn btn-sm variant-ghost-surface" href="/" on:click="{drawerClose}">Blog</a>
<a class="btn btn-sm variant-ghost-surface" href="/weekly-news" on:click="{drawerClose}"
>Weekly News</a
>
<a class="btn btn-sm variant-ghost-surface" href="/about" on:click="{drawerClose}">About</a>
<a
class="btn btn-sm variant-ghost-surface"
href="https://github.com/theseazhang"
target="_blank"
rel="noreferrer"
on:click="{drawerClose}"
>
GitHub
</a>
</div>
</Drawer>
<AppShell>
<svelte:fragment slot="header">
<AppBar class="shadow-xl" slotDefault="place-self-center">
<svelte:fragment slot="lead">
<a href="/">
<strong class="text-base md:text-xl uppercase"> Dylan Zhang </strong>
</a>
</svelte:fragment>
<svelte:fragment slot="trail">
<div class="hidden md:flex">
<a class="btn hover:variant-soft-primary" href="/">Blog</a>
<a class="btn hover:variant-soft-primary" href="/weekly-news">Weekly News</a>
<a class="btn hover:variant-soft-primary" href="/about">About</a>
<a
class="btn-icon hover:variant-soft-primary"
href="https://github.com/theseazhang"
target="_blank"
rel="noreferrer"
>
<Github />
</a>
</div>
<button class="md:hidden" aria-label="Menu Button" on:click="{drawerOpen}">
<menu />
</button>
<button
class="btn p-2 px-4 space-x-4 variant-soft hover:variant-soft-primary"
on:click="{modalOpen}"
>
<Search />
<span class="hidden md:inline-block badge variant-soft">⌘+K</span>
</button>
<LightSwitch />
</svelte:fragment>
</AppBar>
</svelte:fragment>
<slot />
</AppShell>
Static Assets
md 文件中的图片或视频,需要放在static
目录下,这样才能正确引用。 或者使用 mdx
等类似的库,可以在 md 文件中直接导入图片。目前懒得折腾,够用即可。
Pagination
分页功能最开始走了点弯路,因为托管在 Vercel 上,ssr 动态读取 md 文件走不通,报错找不到文件。
只能 build 时静态生成。这种模式下,load 时又不能使用 search params。所以最后直接在路由里写死了,比如/page/2
。
What I Learned
整个项目重构下来,原本陌生的一些概念和写法,慢慢开始熟悉,对于使用 svelte 开发应用有了更多的信心。
总的体验来说,是要比 Next.js 舒服。接下来会继续使用 SvelteKit 完成一个英语学习的单页应用。