cd ..

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

2025年1月20日
约30分钟

从零开始搭建博客网站(三):像样的页面与文件结构优化。


log/dev roadmap/blog_site

像样的页面

现状

是的,现在的页面可读性几乎为零。这篇文章的目标是在样式和结构上进行设计。

VitePress 的页面结构

首先,对 Vue 有过了解的读者可能会比较清楚, 整个页面——或者说是整个应用——挂载在 #app 这个 DOM 元素上。 在 index.ts 中交代了一些信息:

ts
import type { Theme } from 'vitepress'
// @noErrors
// https://vitepress.dev/guide/custom-theme
import Layout from './Layout.vue'
import './style.css'

export default {
  Layout, 
  enhanceApp({ app, router, siteData }) {
    // ...
  },
} satisfies Theme

这告诉我们,Layout.vue 是整个页面的总体布局, 且根据 Theme 类型的定义, Layout 实质上是 Theme 唯一一个必要的属性。 文档 称 「So technically, a VitePress theme can be as simple as a single Vue component」, 也就是说,所有的结构都可以仅在 Layout.vue 中定义。 但是为了更好地组织代码, 我们也可以在 Layout.vue 中引入其他组件, 像一个真正的 Vue 应用一样。

再次看看 Layout.vue 的代码:

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

// https://vitepress.dev/reference/runtime-api#usedata
const { site, frontmatter } = useData()

function switchDarkMode(): void {
  const htmlEl: HTMLElement | null = document.querySelector('html')
  htmlEl?.classList.toggle('dark')
}
</script>

<template>
  <div
    un-min-h-100vh
    un-bg="neutral-50 dark:neutral-950"
    un-text="neutral-700 dark:neutral-300"
  >
    <div v-if="frontmatter.home">
      <h1>{{ site.title }}</h1>
      <p>{{ site.description }}</p>
      <ul>
        <li><a href="/markdown-examples.html">Markdown Examples</a></li>
        <li><a href="/api-examples.html">API Examples</a></li>
      </ul>
    </div>
    <div v-else>
      <a href="/">Home</a>
      <Content />
    </div>
    <div
      un-bg="neutral-300 dark:neutral-700"
      un-text="neutral-700 dark:neutral-300"
      class="dark-mode-switcher"
      @click="switchDarkMode"
    >
      Switch
    </div>
  </div>
</template>

<template> 中, 我们可以看到基于 frontmatter.home 的条件渲染。 frontmatter 是 MD 文件的元数据, 这意味着如果当前页面对应的 MD 文件的 YAML 头 (或者 JSON 头) 中有 home: true, 则会渲染 site.titlesite.description 等首页内容。 否则,会渲染一个返回首页的链接和 <Content /> 组件。 <Content /> 组件是 VitePress 的内置组件, 用于将当前页面对应的 MD 文件渲染为 HTML。

初步设计

为了便于区分,我们先微调一下 Layout.vue 的结构,并加上一些样式:

vue
<!-- ... -->

<template>
  <div
    un-min-h-100vh
    un-bg="neutral-50 dark:neutral-950"
    un-text="neutral-700 dark:neutral-300"
  >
    <nav
      un-flex="~ row"

      un-p-4
      un-justify-between
    >
      <a
        un-text-blue-500
        un-hover="underline"
        href="/"
      >Home</a>
      <div
        un-bg="neutral-300 dark:neutral-700"
        un-text="neutral-700 dark:neutral-300"
        class="dark-mode-switcher"
        @click="switchDarkMode"
      >
        Switch
      </div>
    </nav>
    <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>
    <div
      v-else
      un-mx-auto
      un-max-w="[700px]"
    >
      <Content />
    </div>
  </div>
</template>

这样,我们就有了一个简单的导航栏和一个居中的内容区域, 如下图所示:

简单的博客首页
简陋的博客内容页

主页的特殊处理

注意到 ./docs/ 目录下存在一个 index.md 文件, 这就是主页的 MD 文件, 同样通过 <Content /> 组件渲染。 注意到在 Layout.vue 中, <Content /> 组件的渲染是在 v-else 分支中, 这意味着当 frontmatter.hometrue 时, <Content /> 组件不会被渲染。 我们可以试着把 <Content /> 组件放在分支外, 并向 index.md 文件中添加一些内容:

vue
<!-- ... -->

<template>
  <div
    un-min-h-100vh
    un-bg="neutral-50 dark:neutral-950"
    un-text="neutral-700 dark:neutral-300"
  >
    <div
      v-if="frontmatter.home"
      un-mx-auto
      un-max-w="[700px]"
    >
      <!-- ... -->
    </div>
    <div
      v-else
      un-mx-auto
      un-max-w="[700px]"
    />
    <Content
      un-mx-auto
      un-max-w="[700px]"
    />
  </div>
</template>
markdown
---
home: true
---

This is definitely the home page.

现在可以看到,主页的内容用一样的方式渲染了出来:

有点内容在身上的的博客首页

不过我想说,主页是一个特殊的页面, 主页内容的样式应该与其他页面有所区别, 谁支持谁反对? 有很多种思路可以实现主页的特殊处理。

保留 index.md 内容,通过 frontmatter 区分

根据前面的讨论, 其中一种方案是: 我们可以通过在 index.md 的 frontmatter 中 添加一些独特的元数据来区分主页和普通页面, 如默认的 home: true。 在这个基础上, 我们可以在 Layout.vue 中 根据 frontmatter 的值来为主页 <Content /> (或其相对于普通页面特有的祖先元素)添加类名, 以便在 style.css 中进行样式设计:

vue
<!-- ... -->

<template>
  <div
    un-min-h-100vh
    un-bg="neutral-50 dark:neutral-950"
    un-text="neutral-700 dark:neutral-300"
  >
    <!-- ... -->
    <Content
      id="content"
      un-max-auto
      un-max-w="[700px]"
      :class="frontmatter.home ? 'home-page' : ''"
    />
  </div>
</template>
css
/* ... */

#content {
  &.home-page {
    p {
      @apply border border-red-500 text-3xl;
    }
  }
}

这样一来, 加载在主页上的 <Content /> 组件的内容就会被添加上 home-page 类名, 从而可以在 style.css 中对其进行样式设计。 这里我们简单地添加了一个红色的边框和大字体样式。

有特殊内容在身上的博客主页

除了通过 frontmatter, 还可以通过其他方式区分主页和普通页面, 比如通过文件名、文件路径等。 这里只是提供了一种思路。

index.md 空空如也

另一种思路是, 反正 index.md 的内容是被渲染为 HTML 的, 为何不直接在 Layout.vue 中写入主页的内容呢? 事实上这正是初始化时的默认行为, site.titlesite.description 以及紧跟的文章列表(两个也是列表)都直接写在 <template> 中。 这样做的最大优势是更加灵活, 因为我们实际上是在写 Vue 组件, 可以随心所欲地添加任何内容, 而不用受到 MD 文件结构的限制。

(算是?)某种奇技淫巧:在 MD 文件中使用 Vue

事实上,由于每个 MD 文件都将被编译为 HTML, 然后作为 Vue SFC 来处理。 这意味着我们可以在 MD 文件中使用任何 Vue 功能。 在这个例子中, 我们可以直接将 index.md 中的内容写为 Vue 模板, 从而实现更加灵活的内容设计, 并且使用 UnoCSS 的类名为其添加样式:

md
---
home: true
---

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

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

<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>

This is definitely the home page.

看看效果:

魔改的博客主页

注意到两点: 首先, <p> 标签的样式同样应用了 style.css 中的红色边框和大字体样式, 懂了吗?懂了过了。 其次, 由于 <Content /> 组件的均在 Layout.vue 中渲染, 所以 UnoCSS 也可以直接在 MD 文件中使用。

好了, 把刚刚魔改的部分删除, 把 index.md 的添加的那行文字也删掉, 样式表也删掉, 我们直接在 Layout.vue 中进行主页设计。

文件结构优化

我们当然可以在 Layout.vue 中实现所有的页面设计, 但是我们当然不可以在 Layout.vue 中实现所有的页面设计( 💦 )。 现在, 让我们将 Layout.vue 中的内容拆分开来。 这里我的拆分思路是: 将顶部导航栏和底部可能存在的页脚拆分为 PageNav 和 PageFooter, 主要内容区域放在 PageContent 中, 再具体区分 PageContentHome 和 PageContentPost。 这样, 我们就可以在 Layout.vue 中引入这些组件, 而不用在 Layout.vue 中写一堆。

./docs/.vitepress/theme 目录下新建 components/ 目录, 然后新建文件写东西:

DANGER

这里仅仅是把上一篇文章最末的代码分离到多个文件中, 如果您使用的是 JS, 请不要到直接把下面这段 TS 复制粘贴了。 如有冒犯,请被冒犯。

这里可以看出一些提倡, 即不要看都不看一眼就复制粘贴, 这种行为导致的问题我也不会进行解答, 请自行解决。

如果您确实是初学者, 在一些确实没有给出具体 JS 实现的部分遇到了困难, 请先自行探索思考。 如果在经过尝试后实在无法解决, 可以跟我交流。

后续的文章中不再赘述此类问题。

vue
<script setup lang="ts">
function switchDarkMode(): void {
  const htmlEl: HTMLElement | null = document.querySelector('html')
  htmlEl?.classList.toggle('dark')
}
</script>

<template>
  <nav
    un-flex="~ row"
    un-justify-between
    un-p-4
  >
    <a
      un-text-blue-500
      un-hover="underline"
      href="/"
    >Home</a>
    <div
      un-bg="neutral-300 dark:neutral-700"
      un-text="neutral-700 dark:neutral-300"
      class="dark-mode-switcher"
      @click="switchDarkMode"
    >
      Switch
    </div>
  </nav>
</template>
vue
<script setup lang="ts"></script>

<template>
  <div />
</template>
vue
<script setup lang="ts">
import { useData } from 'vitepress'
import PageContentHome from './PageContentHome.vue'
import PageContentPost from './PageContentPost.vue'

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

<template>
  <PageContentHome v-if="frontmatter.home" />
  <PageContentPost v-else />
</template>
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>
vue
<script setup lang="ts"></script>

<template>
  <Content
    id="content"
    un-max-auto
    un-max-w="[700px]"
  />
</template>

然后在 Layout.vue 中引入这些组件:

vue
<script setup lang="ts">
import PageContent from './components/PageContent.vue'
import PageFooter from './components/PageFooter.vue'
import PageNav from './components/PageNav.vue'
</script>

<template>
  <PageNav />
  <PageContent />
  <PageFooter />
</template>
前文
后文
2024-PRESENT
CC BY-NC-SA 4.0
©
froQ