跳至內容

用 VitePress Rewrites 按日期整理原始 markdown

在轉移舊文章到 VitePress 部落格的過程中,遇到了一個檔案管理的挑戰,隨著部落格文章越來越多,檔案管理開始變得有點頭痛。 好幾十篇文章全部平鋪在同一個目錄下,每次想找特定時期的文章都要捲半天。 但如果把文章按日期分到子目錄,URL 又會變得很醜,重新生成 URL 也會影響 SEO。

後來發現 VitePress 的 rewrites 功能可以完美解決這個問題,這篇文章就來聊聊實作過程。


問題:檔案太多不好管理

原本的檔案結構是這樣:

docs/notes/
├── integrating-waline-comments-to-vitepress.md
├── javascript-clean-code-practices.md
├── vue3-composition-api-patterns.md
├── ecmascript-5-strict-mode.md 
├── ... (略)
└── index.md

遇到的問題:

  1. 檔案列表太長:IDE 的檔案樹要滾很久才能找到想要的檔案
  2. 不知道文章年份:光看檔名很難知道這是新文章還是舊文章
  3. 難以批次處理:想要對特定時期的文章做處理(例如更新 frontmatter)很麻煩

考慮過的方案

方案 1:檔名加日期前綴

docs/notes/
├── 2025-11-04-integrating-waline-comments.md
├── 2025-10-31-javascript-clean-code-practices.md
└── 2011-11-27-ecmascript-5-strict-mode.md

優點:

  • 實作超簡單,不用改任何設定
  • 檔案列表自動依照日期排序

缺點:

  • URL 變長:/notes/2025-11-04-integrating-waline-comments.html
  • 日期資訊重複(frontmatter 已經有了)
  • 檔名太長,不好辨識重點

方案 2:完全依照年月分類

docs/notes/2025/11/integrating-waline-comments.md
→ URL: /notes/2025/11/integrating-waline-comments.html

優點:

  • 檔案結構清晰

缺點:

  • URL 層級太深,看起來冗長
  • 所有現有 URL 都會改變,破壞 SEO
  • 如果有內部參考連結也要更新

方案 3:VitePress Rewrites

檔案: docs/notes/2025/11/integrating-waline-comments.md
URL:  /notes/integrating-waline-comments.html

這就是我要的!

  • 原始的 md 檔案依照年月分類,方便管理
  • URL 保持簡潔扁平
  • 完全相容(原有的 URL 不受影響)

實作步驟

第一步:設定 Rewrites

docs/.vitepress/config.ts 加入 rewrites 規則:

typescript
export default defineConfig({
  // Rewrites: 將年月目錄結構的文章對外呈現為扁平 URL
  rewrites: {
    'notes/:year/:month/:article.md': 'notes/:article.md',
    'learn/:year/:month/:article.md': 'learn/:article.md',
    'misc/:year/:month/:article.md': 'misc/:article.md',
  },

  // 其他設定...
})

這個規則的意思是:

  • 實際檔案路徑:notes/2025/11/my-article.md
  • 對外 URL:/notes/my-article.html

在打包的時候 VitePress 會自動處理這個對應關係。


第二步:更新 Data Loader

這一步有兩個重點:

1. 確認掃描路徑包含子目錄

我的 posts.data.ts 原本就用 **/*.md 來掃描,所以不需要改:

typescript
export default createContentLoader([
  'notes/**/*.md',   // ** 會遞迴掃描所有子目錄
  'learn/**/*.md',
  'misc/**/*.md'
], {
  excerpt: true,
  transform(raw): Post[] {
    // 資料處理...
  }
})

如果你的掃描路徑是寫死的(例如 notes/*.md),記得改成 notes/**/*.md

2. 轉換 URL 為扁平結構

重要! VitePress 的 data loader 會根據實際檔案路徑生成 URL,所以需要手動轉換:

typescript
// 將年月目錄結構的 URL 轉換為扁平 URL(對應 VitePress rewrites 設定)
// 範例: /notes/2025/11/article.html → /notes/article.html
function rewriteUrl(url: string): string {
  // 匹配格式: /(notes|learn|misc)/YYYY/MM/article.html
  const match = url.match(/\/(notes|learn|misc)\/\d{4}\/\d{2}\/(.+\.html)/)
  if (match) {
    const [, category, filename] = match
    return `/${category}/${filename}`
  }
  // 如果不符合年月格式,直接返回原 URL
  return url
}

export default createContentLoader([...], {
  excerpt: true,
  transform(raw): Post[] {
    return raw
      .filter(...)
      .map(({ url, frontmatter, excerpt }) => ({
        title: frontmatter.title,
        url: rewriteUrl(url),  // ← 轉換 URL
        excerpt,
        // ...
      }))
  }
})

如果不加這個轉換,首頁和分類頁的連結會指向 /notes/2025/11/article.html,導致 404 錯誤。


第三步:建立轉移的 script

這裡我寫了一個 script 來自動搬移所有文章到正確的年、月目錄。

javascript
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');

const categories = ['notes', 'learn', 'misc'];

categories.forEach(category => {
  const categoryDir = path.join('docs', category);
  const files = fs.readdirSync(categoryDir)
    .filter(file => file.endsWith('.md') && file !== 'index.md');

  files.forEach(filename => {
    const sourcePath = path.join(categoryDir, filename);
    const content = fs.readFileSync(sourcePath, 'utf-8');
    const { data } = matter(content);

    if (!data.date) {
      console.log(`跳過(無日期): ${filename}`);
      return;
    }

    // 解析日期
    const date = new Date(data.date);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');

    // 建立目標路徑
    const targetDir = path.join(categoryDir, String(year), month);
    const targetPath = path.join(targetDir, filename);

    // 建立目錄並移動檔案
    if (!fs.existsSync(targetDir)) {
      fs.mkdirSync(targetDir, { recursive: true });
    }

    fs.renameSync(sourcePath, targetPath);
    console.log(`已移動: ${filename} → ${year}/${month}/`);
  });
});

最終效果

開發環境(檔案結構)

docs/notes/
├── 2025/
│   └── 11/
│       ├── integrating-waline-comments-to-vitepress.md
│       ├── javascript-clean-code-practices.md
│       └── vite-optimization-guide.md
├── 2017/
│   ├── 09/
│   ├── 08/
│   └── 07/
├── 2015/
│   ├── 05/ (6 篇文章)
│   ├── 04/
│   └── ...
└── index.md

優點:

  • IDE 側邊欄可以依照年份折疊,方便瀏覽
  • 可以快速找到特定年份或月份的文章
  • 新增文章時很清楚該放哪裡

正式環境(URL 結構)

所有 URL 保持簡潔扁平:

/notes/integrating-waline-comments-to-vitepress.html
/notes/javascript-clean-code-practices.html
/notes/ecmascript-5-strict-mode.html

優點:

  • URL 簡短易記
  • 對 SEO Friendly
  • 舊的分享連結完全不受影響

新增文章的流程

以後新增文章只需要:

  1. 確認日期:例如今天是 2025-11-06

  2. 建立年月目錄(如果不存在):

    bash
    mkdir -p docs/notes/2025/11
  3. 建立文章檔案

    bash
    docs/notes/2025/11/my-new-article.md
  4. 寫入 Frontmatter

    markdown
    ---
    title: 我的新文章
    date: 2025-11-06
    tags: [vitepress, vue]
    ---
  5. 完成! VitePress 會自動:

    • 掃描到這篇文章
    • 產生 /notes/my-new-article.html URL
    • 加入文章列表和標籤頁面

注意事項

1. 檔名不能重複

因為 URL 是扁平的,所以即使在不同年月目錄,檔名也不能重複。

錯誤示範:

docs/notes/2025/11/vue-tips.md
docs/notes/2024/05/vue-tips.md  ← 衝突!

兩個檔案都會產生 /notes/vue-tips.html,後者會覆蓋前者。

正確做法:

docs/notes/2025/11/vue-composition-api-tips.md
docs/notes/2024/05/vue-options-api-tips.md

2. Data Loader 需要重啟

修改 frontmatter 或新增檔案後,需要重啟開發伺服器:

bash
# Ctrl+C 停止,然後重新啟動
pnpm dev

VitePress 的 data loader 會快取結果,不重啟的話看不到新文章。

3. Tag 路由也要更新掃描路徑

如果你有動態標籤頁面(docs/tags/[tag].paths.ts),確認掃描路徑有包含所有子目錄:

typescript
const posts = await createContentLoader([
  'notes/**/*.md',  // ← 確保有 **
  'learn/**/*.md',
  'misc/**/*.md'
]).load()

總結

VitePress 的 rewrites 功能真的很強大,讓我可以:

  • 在開發時享受有組織的檔案結構
  • 在正式環境保持簡潔的 URL
  • 完全相容,不會破壞任何現有連結

這次重構花了大約一小時,包含寫轉移的 script、測試、更新文件。 如果你的 VitePress 部落格也有檔案管理問題,非常推薦試試這個方法!


相關閱讀:

💬 留言討論

Released under the MIT License.