Files
org_web/setup.org

15 KiB

Setup

Introduction

This page will highlight the build-script.el used to generate this website. This entire website is generated through org files using the command:

emacs -Q --script build-site.el

The script

The script is as follows, at the start I have some metadata relating to the file, followed by package management,then the declaration of variables/functions and finally the org-publish-project-alist which handles nearly all of the project generation instructions.

The following code snippet will probably change often, as I can never seem to stick to a single configuration 😀

  ;;; build-site.el --- Publish my website using org-publish -*- lexical-binding: t; -*-

  ;; Author: Zaine Qayyum <zaineulabideen@outlook.com>
  ;; Created: 2025-08-08
  ;; Purpose: Build and publish my static site from Org files.

  ;;; Commentary:
  ;; Run this file with:
  ;; emacs -Q --script build-site.el

  ;;; Code:

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

  (require 'ox-publish)
  (require 'cl-lib)
  (require 'org)

  ;; Initialize the package system
  (package-initialize)
  (unless package-archive-contents
    (package-refresh-contents))

  ;; Install dependencies
  (package-install 'htmlize)

  ;; Load the publishing system
  (setq org-html-htmlize-output-type 'css)
  (setq org-html-head-include-default-style nil)

  (defvar z-shared-head
        "
  <link rel=\"stylesheet\" href=\"https://gongzhitaao.org/orgcss/org.css\" /> <link rel=\"stylesheet\" href=\"/assets/style.css\" /> <script src=\"/assets/script.js\" defer></script> ")

  (defvar z-preamble
    "
  <div class=\"banner-header\">
    <a href=\"/\"> <img src=\"/assets/gr.png\" alt=\"Site Logo\" class=\"banner-logo\" /> </a>
    <nav>
      <a href=\"/\">Home | </a>
      <a href=\"/posts/posts-list.html\">Posts | </a>
      <a href=\"/blogs/blogs-list.html\">Blogs | </a>
      <a href=\"/contact.html\">Contact</a>
    </nav>
    </div>
    <div id=\"updated\">Updated: %C</div>"
  )

  (defvar z-postamble
    "<footer>
  <div class=\"copyright-container\">
  <div class=\"copyright\">
  Copyright &copy; 2022-2025 Zaine Qayyum. All rights reserved unless otherwise noted.</div></div>
  <div class=\"generated\">
  Created with %c on <a href=\"https://www.archlinux.org/\">Arch</a> <a href=\"https://www.gnu.org\">GNU</a>/<a href=\"https://www.kernel.org/\">Linux</a>
  </div>
  </footer>")



  (defun z/posts-sitemap (title list)
    "sitemap that lists post links as bullet points with dates and tags."
    (concat
     "#+TITLE: " title "\n"
     "#+OPTIONS: toc:nil num:nil \n\n"
     "See the categories: @@html:<a href=\"../categories.html\">Categories</a>@@\n\n"
     "* Posts:\n"
     (mapconcat
      (lambda (entry)
        (let* ((link (car entry))
               ;; extract relative file name from the link
               (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link)
                             (match-string 1 link)
                           link))
               (full-path (expand-file-name filename "~/master-folder/org_files/org_web/posts/"))
               (date-str "no date")
               (tags-str ""))
          ;; Get publish date
          (let ((date (org-publish-find-date full-path org-publish-project-alist)))
            (when date
              (setq date-str (format-time-string "%Y-%m-%d" date))))
          ;; Get FILETAGS from file buffer
          (when (file-exists-p full-path)
            (with-temp-buffer
              (insert-file-contents full-path)
              (org-mode)
              (let* ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS"))))))
                (when tags
                  (setq tags-str (mapconcat (lambda (tag)
                                              (format "@@html:<span class=\"post-tag\">%s</span>@@" tag))
                                            (split-string tags ":" t)  ;; <- Splits by ":" and removes empty strings
                                            " "))))))
          ;; Final line output
          (format "- %s @@html:<span class=\"post-date\">%s</span>@@ %s" link date-str tags-str)))
      (cdr list)
      "\n")))


  (defun z/blogs-sitemap (title list)
    "sitemap that lists blog links as bullet points with dates and tags."
    (concat
     "#+TITLE: " title "\n"
     "#+OPTIONS: toc:nil num:nil \n\n"
     "See the categories: @@html:<a href=\"../categories.html\">Categories</a>@@\n\n"
     "* Blogs:\n"
     (mapconcat
      (lambda (entry)
        (let* ((link (car entry))
               ;; extract relative file name from the link
               (filename (if (string-match "\\[\\[file:\\([^]]+\\)\\]" link)
                             (match-string 1 link)
                           link))
               (full-path (expand-file-name filename "~/master-folder/org_files/org_web/blogs/"))
               (date-str "no date")
               (tags-str ""))
          ;; Get publish date
          (let ((date (org-publish-find-date full-path org-publish-project-alist)))
            (when date
              (setq date-str (format-time-string "%Y-%m-%d" date))))
          ;; Get FILETAGS from file buffer
          (when (file-exists-p full-path)
            (with-temp-buffer
              (insert-file-contents full-path)
              (org-mode)
              (let* ((tags (cadr (assoc "FILETAGS" (org-collect-keywords '("FILETAGS"))))))
                (when tags
                  (setq tags-str (mapconcat (lambda (tag)
                                              (format "@@html:<span class=\"post-tag\">%s</span>@@" tag))
                                            (split-string tags ":" t)  ;; <- Splits by ":" and removes empty strings
                                            " "))))))
          ;; Final line output
          (format "- %s @@html:<span class=\"post-date\">%s</span>@@ %s" link date-str tags-str)))
      (cdr list)
      "\n")))



  (defun z/categories-sitemap (title _list)
    "Generate a categories page by scanning tags across org-posts and org-blogs."
    (let* ((expanded (org-publish-expand-projects org-publish-project-alist))
           (posts    (assoc "org-posts" expanded))
           (blogs    (assoc "org-blogs" expanded))
           ;; DO NOT set :exclude to "" — it excludes everything
           (files (cl-remove-duplicates
                   (append (and posts (org-publish-get-base-files posts))
                           (and blogs (org-publish-get-base-files blogs)))
                   :test #'file-equal-p))
           ;; optional: drop the generated sitemaps
           (files (cl-remove-if
                   (lambda (f)
                     (member (file-name-nondirectory f)
                             '("posts-list.org" "blogs-list.org" "sitemap.org" "categories.org")))
                   files))
           (counts (make-hash-table :test 'equal)))
      ;;(message "files I will scan: %S" files)
      (dolist (f files)
        (when (file-readable-p f)
          (with-temp-buffer
            (insert-file-contents f)
            (org-mode)
            (let* ((kw (org-collect-keywords '("FILETAGS" "TAGS")))
                   (raw (car (or (cdr (assoc "FILETAGS" kw))
                                 (cdr (assoc "TAGS" kw))))))
              (when raw
                (dolist (tag (split-string raw ":" t))
                  (puthash tag (1+ (gethash tag counts 0)) counts)))))))

      (let (tags)
        (maphash (lambda (k _) (push k tags)) counts)
        (setq tags (sort tags #'string-lessp))
        (concat
         "#+TITLE: " title "\n#+OPTIONS: toc:nil num:nil title:nil\n\n* Categories (Includes both blogs and posts)\n"
         (if tags
  	   (mapconcat
  	    (lambda (tag)
  	      (format "- [[file:tags/%s.org][@@html:<span class=\"post-tag\">%s</span>@@]] (%d)"
  		      (z/tag-slug tag) tag (gethash tag counts))
  	      )
  	    tags
  	    "\n")
           "_No tags found yet._")))))


  (defun z/tag-slug (s)
    "Turn a tag into a safe filename."
    (let ((down (downcase s)))
      (replace-regexp-in-string "[^a-z0-9]+" "-" down)))

  (defun z/collect-post-files ()
    "Return all .org files from org-posts and org-blogs."
    (let* ((expanded (org-publish-expand-projects org-publish-project-alist))
           (posts    (assoc "org-posts" expanded))
           (blogs    (assoc "org-blogs" expanded)))
      (cl-remove-duplicates
       (append (and posts (org-publish-get-base-files posts))
               (and blogs (org-publish-get-base-files blogs)))
       :test #'file-equal-p)))

  (defun z/gather-tag-index ()
    "Return hash: tag -> list of (FILE TITLE DATE-ISO)."
    (let ((idx (make-hash-table :test 'equal)))
      (dolist (f (z/collect-post-files))
        (when (and (string-match-p "\\.org\\'" f)
                   (file-readable-p f)
                   ;; ignore generated lists
                   (not (member (file-name-nondirectory f)
                                '("posts-list.org" "blogs-list.org" "sitemap.org" "categories.org"))))
          (with-temp-buffer
            (insert-file-contents f)
            (org-mode)
            (let* ((kw (org-collect-keywords '("TITLE" "FILETAGS" "TAGS" "DATE")))
                   (title (or (car (cdr (assoc "TITLE" kw)))
                              (file-name-base f)))
                   (date  (or (car (cdr (assoc "DATE" kw))) "")) ;; optional
                   (raw   (car (or (cdr (assoc "FILETAGS" kw))
                                   (cdr (assoc "TAGS" kw))))))
              (when raw
                (dolist (tag (split-string raw ":" t))
                  (push (list f title date) (gethash tag idx))))))))
      idx))

  (defun z/write-tag-pages ()
    "Generate tags/*.org pages listing posts for each tag."
    (let* ((site-root (expand-file-name "~/master-folder/org_files/org_web/"))
           (tags-dir  (expand-file-name "tags" site-root)))
      (unless (file-directory-p tags-dir)
        (make-directory tags-dir t))
      (let ((idx (z/gather-tag-index)))
        (maphash
         (lambda (tag items)
           (let* ((slug (z/tag-slug tag))
                  (outfile (expand-file-name (format "%s.org" slug) tags-dir)))
             (with-temp-file outfile
               (insert (format "#+TITLE: Tag: %s\n#+OPTIONS: toc:nil num:nil title:nil \n\n* Posts tagged %s\n"
                               tag tag))
               ;; sort newest first if DATE present
               (setq items (sort items (lambda (a b) (string> (nth 2 a) (nth 2 b)))))
               (dolist (it items)
                 (let* ((file (nth 0 it))
                        (title (nth 1 it))
                        (rel   (file-relative-name file tags-dir)))
                   ;; link to the source .org; org-publish will rewrite to the .html
                   (insert (format "- [[file:%s][%s]]\n" rel title)))))))
         idx)
        )))


  ;; Define the publishing project
  (setq org-publish-project-alist
        `(("org-main"
           :recursive t
           :base-directory "~/master-folder/org_files/org_web/"
           :publishing-function org-html-publish-to-html
           :publishing-directory "~/master-folder/org_files/org_web/output"
           :with-author nil
           :with-creator t
           :with-toc t
           :base-extension "org"
           :section-numbers nil
           :time-stamp-file nil
  	 :auto-sitemap t
  	 :sitemap-filename "sitemap.org"
  	 :sitemap-title "Sitemap"
  	 :sitemap-sort-files chronologically
           :html-preamble ,z-preamble
           :html-postamble ,z-postamble
  	 :html-head ,z-shared-head)
  	("org-categories-sitemap"
           :recursive t
           :base-directory "~/master-folder/org_files/org_web/"
           :publishing-directory "~/master-folder/org_files/org_web/output"
           :base-extension "org"
  	 :auto-sitemap t
  	 :sitemap-filename "categories.org"
  	 :sitemap-title "Categories"
  	 :sitemap-function z/categories-sitemap
           :html-preamble ,z-preamble
           :html-postamble ,z-postamble
  	 :html-head ,z-shared-head)
          ("org-posts"
           :base-directory "~/master-folder/org_files/org_web/posts"
           :publishing-directory "~/master-folder/org_files/org_web/output/posts/"
           :recursive t
           :base-extension "org"
           :publishing-function org-html-publish-to-html
           :with-author nil
           :with-creator nil
           :html-validation-link nil
           :with-toc t
           :section-numbers t
           :html-preamble ,z-preamble
           :html-postamble ,z-postamble
  	 :auto-sitemap t
  	 :sitemap-filename "posts-list.org"
  	 :sitemap-title "Posts List"
  	 :sitemap-style list
  	 :sitemap-function z/posts-sitemap
  	 :html-head ,z-shared-head)
  	("org-blogs"
           :base-directory "~/master-folder/org_files/org_web/blogs"
           :publishing-directory "~/master-folder/org_files/org_web/output/blogs/"
           :recursive t
           :base-extension "org"
           :publishing-function org-html-publish-to-html
           :with-author nil
           :with-creator nil
           :html-validation-link nil
           :html-preamble ,z-preamble
           :html-postamble ,z-postamble
  	 :auto-sitemap t
  	 :sitemap-filename "blogs-list.org"
  	 :sitemap-title "Blogs List"
  	 :sitemap-style list
  	 :sitemap-function z/blogs-sitemap
  	 :html-head ,z-shared-head)
  	("org-tags"
  	 :base-directory "~/master-folder/org_files/org_web/tags"
  	 :publishing-directory "~/master-folder/org_files/org_web/output/tags"
  	 :recursive t
  	 :base-extension "org"
  	 :publishing-function org-html-publish-to-html
  	 :with-author nil
  	 :with-creator nil
  	 :html-preamble ,z-preamble
  	 :html-postamble ,z-postamble
  	 :html-head ,z-shared-head)
  	("org-assets"
           :base-directory "~/master-folder/org_files/org_web/assets/"
           :base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg\\|pdf\\|woff\\|woff2\\|ttf"
           :publishing-directory "~/master-folder/org_files/org_web/output/assets/"
           :recursive t
           :publishing-function org-publish-attachment)
  	))

  (delete-directory "~/master-folder/org_files/org_web/output/" t)
  (delete-directory "~/master-folder/org_files/org_web/tags/" t)
  (message "Directory deleted")

  ;; Generate the site output
  (z/write-tag-pages)
  (org-publish-all t)

  (message "Build complete!")

  ;;; build-site.el ends here

Publish Script

As the project is hosted on Github, I have created a small script that is able to push changes to the remote repository, which Cloudflare will automatically detect and rebuild the website:

  #!/bin/bash

  # Exit immediately if any command fails
  set -e

  # Define paths
  ORG_PROJECT_NAME="website"
  ORG_OUTPUT_DIR="$HOME/master-folder/org_files/org_web/output"

  # commands used prior to having the `build-script.el`
  # echo "Running org-publish..."
  # emacs --batch \
  #       --eval "(require 'org)" \
  #       --eval "(require 'ox-publish)" \
  #       --eval "(org-publish \"$ORG_PROJECT_NAME\" t)"

  cd "$ORG_OUTPUT_DIR"

  echo "HTML published to: $ORG_OUTPUT_DIR"

  # Git commands
  echo "Adding changes to Git..."
  git add .

  echo "Committing..."
  git commit -m "Auto-publish on $(date)" || echo "Nothing to commit."

  echo "Pushing to GitHub..."
  git push origin main

  echo "Done! Changes pushed and Cloudflare should rebuild the site."