Site Generation With Org Mode

How is this blog created? Why were the tools used selected? Join in for a journey through the progress of this site, and it’s future direction, gaining knowledge along the way as to how all this is possible without too much complexity.

I wanted a blog

In the past, I’ve tried, and failed, at blogging. I’ve in general failed at creating and maintaining a site as well.

Maintaining a site, or a blog, is difficult, and the tools available increase the difficulty in general (in my opinion). I’m sure the tools are great for a lot of people but for myself, they just continued to be found lacking.

So what’s so difficult about other systems:

  • Context switches

    If you’re a programmer, or someone that uses a sufficiently advanced text editor, be it Vim, Emacs or otherwise, you might find that a lot of blog systems, or a CMS, or even some form of static site generation or manual HTML creation unwieldy as you try to get your thoughts out to a textual document.

    • CMS/Blog systems

      Starting with addressing CMS and Blog systems, such as Wordpress, the typical issue by and large is that the editor is exceedingly awful. The control sequences to select textual styles or insert stuff from elsewhere most certainly differ from a programmer’s editor wherein they just write the markup directly. Let’s assume that you use a Vim-alike though. Having to navigate through the site’s text editor with mouse clicks, arrow keys, and poor selection/modification/search/replace mechanics will soon get annoying. All of the ways in which the CMS/Blog system’s editor hinders you causes frustration and context switches. The context switch is where you move away from writing your thoughts towards managing the idiosynchracies of the awful editor for a moment. Now your flow of thought has been interrupted. An answer may be to edit the post in your favorite editor, then copy/paste into the CMS/Blog system. This will work but ends up being a lot of work. You’ve got placeholder text to resolve once you paste in for instance. All of a sudden you’ll have the text written, data available, and lack the motivation to actually post it all because you have to deal with the CMS/Blog system’s way of handling things. You begin to realize, you can do it, but the system is hindering you. Wait until you must deal with their setup too.

      Years and years ago, and more recently, I tried things with Wordpress and Serendipity. While they are wonderful as a way to get someone’s foot in the door, I never did much besides a “hey, this is a blog” post, all because they inflicted such tedium on me. I’m not saying they are awful, or shouldn’t be used, but I do know they seem to be in my way instead of helping me press forward.

    • Static Site Generators

      Wouldn’t it be nice if you could write in a link, in a general markup fashion? Maybe a few keypresses and it’ll let you pick a file/image/whatnot and then type up a description. What about alternative text for the visually impaired? Wait, why does this markup work on some odd site like GitHub but fails with this static site generator. The organization scheme doesn’t fit my mental model and increases web server configuration complexity too (possibly)! Enter the world of static site generators, where you need to know the intricacies in what it expects and how it works with little room for extending or configuring it to act in the simplest way that achieves the results you want without maintaining your own fork of the static site generator itself. The context switches here deal with complying with the way it works, versus making it work the way you want. Sure there are some points of configuration possible, but flexibility is not available beyond a certain degree. What about those links though? What about inlining images? In general, you’re likely to lose your train of thought, as you end up having to figure out how the generator does certain things, and linking is probably the biggest one. Sure you can use placeholders and come back to it later but why? If you want to link within your own site, having a barrier to ease of doing so leaves you in a similar situation as with the CMS/Blog system’s issue where your motivation gets cut as the system hinders you by making you come back to it later. For external links that’s not a big deal as you are definitely going to need to swap from your editor over to your browser, but within your site itself, being hindered and interrupting the thought flow can be the difference between completing the post today, or tomorrow, or never.

      I’ve done this once, and the setup, configuration, and discovery of these things were luckily hidden from me by using an Org Mode pre-processor basically. I looked at the generated markdown though and realized I’d hate to have had to write it myself. Some people love these systems, and they are likely the right fit for some, but for me, again, the tedium and hindrance of using them was a barrier to blogging in general.

    • Failures of Markdown

      How do you link something? Is it ()[] or []() and does the description or link go in which of the “brackets”. What is the spec you use for an internal link? How many different ways should you be able to do headlines? How deep can the headlines be? What about lists, unordered or ordered, and their tree depth? What if those are controlled by the markdown parser or the system responsible for the markdown conversion? Why must code be indented 4 spaces (or was it 2)? Is it 2 spaces to increase tree depth in a list? Or is it 4? Or is it a matter of pick one and use it consistently? How do you do tables? Is it even standardized amongst parsers? I want my code syntax highlighted too, but how?

      The major issue with markdown isn’t that it’s awful, it’s that every system uses their own flavor to bandaid over the fact that certain features that are necessary are not standardized.

      I’ve dealt with answering all the above questions, and more, and don’t want to have to ever do so again. I really didn’t enjoy it the first time I tried, I won’t try again if I can help it.

      Markdown seems ubiquitous, up till the point you remember that features you find necessary or enjoy are not necessarily standard amongst parsers or behave in different manners.

    • Manual HTML

      When I have a thought, I think of <p> and <span> as the normal flow therein no one said ever. Manual HTML editing can get you the results you want assuredly, but you will be hindered every time you need to think of in which way would I like to enclose this paragraph. It goes beyond that… try making a list and checking it twice for who’s been naughty or nice in plain ole HTML then try it with a piece of paper. How many names did you forget/skip in HTML? Did you quit because it was tedious? Editing this list as mostly text with minimal markup (no <li> or <ul> or so forth) is far closer to paper and you are less likely to skip, forget, or quit due to tedium. You could most definitely do a site or a blog in naught but HTML, but I doubt anyone who did would want to write very many posts.

  • Licensing

    Finding systems licensed in a way that is acceptable to your sensibilities can be difficult. I would find it hard to accept a non-copyfree license for a CMS/Blog system because of the way they hinder me. I’d be more lax if it enables me to nearly make my thoughts slower than my typing speed and doesn’t keep me from using the editor I choose, and doesn’t make me fill the document with placeholders in an effort to avoid context switching.

  • Setup

    There will always be setup. The key is how difficult or simple it is.

    A static site potentially is just a configuration for the generator itself and a web server configuration, and the setup of the generation tool. This is acceptable but might still be more than what one wants to deal with when one picks a generator that relies on some odd scripting language that then wants a thousand libraries.

    A generator that uses tools I already have present for other purposes, and is inherently flexible to the degree I can almost certainly make it do what I want rather than comply to how it wants things done, beats any other generator by a large degree. You still have a web server configuration to deal with of course, but you aren’t fighting against the generator to accomplish your needs whilst barely skirting compliance with it’s strict requirements.

    A CMS/Blog system increases complexity immensely on the setup side. Which server side language do you need to get up and running? What version? What database system (if it uses one)? You now need to configure those 3 things to integrate with each other. I’m sure a lot of people will point at this as easy, and it can be, as long as you’re okay with likely awful defaults. The way I handle security strictly guarantees I have to inspect the configuration of those things down to the most minute detail. Then you have to maintain these configurations, and check them when new versions of the software roll in. Keeping it simple and non-reliant on such software combinations reduces headache.

So what do I do since it sounds like nearly everything disqualifies itself. I apparently found something that fits my need with minimum pomp and circumstance one would think. I’m of course writing things in plain HTML. I joke.

Enter Org Mode. The publishing system of Org Mode is exceedingly flexible, I already use it for other things, and it requires no further software aside from that which I already have installed for Org Mode purposes since I already use Org Mode. Org Mode doesn’t force me to fit into it’s way of handling things either, due to the immense flexibility, so I simply have to deal with configuring it to act how I want once and then everything else is just writing posts. It’s extensible so if I need or want a certain feature which is not yet present, I can add it in with relative ease. It might seem barebones but I know every detail of how/why I generate the site the way I do, from the CSS, to the site navigation. I don’t have to figure out how to achieve what I want in the way they’ve determined for me either. If I found Org Mode’s publishing system too inflexible… I could very well just make my own. There are those who have done that as well.

This post will illustrate how I do what I do, and why I do some things certain ways. The source code is available as well.

Tools used for this site

  • DOOM Emacs

    Emacs is immensely configurable and flexible, and Org Mode is best done within Emacs. My site generation happens under a configuration that uses the DOOM Emacs configuration framework.

    My site publishing configuration is not self-contained, and relies on this to a degree. In general anything shown in the configuration and exporting code should work in the absence of DOOM but has not been tested. I’d suggest if you want to use the code I have available for site generation, that you either use it as a reference, or use DOOM as well. I’m happy to incorporate changes others make to make it work outside of DOOM so long as it remains operable under DOOM as well.

  • NGINX

    NGINX is what I use to serve the site up on the web. Configuration in NGINX is simpler than most other HTTP servers I’ve dealt with. Resource utilization is low vs others as well.

Using Emacs and Org Mode to Generate the Site

In general, you can do a lot without extensive customization just by way of CSS and defining the publishing projects carefully. That said, if you want some features, you have to be willing to in essence write them yourself. I did so, with the help of many reference Org Mode blogs.

In general, the code for this should be self explanatory to a large degree but we’ll discuss the major points regarding it.

I create a new backend, based upon 'ox-html that I called ’org-ssg which stands for static site generator. In general it redoes a lot of the defaults in 'ox-html to be inline with what I want for the site, which includes replacing quite a few transcoding functions and changing the default values for certain variables.

You could of course do this without a new backend, be it by careful configuration, inflexible defaults tied to the publishing project, or advising functions such that you need to remain careful that the advice does not effect except when wanted by the publishing project. In general, I chose to do a new backend to keep from dealing with inflexibility and function advisement.

You’ve likely seen I’ve mentioned publishing project a few times. In general, a publishing project is a configuration that references Org Mode documents and other files and determines how to transcode and where to place the results.

  • Transcoding Functions & Variables

    What’s a transcoding function? Org Mode has many element/section/block types and the job of a transcoder is to turn it from Org Mode syntax into something in another format. The transcoding functions I’ve wrote of course translate over to HTML.

    Some of the transcoding relies on the value of variables; let’s start with those.

    • Variables

      (defvar org-ssg-html-divs '((preamble "header" "nav")
                                  (content "main" "content")
                                  (postamble "footer" "footer"))
        "Default containers used for each section of the HTML document.")
      
      (defvar org-ssg-footnotes-section
        (concat "<section id=\"footnotes\">\n"
                "<h2 class=\"footnotes\">%s: </h2>\n"
                "<section id=\"text-footnotes\">\n%s\n</section>\n"
                "</section>")
        "The way the footnotes section will be formatted for CSS purposes.")
      
      (defvar org-ssg-container-element
        "section"
        "The default container element for content.")
      
      (defvar org-ssg-checkbox-type 'html
        "How to display checkboxes.")
      

      For the most part it’s self explanatory, but to review…

      We first define what the default should be for the 3 main sections generated in an HTML page by Org Mode’s export function. Since we are only doing HTML5, we set them to something semantically meaningful.

      Next, We define how footnotes should look, or rather, add classes/id’s to the HTML so that CSS can differentiate and set differing styles.

      The default container element is <div> but to be fancy, we use <section> which also has a more semantic meaning as well.

      Finally, when we have checkboxes like below, we show the HTML checkbox, rather than some text to indicate a checkbox.

      • An example checkbox.
      • An example checked checkbox.
    • Functions

      I’m only going to address functions that don’t contain a -- in them. To include the content of every function would lengthen the article unduly. I would instead point towards looking at the source code itself to see those function definitions. Even the functions without a -- in them might not be fully listed for the same reason here. Some functions are basically the same as builtins from 'ox-html minus a few changes and instead, where sensible, I’ll try to highlight the changes.

      (defun org-ssg-build-ssg-head (file project)
        "Add more elements into the head fromt he property `:ssg-head'.
      PROJECT is the project spec from the current `org-publish-project-alist'.
      FILE is the name of the current file being processed.
      
      TODO: Document the `:ssg-head' and `:ssg-project-root' properties."
        (let ((fills (org-ssg--build-attr file project)))
          (mapconcat (delq nil
                           (lambda (it)
                             (let* ((full-close (car it))
                                    (tag (cadr it))
                                    (attr (org-ssg--fill-attr (cdr (cdr it))
                                                              fills)))
                               (when attr
                                 (apply #'org-ssg--html-tag full-close
                                        tag
                                        attr)))))
                     (plist-get (cdr project) :ssg-head)
                     "\n")))
      

      This is a function that takes an :ssg-head specification and turns it into the head section in the HTML. It’s a list of head specs that allows for a simple definition of where is the CSS, links to various icons, meta properties, and anything else that might go in the head. We’ll see it in action later with the publishing project config.

      (defun org-ssg-timestamp (timestamp _contents info)
        "Transcode a TIMESTAMP object from Org to HTML.
      CONTENTS is nil.  INFO is a plist holding contextual
      information.
      
      This strips <>[] from the timestamp before rendering it to html
      as css should be used to give delimiters. When CSS is not used,
      it should remain human readable if not less pretty."
        (let ((value (org-html-plain-text
                      (org-timestamp-translate timestamp) info)))
          (format (concat "<span class=\"timestamp-wrapper\">\n"
                          "<span class=\"timestamp\">\n%s</span>\n"
                          "</span>")
                  (replace-regexp-in-string "&lt;\\|&gt;\\|\\]\\|\\[" ""
                                            (replace-regexp-in-string "--"
                                                                      "&#x2013;"
                                                                      value)))))
      

      Again, this one’s fairly self explanatory. We are relying on CSS to delimit the timestamp in a special manner instead of the Org Mode delimiters so we strip said delimiters out of the HTML element.

      (defun org-ssg-special-block (special-block contents info)
        "Transcode a SPECIAL-BLOCK element from Org to HTML.
      CONTENTS holds the contents of the block. INFO is a plist holding
      contextual information.
      
      This uses either the value of the block-type, or
      `org-html-container-element' as the container of the block, vs a
      forced usage of div as the default ox-html implementation
      provides."
        (let* ((block-type (org-element-property :type special-block))
               (html5-fancy (and (org-html--html5-fancy-p info)
                                 (member block-type org-html-html5-elements)))
               (attributes (org-export-read-attribute :attr_html special-block)))
          (unless html5-fancy
            (let ((class (plist-get attributes :class)))
              (setq attributes (plist-put attributes :class
                                          (if class (concat class " " block-type)
                                            block-type)))))
          (let* ((contents (or contents ""))
                 (reference (org-html--reference special-block info))
                 (a (org-html--make-attribute-string
                     (if (or (not reference) (plist-member attributes :id))
                         attributes
                       (plist-put attributes :id reference))))
                 (str (if (org-string-nw-p a) (concat " " a) ""))
                 (container-element (if html5-fancy
                                        block-type
                                      org-html-container-element)))
            (format "<%s%s>\n%s\n</%s>"
                    container-element
                    str
                    contents
                    container-element))))
      

      This kind of fixes what I believe is a poor default in 'ox-html. In general, we want the container element specified earlier in the variables to be used, not <div>. By default the built in special block code won’t use that value for the tag and instead uses the special block’s type as the tag if the type is in the list of HTML5 tags. We fix this to use the container element unless the block type is in the list of HTML5 tags, in which case use that.

      (defun org-ssg-drawer (drawer contents info)
        "Transcode a DRAWER element from Org to HTML.
      CONTENTS holds the contents of the block.  INFO is a plist
      holding contextual information.
      
      This generates a container that is classified in CSS as both
      drawer, and whatever the drawer's name is."
        (let ((contents (or contents ""))
              (drawer-name (org-element-property :drawer-name drawer))
              (drawer-function (plist-get info :html-format-drawer-function))
              (container (or (plist-get info :html-container)
                             org-html-container-element)))
          (if drawer-function
              (funcall drawer-function drawer contents)
            (format "<%s class=\"drawer %s\">\n%s</%s>"
                    container
                    (downcase drawer-name)
                    contents
                    container))))
      

      The drawer formatter builtin just plops the contents into the page, no container. This is I guess a sane and safe default but is underwhelming and not useful if a drawer needs some style. Here we make sure the contents are not nil and set up a container that sets the class to drawer and whatever the drawer’s name was so that CSS can style these appropriately if wanted.

    Next we get to the publishing functions, which deal with sitemaps (in our case the posts index basically) and converting Org Mode documents to HTML.

    First we start with the publishing function used in the publishing projects’ configuration.

    (defun org-ssg-publish-html (plist filename pub-dir)
      "Publish an org file to HTML.
    
    FILENAME is the filename of the Org file to be published.  PLIST
    is the property list for the given project.  PUB-DIR is the
    publishing directory.
    
    This does some preprocessing on the PLIST, before subbing out to
    essentially ox-html through the custom backend."
      (unless (and (plist-get plist :ssg-exclude-sitemap)
                   (string= (plist-get plist :sitemap-filename)
                            (file-name-nondirectory filename)))
        (let ((project (cons (car
                              (org-publish-get-project-from-filename filename))
                             plist)))
          (plist-put plist :html-head-extra
                     (org-ssg-build-ssg-head filename project))
          (org-publish-org-to 'org-ssg filename
                              (concat (when (> (length org-html-extension) 0) ".")
                                      (or (plist-get plist :html-extension)
                                          org-html-extension
                                          "html"))
                              plist pub-dir))))
    

    The job of this function is to take a filename, the project’s settings, and the directory to publish to and generate HTML from the file at filename. It first checks that we’re not excluding the sitemap from being published by this publishing project. The reason is that I use a separate project to publish the sitemap and not the project that generates the sitemap, as you’ll see later on. Next it uses a function to generate extra head elements and place them in the project’s settings. Finally, it uses Org Mode’s publishing framework to publish the HTML file to the correct location with the correct file name extension using the 'org-ssg backend that we’ve created. This function basically is the entry point to a lot of what makes 'org-ssg 'org-ssg.

    Next is how we generate the sitemap (or the posts index). This is a fairly blog specific function and likely should be replaced (via the project’s settings) or not used at all if not doing a blog.

    (defun org-ssg-sitemap-posts (title entries)
      "Populates an org document with the posts in ENTRIES.
    TITLE is the title for this sitemap."
      (let ((filtered (seq-filter (lambda (it)
                                    (or (symbolp it)
                                        (stringp (car it))))
                                  entries)))
        (concat "#+title: " title "\n"
                "#+OPTIONS: title:nil\n"
                "#+META_TYPE: website\n"
                "#+DESCRIPTION: " title " \n"
                "\n#+ATTR_HTML: :class sitemap\n\n"
                (org-list-to-subtree filtered))))
    
    (defun org-ssg-sitemap-posts-entry (entry _style project)
      "Format a sitemap entry.
    
    ENTRY is the file to which the posts sitemap will create a headline.
    PROJECT is the current operative project.
    
    Each entry will have their tags, publication date, and their
    preview included."
      (let ((preview (org-ssg-get-preview entry project))
            (draftp (member "draft" (org-publish-find-property entry
                                                               :filetags
                                                               project))))
        (unless draftp
          (format "[[file:%s][%s]] :%s:\n%s\n\n%s"
                  entry
                  (org-publish-find-title entry project)
                  (mapconcat 'identity
                             (org-publish-find-property entry :filetags project)
                             ":")
                  (concat ":Published:\n"
                          (format-time-string "posted on %m/%d/%Y"
                                              (org-publish-find-date entry
                                                                     project))
                          "\n:END:")
                  preview))))
    

    There are 2 functions involved in this endeavor.

    The first one (org-ssg-sitemap-posts) takes a list of sitemap entries, which are generated by org-ssg-sitemap-posts-entry and turns it into an org document. It takes care of setting the document’s title, as well as a few options for the document. It then just takes the entries and places them in the file. It skips nil entries which will make more sense below.

    The second one, org-ssg-sitemap-posts-entry is responsible for generating a link and the corresponding post data such as when it was published and what the preview is into a string that org-ssg-sitemap-posts understands. The other thing it does is figure out the tags for the post, and uses a key tag named draft to return nil instead of actual data. This keeps draft pages from showing up in the posts index (or sitemap). This does not prevent the draft from being generated into HTML, nor does it prohibit manually linking to it elsewhere in the project, so don’t count on it as a safeguard to keep a post hidden. The only purpose of the draft tag is to keep it from showing up in the posts listing until you are ready for it to be there.

    Finally, though it won’t be in this post, is the backend definitionf or 'org-ssg which sets up some defaults such as making it use our transcoders by default, among other things. I’d encourage you to browse the source to see the key differences and all the new options introduced.

With our new backend defined, we then can proceed to project configuration. What might not have been noticed without browsing the code is that 'org-ssg does, to some degree, expect the content of the directory that is to be published, to have some structure. This isn’t exactingly strict, and can easily be worked around with careful project configuration if needed but is mostly there to help it generate appropriate links for head meta tags.

Let’s explore the directory layout I’ve used, as well as some special files, before we dive into the projects’ configurations.

.
├── conf
├── includes
├── pages
├── posts
└── static
    ├── doc
    ├── img
    ├── resources
    │   ├── fonts
    │   │   └── woff2
    │   ├── icons
    │   ├── js
    │   └── styles
    └── src

In general, all configuration (including the backend itself) is in the conf folder. Next is includes, which are where the preamble.html and postamble.html files that will be discussed next. pages are where top level pages (like index.org, contact.org) and such are. They get published to the root of the site but the directory organization makes it easier to look for what’s a top level page. You then have posts which is where each blog post goes in our directory layout. Finally you have static which is where all images, documents, videos, and other such things will go, as well as the site’s style (CSS), javascript, and fancy fonts live. In there is also the “icons” folder which contains stuff like the favicon.ico among others.

So there are project settings that allow you to define some custom HTML that’ll get inserted as the preamble or postamble (header or footer sections) in every page the site’s project generates. Mainly, the header contains navigation between top level pages, and the footer contains some other data (which you can see by scrolling to the end of this page). I’d recommend checking the source to see what I do for these 2 HTML files but they aren’t exactly super special.

I have a firm belief that a site MUST operate with or without CSS and/or Javascript. Looking ugly but having all content visible is perfectly reasonable CSS-less. Having navigation impossible without Javascript is completely wrong. The most special thing about the CSS, JS, and pre/post-amble files you’ll find in the source is that they are all designed to match my belief. There are improvements that are possible to be made but they’re not strictly necessary. In general, my site will comply to your wishes to not use either of the above and not degrade into unusability.

Now we can address the site’s publishing projects’ configuration with all the above mentioned.

First are the variables.

(defvar ramblings-project-root
  (substitute-in-file-name
   "~/workspace/docs/organizer/notebook/sites/ramblings")
  "Project root for the ramblings site.")

(defvar ramblings-ssg-head-common
  '((t "script" (src "/static/resources/js/mobile-nav.js"))
    (nil "link"
         (href  "/static/resources/styles/site.css")
         (rel stylesheet)
         (type text/css))
    (nil "link" (rel icon) (type image/x-icon) (href :ssg-favicon))
    (nil "link"
         (rel apple-touch-icon)
         (sizes "180x180")
         (href "/apple-touch-icon.png"))
    (nil "link"
         (rel icon)
         (type "image/png")
         (sizes "32x32")
         (href "/favicon-32x32.png"))
    (nil "link"
         (rel icon)
         (type "image/png")
         (sizes "16x16")
         (href "/favicon-16x16.png"))
    (nil "link"
         (rel mask-icon)
         (color "#6d6d6d")
         (href "/safari-pinned-tab.png"))
    (nil "meta" (name msapplication-TileColor) (content "#00aba9"))
    (nil "meta" (name theme-color) (content "#6d6d6d"))
    (nil "meta" (property og:title) (content :ssg-title))
    (nil "meta" (property og:url) (content :ssg-full-url))
    (nil "meta" (property og:image) (content :ssg-full-image-url))
    (nil "meta" (property twitter:image) (content :ssg-full-image-url))
    (nil "meta" (property og:type) (content :ssg-meta-type))
    (nil "meta" (property twitter:title) (content :ssg-title))
    (nil "meta" (property twitter:url) (content :ssg-full-url))
    (nil "meta" (property og:description) (content :ssg-description))
    (nil "meta" (property twitter:description) (content :ssg-description))
    (nil "meta" (property twitter:card) (content :ssg-description))
    (nil "link" (rel canonical) (href :ssg-full-url))))

(defvar ramblings-project-common
  `(:author                        "llmII"
    :meta-image                    "static/resources/icons/meta-image.png"
    :html-link-home                "https://ramblings.amlegion.org"
    :ssg-project-root              ,ramblings-project-root
    :ssg-append-title              " - llmII's Ramblings"
    :ssg-preamble                  "includes/preamble.html"
    :ssg-postamble                 "includes/postamble.html"
    :htmlized-source               t)
  "Common settings for the html projects in the project alist.")

(defvar ramblings-publishing-locations
  `(:local ,(substitute-in-file-name
             (concat
              "~/workspace/docs/organizer/notebook/"
              "sites/.published/ramblings"))
    :remote "/rsync:llmII:~/foreign/export/site")
  "Locations this site can be published to")

Most of these are just convenience that keeps us from repeating ourselves. It sets up a few common settings to be shared amongst the projects’ as well. In general ramblings-project-common follows the org-publish-project-alist specification but with a few 'org-ssg specific settings. The big one is ramblings-ssg-head-common and this deals with all the meta tags in the head, as well as links and CSS and scripts. Basically, this is a list of lists, each list should begin with either nil or non-nil, where nil indicates that the tag will be like <tag properties> and non-nil indicates it will be like <tag properties></tag>. The next element in the list is the tag’s name. Finally, the rest of the list is key value pairs. For an example, (t "script" (src "script.js")) would generate something along the lines of <script src="script.js"></script>. If it were (nil "script" src="script.js") the result would be invalid HTML but would be <script src="script.js">.

After variables, we have a function. It’s kind of long but important so it’s included.

(defun ramblings-generate-publish (publishing-location type project-root)
  `((,(concat "ramblings.pages-" type)
     ,@ramblings-project-common
     :publishing-directory ,publishing-location
     :publishing-function org-ssg-publish-html
     :ssg-no-publish-relative t
     :meta-type "page"
     :with-title nil
     :ssg-head ,ramblings-ssg-head-common
     :base-directory ,(concat project-root "/pages"))

    (,(concat "ramblings.posts-" type)
     ,@ramblings-project-common
     :publishing-function org-ssg-publish-html
     :auto-sitemap t
     :exclude "index.org"
     :with-properties '("Published")
     :sitemap-filename "index.org"
     :sitemap-title "Blog"
     :sitemap-function org-ssg-sitemap-posts
     :sitemap-format-entry org-ssg-sitemap-posts-entry
     :sitemap-sort-files anti-chronologically
     :meta-type "article"
     :ssg-exclude-sitemap t
     :ssg-head (,@ramblings-ssg-head-common
                (nil "meta" (property article:author) (content :ssg-author))
                (nil "meta"
                     (property article:published_time)
                     (content :ssg-publish-time)))
     :publishing-directory ,(concat publishing-location "/posts")
     :base-directory ,(concat project-root "/posts"))

    (,(concat "ramblings.posts.page-" type)
     ,@ramblings-project-common
     :publishing-function org-ssg-publish-html
     :html-self-link-headlines nil
     :html-divs ((preamble "header" "nav")
                 (content "main" "posts")
                 (postamble "footer" "posts-footer"))
     :include ("index.org")
     :exclude ".*"
     :ssg-head ,ramblings-ssg-head-common
     :base-directory ,(concat project-root "/posts")
     :with-title nil
     :publishing-directory ,(concat publishing-location "/posts"))

    (,(concat "ramblings.resources-" type)
     :recursive t
     :publishing-function org-publish-attachment
     :exclude ".*/icons/.*"
     :base-extension "css\\|js\\|woff2\\|png\\|jpg\\|gif\\|pdf\\|ico\\|svg"
     :publishing-directory ,(concat publishing-location "/static")
     :base-directory ,(concat project-root "/static"))

    (,(concat "ramblings.icons-" type)
     :recursive t
     :publishing-function org-publish-attachment
     :base-extension "png\\|jpg\\|gif\\|ico\\|svg"
     :publishing-directory ,publishing-location
     :base-directory ,(concat project-root "/static/resources/icons"))

    (,(concat "ramblings-" type)
     :components (,(concat "ramblings.pages-" type)
                  ,(concat "ramblings.posts-" type)
                  ,(concat "ramblings.posts.page-" type)
                  ,(concat "ramblings.icons-" type)
                  ,(concat "ramblings.resources-" type)))))

In general, this function takes arguments that define the type of project, publishing location, and the project root. The entire point of it is to make it where I have 2 configs for the site, one that publishes locally, and one that publishes remotely. This allows for easy testing before we push things up for the world to see. I’d hate to make available something unreviewed.

This brings us to the project settings that you saw in ramblings-project-common and in the function that generates the publishing projects (ramblings-generate-publish) itself. I’ll touch on the :ssg prefixed functions, and the purpose of each project defined, but I’m not going to explain Org Mode builtin settings.

There is :ssg-project-root which basically lets 'org-ssg know where things will be relative to. There is :meta-image which defines the image used in the head that perhaps sites using those meta tags will make use of. There is :ssg-append-title which we discussed earlier enables every element in the head that uses the title of the Org Mode document to append to the title something distinguishing the page from other pages that might have similar titles due to such a title being highly generic. Then you have :ssg-preamble and :ssg-postamble which tells 'org-ssg where to source these files for insertion into each page.

Remember that the project root is / (for exemplary purposes) and our directory layout has the top level pages at /pages? The first project generated by ramblings-generate-publish uses the :ssg-no-publish-relative setting so as to keep the relative directory from being used when generating urls in the head. The reason is because the publishing location (:publishing-directory) is the root of the site, not in /pages. We also set the :meta-type to page and drop titles from the pages as well. It uses the common head settings from ramblings-ssg-head-common for the value of :ssg-head. The ramblings.pages- project is what generates all the pages in the /pages directory.

One thing the ramblings.posts-, ramblings.pages- and ramblings.posts.page- projects have in common is they use the publishing function we mentioned earlier in this post (org-ssg-publish-html). That said, ramblings.posts differs in that it adds more elements to :ssg-head and sets up sitemap functionality. The purpose of the sitemap functionality is to generate a listing of all posts in the /posts directory in an Org Mode document. That said, the ramblings.posts- project does not publish this sitemap to HTML, just builds it, which will make sense further along in this article. It does this by setting :ssg-exclude-sitemap to a value. It does not matter what the value is so long as it is not nil. It sets the sort order for the sitemap, and uses the :sitemap-function and :sitemap-format-entry properties to specify to use 'org-ssg implementations of such.

Next we have ramblings.posts.page- which changes the :html-divs id’s so as to let CSS differentiate between a post, a page, and the posts index. It only publishes the posts index generated by the sitemap functions of ramblings.posts.

The remaining projects (excluding ramblings-) handle publishing content that is not transcoded into their expected locations.

ramblings- is a componentized project that publishes all the projects together as a unit.

The rest of the file, ramblings.el, which is the configuration for the site project, defines a function that is used to publish the site. This function makes it simple to pick if we’re publishing locally, or locally and remotely, and allows for us to define if we want to publish the entirety, or just the changes. It’s source is below.

(defvar ramblings-project-alist
  `(,@(ramblings-generate-publish
       (plist-get ramblings-publishing-locations
                  :local)
       "local"
       ramblings-project-root)
    ,@(ramblings-generate-publish
       (plist-get ramblings-publishing-locations
                  :remote)
       "remote"
       ramblings-project-root))
  "Projects that can be published from this file.")

(defun ramblings-publish (&optional full-publish local-only)
  (let ((org-publish-project-alist ramblings-project-alist))
    (if full-publish
        (org-publish "ramblings-local" t)
      (org-publish "ramblings-local"))
    (unless local-only
      (if full-publish
          (org-publish "ramblings-remote" t)
        (org-publish "ramblings-remote")))))

Configuring NGINX

The way the site project exports HTML files makes for a relatively simple server configuration section in NGINX. I’m not going to document the full setup of NGINX, but in general, the below (minus a few changes to obscure actual placements of files and extraneous settings of non-importance) is all that’s needed in a server section of the configuration.

server {
    listen ssl http2;
    server_name ramblings.amlegion.org;
    ssl_certificate /data/ssl/certs/certificates/amlegion.org.crt;
    ssl_certificate_key /data/ssl/certs/certificates/amlegion.org.key;
    access_log  /data/logs/nginx/ramblings/access.log;
    error_log   /data/logs/nginx/ramblings/error.log;
    root /data/www/;
    error_page 404 /404.html;
    index index.html;
}

Future Directions

In the future, it is intended for 'org-ssg to support RSS feeds for the posts page, as well as generating tags pages for each tag, and supporting for tags to link to the corresponding tags page, as well as a tags index page.

It’ll come, and then… a post regarding them will come as well!

Let me know if any of this was useful to you, either in source or just reading the post to see what’s going on.

PS:

This post is a little rough around the edges. I intend to come back and re-review it again and fix/correct/add to it a little here/there. Where I discuss other tools, there is no offense meant and it is overall an opinion and the limitations I felt with regards to those things. What works for one person might not work for everyone. If you spot any issues with this document, be it spelling, or find me wrong on some odd subject discussed, please let me know.

Why publish it if I feel it’s not ready? Because I believe this might have something useful to be gleaned despite the rough edges and I’ve sat on it too long already. Code errors, grammar, spelling, and topical digression are all things that can be fixed, but I can’t fix someone needing something useful from here before I publish it never getting to see it.