Written on 17th March 2025. For my own use/Warning: Emacs talk ahead.

I'm aware that I'm polluting the internet by writing a blog post about how I write a blog post. I'm sorry.

I wrote in How I take computer notes using emacs about how I've been taking notes. I thought it would be handy to be able to quickly publish a note without needing a separate tool. So I guess this website is more of a "personal wiki" than a traditional blog.

The publishing sequence works as follows:

There are more steps than necessary here - if this scheme proves to be useful I will automate them.

I'm quite new to emacs, so you might find that this stuff is un-necessary, and there is likely a better way. But it works for me, at least.

This code uses the org export facility to convert the current org headline into HTML.

Some people use 'org-publish' to publish their sites, but this seems more complex than 'org-export' and I'm a bit confused as to which to use. org-publish seems to be designed to publish a whole "project", with files organised to strict scheme.

My needs are simpler - "Turn this org headline in to HTML in a file, and change the links so they point to other files".

I found blog posts which refer to html-preamble and html-postamble, and passing in template functions. These don't seem to exist in the org-export code, so must be org-publish features. Maybe one day I will study enough to see if there's an advantage in using org-publish, and how to get it to do what I want. But for now, this works…

Note to future me - Advanced Export Configuration in the manual might have things which I can use instead.

  (require 'org)
  (require 'ox-html)
  (require 'ox)

  ;; Configure output directories here: The base directory:
  (setq blog-dir "blog-output-html")
  ;; The URL - used for RSS feed generation
  (setq base-url "https://pointinthecloud.com/")
  ;; And then two relative...
  (setq static-source-dir "WEB_STATIC")
  (setq images-dir-relative "media")
  (setq blog-images-dir (format "%s/%s" blog-dir images-dir-relative))

  ;; Override the org HTML link export here, so it writes file paths
  ;; instead of paths relative to the current org document
  ;; TODO - fix this so that it works with all resources; not just images
  ;; For images: Maybe convert images to webp and have a hyperlink to the full image?

  (defun blog-special-html-link-export (link description info)
    ;; The link has format type:path and the description
    ;; argument contains the text
    (let* ((path (org-element-property :path link))
           (type (org-element-property :type link)))
      ;; Is it an image?
      (when (org-html-inline-image-p link info)
        ;; Change the path in the link
        (org-element-put-property link :path (format "%s/%s" images-dir-relative (file-name-nondirectory path)))
        ;; ..and copy the file to the images directory
        (shell-command (format "cp %s %s" path blog-images-dir))
        )
      ;; Check for id links, which we change.
      (if (string= "id" type)
          ;;  Actually format the link here...
          (concat "<a href='" path ".html'>" description "</a>")
        ;; Otherwise just use the normal link
        (org-html-link link description info)
        )
      )
    )

  (defun export-current-org-subtree-to-html-string ()
    "Take the current org subtree and return an HTML string of the export"

    ;; Define a custom exporter, based on the HTML one but over-riding the link
    ;; generator with the function above
    (org-export-define-derived-backend 'colin-html-link-exporter 'html
      :translate-alist
      '((link . blog-special-html-link-export))
      )

    ;; Do the export. The only way appears to be to narrow, export, then widen.
    (org-narrow-to-subtree)
    (outline-show-all)
    (let (( html-body-text
            (org-export-as 'colin-html-link-exporter nil 't 't `(:with-toc nil
                                                                           :section-numbers nil)))
          )
      (widen)
      html-body-text
      )
    )

  (defun header (title)
    (format "<!DOCTYPE html>
                  <html lang='en'>
                  <head>
                  <meta charset='utf-8'/>
                  <link rel='stylesheet' type='text/css' href='colin_styles.css'/>
                  <link href='/blog.xml' rel='alternate' type='application/atom+xml'/>
                  <title>%s</title>
                  </head>

                  <body>
                  <nav> <div class='thetop'>
                  <h1 class='title'><img src='media/rosie-flying-dog.jpg'/>%s</h1>
                  </div></nav>
    <div class='content'>\n" title title)
    )

  (defun footer ()
    "</div>
  <footer id='thebottom'>
                  <div>
                  <a href='/'>Go to the beginning</a>&nbsp <a href='/blog-contact-me.html'>Contact me</a>
                  </div>
                  </footer>
                  </html>\n"
    )

  ;; The function that does the work.
  ;; The easiest way to do this seems to get org to export to a string,
  ;; then add the header and footer in a temporary buffer. There may be better ways but I'm quite new to elisp development, so...
  ;; Can possibly use export filters (see the source in ox-rss for an example) 

  (defun colin-export-current-org-subtree-to-html-file (title filename)
    (interactive "Fexport to file: ")
    (let (( html-body-text (export-current-org-subtree-to-html-string)))
      ;; Create HTML in a temporary buffer
      (with-temp-buffer
        ;; The header..
        (insert (header title))
        (setq exported-text-beginning (point))
        ;; ...the body...
        (insert html-body-text)
        ;; .. the footer
        (insert (footer))

        ;; TODO: This is icky. Not sure if it's the best way. Remove the first header with an ID, as it was
        ;; added by the org export and I don't want it.
        (goto-char exported-text-beginning)
        ;; Regexp isn't usually great for HTML, but it's a self contained format we know
        ;;(re-search-forward "<h[0-9>[:0123456789-abcdefghijklmnopqrstuvwxyz !]+</h[0-9]>")
        ;;(replace-match "")
        ;; Bodge for now - remove the first 2 lines; the generated heading
        ;; Also ideally don't put in kill ring
        (kill-line 2)
        ;; And then write the buffer
        (write-file filename)
        (kill-buffer)
        )
      )
    )

  (defun setup-blog ()
    "Make the required folders and boilerplate"
    (unless (file-exists-p blog-dir)
      (make-directory blog-dir))
    (unless (file-exists-p blog-images-dir)
      (make-directory blog-images-dir))
    ;; Copy CSS and static files etc also.
    ;; Name them here to make sure we only copy the needed ones
    (copy-file (format "%s/colin_styles.css" static-source-dir) (format "%s/colin_styles.css" blog-dir) 't)
    (copy-file (format "%s/rosie-flying-dog.jpg" static-source-dir) (format "%s/rosie-flying-dog.jpg" blog-images-dir) 't)
    (copy-file (format "%s/rosie-flying-dog.jpg" static-source-dir) (format "%s/favicon.ico" blog-dir) 't)
    (copy-file (format "%s/Asap-VariableFont_wdth,wght.ttf" static-source-dir) (format "%s/Asap-VariableFont_wdth,wght.ttf" blog-dir) 't)
    )

;; A convenience for blogging the current note. Get the ID and use it as the filename
;; Set a timestamp "BLOGGED" property so we know what has been published.
;; Fill the FEED-DESCRIPTION property and run the RSS feed generator (described below)


(defun colin-blog-this-note ()
  (interactive)
  (setup-blog)
  (let* ((title (or (org-entry-get nil "Title")
                    (read-from-minibuffer "Enter the title: "))
                )
         (identifier (colin-get-or-set-id-from-headline))
         (filename (format "%s/%s.html" blog-dir identifier))
         )
    (org-set-property "FEED-DESCRIPTION" (or (org-entry-get nil "FEED-DESCRIPTION") (read-from-minibuffer "Enter a description: ")))
    (org-set-property "TITLE" title)
    (colin-export-current-org-subtree-to-html-file title filename)
    ;; Record when 'published'
    (org-set-property "BLOGGED" (current-time-string))
    )
  (colin-generate-rss-feed)
  ))

Adding an RSS feed

I couldn't make any of the RSS exporters provide a feed using the inputs I had. For now I'm creating my own file until I figure it out properly. This takes a list defining the entries and generates an Atom XML file.

The RSS is generated by two functions:

  • colin-extract-org-rss-headlines

This looks in the current org file for items containing a FEED-DESCRIPTION entry as a property. It writes these into a list with one item for each item to go into the RSS feed.

;; For the blog tool, extract headlines from the current org file
(defun colin-extract-org-rss-headlines ()
  "Process all Org headlines to get a title and description for an RSS feed"
  (interactive)
  (let (
        (entries '())
        )
  (org-element-map (org-element-parse-buffer) 'headline
    (lambda (hl)
      (let ((id      (org-element-property :ID hl))
            (title   (or (org-element-property :TITLE hl)
                         (org-element-property :raw-value hl)))
            (description (org-element-property :FEED-DESCRIPTION hl))
            (blogged (org-element-property :BLOGGED hl))
            (level   (org-element-property :level hl))
            )
        ;; Only output items with a FEED-DESCRIPTION field
        (when (and (= level 1) description)
          (message "RSS headline extraction: Found: %s Date: %s" title blogged)
          (push
           (list :updated
                 (format-time-string "%Y-%m-%dT%H:%M:%SZ"
                                     (encode-time (parse-time-string blogged)))
                 :title title
                 :url (format "%s.html" id)
                 :description description) entries)

          )

        ) ; end let
      ) ;; end lambda
    ) ;; end org element map

  entries ;; Return the value
  ) ; end let
  ); end defun

…this list looks similar to:

;; List used to generate the RSS feed.
(setq rss-entries `(
                    (:updated "2025-11-25T15:05:00Z"
                              :title "The secret names within Bob the dishwasher"
                              :url "2025-11-24-140900.html"
                              :description "I repaired our dishwasher and was surprised to see names inside it"
                              )
                    (:updated "2025-11-25T15:05:00Z"
                              :title "Replacing a 48 year old Commodore PET with a Raspberry Pi Pico"
                              :url "2025-11-18-140100.html"
                              :description "I begun replacing the main board of an old computer with a tiny £0.89 microcontroller chip"
                              )
      )

(colin-generate-rss-feed)

…and a function which takes the list above and generates a 'blog.xml' output file.

(defun colin-generate-rss-feed ()
  "Generate a 'blog.xml' file containing an atom feed, using entries from rss-entries.
This is slightly misnamed as technically it doesn't generate an RSS feed"
(interactive)
  ;; Fill the rss-entries structure with extracted feed items.
  ;; Use setq to allow inspection later.
  (setq rss-entries (colin-extract-org-rss-headlines))

  (with-temp-buffer
    ;; Insert the header
    (insert
     (format "<?xml version='1.0' encoding='utf-8'?>
  <feed xmlns='http://www.w3.org/2005/Atom'>
    <title>Colin's Things that might be useful or interesting</title>
    <subtitle>Written by Colin</subtitle>

    <link href='https://pointinthecloud.com'/>
    <updated>%s</updated>
    <link href='https://pointinthecloud.com/blog.xml' rel='self'/>
    <id>urn:uuid:67ce5073-523f-44fe-a8a2-c51a60e3c2ce</id>
    <author>
      <name>Colin</name>
      <email>me@me.com</email>
    </author>\n" (format-time-string "%Y-%m-%dT%H:%M:%SZ" (current-time)))
     )

    (dolist (entry rss-entries )
      (let* ((updated (plist-get entry :updated))
            (title (plist-get entry :title))
            (url (concat base-url (plist-get entry :url)))
            (description (plist-get entry :description))
            (image_url (plist-get entry :image))
            ; Image url is optional
            (image_html (if image_url (format "<img src='/%s'/>" image_url) ""))
            )

        (insert (format "<entry>
      <title>%s</title> 
      <link href='%s'/>
      <updated>%s</updated>
      <id>%s</id>
      <content type='html'><![CDATA[<p>%s</p>%s]]></content>
    </entry>\n" title url updated url description image_html))
        )
      )

    (insert "</feed>\n")
    (write-file (concat blog-dir "/blog.xml"))
    )
  )

I used these to help write this:

https://www.reddit.com/r/emacs/comments/swvbmm/you_want_to_write_a_custom_org_backend_lets_write/ https://ogbe.net/blog/emacs_org_static_site

TODO:

  • Maybe make a simple list function which also adds the post and a hyperlink to a list - by appending to the end of file or something
  • Add a link to images to view the fullsize image, and add alt-text.
  • Copy the files to the blog server automatically