使用SvelteKit重构网站

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 完成一个英语学习的单页应用。