cd ..

从零开始搭建博客网站(五)

2025年2月6日
约19分钟

从零开始搭建博客网站(五):文章列表。


log/dev roadmap/blog_site

文章列表

现状

PageContentHome.vue 中可以看到, 当前的文章列表是通过手写 DOM 添加的, 这种重复性的工作显然是不应该存在的。

vue
<script setup lang="ts">
import { useData } from 'vitepress'

const { site } = useData()
</script>

<template>
  <div
    v-if="frontmatter.home"
    un-mx-auto
    un-max-w="[700px]"
  >
    <h1
      un-text="center 4xl"
      un-m-4
    >
      {{ site.title }}
    </h1>
    <div
      un-m-4
      un-min-h="[200px]"
      un-text-center
    >
      <p>{{ site.description }}</p>
    </div>
    <ul
      un-flex="~ col"
      un-space-y-2
    >
      <li un-text-blue-500>
        <a href="/markdown-examples.html">Markdown Examples</a>
      </li>
      <li un-text-blue-500>
        <a href="/api-examples.html">API Examples</a>
      </li>
    </ul>
  </div>
</template>

构建时数据加载|Build-Time Data Loading

如果您的创造性足够强, 也可以直接去 看 VitePress 官方文档。 这里我会用我自己的方式来操作。

简要来说, 对于目前的需求, 我们需要提取所有文章的一些元数据, 比如标题、 发布时间、 地址等, 并渲染一个文章列表作目录使用。

所有的构建时数据加载都需要在 *.data.ts 文件中完成。 我们在 theme/ 文件下新建 src/ 文件夹, 并新建一个 posts.data.ts 文件:

ts
import { 
createContentLoader
} from 'vitepress'
export default
createContentLoader
('posts/*.md', {
transform
(
raw
) {
return
raw
.
map
(({
url
,
frontmatter
}) => ({
url
,
frontmatter
,
})) }, })

这里我们使用 VitePress 提供的 createContentLoader() 辅助函数来创建一个内容加载器。 它的第一个参数是一个 glob 模式, 用于匹配需要加载的文件。 第二个参数是一个选项对象, 用于配置加载器的行为。 更细节的内容可以参考 官方文档

在这里, 我们匹配了 posts/ 目录下的所有 .md 文件, 并提取其 urlfrontmatter 属性。 在 transform() 成员中可以对数据进行操作, 这里先不进行任何处理。

当然, 我们需要把除了 index.md 外的所有的 MD 文件都放在 ./docs/posts/ 目录下。

或者如果您自己有管理文档和路由的思路,请自由发挥。

现在我们可以在 PageContentHome.vue 中使用导出的数据:

vue
<!-- 这两行红的没用,只是用来正确显示下面的 error,如果要复制粘贴,记得删掉。 -->
<script lang="ts">
</script>

<script setup lang="ts">
// ...
import { data as 
posts
} from '../src/posts.data'
Module '"../src/posts.data"' has no exported member 'data'. Did you mean to use 'import data from "../src/posts.data"' instead?
// ... /* eslint-enable import/first */ </script> <!-- ... -->

一些类型问题

不出意外的话,TS 用户又要出意外了。 根据 VitePress 文档 所述, VitePress 在后台调用了 load() 方法, 将 *.data.ts 的默认导出用 data 具名导出来隐式暴露。 所以在使用具名导入的时候, 编译器会认为这个模块没有名为 data 的具名导出。

解决方法也很简单, 在 posts.data.ts 中导出相应类型即可:

ts
import { 
createContentLoader
} from 'vitepress'
export interface Data { // ... } declare const
data
: Data[]
export {
data
}
// ...

生成文章列表

Data 接口中, 需要定义经过 transform() 后的属性。 要做一个最基本的文章列表, 我们需要每篇文章的链接和标题, 其中标题可以在文章的 frontmatter 中定义, 从而通过上面的 frontmatter 访问:

ts
// ...
export interface Data {
  
url
: string
frontmatter
:
Record
<string, any>
} // ...
vue
<script setup lang="ts">
// ...
</script>

<template>
  <div
    v-if="frontmatter.home"
    un-mx-auto
    un-max-w="[700px]"
    un-min-h="[calc(100vh-100px)]"
    un-block
  >
    <h1
      un-text="center 4xl"
      un-m-4
    >
      {{ site.title }}
    </h1>
    <div
      un-m-4
      un-min-h="[200px]"
      un-text-center
    >
      <p>{{ site.description }}</p>
    </div>
    <div>
      <div
        v-for="post in posts"
        :key="post.url"
        un-py-2
        un-text="neutral-600 hover:neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
        un-transition-colors
        un-duration-200
      >
        <a
          un-text-2xl
          :href="post.url"
        >{{ post.frontmatter.title }}</a>
      </div>
    </div>
  </div>
</template>

当然, 一定要确保每个 MD 文件都有 title 这个 frontmatter:

markdown
---
title: "Example Title"
---

效果如下图所示:

老是在改改改的主页

最后, 注意到我们应该会经常使用 mx-auto max-w="[700px]" 这个类名, 它实现了居中和可读性较高的宽度。 可以再加上 min-h="[calc(100vh-100px)] block" 来确保高度不会太小。 我们可以把这个玩意提出来写成 UnoCSS 的 shortcuts

ts
import {
// ...
} from 'unocss'

export default defineConfig({
  shortcuts: {
    'page-content': 'mx-auto max-w-[700px] min-h-[calc(100vh-100px)] block',
  },
  // ...
})
vue
<template>
  <un-page-content>
    <!-- ... -->
  </un-page-content>
</template>

除此之外,为了不让 Vue 的编译器报错,我们还需要在 config.mts 中设置一下 compilerOptions

ts
import unocss from 'unocss/vite'
import { defineConfig } from 'vitepress'

// https://vitepress.dev/reference/site-config
export default defineConfig({
  title: 'Example Site',
  description: 'A VitePress Site.',
  vite: {
    plugins: [
      unocss(),
    ],
  },
  vue: {
    template: {
      compilerOptions: {
        isCustomElement: tag => tag.startsWith('un-'), 
      },
    },
  },
})
前文
没了
后文
2024-PRESENT
CC BY-NC-SA 4.0
©
froQ