Website With Emacs

NOTE: I am no longer using this setup for my website.

In this post, I will briefly go over my setup of using Emacs Org-mode for managing and publishing my blog website.


I write all of my posts and static sites in Org-mode within Emacs and when I want to update my site I run a custom publishing function which exports Org-mode files into HTML. My custom function also creates a sitemap for my blog posts and an RSS feed.


My setup is in my Emacs configuration file, which you can find in its entirety from my GitHub.

File Structure

├── org
│   ├──
│   ├── blog
│   │   └──
│   ├── css
│   │   └── site.css
│   ├── html
│   │   ├── html_head.html
│   │   ├── html_postamble.html
│   │   └── html_preamble.html
│   ├── img
│   │   ├── favicon.ico
│   │   └── miika.jpg
│   ├──
│   ├──
│   └──
└── www
    ├── about.html
    ├── blog
    │   └── post1.html
    ├── css
    │   └── site.css
    ├── img
    │   ├── favicon.ico
    │   └── miika.jpg
    ├── index.html
    ├── rss.xml
    └── sitemap.html

9 directories, 19 files

Sitemap / Blog Index

These functions are used to format a sitemap to have a date for each entry.

(defun m/org-publish-org-sitemap (title list)
  "Sitemap generation function."
  (concat "#+TITLE: Sitemap\n\n"
          (org-list-to-subtree list)))

(defun m/org-publish-org-sitemap-format-entry (entry style project)
  (cond ((not (directory-name-p entry))
         (let* ((date (org-publish-find-date entry project)))
           (format "%s - [[file:%s][%s]]"
                   (format-time-string "%F" date) entry
                   (org-publish-find-title entry project))))
        ((eq style 'tree)
         ;; Return only last subdir.
         (file-name-nondirectory (directory-file-name entry)))
        (t entry)))

RSS Feed

RSS-feed generation is done with the ox-rss package, but it requires some additional customization for my needs.

(defun m/org-rss-publish-to-rss (plist filename pub-dir)
  "Publish RSS with PLIST, only when FILENAME is ''.
PUB-DIR is when the output will be placed."
  (if (equal "" (file-name-nondirectory filename))
      (org-rss-publish-to-rss plist filename pub-dir)))

(defun m/format-rss-feed (title list)
  "Generate RSS feed, as a string.
TITLE is the title of the RSS feed.  LIST is an internal
representation for the files to include, as returned by
`org-list-to-lisp'.  PROJECT is the current project."
  (concat "#+TITLE: " title "\n\n"
          (org-list-to-subtree list 1 '(:icount "" :istart ""))))

(defun m/format-rss-feed-entry (entry style project)
  "Format ENTRY for the RSS feed.
ENTRY is a file name.  STYLE is either 'list' or 'tree'.
PROJECT is the current project."
  (cond ((not (directory-name-p entry))
         (let* ((file (org-publish--expand-file-name entry project))
                (title (org-publish-find-title entry project))
                (date (format-time-string "%Y-%m-%d"
                (org-publish-find-date entry project)))
                (link (concat (file-name-sans-extension entry) ".html")))
            (insert (format "* %s\n" title))
            (org-set-property "RSS_PERMALINK" link)
            (org-set-property "PUBDATE" date)
            (insert-file-contents file)
        ((eq style 'tree)
         ;; Return only last subdir.
         (file-name-nondirectory (directory-file-name entry)))
        (t entry)))

Project Spec for Org-publish

This spec defines the directory structure and exporting options for org-publish

(defun m/get-publish-project-spec ()
    "Return project settings for use with `org-publish-project-alist'."
    (let* ((website-root (file-name-as-directory
           (website-org (file-name-as-directory
                         (concat website-root "org")))
           (website-www (file-name-as-directory
                         (concat website-root "www")))
           (website-org-img (file-name-as-directory
                             (concat website-org "img")))
           (website-www-img (file-name-as-directory
                             (concat website-www "img")))
           (website-org-css (file-name-as-directory
                             (concat website-org "css")))
           (website-www-css (file-name-as-directory
                             (concat website-www "css")))
           (website-org-html (file-name-as-directory
                              (concat website-org "html")))
           (website-org-blog (file-name-as-directory
                               (concat website-org "blog")))
           (get-content (lambda (x)
                            (insert-file-contents (concat website-org-html
           (website-html-head (funcall get-content "html_head.html"))
           (website-html-preamble (funcall get-content "html_preamble.html"))
           (website-html-postamble (funcall get-content "html_postamble.html")))
           :base-directory ,website-org
           :base-extension "org"
           :recursive t
           :exclude "rss\\.org\\|sitemap\\.org"
           :publishing-directory ,website-www
           :publishing-function org-html-publish-to-html
           :author "Miika Nissi"
           :email ""
           :with-title t
           :description "This is my personal website: a place where you can read and learn about technology related subjects."
           :keywords "Miika Nissi, blog, resume, technology, programming"
           :section-numbers nil
           :headline-levels 4
           :language en
           :with-toc nil
           :with-date t
           :with-email t
           :with-statistics-cookies nil
           :with-todo-keywords nil
           :auto-sitemap t
           :sitemap-sort-files anti-chronologically
           :sitemap-format-entry m/org-publish-org-sitemap-format-entry
           :html-head-include-default-style nil
           :html-head-include-scripts nil
           :htmlized-source t
           :html-doctype "html5"
           :html-html5-fancy t
           :html-head ,website-html-head
           :html-preamble ,website-html-preamble
           :html-postamble ,website-html-postamble)
           :base-directory ,website-org-img
           :base-extension "png\\|jpg\\|gif\\|svg\\|ico"
           :recursive t
           :publishing-directory ,website-www-img
           :publishing-function org-publish-attachment)
           :base-directory ,website-org-css
           :base-extension "css"
           :publishing-directory ,website-www-css
           :publishing-function org-publish-attachment)
           :base-directory ,website-org
           :base-extension "org"
           :exclude "rss\\.org\\|index\\.org\\|about\\.org\\|sitemap\\.org"
           :recursive t
           :publishing-directory ,website-www
           :publishing-function m/org-rss-publish-to-rss
           :title "Miika's Blog"
           :description "This feed contains blog posts from Miika Nissi. Topics ranging from lifestyle to technology."
           :author "Miika Nissi"
           :html-link-use-abs-url t
           :html-link-home ""
           :with-broken-link t
           :with-toc nil
           :rss-image-url ""
           :with-date t
           :with-author t
           :creator "Miika Nissi"
           :with-description t
           :auto-sitemap t
           :sitemap-filename ""
           :sitemap-title "Miika's blog"
           :sitemap-style list
           :sitemap-sort-files anti-chronologically
           :sitemap-function m/format-rss-feed
           :sitemap-format-entry m/format-rss-feed-entry)
          ("" :components ("org" "images" "css" "rss")))))

Project Publishing Function

The website is updated when calling m/publish-website, which only publishes newly modified files. When used with additional arguments, a full update can be forced: (m/publish-website "" t).

(defun m/publish-website (&optional project force)
    "Publish personal website."
    (unless project (setq project ""))
    (let ((org-publish-project-alist (m/get-publish-project-spec))
          (org-export-date-timestamp-format "%Y-%m-%d")
          (org-todo-keywords '((sequence "TODO" "REVIEW" "|"
                                         "DONE" "DEFERRED" "ABANDONED"))))
      (org-publish-project project force))
    (templated-html-create-sitemap-xml "~/" "~/" ""))


My setup is constantly changing, but for now I’m happy with the setup I’ve built. For the latest updates, you can check my GitHub.