langliu1216@gmail.com

基于 Astro 搭建博客系统 - 2022/09/08

基于 Astro 搭建博客系统

缘起

最近在少数派看见一篇文章 抛弃知识管理软件的尝试:把博客变为知识库,作者尝试了不使用知识管理软件而使用静态博客系统来管理自己的知识库,恰巧最近看见了 Astro 发布了1.0版本,想要尝试一下于是决定基于 Astro 搭建一个自己的博客系统,使用 Github Page 来做静态网页服务。

博客搭建

💡在搭建前先看一下 Astro 的官方文档,了解一下提供了哪些功能,Astro 官方文档提供了中文版本可以很方便的查看。

前置准备

使用 Astro 目前只有使用 VSCode 编辑器才有一个较为良好的开发体验,因为官方目前只提供了基于 VSCode 的插件 Astro

项目创建

# npm
npm create astro@latest

# yarn
yarn create astro

# pnpm
pnpm create astro@latest

执行以上命令后,Astro 会自动为你创建项目文件夹并初始化项目。

目录结构

一个寻常的 Astro 项目目录可能看起来像这样:

  • src/* - 你的项目源代码(组件、页面、样式等)。
  • public/* - 你的非代码、未处理的资源(字体、图标等)。
  • package.json - 项目列表。
  • astro.config.mjs - Astro 配置文件(可选)。
├── src/
│   ├── components/
│   │   ├── Header.astro
│   │   └-─ Button.jsx
│   ├── layouts/
│   │   └-─ PostLayout.astro
│   └── pages/
│   │   ├── posts/
│   │   │   ├── post1.md
│   │   │   ├── post2.md
│   │   │   └── post3.md
│   │   └── index.astro
│   └── styles/
│       └-─ global.css
├── public/
│   ├── robots.txt
│   ├── favicon.svg
│   └-─ social-image.png
├── astro.config.mjs
└── package.json

astro 文件结构

astro 文件由两部分组成:组件 script组件模板,写过Vue的同学可能对这种结构比较熟悉:

---
// 组件 Script(JavaScript)
---
<!-- Component Template (HTML + JS Expressions) -->

组件 Script

Astro 使用代码栅栏(---)来识别 Astro 组件中的组件 script。

你可以使用组件 script 来编写渲染模板所需 JavaScript 代码。这可以包括:

  • 导入其他 Astro 组件
  • 导入其他框架组件,如 React
  • 导入数据,如 JSON 文件
  • 从 API 或数据库中获取内容
  • 创建你要在模板中引用的变量

例如在下面的代码中,我们导入了一个 Tag 的 Astro 组件,并且读取了 src/pages/posts 文件夹及子文件夹下的所有 Markdown 和 MDX 文件:

---
import Tag from './Tag.astro'
// Use Astro.glob() to fetch all posts, and then sort them by date.
const posts = (await Astro.glob('../pages/posts/**/*.{md,mdx}')).sort(
  (a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf()
)
---

组件模板

Astro 的组件模板支持JSX表达式(没有只能有一个最外层标签元素的要求),可以通过 <slot /> 插槽支持将其他组件嵌入到当前组件模板中

---
import Tag from '@components/Tag.astro'
type Props = {
  emoji?: string
  title?: string
  tags?: string[]
  publishDate?: string
  url?: string
}

const { url, emoji, title, publishDate, tags } = Astro.props as Props
---

<li>
  <a href={url}>
    <span class='title'>
      {emoji}
      {title}
    </span>
    <div>
      {tags?.map((tag: string) => <Tag text={tag} />)}
      <time datetime={publishDate}>
        {
          new Date(publishDate ?? new Date()).toLocaleDateString('zh-CN', {
            year: 'numeric',
            month: 'short',
            day: 'numeric',
          })
        }
      </time>
    </div>
  </a>
</li>

<style>
  li {
    color: var(--theme-ui-colors-primary, white);
    box-sizing: border-box;
    min-width: 0px;
    padding: 0.5rem;
    border-radius: 0.25rem;
    display: flex;
    transition: all 200ms ease-in-out 0s;
    -webkit-box-pack: justify;
    justify-content: space-between;
    overflow: hidden;
  }

  li:hover {
    background-color: var(--theme-ui-colors-codeBackground, hsl(285, 5%, 17%));
  }

  li > a {
    display: flex;
    justify-content: space-between;
    align-items: center;
    text-decoration: none;
    color: white;
  }

  .title {
    box-sizing: border-box;
    line-height: 1.25;
    position: relative;
    font-size: 1rem;
    margin: 0px 1rem 0px 0px;
    font-weight: 700;
    color: var(--theme-ui-colors-textStrong, hsl(210, 38%, 98%));
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
    min-width: 260px;
  }

  li time {
    box-sizing: border-box;
    margin: 0px 0px 0px 0.5rem;
    min-width: 0px;
    font-size: 0.7rem;
    color: var(--theme-ui-colors-text, hsl(210, 17%, 85%));
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
</style>

第一篇文章

基本框架搭好之后就可以进行文章写作了,在src的posts目录下新建一个 Markdown 文件,在 Markdown 中写作,然后运行 npm run dev,在浏览器打开链接 http://localhost:3000/posts/文章标题 即可对文章进行预览。

布局设计

按照通常的博客,都会有一些通用的部分,我们需要在 src 目录下新建这些布局文件,最简单的布局基本分为3个部分:

  • 顶部或侧边的导航栏
  • 主体区域
  • 页脚(通常用于配置备案信息等)

因为 Astro 默认是按照文件目录设置路由,所以我们在 markdown/mdx 文件中需要在文件顶部手动声明使用的布局,例如 layout: '@layouts/BlogPost.astro'

文章列表页设计

Astro 提供了 glob API用于获取本地相关数据,可以使用下面的例子获取所有的 markdown/mdx 文件,获取所有文章后可以通过 frontmatter 字段来获取文章的相关信息。

const posts = (await Astro.glob('../pages/posts/**/*.{md,mdx}')).sort(
  (a, b) => new Date(b.frontmatter.publishDate).valueOf() - new Date(a.frontmatter.publishDate).valueOf()
)

在 markdown/mdx 文件顶部通常会声明文章的一些简要信息,字段可以自定义,通过 frontmatter 字段可以拿到这些声明的字段:

---

title: 'Windows 下使用7-Zip批量压缩文件夹'
description: 'Windows 下使用7-Zip批量压缩文件夹'
publishDate: '2022-09-03'
heroImage: '/7-zip-batch-compression.png'
tags:
  - Tools
  - 7-Zip
emoji: 😁
---

文章布局页设计

路由在访问到文件的时候,如果 markdown/mdx 文件有指定布局,则会将文章的相关信息通过参数的方式传入布局组件,文章主体通过 注入到布局组件中,在设计文章布局页是,首先要知道参数是如何传入的(可以参考下面的类型定义)

export interface Heading {
  depth: number
  slug: string
  text: string
}
export interface Props {
  content: {
    title: string
    description: string
    publishDate?: string
    updatedDate?: string
    heroImage?: string
    tags?: string[]
  }
  headings: Heading[]
}

const {
  content: { title, description, publishDate, updatedDate, heroImage, tags },
  headings,
} = Astro.props as Props

这里定义的 Heading 是文章的目录层级

标签页设计

标签页设计分为两个部分:标签列表页及标签文章页。

标签列表页

标签列表页可以通过查询所有文章然后获取到所有的标签,对获取到的标签进行唯一值过滤,代码如下:

const posts = (await Astro.glob('../pages/posts/*.{md,mdx}'))
let tags: string[] = []
posts.forEach((post) => {
  tags = [...tags, ...post.frontmatter.tags]
})
tags = [...new Set(tags)]

标签文章页

  • pages/tags/[tag].astro → (/tags/hello-world, /tags/post-2, etc.)

点击某个标签,需要查看到所有包含这个标签的文章列表,哪么进入页面后如何知道当前是哪个标签呢?这是就需要使用到 Astro 的动态路由了,动态生成路由的 Astro 组件可以访问每个路由的 Astro.params 对象。这使得你可以在组件脚本和模板中使用那些生成的链接部分。代码如下:

type Props = {
  /** 标签名称 */
  tag: string
}

export async function getStaticPaths() {
  let tags: string[] = []
  const posts = (await Astro.glob('../../pages/posts/**/*.{md,mdx}'))

  posts.forEach((post) => {
    tags = [...tags, ...post.frontmatter.tags]
  })
  tags = [...new Set(tags)]
  return tags.map((tag) => ({ params: { tag } }))
}

const { tag } = Astro.params as Props

getStaticPaths 方法是必须的,提供要使用的值来生成带有 [named] 参数的路由。

美化

代码块美化

Astro 默认提供了 shiki 相关配置,只需要在配置文件中配置即可享有 shiki 支持的代码主题:

export default {
  markdown: {
    shikiConfig: {
      // Choose from Shiki's built-in themes (or add your own)
      // https://github.com/shikijs/shiki/blob/main/docs/themes.md
      theme: 'dracula',
      // Add custom languages
      // Note: Shiki has countless langs built-in, including .astro!
      // https://github.com/shikijs/shiki/blob/main/docs/languages.md
      langs: [],
      // Enable word wrap to prevent horizontal scrolling
      wrap: true,
    },
  },
};

字体美化

在代码块中我们可能想使用一些字体来让显示效果达到和开发时本地配置相近,比如说使用连字符和等宽字体,这时我们可以通过 Fontsource 来加载网络字体,在字体面板找到想要的字体,比如我这里使用的是 Jetbrains Mono 字体:

安装依赖

npm install @fontsource/jetbrains-mono

引入

import "@fontsource/jetbrains-mono"

目前引入字体的时候遇见了一个问题,通过 js 的方式引入后,在打包时会报错,具体什么原因没有搞清楚,但是换为通过 CSS @import 引入则没有问题 @import '@fontsource/jetbrains-mono';

使用字体

:global(code) {
  font-family: 'JetBrains Mono', monospace;
}

部署

Astro 官方文档中提供了多个平台的部署方式,比如如果想要部署到Github Page 官方也提供了专门的Action用于部署:

name: Github Pages Astro CI

on:
  # Trigger the workflow every time you push to the `main` branch
  # Using a different branch name? Replace `main` with your branch’s name
  push:
    branches: [ main ]
  # Allows you to run this workflow manually from the Actions tab on GitHub.
  workflow_dispatch:
  
# Allow this job to clone the repo and create a page deployment
permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v2          
      - name: Install, build, and upload your site
        uses: withastro/action@v0

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1