Featured image of post Obsidian & Hugo:从笔记到博客的无缝自动化

Obsidian & Hugo:从笔记到博客的无缝自动化

通过将Obsidian仓库与Hugo的content目录关联,利用QuickAdd插件实现自动套用模板。

前言

背景

博客昨天迁到了Hugo,详见前篇文章。 我之前的博客从2012年开始就一直用的WordPress,前面几年搬到HomeLab里的web服务器后,不知道什么原因特别卡。这次博士内部答辩完终于下定决心迁移到纯静态了。 选 Hugo 的原因很直接:静态、快、稳,部署简单。托管到 GitHub Pages 之后,基本不需要再关心服务器、数据库、升级和安全问题(点名批评WordPress),维护成本接近于零。

Hugo的问题

问题也很明显:写作入口不够低。 和 WordPress 这类系统相比,Hugo 没有后台管理界面,也没有开箱即用的在线编辑器。写一篇文章之前,必须先完成一堆文件层面的准备工作。

我的Hugo目录约定

为了后续维护方便,我给每篇文章都分配一个独立目录,目录名使用自增序号。目录中包含两类内容:

  • 文章正文:index.md
  • 被文章引用的资源文件:图片、附件等

目录结构大概如下:

1
2
3
4
2026/  
└── 0001/  
    ├── index.md  
    └── image.png

这样做的好处是文章和资源天然归档在一起,迁移、备份、排查都比较方便。

启动成本

但这套结构也带来了额外操作:

  1. 手动创建新目录
  2. 手动计算下一个序号
  3. 手动创建 index.md
  4. 手动复制 front matter 模板

这些步骤本身不复杂,但它们都发生在“开始写作之前”。

结果就是:真正阻碍写作的,不是正文难写,而是启动动作太碎。

为什么选 Obsidian

使用体验

最近把大量日常记录迁到了 Obsidian,整体体验很好。 它的优点比较明确:Markdown 原生、本地文件直存、 响应快、插件生态丰富。这些特性很适合和 AI 协同处理内容,目前 OpenClaw 的工作日志也已经跑在 Obsidian 上,日常记录、整理和迭代都很顺手。

和 Hugo 的兼容性

Hugo 本质上就是基于 Markdown 的静态站点生成器。

而 Obsidian 天然就是 Markdown 编辑器,所以两者之间几乎没有格式转换成本。拿 Obsidian 作为 Hugo 的写作前端,是非常自然的一种组合。

解决方案

真正缺的只是一层自动化

问题并不在于编辑器,而在于写新文章时的初始化动作仍然是手工的。

所以目标就很明确了:

在 Obsidian 里,一次命令完成以下动作:

  • 在指定目录下创建新的自增编号文件夹
  • 自动生成 index.md
  • 自动写入固定 front matter 模板

实现步骤

  1. 安装 QuickAdd:在目标 Vault 中安装并启用 QuickAdd
  2. 放置脚本文件:把下面这个脚本保存为_quickadd/new-numbered-folder.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
const PARENT_FOLDER = "Parent folder";
const START_AT = "Start at";
const PAD_WIDTH = "Pad width";
const INDEX_NAME = "Index file name";
const TEMPLATE = "Template content";

function pad(num, width) {
  return String(num).padStart(width, "0");
}

async function ensureFolder(app, path) {
  const parts = path.split("/").filter(Boolean);
  let current = "";

  for (const part of parts) {
    current = current ? `${current}/${part}` : part;
    const existing = app.vault.getAbstractFileByPath(current);
    if (!existing) {
      await app.vault.createFolder(current);
    }
  }
}

function formatNow(fmt) {
  if (typeof window !== "undefined" && window.moment) {
    return window.moment().format(fmt);
  }
  const d = new Date();
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  const hh = String(d.getHours()).padStart(2, "0");
  const mi = String(d.getMinutes()).padStart(2, "0");

  if (fmt === "YYYY-MM-DD") return `${yyyy}-${mm}-${dd}`;
  if (fmt === "YYYY-MM-DD HH:mm") return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
  return `${yyyy}-${mm}-${dd}`;
}

module.exports = {
  entry: async (params, settings) => {
    const { app } = params;

    const parentFolder = String(settings[PARENT_FOLDER] || "").trim().replace(/^\/+|\/+$/g, "");
    if (!parentFolder) {
      throw new Error("Parent folder 不能为空");
    }

    const startAt = Number(settings[START_AT] ?? 1);
    const padWidth = Number(settings[PAD_WIDTH] ?? 3);
    const indexName = String(settings[INDEX_NAME] || "index").trim() || "index";
    const template = String(settings[TEMPLATE] || "");

    await ensureFolder(app, parentFolder);

    const parent = app.vault.getAbstractFileByPath(parentFolder);
    if (!parent || !Array.isArray(parent.children)) {
      throw new Error(`找不到目标目录:${parentFolder}`);
    }

    const existingNumbers = parent.children
      .filter((child) => Array.isArray(child.children)) // 只要子文件夹
      .map((child) => child.name)
      .filter((name) => /^\d+$/.test(name))
      .map((name) => Number(name));

    let nextNumber = existingNumbers.length > 0
      ? Math.max(...existingNumbers) + 1
      : startAt;

    let folderName = pad(nextNumber, padWidth);
    let newFolderPath = `${parentFolder}/${folderName}`;

    while (app.vault.getAbstractFileByPath(newFolderPath)) {
      nextNumber += 1;
      folderName = pad(nextNumber, padWidth);
      newFolderPath = `${parentFolder}/${folderName}`;
    }

    await app.vault.createFolder(newFolderPath);

    const content = template
      .replace(/{{NUM}}/g, folderName)
      .replace(/{{RAW_NUM}}/g, String(nextNumber))
      .replace(/{{DATE}}/g, formatNow("YYYY-MM-DD"))
      .replace(/{{DATETIME}}/g, formatNow("YYYY-MM-DD HH:mm"))
      .replace(/{{FOLDER_PATH}}/g, newFolderPath);

    const filePath = `${newFolderPath}/${indexName}.md`;
    const file = await app.vault.create(filePath, content);

    await app.workspace.getLeaf(true).openFile(file);
  },

  settings: {
    name: "New Numbered Folder",
    author: "LiuJason",
    options: {
      [PARENT_FOLDER]: {
        type: "text",
        defaultValue: "Projects",
        placeholder: "例如:Projects/Active",
        description: "新文件夹要创建到哪个父目录下",
      },
      [START_AT]: {
        type: "text",
        defaultValue: "1",
        placeholder: "1",
        description: "如果还没有数字文件夹,就从几开始",
      },
      [PAD_WIDTH]: {
        type: "text",
        defaultValue: "3",
        placeholder: "3",
        description: "编号补零位数;3 = 001, 002, 003",
      },
      [INDEX_NAME]: {
        type: "text",
        defaultValue: "index",
        placeholder: "index",
        description: "自动创建的 markdown 文件名,不带 .md",
      },
      [TEMPLATE]: {
        type: "textarea",
        defaultValue:
`---
title: 
url: /post/{{NUM}}.html
date: {{DATETIME}}
draft: false
image:
description: 
tags:
categories:
toc: true
comment: true
---
`,
        description: "index.md 的初始内容;支持 {{NUM}} {{RAW_NUM}} {{DATE}} {{DATETIME}} {{FOLDER_PATH}}",
      },
    },
  },
};
  1. 用 QuickAdd 新建一个 Macro,往里面加一个 User Script,指向这个脚本。QuickAdd 的 Macro 本来就是用来把命令和脚本串起来执行的。

QuickAdd中设置New-Post

  1. 给这个 Macro 绑一个快捷键,或者放到命令面板里。以后你点这个命令,就会自动创建 0001/0002/0003/ …… 这样的文件夹,并在里面生成 index.md

结语

都2026年了,博客确实没什么人看了,就连我自己每天最常看的也是小红书这种平台。会看自己博客的更新情况,最近几年几乎没怎么动…. 其实对于博客来说,真正影响更新频率的,很多时候不是渲染速度,也不是部署复杂度,而是“我现在想写的时候,能不能立刻开始写”。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy