This post talks about my blogging setup. I am using simple plain org-publishing for blogging here. There is a link to the complete source in the footer. I assume that the reader has already skimmed through documentation available at Org Publishing.
We will go through the setup in steps. Here is the layout of files that make up the blog:
├── archive.html ├── archive.xml ├── assets │ ├── css │ │ └── bootstrap.css │ ├── images │ │ └── emacs.svg │ └── js │ ├── custom.js │ └── turbolinks.js ├── index.html ├── posts │ ├── lcm-hcf-as-ops-on-sets.html │ └── ... └── src ├── archive.org ├── assets/ ├── drafts/ ├── index.org ├── org-blog.el └── posts ├── blogging-with-org.org ...
New blog posts are added as org files inside
src/posts/. These files are then
exported to HTML through org-publishing and kept in
posts/. The directory tree
src/ is almost replicated under the top-level directory.
The org project definition is in
src/org-blog.el. We load
src/org-blog.el. From one of the project org-files, we can initiate an export
for the project with
C-c C-e. Then choose,
P for publishing and select the
Org-publish gives us hooks that are called during various phases of exporting org files to HTML. These hooks are simple functions that take metadata about the project as input (mainly as a property list) and return HTML for pieces of a page or the whole page.
We will go through the project definition in
A few imports:
(require 'org) (require 'ox-publish) (require 'ox-html) (require 'org-element) (require 'ox-rss)
This function simply touches
index.org so that it is always exported anew
even if none of the other files in the project changed.
called before any of the other functions in
(defun org-blog-prepare (project-plist) "With help from `https://github.com/howardabrams/dot-files'. Touch `index.org' to rebuilt it. Argument `PROJECT-PLIST' contains information about the current project." (let* ((base-directory (plist-get project-plist :base-directory)) (buffer (find-file-noselect (expand-file-name "index.org" base-directory) t))) (with-current-buffer buffer (set-buffer-modified-p t) (save-buffer 0)) (kill-buffer buffer)))
This function adds a few sytlesheets to the
<head> of exported HTML
files. Anything that is required to be placed into the
<head> should go here.
(defvar org-blog-head "<link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/css/bootstrap.css\"/> <link rel=\"stylesheet\" type=\"text/css\" href=\"https://fonts.googleapis.com/css?family=Amaranth|Handlee|Libre+Baskerville|Bree+Serif|Ubuntu+Mono|Pacifico&subset=latin,greek\"/> <link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"favicon.ico\">")
org-publish organizes content of a page into preamble, post contents and
post-amble. Each of these can be provided to org-publish through functions that
take a property list as an argument and return an HTML string.
For this blog, the preamble is a simple banner and two
(defun org-blog-preamble (_plist) "Pre-amble for whole blog." "<div class=\"banner\"> <a href=\"/\"> Ramblings from a Corner </a> </div> <ul class=\"banner-links\"> <li><a href=\"/\"> About Me </a> </li> <li><a href=\"/archive.html\"> Posts </a> </li> </ul> <hr>")
The post-amble for this blog has a nice Emacs icon, Google Analytics and
that gets passed into this function by
org-publish. To find out all the keys
plist, you can print it once during the publishing process. Here, we
use the fact that all ours posts are kept under
posts/ path to decide whether
we should show a Disqus comment block or not. We get the path of the input file
:input-file key in input
(defun org-blog-postamble (plist) "Post-amble for whole blog." (concat "<footer class=\"footer\"> <!-- Footer Definition --> </footer> <!-- Google Analytics Js -->" ;; Add Disqus if it's a post (when (s-contains-p "/posts/" (plist-get plist :input-file)) "<!-- Disqua JS --> ")))
These two functions are for printing the archive of blog posts. Org-publish
already has this feature. We are only configuring how post links and the dates
of their publishing should be displayed.
formats a single blog entry and returns its HTML. Here, it's returning a simple
<span> element with the date and the title of the blog
org-blog-sitemap-function is then passed formatted entries derived
org-blog-sitemap-format-entry as a list argument. It then returns the
HTML for the whole index/archive.
(defun org-blog-sitemap-format-entry (entry _style project) "Return string for each ENTRY in PROJECT." (when (s-starts-with-p "posts/" entry) (format "@@html:<span class=\"archive-item\"><span class=\"archive-date\">@@ %s @@html:</span>@@ [[file:%s][%s]] @@html:</span>@@" (format-time-string "%h %d, %Y" (org-publish-find-date entry project)) entry (org-publish-find-title entry project)))) (defun org-blog-sitemap-function (title list) "Return sitemap using TITLE and LIST returned by `org-blog-sitemap-format-entry'." (concat "#+TITLE: " title "\n\n" "\n#+begin_archive\n" (mapconcat (lambda (li) (format "@@html:<li>@@ %s @@html:</li>@@" (car li))) (seq-filter #'car (cdr list)) "\n") "\n#+end_archive\n"))
This last function is a hack that I use to wrap contents of published org-mode
files into Bootstrap's row and column =<div>=s. This function is exactly same
as the original function but opens the file and rewrites the HTML. This causes
the value of
(point) to change fanatically for me while Emacs is exporting
the project and is working on the file visited by my current buffer. I have
grown accustomed to see this as progress indication for the export process. :)
(defun org-blog-publish-to-html (plist filename pub-dir) "Same as `org-html-publish-to-html' but modifies html before finishing." (let ((file-path (org-html-publish-to-html plist filename pub-dir))) (with-current-buffer (find-file-noselect file-path) (goto-char (point-min)) (search-forward "<body>") (insert (concat "\n<div class=\"content-wrapper container\">\n " " <div class=\"row\"> <div class=\"col\"> </div> " " <div class=\"col-sm-6 col-md-8\"> ")) (goto-char (point-max)) (search-backward "</body>") (insert "\n</div>\n<div class=\"col\"></div></div>\n</div>\n") (save-buffer) (kill-buffer)) file-path))
With all of the above function definitions, we are now ready to glue everything
together by setting the list of projects known to
about the keys used here can be found in different sections of documentation
for Org Publishing. If you have more than one projects, you might want to
add-to-list instead of setting
(setq org-publish-project-alist `(("orgfiles" :base-directory "~/blog/src/" :exclude ".*drafts/.*" :base-extension "org" :publishing-directory "~/blog/" :recursive t :preparation-function org-blog-prepare :publishing-function org-blog-publish-to-html :with-toc nil :with-title t :with-date t :section-numbers nil :html-doctype "html5" :html-html5-fancy t :html-head-include-default-style nil :html-head-include-scripts nil :htmlized-source t :html-head-extra ,org-blog-head :html-preamble org-blog-preamble :html-postamble org-blog-postamble :auto-sitemap t :sitemap-filename "archive.org" :sitemap-title "Blog Posts" :sitemap-style list :sitemap-sort-files anti-chronologically :sitemap-format-entry org-blog-sitemap-format-entry :sitemap-function org-blog-sitemap-function) ("assets" :base-directory "~/blog/src/assets/" :base-extension ".*" :publishing-directory "~/blog/assets/" :publishing-function org-publish-attachment :recursive t) ("rss" :base-directory "~/blog/src/" :base-extension "org" :html-link-home "https://vicarie.in/" :html-link-use-abs-url t :rss-extension "xml" :publishing-directory "~/blog/" :publishing-function (org-rss-publish-to-rss) :exclude ".*" :include ("archive.org") :section-numbers nil :table-of-contents nil) ("blog" :components ("orgfiles" "assets" "rss"))))
With this setup, I can now publish posts with
C-c C-e P p from any of the org
files in this project. I like to have a look at the post locally before
publishing. To do so, I have a prodigy service defined:
(prodigy-define-service :name "blog@localhost" :command "python2" :args '("-m" "SimpleHTTPServer" "8000") :cwd "~/blog/" :tags '(file-server) :stop-signal 'sigkill :kill-process-buffer-on-stop t)
Share your thoughts on the setup in the comments below. Thanks for reading.