Adam Richardson's Site

Org Publish Notes

Table of Contents

<2023-10-28 Sat>

Publish Script

CSS

.org-doc { color: #7E85A0; }
.org-keyword { color: #E63462; }
.org-string { color: #C7EFCF; }
.org-variable-name { color: #F8FBEF; font-weight: bold; }
.org-function-name { color: #A8A6F2; }
.org-constant { color: #F6B6C7; }
.org-comment { color: #7E85A0; }
.org-type { color: #FE5F55; font-weight: bold; }
.org-warning { color: #C81947; }
.timestamp { color: #1A1C23; font-size: smaller; }
.timestamp { text-decoration: underline; }
.timestamp:before { content: "Last Updated: "; }

table {
    margin: auto;
}

body {
    color: #1A1C23;
    background-color:  #f8fbef;
    font-family:  "Liberation Sans", Helvetica, "Trebuchet MS", sans-serif;
    margin: auto;
    max-width: 72em;
    padding: 0 1em 0 1em;
}

pre {
    background-color: #333745;
    color:  #f8fbef;
}

a {
    font-weight: bold;
    padding-right: .25em;
}

a:visited {
    color: black;
}

a:link {
    color: black;
}

ul {
    margin-top: 1em;
    padding-left: 1em;
}

li {
    margin-bottom: 1em;
}

#preamble {
    text-align: right;
    margin-left: auto;
    margin-bottom: 2em;
    max-width: 16em;
}

#preamble h4 {
    margin-bottom: 0.2em;
    padding-bottom: 0.2em;
    border-bottom: solid;
}

#postamble {
    max-width: 13.5em;
    margin-left: auto;
    margin-top: 4em;
    font-size: 0.7em;
}

img {
    max-width: 100%;
}

#content {
    max-width: 48em;
}

@media only screen and (min-width: 1450px) {
    body {
        font-size: 1.25em;
    }

    p {
        line-height: 1.5em;
    }

    #table-of-contents {
        position: fixed;
        left: 0;
        top: 0;
        font-size: 0.75em;
        padding: 1em;
        z-index: 1;
        display:flex;
        flex-direction: column;
        height: 100%;
        max-width: 14em;
        background-color: #f8fbef;
    }

    #table-of-contents h2 {
        font-size: 1em;
    }

    #text-table-of-contents, #table-of-contents:active #text-table-of-contents {
        display: block;
        overflow-y: auto;
        flex: 1;
    }

}

Use package

Packages

(require 'package)
(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)

(require 'use-package)

(use-package ledger-mode :ensure t)
(use-package lua-mode :ensure t)
(use-package go-mode :ensure t)
(use-package htmlize :ensure t)

Ox Publish

Setup

Require Org Publish Features

(require 'ox-publish)

Syntax Highlighting

  • To get syntax highlighting for source code blocks I set the htmlize output type to CSS
(setq org-html-htmlize-output-type 'css)

Disable Heading Numbers

(setq org-export-with-section-numbers nil)

Table of Contents Heading Depth

(setq org-export-with-toc 1)

Link to Custom CSS

  • I also create a head extra that includes the custom CSS
(defvar ajr-html-head-extra "\n<link rel='stylesheet' href='/css/main.css' />\n")

Nav Bar HTML Generation

  • Wrote a few functions that take a list of cons pairs and generate an html nav bar
  • The first element in the cons pair is the URL the second is the title
(defun ajr-nav (items)
  (let ((atags (apply #'concat
                      (mapcar
                       (lambda (item)
                         (concat "  "
                                 (ajr-nav-item
                                  (car item)
                                  (cdr item))
                                 "\n"))

                       items))))
    (concat
     "<h4>Adam Richardson's Site</h4>\n"
     "<nav>\n"
     atags
     "</nav>\n")))

(defun ajr-nav-item (url title)
  (concat
   "<a href=\"" url "\">" title "</a>"))

Nav Bar Items

  • I created variables for each nav bar item so they can be reused across multiple navs
(defvar ajr-nav-blog
  '("/" . "Blog"))

(defvar ajr-nav-notes
  '("/notes/index.html" . "Notes"))

(defvar ajr-nav-about
  '("/about.html" . "About"))

(defvar ajr-nav-rss
  '("/rss.xml" . "RSS"))

Defining Preamble Variables

  • The nav bars are going to be added to each page as html-preamble
  • This section of code creates variables that represent different nav bars for different sections of the published site
(defvar ajr-html-preamble
      (ajr-nav
       (list ajr-nav-blog
             ajr-nav-notes
             ajr-nav-about
             ajr-nav-rss)))

Defining Postamble Format

(defvar ajr-html-postamble "
<p class=\"author\">Author: %a</p>
<p class=\"date\">Date: %d</p>")

Publish Project alist

Posts

(list "org-site"
      :recursive t
      :base-directory "./"
      :exclude "notes\\|about"
      :publishing-directory "./public"
      :with-author "Adam Richardson"
      :with-email nil
      :auto-sitemap t
      :sitemap-title "Blog Posts"
      :sitemap-sort-folders 'ignore
      :sitemap-sort-files 'anti-chronologically
      :sitemap-filename "index.org"
      :sitemap-format-entry (lambda (file-or-dir style project)
                              (if (equal file-or-dir "posts/")
                                  "**Welcome to my personal blog**"
                                (concat
                                 (format-time-string
                                  "%Y-%m-%d"
                                  (org-publish-find-date
                                   file-or-dir project))
                                 ": [["
                                 (concat "file:" file-or-dir)
                                 "]["
                                 (org-publish-find-title
                                  file-or-dir project)
                                 "]]")))
      :html-head-extra ajr-html-head-extra
      :html-preamble-format `(("en" ,ajr-html-preamble))
      :html-preamble t
      :html-postamble-format `(("en" ,ajr-html-postamble))
      :html-postamble nil
      :html-validation-link nil
      :publishing-function 'org-html-publish-to-html)

Notes

(list "org-site"
      :recursive t
      :base-directory "./notes"
      :exclude "posts/"
      :publishing-directory "./public/notes"
      :auto-sitemap t
      :sitemap-title "Notes"
      :sitemap-sort-files 'alphabetically
      :sitemap-filename "index.org"
      :html-head-extra ajr-html-head-extra
      :html-preamble-format `(("en" ,ajr-html-preamble))
      :html-preamble t
      :html-postamble nil
      :html-validation-link nil
      :publishing-function 'org-html-publish-to-html)

Top Level

(list "org-site"
      :recursive nil
      :base-directory "./"
      :publishing-directory "./public/"
      :html-head-extra ajr-html-head-extra
      :html-preamble-format `(("en" ,ajr-html-preamble))
      :html-preamble t
      :html-postamble nil
      :html-validation-link nil
      :publishing-function 'org-html-publish-to-html)

CSS

(list "org-static"
      :recursive t
      :base-directory "./css"
      :base-extension "css"
      :publishing-directory "./public/css"
      :publishing-function 'org-publish-attachment)

Assets

(list "org-static"
      :recursive t
      :base-directory "./"
      :base-extension "png\\|gif\\|jpg\\|jpeg\\|svg\\|webm\\|webp"
      :publishing-directory "./public/"
      :publishing-function 'org-publish-attachment)

Root Level Static Files

(list "org-static"
      :recursive t
      :base-directory "./"
      :base-extension "txt"
      :publishing-directory "./public/"
      :publishing-function 'org-publish-attachment)

Static HTML

(list "org-static"
      :recursive t
      :base-directory "./static-html"
      :base-extension "html\\|js\\|j5"
      :publishing-directory "./public/static-html"
      :publishing-function 'org-publish-attachment)

Actually Publishing

(org-publish-all t)

(message "Build Complete")

Generate RSS

  • This series of Elisp will generate an rss.xml file in the public directory

Dependencies

(require 'org)

Constants

  • These constants define the location of published html posts as well as the path to the org files for those posts
(defconst posts-html-dir "public/posts")
(defconst posts-org-dir "posts")
(defconst post-url-root "https://thales17.srht.site")
(defconst channel-title "Adam Richardson's Site")
(defconst channel-description "RSS feed of blog posts from Adam Richardson's site")
(defconst rss-file "public/rss.xml")
(defconst rfc-822-format "%a, %d %b %y %H:%M:%S %z")

Get RSS Link for a Post

(defun post--link (post-file)
  "Gets the RSS link for a post html file. The file is assumed to be
just the name of the html file. No path is necessary."
  (concat
   post-url-root
   "/posts/"
   post-file))

Get the Org Path for a Post

(defun post--org-path (post-file)
  "Returns the relative path to the org file that corresponds to the
html file."
  (concat
   posts-org-dir
   "/"
   (file-name-sans-extension post-file)
   ".org"))

Get the Date / Title / Description for a Post

(defun post--keyword-values (post-org-file)
  "Returns a list of keywords from the post org file. This looks for
`date', `title' and `description' keyword values from the
`post-org-file'.  The format of the results list will match what
`org-collect-keywords' returns."
  (with-temp-buffer
    (insert-file-contents post-org-file)
    (org-collect-keywords '("date" "title" "description"))))

Get the RSS Item Tag for a Post

(defun post--rss-item (post-file)
  "Returns the rss xml formatted item for a post html file. The
`post-file' should just be the name of the file and not have any
directory pathing."
  (let* ((keyword-alist (post--keyword-values
                         (post--org-path post-file)))
         (title (cadr (assoc "TITLE" keyword-alist)))
         (org-date (org-timestamp-from-string
                    (cadr (assoc "DATE" keyword-alist))))
         (description (cadr (assoc "DESCRIPTION" keyword-alist))))
    (concat
     "    <item>\n"
     "      <title>"
     title
     "</title>\n"
     "      <link>"
     (post--link post-file)
     "</link>\n"
     "      <pubDate>"
     (org-timestamp-format org-date rfc-822-format)
     "</pubDate>\n"
     "      <description>"
     description
     "</description>\n"
     "    </item>\n")))

Get the Emacs Internal Time for A Post

(defun post--date (post-file)
  "Returns the date property set in the corresponding org file for the
`post-file'. The `post-file' is assumed to be an html file in the
`posts-html-dir'."
  (let* ((keyword-alist (post--keyword-values
                         (post--org-path post-file)))
         (org-date (cadr (assoc "DATE" keyword-alist))))

    (org-timestamp-to-time
     (org-timestamp-from-string org-date))))

Generate the Full RSS XML

(defun post-gen-rss ()
  (let ((sorted-posts (sort (directory-files
                             posts-html-dir
                             nil
                             "\\.html$")
                            (lambda (a b)
                              (let ((a-date (post--date a))
                                    (b-date (post--date b)))
                                (time-less-p b-date a-date))))))
    (concat
     "<rss version=\"2.0\">\n"
     "  <channel>\n"
     "    <title>"
     channel-title
     "</title>\n"
     "    <description>"
     channel-description
     "</description>\n"
     (apply #'concat
            (mapcar #'post--rss-item sorted-posts))
     "  </channel>\n"
     "</rss>")))

Save the Contents of a String to File

(defun save-file (contents filename)
  (with-temp-buffer
    (insert contents)
    (write-file filename)))

Actually Generate RSS

(save-file (post-gen-rss) rss-file)