使用 Emacs + org-mode + ox-hugo 搭建静态博客

前言

如果你是 Emacs 用户,一定对 org-mode 这个强大的文本组织模式有所耳闻,本文将详细介绍如何结合 org-mode 与静态网站生成器 Hugo 来搭建一个极其高效且简单的博客创作流程,内容包括:

  • Hugo 的安装与配置
  • ox-hugo 的配置
  • 写作与发布流程
  • 自定义博客首页
  • 快速部署
  • 常见问题解决

想跟随本文搭建博客,你可能需要:

  • 能进行简单的 Emacs 配置和 org-mode 使用,了解如何安装需要的插件
  • (非常)基础的 git 与命令行经验

环境准备

1. 安装 Hugo

Hugo 是用 Go 语言编写的静态网站生成器,以速度著称。

以 Windows 平台为例,使用 Scoop 安装:

scoop install hugo-extended

安装完成后验证:

hugo version

看到类似输出便安装成功(注意要有 +extended):

hugo v0.157.0+extended windows/amd64

2. 创建 Hugo 站点

cd your-blog-directory
hugo new site your-blog-name
cd your-blog-name

3. 安装主题

此处以简洁快速的 nostyleplease 主题演示:

git clone https://github.com/hanwenguo/hugo-theme-nostyleplease.git themes/nostyleplease

修改 hugo.toml 启用主题:

baseURL = 'https://yourdomain.com'
languageCode = 'zh-cn'
title = 'your blog name'
theme = 'nostyleplease'

[markup.goldmark.renderer]
unsafe = true

最后两行用于允许不安全 HTML , 例如换行标签。

Emacs 配置

1. 安装 ox-hugo

ox-hugo 是一个将 org-mode 导出为 Hugo 兼容的 Markdown 的工具。

在 Emacs 配置中使用 use-package 或其他包管理器安装:

(use-package ox-hugo
  :ensure t
  :after ox)

2. 创建 org 文件结构

在项目根目录或新建目录中创建 all-posts.org ,或其他任意名称的 .org 文件:

#+hugo_base_dir: your-absolute-path
#+hugo_auto_set_lastmod: t
#+OPTIONS: \n:t

* Blog Ideas

** TODO 第一篇博客
:PROPERTIES:
:EXPORT_FILE_NAME: first-post
:END:
这是第一篇博客的内容...

配置说明:

#+hugo_base_dir:
指定 Hugo 站点的根目录
#+hugo_auto_set_lastmod: t
自动添加最后修改时间
#+OPTIONS: \n:t
保留换行符
EXPORT_FILE_NAME
导出的文件名(不带 .md 后缀)
CLOSED
文章发布时间(使用 org 的 TODO 状态切换自动生成, DONE 即为发布)

3. 添加 org capture 模板(推荐)

在你的 Emacs 配置文件中添加:

;; Populates only the EXPORT_FILE_NAME property in the inserted heading.
(with-eval-after-load 'org-capture
  (defun org-hugo-new-subtree-post-capture-template ()
    "Returns `org-capture' template string for new Hugo post.
See `org-capture-templates' for more information."
    (let* ((title (read-from-minibuffer "Post Title: ")) ;Prompt to enter the post title
           (fname (org-hugo-slug title)))
      (mapconcat #'identity
                 `(
                   ,(concat "* TODO " title)
                   ":PROPERTIES:"
                   ,(concat ":EXPORT_FILE_NAME: " fname)
                   ":END:"
                   "%?\n")          ;Place the cursor here finally
                 "\n")))

  (add-to-list 'org-capture-templates
               '("h"                ;`org-capture' binding + h
                 "Hugo post"
                 entry
                 ;; It is assumed that below file is present in `org-directory'
                 ;; and that it has a "Blog Ideas" heading. It can even be a
                 ;; symlink pointing to the actual location of all-posts.org!
                 (file+olp "all-posts.org" "Blog Ideas")
                 (function org-hugo-new-subtree-post-capture-template))))

将最下方的 all-posts.org 替换为你的博客绝对路径, Blog Ideas 任意更改。

然后打开 org-capture ,便可以看到新添加的 Hugo post 选项。

更多教程参考 ox-hugo 官方文档相关章节

写作流程

1. 新建博客

在 org 文件中添加新的 TODO 项:

** TODO 文章标题
:PROPERTIES:
:EXPORT_FILE_NAME: article-slug
:END:

文章正文...

或使用刚刚配置的 capture 模板。

2. 发布文章

当文章完成后,将 TODO 改为 DONE:

** DONE 文章标题
CLOSED: [2026-03-15 Sun 17:30]
:PROPERTIES:
:EXPORT_FILE_NAME: article-slug
:END:

使用快捷键 C-c C-e H H 手动导出当前文章。

3. 自动导出

在与 all-posts.org 相同的目录下添加 .dir-locals.el 文件,在文件中粘贴:

((org-mode . ((eval . (org-hugo-auto-export-mode)))))

即可在保存文件时自动导出 markdown 。

详细配置参考 ox-hugo 官方文档相关章节

4. 本地预览

在终端中运行:

hugo server

访问对应地址实时预览效果。

如果要预览未发布(状态为 draft/TODO )的笔记,可以使用:

hugo server -D

样式定制

自定义首页布局

这是一个简单的首页模板,创建 layouts/index.html 可以覆盖主题默认:

{{ define "main" }}

<style>
  .post-list {
    list-style: none;
    padding: 0;
  }
  .post-item {
    margin: 10px 0;
    border: 1px solid #ddd;
    border-radius: 4px;
  }
  .post-summary {
    padding: 10px;
  }
  .post-meta {
    color: #666;
    font-size: 0.9em;
  }
</style>

<ul class="post-list">
{{ range (where .Site.Pages "Section" "posts") }}
  {{ if not .IsNode }}
  <li class="post-item">
    <details>
      <summary>
        <strong><a href="{{ .Permalink }}">{{ .Title }}</a></strong>
        <span class="post-meta"> — Date: {{ .Date.Format "2006-01-02 15:04" }} | Lastmod: {{ .Lastmod.Format "2006-01-02 15:04" }}</span>
      </summary>
      <div class="post-summary">
        {{ if .Summary }}
          {{ .Summary }}
          <p><a href="{{ .Permalink }}">More →</a></p>
        {{ else }}
          <p><a href="{{ .Permalink }}">More →</a></p>
        {{ end }}
      </div>
    </details>
  </li>
  {{ end }}
{{ end }}
</ul>

{{ end }}

数学公式支持

如果 LaTex 因某些原因无法正常渲染,需要添加 shortcode 配置。

创建 shortcode 文件 layouts/shortcodes/texi.html

<span class="math inline">\( {{ .Inner }} \)</span>

layouts/shortcodes/texd.html

<div class="math display">\[ {{ .Inner}} \]</div>

在 org 中使用:

行内公式: {{< texi >}} \varphi {{< /texi >}}

显示公式:
\[ \]

在线部署

准备仓库

将 hugo 根目录初始化为 git 仓库,该仓库用于管理源码, push 后再新建一个仓库,新仓库用于部署,必须设为公开(除非升级 GitHub Pro)。

如果将新仓库的仓库名设为 用户名.github.io ,部署后的博客域名便没有仓库名后缀。

由于 hugo 默认不会清理 public/ 目录,推荐将其添加到 gitignore 中,避免上传已删除的博客。

生成 Token

  • 点击 github 头像 -> Settings -> Developer settings -> Personal access tokens -> Tokens(classic) -> Generate new token(classic)
  • 勾选所有 repo 相关
  • 复制生成的 token (该 token 只显示一次)

配置 Actions

在私有仓库中

  • 点击 Settings -> Secrets and variables -> Actions -> New repository secret
  • Name 填任意名称,此处以 PERSONAL_TOKEN 为例
  • Secret 粘贴刚刚生成的 token -> Add secret

配置好 token 后,在私有仓库根目录创建 .github/workflows/deploy.yml

name: Deploy Hugo

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          personal_token: ${{ secrets.PERSONAL_TOKEN }}
          external_repository: 你的用户名/你的用户名.github.io
          publish_branch: main
          publish_dir: ./public
          force_orphan: true
          user_name: 'github-actions[bot]'
          user_email: 'github-actions[bot]@users.noreply.github.com'

其中, user_nameuser_email 用于以 bot 身份进行提交,避免不必要的个人 commit 记录和贡献统计,推荐保留。

注意替换你的实际 token 名称与 github 用户名。

触发部署

将这个 workflow 文件 push 到仓库中后,可以在 Actions 中查看运行状态。

成功后点击 Settings -> Pages 查看链接,点击即可访问,此后只需修改私有仓库便会自动部署。

一键发布或预览

如果想要更加便捷地发布博客,可以在 Emacs 配置中添加:

(defun my-blog-publish ()
  "publish blog , git add/commit/push , generate commit message"
  (interactive)
  (save-buffer)
  (let* ((blog-dir "你的 git 仓库绝对路径")
         (timestamp (format-time-string "%Y-%m-%d %H:%M:%S"))
         (default-directory blog-dir))

    ;; git add
    (message "Adding files...")
    (eshell-command "git add .")

    ;; git commit with timestamp
    (message "Committing: %s" timestamp)
    (eshell-command (format "git commit -m \"Update: %s\"" timestamp))

    ;; git push
    (message "Pushing to remote...")
    (let ((exit-code (eshell-command "git push")))
      (if (eq exit-code t)
      (message "Push may have failed or nothing to push, check *Messages* buffer for details")
    (message "Push successfully %s" timestamp)))))

(global-set-key (kbd "你想要的快捷键") 'my-blog-publish)

使用此命令便自动生成 commit 信息并发布博客。

同样,添加这段配置可以自动打开浏览器进行本地预览:

(defun my-blog-preview ()
  (interactive)
  (save-buffer)
  (let ((default-directory "你的 hugo 仓库绝对路径"))
    ;; 启动 hugo server
    (start-process "hugo-server" "*hugo-server*" 
                   "hugo" "server" "-D" "--bind" "0.0.0.0" "--port" "1313")
    ;; 等待服务器启动
    (sleep-for 2)
    ;; 自动打开浏览器
    (browse-url "http://localhost:1313")))

(global-set-key (kbd "你想要的快捷键") 'my-blog-preview)

如果想结束预览,关闭 *hugo-server* buffer。

常见问题

文章不显示

检查:

  1. 是否设置了 CLOSED 时间戳
  2. 是否将 TODO 改为 DONE
  3. 是否成功导出

主题中的示例文章

主题目录下可能有 content/posts/ ,删除其中的示例文件。

lastmod 不更新

确保 org 文件中有:

#+hugo_auto_set_lastmod: t

并且每次导出前保存文件。

总结

这套工作流的优势:

  1. 高效文本写作 :: 使用 org-mode,专注内容
  2. 版本控制友好 :: 便于 git 管理
  3. 自动化 :: 自动生成时间戳、目录、RSS 等
  4. 部署便捷 :: 更新仓库自动部署
  5. 可定制 :: Hugo 模板完全可自定义
  6. 快速 :: Hugo 生成速度极快,秒级构建,实时预览

Happy Blogging!

参考资源