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 (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 "TITLE" title) (colin-export-current-org-subtree-to-html-file title filename) ;; Record when 'published' (org-set-property "BLOGGED" (current-time-string)) ) )
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.
This will do for now, but later I'd like to search the org files for the 'BLOGGED' property and fill this list automatically, sorting by date.
;; List used to generate the RSS feed. (setq rss-entries `( (:updated "2025-03-21T16:17:00Z" :title "Explosions, overturning cars, and tea - operating the Scottish electrical grid in the 1980s" :url "2025-03-21-120000.html" :description "My dad's work for SSEB in the 1980s/1990s" :image "media/BRW5CF370CB6589_11232024_105123_000724.jpg" ) (:updated "2025-03-15T19:17:00Z" :title "Blogging like it's 1987: A post from my old text terminal" :url "2025-03-15-190000.html" :description "I wrote this blog post from a DEC VT320 text terminal" :image "media/PXL_20250315_223859543.jpg" ) (:updated "2025-03-10T15:39:00Z" :title "A test of the new blog" :url "2025-03-10-153900.html" :description "Something to see if the new blog works" :image "media/PXL_20250302_151336585-2.MP.jpg" ) (:updated "2025-03-07T17:14:00Z" :title "My emacs blog thing" :url "2025-03-07-171400.html" :description "I talk about how I blog using emacs" ) (:updated "2025-03-03T21:30:29Z" :title "My way of hyperlinking in emacs" :url "2025-03-03-213029.html" :description "My way of hyperlinking in emacs" ) (:updated "2024-12-15T21:01:00Z" :title "Robotknitting" :url "2024/12/15/empisal-knitmaster-hacking/" :description "Knitting Knitting Knitting" :image "assets/empisal-knitmaster-hacking/robot_sm.jpg") ) )
…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) (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