池塘

你今日飲咗未﹖

丝滑的 Obsidian 和 Hugo

自从 Typora 开始收费过后我一直找不到好用的 Markdown 编辑器。大部分支持 Markdown 的编辑器都不支持所见即所得,这可是 Typora 的 Killer Feature,支持的那几个对中文排版也不怎么友好。

后来我开始用 Obsidian,这东西在我最开始尝试的时候还很不成熟,但如今可能是最好的笔记软件之一了。支持所见即所得、没有复杂的概念(双链和 Markdown 扩展语法是可选的)、默认配置趁手(简简单单用文件夹分类)、拥有良好的生态、本地优先… 很少有软件让我挑不出什么毛病。除了它不是开源的,不过我也没有这么强的许可证洁癖,能和 Obsidian 扳扳手腕的 Logseq 用起来太卡,我没什么兴趣。

唯一的不足是,没有什么现成的方法让 Hugo 和 Obsidian 联动起来,导致我不能很方便的发表写的东西。作为个人笔记倒是没什么,不过写东西出来多少希望有点反馈,否则自说自话,意义太过有限。网上大部分联动的方案是用 QuickAdd 写一个宏,快速往 Hugo 的内容目录里新建文档,然后手动往里面粘贴内容,我这么用过一段时间,效果不错,不过依旧没有达到我想要的效果。

把 Hugo 的内容目录软链接到 Obsidian 的库里

后来偶然间发现了 Templater,一款和 QuickAdd 类似的 Obsidian 扩展,不过功能强大很多。这玩意似乎是用来作为编辑宏的,不过鉴于它提供了很多 API,你完全可以用它来实现上面提到的功能,还能更进一步。以往我需要手动粘贴内容到新建的文档里,现在直接在 Obsidian 里写好,调用一下模板,就直接把内容和图片“复制”过去了。

现在,我只需要在 Obsidian 里写/编辑文章:

在 Obsidian 中写作

写完之后调用一下模板:

调用 Templater 模板

输入文章 URL 路径

就能自动把文章内容和图片复制到 Hugo 里:

成功新建博文

最后附上我的 Templater 模板:

<%*
// Capture post name and sanitize it
let postName = await tp.system.prompt("Post Path");
let sanitizedName = postName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();

// Define Hugo content paths
const basePath = "Blog/posts";
const folderPath = `${basePath}/${sanitizedName}`;
const filePath = `${folderPath}/index.md`;

// Get current note's content and directory
let content = tp.file.content;
const currentDir = tp.file.folder(true);

// Improved image detection regex
const imageRegex = /!\[.*?\]\((.*?)\)/g;
let matches = [...content.matchAll(imageRegex)];
let imagePaths = matches.map(m => m[1]);

// Create folder first, for images
await app.vault.createFolder(folderPath).catch(() => {});

// Process images
for (let imagePath of imagePaths) {
    // Skip external URLs
    if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) continue;
    
    // Resolve absolute path
    const absoluteImagePath = `attachments/${imagePath}`;
    const imageFile = app.vault.getAbstractFileByPath(absoluteImagePath);
    
    if (imageFile) {
        try {
            // Copy to target directory
            const newPath = `${folderPath}/${imageFile.name}`;
            await app.vault.copy(imageFile, newPath);
            
            // Update link - use relative path from content folder
            content = content.replace(imagePath, imageFile.name);
        } catch (e) {
            console.error(`Failed to copy image: ${imageFile.path}`, e);
        }
    } else {
        console.warn(`Image not found: ${absoluteImagePath}`);
    }
}

// Create front matter with fixed date format
const frontMatter = `---
title: "${tp.file.title}"
date: "${tp.date.now("YYYY-MM-DDTHH:mm:ss+08:00")}"
draft: true
description: 
build:
  publishResources: false
---
`;

// Create folder index.md
await app.vault.create(filePath, frontMatter + content);

// Open the new file
app.workspace.openLinkText(filePath, "", true);
%>

要注意,这个东西还有点小 Bug:它没法很好的处理带有空格的图片文件名,也不能识别 Obsidian 默认的双链图片格式。要使用这个模板,请把 Obsidian 插入图片的行为设置为标准 Markdown 格式,并确保 Obsidian 把图片存储在库的 attachments 文件夹下。

Obsidian 设置成这样