使用 VitePress 快速建立部落格
前言
這陣子在複習 Nuxt3 運用時,無意間一直掃到 VitePress 的關鍵字,好奇 google 一下原來是 Vue 官方出的一款靜態站點生成器,特別之處在於頁面除了可用 Markdown + HTML 編寫外,也能直接在 Markdown 檔中使用 Vue 語法,編譯後是靜態 HTML 頁面,部屬到伺服器上能作為 Vue SPA 運行。官方說明:
快速的初始加載
對任何頁面的初次存取都會是靜態的、預先呈現的 HTML,以實現極快的載入速度和最佳的 SEO。然後頁面載入一個 JavaScript bundle,將頁面變成 Vue SPA。
建立專案的方式官方文件寫的滿清楚的,過程中反而是爬文件比較花時間,需要了解整體架構的安排跟一些細部設定方式。 比較需要關注的地方是 vitepress.config.ts
設定檔,這裡可以設定網站的全局配置,像是文件目錄配置、網站 Head 配置、多語系配置、插件配置及主題配置等等。
主題配置 themeConfig
我是使用官方的預設主題來微調,預設主題有提供三種 layout: doc (預設值,文檔排版)、home (首頁排版,可參考 vue 官網) 及 page (單頁排版)。其中 page 官方是說不會應用任何樣式,但自己測試下來,會涵蓋 header、footer,如果有設定 sidebar 也會一併顯示,比較適合拿來當作部落格主體下的延伸頁面。
而在頁面的 frontmatter 設定 layout: false
才會是完全的空白頁面,可用這個方式在同個專案裡做一些 demo 頁,不用擔心會受到主題樣式影響。
<!-- frontmatter 位置需要在文件的最上方 -->
---
layout: doc | home | page | false
---
側邊欄 sidebar
文檔排版模式下,可以設定側邊欄作為文章的快捷索引,側邊欄最多接受六個層級,預設用陣列的格式配置會只有一組側邊欄,如果今天想依據頁面路徑不同而有不同的側邊欄,最外層格式要以物件的方式設定,key 要對應資料夾名稱(資料夾名稱會轉換為路徑名)。另外側邊欄如果有設定 collapsed
屬性,會啟用收合功能,false 是預設展開,true 是預設收合。
sidebar: [
{
text: '筆記總覽',
collapsed: false,
items: [
{ text: '使用 VitePress 快速建立部落格', link: '/posts/get-start-with-vitepress' }
]
}
],
VitePress Sidebar
官方沒有提供 sidebar 的自動生成功能,找到了這個 VitePress Sidebar 由其他開發者提供的套件,透過套件做一些設定便可將轉換後的路徑結構注入到 themeConfig 的 sidebar 屬性中。不過用這套件的前提是專案結構要符合他的設計方式,源目錄下的 md 檔會視為第一層級,同層級的資料夾則會轉為路徑名,資料夾內的 md 檔路徑就會包含資料夾名稱。如果放置 md 檔的位置不是從源目錄開始的話就會出現路徑編譯錯誤的問題。套件提供滿多可設定的選項,有興趣可以試試看。
其他設定基本上就是替換為網站要呈現的內容,其中 search 的部分,官方提供開箱即用的本地搜索功能(MiniSearch),如果習慣使用 algolia 搜尋引擎,也有相對應的配置可用。
建構時的數據加載
官方說明
VitePress 提供了資料載入的功能,它允許載入任意資料並從頁面或元件中匯入它。資料載入只在建置時執行:最終的資料將被序列化為 JavaScript 套件中的 JSON。資料載入可以用來取得遠端數據,也可以基於本地文件產生元數據。
這個部落格有使用本地文件的數據加載來實現筆記總覽頁。
數據加載會在建構階段執行,文件檔名必須以 .data.js
或 .data.ts
結尾。官方有提供 createContentLoader
這個輔助函數來簡化抓取數據的方式,依據畫面需要的資料回傳對應的數據出來,於 md 檔或 vue 檔就能以 import 的方式來取得回傳的數據。
過程中有碰到 import 檔案報錯的問題,錯誤提示如下:
Build failed with 1 error:
node_modules/esbuild/lib/main.js:1374:27: ERROR: [plugin: externalize-deps] "vitepress" resolved to an ESM file. ESM file cannot be loaded by `require`. See https://vitejs.dev/guide/troubleshooting.html#this-package-is-esm-only for more details.
解法是在 package.json 中增加 "type": "module"
設定,重啟就可運作了。
GA4、GTM 設定
config 檔中沒有提供可以直接設定 ID 的位置,但可以透過 head
屬性來增加 <script>
標籤,像是 GA4 的設定如下:
head: [
['script', { src: 'https://www.googletagmanager.com/gtag/js?id={GA4_ID}' }],
['script', {}, `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{GA4_ID}');
`]
],
運行後就可以在頁面中的 <head>
看到 GA4 的 <script>
標籤。
GTM 的設定也類似,但有一塊 <noscript>
代碼需要加在 <body>
開頭標記後方的位置,爬文沒看到有相關的設定可以參考,後來想到的解法是把這段代碼塞到 layout 提供的 slot 裡,由於 VitePress 可以直接使用 vue component,可以建立一個含有 GTM 代碼的 component,再用插槽的方式掛載到 layout 上面,官方的預設 layout 提供很多位置的插槽可用。使用插槽的方式如下:
- 建立一個含有 GTM 代碼的 component,例如
GtmScript.vue
:
<template>
<!-- Google Tag Manager (noscript) -->
<noscript
><iframe
:src="`https://www.googletagmanager.com/ns.html?id=${VITE_GTM_TRACKING_ID}`"
height="0"
width="0"
style="display: none; visibility: hidden"
></iframe
></noscript>
<!-- End Google Tag Manager (noscript) -->
</template>
<script setup lang="ts">
const VITE_GTM_TRACKING_ID = process.env.VITE_GTM_TRACKING_ID;
</script>
- 在
.vitepress/theme/index.ts
主題設定檔中,把 GTMScript 加入到 layout 插槽:
// https://vitepress.dev/guide/custom-theme
import { h } from "vue";
import type { Theme } from "vitepress";
import DefaultTheme from "vitepress/theme";
import "./style.css";
import GtmScript from "./GtmScript.vue";
import Giscus from "./Giscus.vue";
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
"layout-top": () => h(GtmScript),
"doc-footer-before": () => h(Giscus),
});
},
enhanceApp({ app, router, siteData }) {
// ...
},
} satisfies Theme;
運行環境就可以在 devtool 面板看到這段設定囉! 唯一問題是頁面如果設定 layout: false 會吃不到設定,不過還是能直接在頁面中自己引入這個 component 來使用。
創建 Sitemap
VitePress 提供了自動生成 sitemap 的功能,只要在 vitepress.config.ts
設定即可,建置後會在專案根目錄下產生 sitemap.xml
檔案。如果專案本身有啟用 lastUpdated
(最後更新時間)選項,也會將更新時間同步到 sitemap 的 <lastmod>
標籤。
import { defineConfig } from 'vitepress'
export default defineConfig({
sitemap: {
hostname: 'https://example.com'
}
})
環境變數設定
測試了 VitePress 設定環境變數的方式,狂踩坑後得出的做法如下:
- 根目錄建立
.env
檔,變數名稱帶上 VITE_ 前綴,例如VITE_GTM_TRACKING_ID=GTM-XXXXXXX
- config 檔中可透過官方提供的
loadEnv
函數來載入當前環境的變數檔,因為 config 檔只會在建構階段執行,這個階段是無法使用 import.meta.env 來載入的。
import { loadEnv } from 'vitepress'
// 第一個參數是 mode,第二個參數是環境檔的目錄位置
const env = loadEnv("", process.cwd());
console.log(env.VITE_GTM_TRACKING_ID);
- 由於我的
.env
檔是放在根目錄下,vitepress 是在 /docs 資料夾內,運行環境無法抓到外層的環境變數,所以用了繞彎的方式來寫,在 config 檔中已用第二點的方式拿到當前環境變數,透過設定 vite -> define 來定義全局常數,這樣就可以在運行環境下取得這些變數了。
import { defineConfig, loadEnv } from 'vitepress'
const env = loadEnv("", process.cwd());
export default defineConfig({
vite: {
define: {
'process.env': env
}
}
})
<template>
<!-- Google Tag Manager (noscript) -->
<noscript
><iframe
:src="`https://www.googletagmanager.com/ns.html?id=${VITE_GTM_TRACKING_ID}`"
height="0"
width="0"
style="display: none; visibility: hidden"
></iframe
></noscript>
<!-- End Google Tag Manager (noscript) -->
</template>
<script setup lang="ts">
const VITE_GTM_TRACKING_ID = process.env.VITE_GTM_TRACKING_ID;
</script>
部署
由於建這個站目的是要汰換掉之前在用的 hexo blog (絕對不是因為我忘記怎麼用了),會使用 GitHub Page 來部署。部署這塊官方文件也很佛心的列了目前常見部署站點的使用方式。 我是使用 GitHub Actions 來自動化部署,大致流程如下:
- 專案中建立
.github/workflows/deploy.yml
檔,內容可以拿官方提供的範例來套用 - 在 GitHub repository -> Settings -> Pages 選單下,選擇 Build and deployment > Source > GitHub Actions 表示專案要使用 GitHub Actions 的方式來部署
- 專案 commit 並推送到 main 分支,就會觸發 Actions 進行自動部署的流程。分支預設是用 main,如果要建立在其他分支上,記得要更改 yml 檔中的 push 設定。
Github Pages 使用 secrets 設定環境變數
GitHub Pages 有提供 Actions secrets and variables 的設定,使用 Actions 方式部署的話,就可以在部署過程植入環境變數。
- 專案 repository -> Settings -> Secrets and Variables -> Repository secrets -> New repository secret 來建立 repository 層級的 secrets,名稱建議跟
.env
檔中定義的一樣,於 yml 檔中可以這樣引用${{ secrets.VITE_GTM_TRACKING_ID }}
。 .github/workflows/deploy.yml
中,在run: npm run docs:build
指令下方加上env
設定。
jobs:
# 构建工作
build:
runs-on: ubuntu-latest
environment:
name: github-pages
steps:
# 省略前面步驟..
- name: Build with VitePress
run: npm run docs:build
env:
VITE_GTM_TRACKING_ID: ${{ secrets.VITE_GTM_TRACKING_ID }}
總結
如果單純拿來寫寫筆記用,建置時間真的超快,專案初始化其實就會幫你把基本檔案建好了,要做的只是設定的微調,麻煩的反而是筆記的內容XD,寫一寫都會想說官方文件比我寫的還清楚...這 part 跳過好了。
自己比較驚奇的部分是可以在 md 檔裡面直接寫 vue,等於把 md 檔當成單文件組件來寫,差別只在不會有 <template>
標籤,可以導入組件、註冊全局組件、使用 CSS 預處理器等,真的很像在寫一般的 vue 檔,只是使用上要注意 SSR 的問題,避免在組件 mounted 之前有針對瀏覽器或 DOM 元素的操作,這塊官方也有提到可以使用 onMounted
、!import.meta.env.SSR
、<ClientOnly>
等方式確保代碼只在客戶端環境下執行。