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:
- I write a note or post, as previously described.
- I add a link to the note to the appropriate place - e.g. the front page.
- I run (colin-blog-this-note) and it gets published as HTML, and links between notes get turned into web hyperlinks.
- I then run another function to update the RSS feed if I deem it useful to publish the note.
- I use 'rsync' to copy the files to the blog server via a 'publish-blog' script.
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>  <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