Using a Blog post as build instructions

Table of Contents

I will be continually updating and editing this blog post as I grow this website so consider this to be a small forever post.

The why

I had an interesting thought as I was switching my website away from using hugo as the static site generator to using org-publish. Why not try and use the literal programming feature of Org to have the actual build instructions for org-publish as an actual blog post.

Now why org-publish?

Well because org mode is the only decent way of writing documents without going insane.

The real reason is that one of the many fantastic things about org mode is the ability to write blocks of code in many different languages, and execute them all in the same session, a superior Jupyter notebook but for all languages. This comes in handy when you are engaging in exploratory work which is often what I do. Therefore, compiling it into a blog post becomes the natural next step and I just have to move the file over and walla its there, ready to be published.

This specific site uses this blog post, a shell script and github pages to actually build and host the website.

I do have a few other files that I need to move over such as assets, the preamble and postamble to this blog post

Packages

Set the packages into a hidden folder

(require 'package)
(require 'subr-x)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

Initialise the package system

(package-initialize)
(unless package-archive-contents
  (package-refresh-contents))

Install dependencies

(package-install 'htmlize)

Load the publishing system

(require 'ox-publish)

Setting the stylesheets

To set the css style sheets in the HTML-HEAD.

(setq set-css "<link rel=\"stylesheet\" type=\"text/css\" href=\"/asset/css/style.css\"/><link rel=\"stylesheet\" href=\"/asset/css/prism.css\"/><script src=\"/asset/js/prism.js\"></script><link rel=\"alternate\" type=\"application/rss+xml\" title=\"RSS Feed\" href=\"/feed.xml\">")

Editing the backend configuration functions

This overrides the default back-end filter for the src-block and changes the HTML template. This will also change the class to match Prism’s recommendations, which is language-*, where * is the language.

This code is originally from here

(defun my/org-html-src-block (src-block _contents info)
  "Transcode a SRC-BLOCK element from Org to HTML.
  CONTENTS holds the contents of the item.  INFO is a plist holding
  contextual information."
  (if (org-export-read-attribute :attr_html src-block :textarea)
      (org-html--textarea-block src-block)
    (let* ((lang (org-element-property :language src-block))
           (code (org-html-format-code src-block info))
           (label (let ((lbl (org-html--reference src-block info t)))
                    (if lbl (format " id=\"%s\"" lbl) "")))
           (klipsify  (and  (plist-get info :html-klipsify-src)
                            (member lang '("javascript" "js"
                                           "python" "scheme" "clojure" "php" "html" "shell" "rust")))))
      (if (not lang) (format "<pre class=\"example\"%s>\n%s</pre>" label code)
        (format "<div class=\"org-src-container\">\n%s%s\n</div>"
                (let ((caption (org-export-get-caption src-block)))
                  (if (not caption) ""
                    (let ((listing-number
                           (format
                            "<span class=\"listing-number\">%s </span>"
                            (format
                             (org-html--translate "Listing %d:" info)
                             (org-export-get-ordinal
                              src-block info nil #'org-html--has-caption-p)))))
                      (format "<label class=\"org-src-name\">%s%s</label>"
                              listing-number
                              (org-trim (org-export-data caption info))))))
                (if klipsify
                    (format "<pre><code class=\"src language-%s\"%s%s>%s</code></pre>"
                            lang
                            label
                            (if (string= lang "html")
                                " data-editor-type=\"html\""
                              "")
                            code)
                  (format "<pre><code class=\"src language-%s\"%s>%s</code></pre>"
                          lang label code)))))))

Next, a new backend that includes the new filter needs to be defined. According to org-mode’s documentation, we can do that by calling org-export-define-derived-backend, specifying the derived backend’s name and the derived backend’s parent as the first and second parameter, and modifying the :translate-alist property to include the new filter.

(org-export-define-derived-backend 'site-html
                                   'html
                                   :translate-alist
                                   '((src-block . my/org-html-src-block)))

Customise the HTML output by using our own scripts and styles without showing our validations links

(setq org-html-validation-link nil org-html-head-include-scripts nil
      org-html-head-include-default-style nil
      org-html-head set-css)

This is a wrapper function editing the original org-publish-to-html function. It publishes the file only if it contains the line '#+SELECTTAGS: publish'.

(defun my/org-publish (plist filename pub-dir)
  "Publish the file only if it contains the line '#+SELECT_TAGS: publish'."
        (org-publish-org-to 'site-html filename
                            (concat (when (> (length org-html-extension) 0) ".")
                                    (or (plist-get plist :html-extension)
                                        org-html-extension
                                        "html"))
                            plist pub-dir))

RSS Feed Generation

Someone asked if I had an rss feed to my blog and my answer to that question is "yes I do now". It was not too hard to add RSS functionality to my blog, as all I needed to do was add a few more additional functions to this specific blog post and then publish it.

This function takes the cleaned post content and extracts the first 3 sentences, truncating to 200 characters if needed.

(defun my/extract-description (content)
  "Extract first few sentences from post content for RSS description."
  (let* ((sentences (split-string content "[.!?]" t))
         (first-sentences (cl-subseq sentences 0 (min 3 (length sentences))))
         (description (string-join first-sentences ". ")))
    (if (> (length description) 200)
        (concat (substring description 0 197) "...")
      (concat description "."))))

This function extracts the title and date from org headers, finds where the actual content starts (either after the first heading or after the title), and then extracts clean content while skipping org-specific syntax like properties drawers, directives, and headings.

(defun my/get-post-metadata (file-path)
  "Extract title, date and clean content from org file."
  (with-temp-buffer
    (insert-file-contents file-path)
    (let ((title nil)
          (date nil)
          (content-start nil)
          (clean-content ""))
      (goto-char (point-min))
      (when (re-search-forward "^#\\+TITLE:\\s-+\\(.+\\)$" nil t)
        (setq title (match-string 1)))
      (goto-char (point-min))
      (when (re-search-forward "^#\\+DATE:\\s-+\\(.+\\)$" nil t)
        (setq date (match-string 1)))

      (cond
       ((re-search-forward "^\\*+\\s-+" nil t)
        (forward-line 1)
        (setq content-start (point)))
       ((re-search-forward "^#\\+title:" nil t)
        (forward-line 1)
        (setq content-start (point)))
       (t
        (setq content-start (point-min))))

      (let ((in-properties nil))
        (when content-start
          (goto-char content-start)
          (while (not (eobp))
            (let ((line (buffer-substring-no-properties (point-at-bol) (point-at-eol))))
              (cond
               ((string-match-p "^\\s-*:PROPERTIES:" line)
                (setq in-properties t))
               ((string-match-p "^\\s-*:END:" line)
                (setq in-properties nil))
               ((and (not in-properties)
                     (not (string-match-p "^#\\+" line))
                     (not (string-match-p "^\\s-*$" line))
                     (not (string-match-p "^\\*" line)))
                (setq clean-content (concat clean-content line " ")))))
            (forward-line 1))
          (setq clean-content (replace-regexp-in-string "\\s-+" " " clean-content))
          (setq clean-content (string-trim clean-content))))

      (list :title title :date date :file file-path :content clean-content))))

This function processes all org files in the posts directory, extracts their metadata and content, and generates a valid RSS 2.0 XML feed with proper formatting and auto-discovery links.

(defun my/generate-rss-feed ()
  "Generate RSS feed from all posts."
  (let* ((posts-dir "./posts")
         (rss-file "./public/feed.xml")
         (base-url "https://hamzahamud.com")
         (posts (directory-files posts-dir t "\\.org$"))
         (rss-items nil))

    (dolist (post posts)
      (unless (string-match-p "posts.org" post)
        (let* ((metadata (my/get-post-metadata post))
               (title (plist-get metadata :title))
               (date (plist-get metadata :date))
               (content (plist-get metadata :content))
               (filename (file-name-base post))
               (url (concat base-url "/posts/" filename ".html")))

          (when (and title content)
            (push (list :title title
                       :date (or date (format-time-string "%Y-%m-%d"))
                       :url url
                       :description (my/extract-description content))
                  rss-items)))))

    (with-temp-buffer
      (insert "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
      (insert "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n")
      (insert "<channel>\n")
      (insert "<title>Hamza Hamud</title>\n")
      (insert "<link>" base-url "</link>\n")
      (insert "<description>Software Developer | GNU Emacs Hacker | Hobbiest</description>\n")
      (insert "<language>en-us</language>\n")
      (insert "<lastBuildDate>" (format-time-string "%a, %d %b %Y %H:%M:%S %z") "</lastBuildDate>\n")
      (insert "<atom:link href=\"" base-url "/feed.xml\" rel=\"self\" type=\"application/rss+xml\" />\n")

      (dolist (item (reverse rss-items))
        (insert "<item>\n")
        (insert "<title>" (plist-get item :title) "</title>\n")
        (insert "<link>" (plist-get item :url) "</link>\n")
        (insert "<guid isPermaLink=\"true\">" (plist-get item :url) "</guid>\n")
        (insert "<description><![CDATA[" (plist-get item :description) "]]></description>\n")
        (insert "<pubDate>" (plist-get item :date) "</pubDate>\n")
        (insert "</item>\n"))

      (insert "</channel>\n")
      (insert "</rss>\n")

      (write-region (point-min) (point-max) rss-file))))

This wrapper function ensures that the RSS feed is regenerated every time a file is published, keeping the feed up to date with the latest posts.

(defun my/publish-with-rss (plist filename pub-dir)
  "Publish file and regenerate RSS feed."
  (my/org-publish plist filename pub-dir)
  (my/generate-rss-feed))

This configuration defines the publishing projects for posts, pages, and static assets, using the RSS-aware publishing function to ensure feeds are updated automatically.

(setq org-publish-project-alist
      (list

       (list "posts"
             :recursive nil
             :base-directory "./posts"
             :publishing-function 'my/publish-with-rss
             :publishing-directory "./public/posts"
             :with-email nil
             :auto-sitemap t
             :sitemap-title "posts"
             :sitemap-filename "posts.org"
             :with-creator t
             :with-author nil
             :with-toc t
             :section-numbers nil
             :time-stamp-file nil)
       (list "pages"
             :recursive nil
             :exclude "README.org"
             :base-directory "./"
             :publishing-function 'my/publish-with-rss
             :publishing-directory "./public/"
             :with-email nil
             :with-creator t
             :with-author nil
             :with-toc t
             :section-numbers nil
             :time-stamp-file nil)

       (list "static"
         :base-directory "./"
         :base-extension "css\\|txt\\|jpg\\|gif\\|png\\|js"
         :recursive t
         :publishing-directory  "./public"
         :publishing-function 'org-publish-attachment)

       (list "site" :components (list "pages" "static" "posts"))
))

This final step publishes all content and generates the RSS feed to ensure everything is up to date.

(org-publish-all t)
(my/generate-rss-feed)

Emacs 30.2 (Org mode 9.7.11)