Emacs: 使用org-mode制作网站

使用 ox-publish 导出我的网站。 如果你折腾过各种建站方式,一定少不了这个。本文介绍一种org-mode自带的网页导出器来生成网站,希望你感觉兴趣!

关于这个网站

org-mode 很强大,可以写作、做计划、做幻灯片、做网站。本站就是用org-mode内置的 ox-publish 导出器制作而成。

以下是制作过程,希望对感兴趣的朋友有所帮助。

网站结构

$ tree  -L 2
.
├── Makefile       #编译文件
├── Org-mode       # 文章目录
│   ├── 404.org
│   ├── about.org
│   ├── index.org  #首页
│   ├── microtalk
│   ├── microtalk.org
│   ├── reading
│   ├── rss.org     #自动生产的rss文件
│   ├── sitemap.org #网站地图
│   ├── skills.org  #技能树
│   └── talk.org
├── assets          #静态文件
│   ├── distro-head.html
│   ├── distro-html_preamble.html
│   ├── dj-head.html
│   ├── dj-html_preamble.html
│   ├── favicon.ico
│   ├── fonts                     #字体文件目录
│   ├── prot-head.html            #页面头部信息,如导航栏、加载的css文件字体文件、广告代码等
│   ├── prot-html_postamble.html  #页脚信息
│   ├── prot-html_preamble.html   #页首信息
│   ├── rss.png
│   ├── static                    #css文件目录
│   ├── systemcrafters-head.html
│   ├── systemcrafters-html_postamble.html
│   └── systemcrafters-html_preamble.html
├── build-site.el                 #主配置,网页导出
├── build.sh                      #命令行网页导出

assets/static  # 网站样式根据兴趣收集
$ tree
.
├── distro-htmlize.css
├── distro-jquery.stickytableheaders.min.js
├── distro-readtheorg.css
├── distro-readtheorg.js
├── dj-style.css
├── font.css
├── images
│   ├── header_bg.png
│   ├── icon-beian.png
│   ├── made-with-emacs.png
│   └── made-with-emacs.webp
├── orgcss-style.css
├── prot-style-print.css
├── prot-style.css
├── systemcrafters-style-code.css
├── systemcrafters-style.css
└── worg-style.css

assets/fonts  # 网站字体使用仓耳今楷
$ tree
.
├── angelina.ttf
├── jinkai.ttf
└── jinkai.woff2

提前准备

  • 安装emacs软件

    可参考我的文章或者官方提供的方法

  • 准备统一引用的html文件关联css样式文件、js文件

    自己写或者浏览器F12拷贝样式。放到assets目录里

  • 准备字体文件(可选)

    放到assets/fonts目录里

  • 准备网站图标

    搜索一下有很多免费在线生成的。 命名为favicon.ico文件

build-site.el 配置

emacs 加载配置文件

emacs仅使用 build-site.el 做配置导出网页。所以配置文件包含初始化包源、引用 ox-publish 包导出网页、引用 ox-rss 生成rss文件

;;; packages
;;;; Initialize the package system
(setq make-backup-files nil)

(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
;; (setq package-archives '(("melpa" . "https://melpa.org/packages/")
;;                          ("elpa" . "https://elpa.gnu.org/packages/")))
(setq package-archives '(
    ("melpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/")
    ("gnu" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
    ("org" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/org/")))


(unless (bound-and-true-p package--initialized)
  (package-initialize))

(when (not package-archive-contents)
  (package-refresh-contents))

;; Check and install dependencies
(dolist (package '(htmlize ox-rss))
  (unless (package-installed-p package)
    (package-install package)))

;; Load publishing system
(require 'ox-publish)
;; (require 'htmlize)
(require 'ox-rss)

(setq org-export-html-coding-system 'utf-8-unix)

生成站点地图

os-publish 自带生成站点地图配置,且支持自定义方法。这里使用自定方法生成站点地图。包含文章预览功能

;;; Sitemap preprocessing
;;;; Get Preview
(defun my/get-preview (file)
  "get preview text from a file

Uses the function here as a starting point:
https://ogbe.net/blog/blogging_with_org.html"
  (with-temp-buffer
    (insert-file-contents file)
    (goto-char (point-min))
    (when (re-search-forward "^#\\+BEGIN_PREVIEW$" nil 1)
      (goto-char (point-min))
      (let ((beg (+ 1 (re-search-forward "^#\\+BEGIN_PREVIEW$" nil 1)))
            (end (progn (re-search-forward "^#\\+END_PREVIEW$" nil 1)
                        (match-beginning 0))))
        (buffer-substring beg end)))))

;;;; Format Sitemap
(defun my/org-publish-org-sitemap (title list)
  "Sitemap generation function."
  (concat "#+OPTIONS: toc:nil")
  (org-list-to-subtree list))

(setq org-export-global-macros
      '(("timestamp" . "@@html:<span class=\"timestamp\">$1</span>@@")))

(defun my/org-publish-org-sitemap-format (entry style project)
  "Custom sitemap entry formatting: add date"
  (cond ((not (directory-name-p entry))
         (let ((filename (org-publish-find-title entry project))
               (preview (if (my/get-preview (concat "Org-mode/" entry))
                            (my/get-preview (concat "Org-mode/" entry))
                          "")))
         (format "{{{timestamp(%s)}}} [[file:%s][%s]]\n%s"
                 (format-time-string "%Y-%m-%d"
                                     (org-publish-find-date entry project))
                 entry
                 filename
                 preview)
         ))
        ((eq style 'tree)
         (file-name-nondirectory (directory-file-name entry)))
        (t entry)))

只需要把方法添加到网页导出配置里就可以了,如

;;; define publishing project
(setq org-publish-project-alist
      `(
        ("org-site:main"
         :recursive t
...
         :auto-sitemap t
         :sitemap-title nil;"Blog"
         :sitemap-format-entry my/org-publish-org-sitemap-format
         :sitemap-function my/org-publish-org-sitemap
         :sitemap-sort-files anti-chronologically
         :sitemap-filename "sitemap.org"
         :sitemap-style tree
...

载入生成的站点地图的方法,只需要在文件中输入 #INCLUDE 指定加载的范围。 如加载sitemap.org文件中microtalk标题下前100个主题

#+INCLUDE: sitemap.org::*microtalk :lines "-100" :only-contents t

配置网页统一样式

ox-publish 默认导出的网页样式并不好看。你完全可以自己设计。

默认配置

;; 设置 org-publish 的项目列表
(setq org-publish-project-alist
      '(
        ;; 笔记部分
        ("org-notes"
         :base-directory "~/org/"
         :base-extension "org"
         :exclude "\\(tasks\\|test\\|scratch\\|diary\\|capture\\|mail\\|habits\\|resume\\|meetings\\|personal\\|org-beamer-example\\)\\.org\\|test\\|article\\|roam\\|hugo"
         :publishing-directory "~/public_html/"
         :recursive t                 ; include subdirectories if t
         :publishing-function org-html-publish-to-html
         :headline-levels 6
         :auto-preamble t
         :auto-sitemap t
         :sitemap-filename "sitemap.org"
         :sitemap-title "Sitemap"

        ;; 静态资源部分
        ("org-static"
         :base-directory "~/org/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|mov"
         :publishing-directory "~/public_html/"
         :recursive t
         :publishing-function org-publish-attachment)

        ;; 项目集合
        ("org"
         :components ("org-notes" "org-static"))
        ))
)

配置说明:

这将配置变量 org-publish-project-alist 以定义我们网站的发布项目。阅读此变量的文档, M-x describe-variable 以获取更多信息!

这里最重要的两个配置是: sitemap-format-entry 以及 org-publish-project-alist 。前者是生成 sitemap 的方式,后者是定义publish要到哪个文件夹,把哪些org文件进行发布。

指定源和目标:

  • :base-directory 指定包含源文件的目录,即需要发布文件的基本目录
  • :publishing-directory 指定转化后存放的目录
  • :preparation-function 指定转化文件前需要调用的函数, 例如发布前需要 make 一下源文件
  • :completion-function 指定转化文件完成后需要调用的函数

选择文件(用于识别需要发布的文件):

  • :base-extension 指定识别的文件拓展名, 不需要点(.)后缀 ,如:*:base-extension “css”*
  • :exclude 用于排除指定文件
  • :include 引入指定文件,存在该文件都会进行发布转化
  • :recursive 为真时, 将递归检查发布 base-directory 指定目录下的文件

发布触发执行动作:

  • :publishing-function 执行发布操作将触发指定函数来完成工作
    • org-html-publish-to-htm 默认 org 文件转化成 HTML
    • org-publish-attachment 复制操作

我们配置完 ox-publish 后,可以通过两种方式来发布静态站点:

发布

  • M-x org-publish-all 来发布(全部)站点
  • 按 C-c C-e 后,再按 P a 来发布(全部)站点

当写完一篇文章之后需要手动调用 org-export-dispatch 选择 p f ,将当前 org 文档生成 HTML 网页文件。

测试

一个简单的网页就生成了,可以使用浏览器打开此 HTML 文件进行预览,也可以使用 Simple-Httpd 这个插件在 website-directory 文件下启动一个 server,之后在浏览器中打开 127.0.0.1:19999 就可以查看生成的博客内容啦。

使用如下命令生成网站,也可以将命令添加到 build.sh 文件中执行

emacs -Q --script build-site.el

本地测试:

python3 -m http.server --directory=~/public_html
Open http://localhost:8000/ in the Web browser

自定义页面展出配置

与手动写 html,css 一样这些在 org 里都是可自定义的,修改 org-publish 中的属性参 数和直接修改对应的 org 的 html 页面展出参数是等效的,请仔细阅读官方文档:https://orgmode.org/manual/Publishing-options.html

;; Formatting
(defun file-contents (file)
  (with-temp-buffer
    (insert-file-contents file)
    (buffer-string)))

;;; define publishing project
(setq org-publish-project-alist
      `(
        ("org-site:main"
         :recursive t
         :base-directory "./Org-mode"
         :publishing-directory "./public"
         :publishing-function org-html-publish-to-html
         :auto-sitemap t
         :sitemap-title nil;"Blog"
         :sitemap-format-entry my/org-publish-org-sitemap-format
         :sitemap-function my/org-publish-org-sitemap
         :sitemap-sort-files anti-chronologically
         :sitemap-filename "sitemap.org"
         :sitemap-style tree
         :exclude "^demo.org\\|^up.org\\|.*/homework/.*"  ; Exclude drafts directory from publishing

         ;; https://orgmode.org/manual/Publishing-options.html#Generic-properties-1
         :with-author nil            ;; Don't include author name
         :with-creator t             ;; Include Emacs and Org versions in footer
         :with-toc t                 ;; Include a table of contents
         :section-numbers nil        ;; Don't include section numbers
         :time-stamp-file nil        ;; Don't include time stamp in file
         :headline-levels 4          ;; Number of headline levels for export
         :with-sub-superscript nil   ;; Don't include TeX-like syntax for sub- and superscripts

         ;; https://orgmode.org/manual/Publishing-options.html#ASCII-specific-properties-1

         ;;; prot
         :html-preamble ,(file-contents "assets/prot-html_preamble.html")
         :html-postamble ,(file-contents "assets/prot-html_postamble.html")
         :html-head ,(file-contents "assets/prot-head.html")

         ;; ;;; dj
         ;; :html-preamble ,(file-contents "assets/dj-html_preamble.html")
         ;; :html-postamble ,(file-contents "assets/prot-html_postamble.html")
         ;; :html-head ,(file-contents "assets/dj-head.html")

         ;; ;;; systemcrafters
         ;; :html-preamble ,(file-contents "assets/systemcrafters-html_preamble.html")
         ;; :html-postamble ,(file-contents "assets/systemcrafters-html_postamble.html")
         ;; :html-head ,(file-contents "assets/systemcrafters-head.html")

         ;; ;;; distro
         ;; :html-preamble ,(file-contents "assets/distro-html_preamble.html")
         ;; :html-postamble ,(file-contents "assets/systemcrafters-html_postamble.html")
         ;; :html-head ,(file-contents "assets/distro-head.html")

         :html-head-include-default-style nil
         ;; ;;; https://emacs.stackexchange.com/questions/76599/how-to-adjust-html-elements-using-org-publish-in-emacs
         ;; :html-container "div class=\"container\""
         :html-divs ((preamble "header" "preamble")
                     (content "main class=\"container\"" "content")
                     (postamble "footer" "postamble"))
         :html-container "section"
         :html-validation-link nil
         :html-html5-fancy t
         :html-doctype "html5"


         ;; :time-stamp-file nil
         :htmlized-source t
         )
        ("org-site:static"
         :base-directory "./Org-mode/"
         :base-extension "png\\|jpg\\|webp\\|gif\\|mp3\\|m4a\\|ogg\\|swf\\|ico"
         :publishing-directory "./public"
         :recursive t
         :publishing-function org-publish-attachment
         :exclude "\\(^demo\\|^up\\)\\.org\\|.*/homework/.*"  ; Exclude drafts directory from publishing
         )
        ("org-site:assets"
         :base-directory "./assets/"
         :base-extension "css\\|js\\|ttf\\|woff2\\|png\\|jpg\\|webp\\|gif\\|pdf\\|mp3\\|m4a\\|ogg\\|swf\\|ico"
         :publishing-directory "./public/"
         :recursive t
         :publishing-function org-publish-attachment)

        ("org-site:rss"
         :recursive nil
         :base-directory "./Org-mode/"
         :base-extension "org"
         :exclude ".*"
         :include ("talk.org")
         :publishing-function my/org-rss-publish-to-rss
         :publishing-directory "./public"
         :rss-extension "xml"
         :auto-sitemap t
         :sitemap-filename "rss.org"
         :sitemap-title "Jasper Hsu' Blog"
         :sitemap-style list
         :sitemap-sort-files anti-chronologically
         :sitemap-function my/format-rss-feed
         ;;:sitemap-format-entry my/format-rss-feed-entry
         :html-link-home "https://xuchangwei.com/"
         :html-link-use-abs-url t
         :html-link-org-files-as-html t
         )

        ("site" :components ("org-site:main" "org-site:static" "org-site:assets" "org-site:rss"))
       ))

;;; generate site output
;;(org-publish-all t)
(org-publish-project "site") ;; del cache org-publish-timestamp-directory  ~/.org-timestamps/

(message "Build Complete!")

结束

(provide 'build-site)

;; C-x C-c
;;(save-buffers-kill-terminal)

;;; build-site.el ends here

我们可以在配置文件的开头部分加入一些开源协议,如

;;; build-site.el --- Building a website -*- lexical-binding: t -*-

;; Copyright (C) 2024 Jasper Hsu

;; Author: Jasper Hsu <[email protected]>
;; Maintainer: Jasper Hsu <[email protected]>
;; URL: https://xuchangwei.com
;; Version: 0.1
;; Keywords:

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or (at
;; your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;; Commentary:

;; Comand: emacs -Q --script build-site.el

;;; Code:

参考:Made with Org-mode