MonadicT
I see dead objects!
Search

Blog site generator

Overview

Org-mode has a flexible export framework which can be leveraged to generate Html content suitable for publishing with Jekyll, a static site generator. This post describes the elisp package which is used to maintain my blog at MonadicT.github.io.

Blog entries are written as a regular .org file. A post should have a #+title, #+summary, #+publish_date, #+tags and #+export_file_name for proper formatting and structure. The export_file_name header is used by Org-mode to create the generated Html file.

The entire site with Home, About, Tags, RSS and all the posts can be regenerated by Meta blog-gen-publish, also bound to C-c C-g. An individual post can be quickly examined by invoking Org export menu (C-c C-e) which shows the options for processing the buffer. C-c C-e C-f will generate a HTML file.

If there is no #+publish-date header, the post will be skipped when the site is regenerated.

A new blog entry with all the required headers can be created by executing blog-gen-new-post.

Design description

  • Customizable variables describe

Implementation

Dependencies

Not many! yatl is my Elisp package for generating Html fragments from s-expressions.

(require 'cl)
(require 'subr-x)
(require 's)
(require 'yatl)

Useful macros/functions

Assert utility

A quick little macro to write assertions.

(defmacro assert-equal (actual expected)
  `(when (not (equal ,actual ,expected))
     (error "Actual: %s expected: %s"
            (format "%s" ',actual)
            (format "%s" ',expected))))

Key-value string parser

Parses key-value string of form @key=val@key=val.

(defun blog-gen-parse-kvp(s)
  (let (res)
    (mapconcat
     (lambda (kv-str)
       (let* ((kv (split-string kv-str "="))
              (key (car kv))
              (val (cadr kv))
              (val (if (string-match "^\".*\"$" val)
                       val
                     (format "\"%s\"" val))))
         (concat key "=" val)))
     (split-string s "@" t)
     " ")))

(assert-equal (blog-gen-parse-kvp "") "")
(assert-equal (blog-gen-parse-kvp "@") "")
(assert-equal (blog-gen-parse-kvp "@a=b") "a=\"b\"")
(assert-equal (blog-gen-parse-kvp "@a=b@c=d") "a=\"b\" c=\"d\"")

  (defmacro lisp (&rest forms)
    `(let ((res ,@forms))
       res))

(assert-equal (lisp 1) 1)
(assert-equal (lisp (+ 1 2 3)) 6)
(assert-equal (lisp (concat "a" "n")) "an")
(assert-equal (lisp (list 1 2 3)) '(1 2 3))

Parse element name

Parses element name and returns the list of element name, id, class and attributes. Multiple class names but id must be unique. Id is introduced by ?, class name with . and attribute with @.

;; Exanple names: div, div?id, div.cls1.clas2, div!k=v!k=v
(defun blog-gen-parse-elem-name(s)
  (let ((nm-id-cls (list '() '() '() '()))
        attrs idx buf)
    (setq  idx 0)
    (mapcar
     (lambda (c)
       (cond
        ((eq c ?.) (progn
                     (setq idx 2)
                     (push c (nth idx nm-id-cls))))
        ((eq c ??) (progn
                     (if (null (cadr nm-id-cls))
                         (setq idx 1)
                       (error "ID specified again!"))))
        ((eq c ?@) (progn
                     (setq idx 3)
                     (push c (nth idx nm-id-cls))))
        ((eq idx -1) (error "Expect one of \".,+,#'"))
        (t (push c (nth idx nm-id-cls)))))
     s)
    (list (concat (reverse (car nm-id-cls)))
          (concat (reverse (cadr nm-id-cls)))
          (concat (reverse (caddr nm-id-cls)))
          (blog-gen-parse-kvp (concat (reverse (nth 3 nm-id-cls)))))))

(assert-equal (blog-gen-parse-elem-name "div")
             '("div" "" "" ""))
(assert-equal (blog-gen-parse-elem-name "div?id")
             '("div" "id" "" ""))
(assert-equal (blog-gen-parse-elem-name "div?id.c1.c2")
             '("div" "id" ".c1.c2" ""))
(assert-equal (blog-gen-parse-elem-name "div?id.c1.c2@foo=bar@baz=qux")
               '("div" "id" ".c1.c2" "foo=\"bar\" baz=\"qux\""))

Custom Variables

(require 'ox-html)

 ;;; Variables and options

(defgroup org-export-blog nil
  "Options specific to RSS export back-end."
  :tag "Org Blog"
  :group 'org-export
  :version "24.4"
  :package-version '(Org . "9.0"))

(defcustom blog-gen-publish-url "https://MonadicT.github.io"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-title "MonadicT"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-tagline "I see dead objects!"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-author "Praki Prakash"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-copyright-message "Copyright © 2014-%s, Praki Prakash"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-style-file "blog-style.css"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-banner-file "banner.org"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-footer-file "footer.org"
  "???"
  :group 'org-export-blog
  :type 'string)

Inner template generator

This function is called from Org-export machinery. The main content of the article is wrapped up as a full blog page.

(defun blog-gen-inner-template (contents info)
   (yatl-html-frag
    (body
     (div@style=display:flex
      (div@style=flex:2 "")
      (div@style=flex:6
       (div
        (blog-gen-top-matter)
        contents))
      (div@style=flex:2 "")
      (div.blog-footer
       (format blog-gen-copyright-message
               (format-time-string "%Y")))))))

Twitter link

(defun blog-gen-twitter-link()
  "<a target=\"_new\" href=\"https://twitter.com/MonadicT\">
<span style={background-color: white; height:48px;width:48px;border-radius:24px}></span>
<img height=\"48px\" width=\"48px\"
   title=\"Visit my Twitter page\"
   src=\"/images/twitter.png\"/></a>")

Github link

(defun blog-gen-github-link()
  "<a id=\"github-link\" target=\"_new\"
  href=\"https://github.com/MonadicT\"><img id=\"github-logo\"
  src=\"/images/github-logo.png\"/></a>")

Home link

(defun blog-gen-home-link()
  "<a href=\"/index.html\">Home</a>")

Articles link

(defun blog-gen-articles-link()
  "<a href=\"/articles.html\">Articles</a>")

About link

(defun blog-gen-about-link()
  "<a href=\"/about.html\">About</a>")

Tags link

(defun blog-gen-tags-link()
  "<a href=\"/tags.html\">Tags</a>")

Site links

(defun blog-gen-site-links()
  (concat
   "<div  id=\"site-links\">"
   (blog-gen-home-link)
   (blog-gen-articles-link)
   (blog-gen-tags-link)
   (blog-gen-about-link)
   "</div>"))

Top matter

Generates the header and the banner. Here is one limitation of yatl-compile-string which can generate a single element with dynamic content but not nested elements as needed here.

(defun blog-gen-top-matter()
  (yatl-html-frag
   (div.blog-nav
    (div?blog-title
     blog-gen-title
     (div?tagline blog-gen-tagline))
    (blog-gen-search-form))
   (div.blog-nav
    (blog-gen-site-links)
    (blog-gen-social-media-icons))
   (div@style=border:1px "")))

Search form

(defun blog-gen-search-form()
  "<div>Search <form action=\"http://www.google.com/search\" id=\"searchform\"
method=\"get\"><div><input class=\"box\" id=\"s\" name=\"q\" type=\"text\" />
<input name=\"sitesearch\" type=\"hidden\" value=\"http://MonadicT.github.io\" />
</div></form></div>")

Social media icons

(defun blog-gen-social-media-icons()
  (concat
   "<div id=\"social-media-icons\">"
   (blog-gen-twitter-link)
   (blog-gen-github-link)
   "</div>"))

Mode implementation

 ;;; Define backend

(org-export-define-derived-backend 'blog 'html
  :menu-entry
  '(?b "Export to Blog"
       ((?b "As Blog buffer"
            (lambda (a s v b) (blog-gen-export-as-blog a s v)))
        (?f "As Blog file" (lambda (a s v b) (blog-gen-export-to-blog a s v)))
        (?o "As Blog file and open"
            (lambda (a s v b)
              (if a (blog-gen-export-to-blog t s v)
                (org-open-file (blog-gen-export-to-blog nil s v)))))))
  :options-alist
  '((:description "DESCRIPTION" nil nil newline)
    (:keywords "KEYWORDS" nil nil space)
    (:with-toc nil nil nil) ;; Never include HTML's toc
    )
  :filters-alist '((:filter-final-output . blog-gen-final-function))
  :translate-alist '((comment . (lambda (&rest args) ""))
                     (comment-block . (lambda (&rest args) ""))
                     (timestamp . (lambda (&rest args) ""))
                     (inner-template . blog-gen-inner-template)
                     (template . blog-gen-template)))

 ;;; Export functions

 ;;;###autoload
(defun blog-gen-export-as-blog (&optional async subtreep visible-only)
  "Export current buffer to a blog buffer.

 Export is done in a buffer named \"*Org Blog Export*\", which will
 be displayed when `org-export-show-temporary-export-buffer' is
 non-nil."
  (interactive)
  (let ((file (buffer-file-name (buffer-base-buffer)))))
  (org-export-to-buffer 'blog "*Org Blog Export*"
    async subtreep visible-only nil nil (lambda () (text-mode))))

 ;;;###autoload
(defun blog-gen-export-to-blog (&optional async subtreep visible-only)
  "Export current buffer to a Blog file.
 Return output file's name."
  (interactive)
  (let ((file (buffer-file-name (buffer-base-buffer)))))
  (let ((outfile (org-export-output-file-name
                  (concat "." "html") subtreep)))
    (org-export-to-file 'blog outfile async subtreep visible-only)))

 ;;;###autoload
(defun blog-gen-publish-to-blog (plist filename pub-dir)
  "Publish an org file to Blog.

 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.

 Return output file name."
  (let ((bf (get-file-buffer filename)))
    (if bf
        (with-current-buffer bf
          (write-file filename))
      (find-file filename)
      (write-file filename) (kill-buffer)))
  (org-publish-org-too
   'log filename (concat "." "html") plist pub-dir))

 ;;; Main transcoding functions

(defun blog-gen-template (contents info)
  "Return complete document string after BLOG conversion.
 CONTENTS is the transcoded contents string.  INFO is a plist
 used as a communication channel."
  (yatl-html5
   (head
    (yatl-compile-string "meta@charset=\"%s\">"
            (symbol-name org-html-coding-system))
    (title blog-gen-title)
    (yatl-compile-string
     "base@href=\"%s\""
     (blog-gen-base-url))
    (yatl-compile-string "meta@name=generator@content=\"%s\">" (emacs-version))
    (yatl-compile-string "meta@name=author@content=\"%s\"" blog-gen-author)
    "<link href=\"https://fonts.googleapis.com/css?family=Source+Code+Pro|EB+Garamond:800|Roboto:100,300,400,400i,700\" rel=\"stylesheet\">"
    (link@href=\"blog-style.css\"@rel=\"stylesheet\"))
   contents))

 ;;; Filters

(defun blog-gen-final-function (contents backend info)
  "Prettify the Blog output."
  (with-temp-buffer
    (xml-mode)
    (insert contents)
    ;;(indent-region (point-min) (point-max))
    (buffer-substring-no-properties (point-min) (point-max))))

 ;;; Miscellaneous


(provide 'ox-blog)

 ;;; ox-blog.el ends here

(require 'ox-html)
(defun my-html-body-filter(text backend info)
  text)

(add-to-list 'org-export-filter-body-functions
             'my-html-body-filter)

Styles

CSS Styles used in this blog are managed using interpolated strings. The code below is an association list which is used to build CSS string later.

Font sizes are specified in percent units which makes the content scalable.

Color definitions

CSS color definitions.

  • Dark primary color.
#616161
  • Default primary color
#9E9E9E
  • Light primary color
#d3d3d3
  • Text primary color
#545454
  • Accent color

Previous color was #ff5722.

#51c0ae
  • Primary text color
#545454
  • Secondary text color
#757575
  • Accent text color

#FF5722

#51c0ae
  • Source code background color

Source code is rendered against this background color for readbility.

#f2f2f2

HTML element styling

Style definitions for general html elements.

/Setting body.height to 100% paints background to visible portion of blog content and leaving it empty results in left and right borders at zero height. The only reasonable fix is to be able to specify heights for all layout divs to be in resolution independent units/

 * { font-family: "Roboto"; }
 html {
     clear: both;
     Height: 100%;
     width: 100%
 }

 body {
     margin: 0; 
     padding: 0;
     width: 100%;
     display: flex;
     flex-direction: column;
     color: #545454;
     background-color: #fffffd;
     line-height: 1.35;
     padding: 1em;
 }

a { color: #51c0ae; text-decoration: none}
a:visited { color: #51c0ae; text-decoration: none}

a:hover {  color: #51c0ae; opacity: 0.5; }

Page layout styling

h2 {
    color: #51c0ae;
}

/* */
#blog-title {
    font-family: 'Cormorant Garamond';
    font-size: 400%;
    font-weight: bolder;
    color: #51c0ae;
}

#tagline {
    font-size: 20%;
    font-weight: lighter;
    padding-bottom: 1em;
}

.blog-nav {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    border-bottom: 2px solid #999
}


Site links styling

#site-links {
}

#site-links a {
    color: #51c0ae;
    font-size: 100%;
    text-decoration: none;
    padding-right: 1em
}

#social-media-icons {
}

#github-logo {
    vertical-align: super
}

Post styling

.post-title {
    font-size: 150%;
    font-weight: bold;
    color: #51c0ae;
    border-bottom: 2px solid #ddd
}

.post-summary {
    font-size: 100%;
    color: #757575;
    padding-top: 0.5em;
    padding-left: 1em;
}

.post-tags {
    font-size: 80%;
    color: #757575;
    padding-top: 1em;
    padding-left: 1em;
}

.pub-date { 
    font-weight: bold;
    color: #757575;
    padding-bottom: 2em;
}

.post-publish-date {
    font-size: 80%;
    color: #777;
    padding-top: 0.5em;
    padding-left: 1em;
    padding-bottom: 1em;
}

Org style overrides

Here we override style information for elements generated by org-mode.

.org-src-container {
    border-left: 0.2em solid #999;
}

.src {
      font-family: 'Source Code Pro';
      font-size: 110%;
      background-color: #f2f2f2;
      padding: 1em;
  }

Class definitions

.blog-footer {
    display: flex;
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 1em;
    font-size: 50%;
    font-weight: bold;
    padding: 5px;
    flex-direction: row;
    justify-content: center;
    border-top: solid 1px #dfe3ee;
    color: #51c0ae; /* #3b5998, #dfe3ee, #8b9dc3*/
    background-color: #d3d3d3; /*#dfe3ee;*/
}
/* , #8b9dc3 */

Tangling Style sheet

.org-src-container {
    border-left: 0.2em solid #999;
}

.src {
      font-family: 'Source Code Pro';
      font-size: 110%;
      background-color: #f2f2f2;
      padding: 1em;
  }

 * { font-family: "Roboto"; }
 html {
     clear: both;
     Height: 100%;
     width: 100%
 }

 body {
     margin: 0; 
     padding: 0;
     width: 100%;
     display: flex;
     flex-direction: column;
     color: #545454;
     background-color: #fffffd;
     line-height: 1.35;
     padding: 1em;
 }

a { color: #51c0ae; text-decoration: none}
a:visited { color: #51c0ae; text-decoration: none}

a:hover {  color: #51c0ae; opacity: 0.5; }
h2 {
    color: #51c0ae;
}

/* */
#blog-title {
    font-family: 'Cormorant Garamond';
    font-size: 400%;
    font-weight: bolder;
    color: #51c0ae;
}

#tagline {
    font-size: 20%;
    font-weight: lighter;
    padding-bottom: 1em;
}

.blog-nav {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    border-bottom: 2px solid #999
}


.post-title {
    font-size: 150%;
    font-weight: bold;
    color: #51c0ae;
    border-bottom: 2px solid #ddd
}

.post-summary {
    font-size: 100%;
    color: #757575;
    padding-top: 0.5em;
    padding-left: 1em;
}

.post-tags {
    font-size: 80%;
    color: #757575;
    padding-top: 1em;
    padding-left: 1em;
}

.pub-date { 
    font-weight: bold;
    color: #757575;
    padding-bottom: 2em;
}

.post-publish-date {
    font-size: 80%;
    color: #777;
    padding-top: 0.5em;
    padding-left: 1em;
    padding-bottom: 1em;
}

.blog-footer {
    display: flex;
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 1em;
    font-size: 50%;
    font-weight: bold;
    padding: 5px;
    flex-direction: row;
    justify-content: center;
    border-top: solid 1px #dfe3ee;
    color: #51c0ae; /* #3b5998, #dfe3ee, #8b9dc3*/
    background-color: #d3d3d3; /*#dfe3ee;*/
}
/* , #8b9dc3 */
#site-links {
}

#site-links a {
    color: #51c0ae;
    font-size: 100%;
    text-decoration: none;
    padding-right: 1em
}

#social-media-icons {
}

#github-logo {
    vertical-align: super
}

Org-file analyzer

Return keywords from org-file

Returns list of OrgMode keywords from the current document.

(defun blog-gen-get-all-keywords()
  (org-element-map
      (org-element-parse-buffer 'element)
      'keyword
    (lambda (kw)
      (cons (org-element-property :key kw)
            (org-element-property :value kw)))))

Get keyword

Returns value of key or default-value if key doesn't exist in keywords.

(defun blog-gen-get-keyword-value(keywords key &optional default-value)
  (if-let ((kw-value (cdr (assoc-ignore-case key keywords))))
      kw-value
    default-value))

Blog publishing

To make any changes to blog generation, this file should be tangled. Tangling this file generates ~/.emacs.d/lisp/blog-gen.el and blog-style.css. blog-gen-publish bound to C-c C-g, processes all org files and generates HTML suitable for publishing as a static web site on a local server. With a prefix argument, the generated HTML will have s different base url suotable for publishing to github.io.

A local server for serving HTML can be run by python -m http.server 8000 command.

Blog source directory

The root directory where the source for blogs is kept.

(defcustom blog-gen-root-dir
  "~/projects/MonadicT.github.io/"
  "")

Blog posts directory

The subdirectory where .org files are stored.

(defcustom blog-gen-posts-dir
  "_resources/posts/"
  "")

Publishing locally

This is a Boolean flag set to use base url for generated html files.

(setq blog-gen-local t)
(defun blog-gen-base-url()
  (if blog-gen-local "http://localhost:8000/" "https://MonadicT.github.io/"))

Extract post details

Extracts post title, summary and publish-date from the file. Nil is returned if publish-date is not present.

(defun blog-gen-post-details (f)
  (with-temp-buffer
    (find-file f)
    (let* ((kws (blog-gen-get-all-keywords))
           (details (make-hash-table  :test #'equal))
           (export-file-name (blog-gen-get-keyword-value kws "export_file_name" nil))
           (href (or export-file-name "")))
      (while (string-match "^\\.\\./" href)
        (setq href (substring href 3)))
      (puthash "post-file" f details)
      (puthash "title" (blog-gen-get-keyword-value kws "title" "") details)
      (puthash "summary" (blog-gen-get-keyword-value kws "summary" "") details)
      (puthash "publish-date" (blog-gen-get-keyword-value kws "publish-date" nil) details)
      (puthash "export_file_name" export-file-name details)
      (puthash "tags" (blog-gen-get-keyword-value kws "tags" "") details)
      (puthash "target" (blog-gen-get-keyword-value kws "target" "") details)
      (puthash "href" (concat (blog-gen-base-url) href) details)
      (unless (string-match "blog-generator.org" f) (kill-buffer))
      details)))

Post files

Returns list of posts stored in .org files. .org files such as index.org, about.org are not returned as posts.

(defun blog-gen-post-files()
  (let* ((posts-dir (concat blog-gen-root-dir "/" blog-gen-posts-dir))
         (org-files (directory-files posts-dir t "[a-ZA-Z0-9_-]*\\.org$"))
         (org-files
          (seq-remove
           (lambda (f)
             (or (string-match "index.org$" f)
                 (string-match "about.org$" f)
                 (string-match "sitemap.org$" f)))
           org-files)))
    (mapcar
     #'blog-gen-post-details
     org-files)))

Published post files

Returns published posts (posts which have export_file_name keyword).

(defun blog-gen-published-posts (posts)
  (seq-filter (lambda (p) (gethash "export_file_name" p)) posts))

Order post files

Orders posts by publish-date descending.

(defun blog-gen-order-posts(posts)
  (seq-sort (lambda (a b) (string> (gethash "publish-date" a) (gethash "publish-date" b))) posts))

Macro to generate html_export blocks

(defmacro html-export(&rest content)
  `(progn (insert "#+BEGIN_EXPORT html\n")
          (insert ,@content)
          (insert "\n#+END_EXPORT\n\n")))

Articles generation

Exports all .org post files to .html files.

(defun blog-gen-create-posts()
  (let ((posts (blog-gen-published-posts
                (blog-gen-post-files))))
    (mapcar
     (lambda (post)
       (let ((post-file (gethash "post-file" post))
             (export-file-name (gethash "export_file_name" post)))
         (message (concat "exporting" post-file "to" export-file-name))
         (when export-file-name
           (with-temp-buffer
             (find-file post-file)
             (org-export-to-file 'blog export-file-name)
             (kill-buffer)))))
     posts)))

Articles page generation

Generates list of articles.

  (defun blog-gen-create-articles()
    (with-temp-buffer
      (find-file (concat blog-gen-root-dir "index.org"))
      (erase-buffer)
      (insert "#+title: MonadictT\n")
      (insert "#+options: num:nil html-style:nil\n")
      (insert "#+HTML_HEAD: <link href=\"https://fonts.googleapis.com/css?family=Cormorant+Garamond|Roboto\" rel=\"stylesheet\">\n")
      (insert "#+HTML_HEAD_EXTRA: <style>* {font-family: 'Roboto';}</style>\n")
      (insert "#+HTML_HEAD_EXTRA: <style>pre {font-family: 'Segoe Print';}</style>\n")

      (insert "* Posts\n")
      (let ((posts (blog-gen-order-posts
                    (blog-gen-published-posts
                     (blog-gen-post-files)))))
        (mapcar
         (lambda (post)
           (let* ((title (gethash "title" post))
                  (summary (gethash "summary"  post))
                  (export-file-name (gethash "export_file_name"  post))
                  (export-file-name
                   (let ((href export-file-name))
                     (while (string-match "^\\.\\./" href)
                       (setq href (substring href 3)))
                     href))
                  (publish-date (gethash "publish-date" post))
                  (l (list (make-symbol (format "a@href=\"/%s\"" export-file-name)) title)))
             (html-export
              (yatl-html-frag
               (div.post-title
                (eval (yatl-compile-fn l)))))
             (html-export
              (yatl-html-frag
               (div.post-summary summary)) "\n\n")
             (html-export
              (yatl-html-frag
               (div.post-publish-date "Published: " publish-date)) "\n")))
         posts))
      (save-buffer)
      (let ((out-file (concat blog-gen-root-dir "articles.html")))
        (when (file-exists-p out-file)
          (delete-file out-file))
        (org-export-to-file 'blog out-file))
      (delete-file (concat blog-gen-root-dir "index.org"))))
;;(blog-gen-create-articles)

Tags generation

Posts have a #+tags header and tags are separated by ,. This function builds a hashtable of tags to a list posts from posts.

(defun create-tag-post-map(posts)
  (let ((tag-post-map (make-hash-table :test 'equal)))
    (mapcar
     (lambda(post)
       (let ((title (gethash "title" post))
             (tags (mapcar #'s-trim (split-string (gethash "tags" post) "[,]+"))))
         (mapcar
          (lambda (tag)
            (when (not (string-empty-p tag))
              (puthash tag (cons post (gethash tag tag-post-map '())) tag-post-map)))
          tags)))
     posts)
    tag-post-map))

Tags should be ordered in lexicographic order. This function returns tags in ascending order.

(defun sorted-keys(tag-post-map)
  (let ((keys '()))
    (maphash
     (lambda (k v) (setq keys (cons k keys)))
     tag-post-map)
    (seq-sort (lambda (a b) (string-lessp (upcase a) (upcase b))) keys)))

Once we have the tag-post-map, we can generate an org-mode representation of it. The tag is listed as second-level header with a bullet list of anchors to posts.

(defun blog-gen-create-tags(posts)
  (let* ((tag-post-map (create-tag-post-map posts))
         (tags  (sorted-keys tag-post-map)))
    (with-temp-buffer
      (find-file "tags.org")
      (erase-buffer)
      (insert "#+title: Tags\n")
      (insert "#+options: num:nil\n")
      (insert "* Tags\n")
      (mapcar
       (lambda (tag)
         (let ((post-list (gethash tag tag-post-map)))
           (insert "** " tag "\n")
           (mapcar
            (lambda (post)
              (princ post)
              (let ((title (gethash "title" post))
                    (href  (gethash "href" post)))
                (insert "- [[" href "][" title "]]\n")))
            post-list)))
       tags)
      (write-file "tags.org")
      (let ((out-file (concat blog-gen-root-dir "tags.html")))
        (org-export-to-file 'blog out-file))
      (kill-buffer))))


;;  (setq x (blog-gen-post-files))

;;  (blog-gen-create-tags x)

;;  (require 'blog-gen)

About page generation.

(defun blog-gen-create-about()
  (with-temp-buffer
    (find-file (concat
                blog-gen-root-dir
                blog-gen-posts-dir
                "about.org"))
    (next-line) ;; When on line 1, org export throws a weird error
    (org-export-to-file 'blog (concat blog-gen-root-dir "about.html") nil )
    (kill-buffer)))

(blog-gen-create-about)

Home page generation

For now, Home points to Articles.

(defun blog-gen-create-home()
  (copy-file
   (concat blog-gen-root-dir "articles.html")
   (concat blog-gen-root-dir "index.html") t))

Generates blog,

Function to regenerate the full site. This is bound to C-c C-g.

  (defun blog-gen-publish(prod)
    (interactive "P")
(message "Blog generattion started")
    (if prod (setq blog-gen-local nil) (setq blog-gen-local t))
    (message (format "Local Gen %S" blog-gen-local))
    (let ((posts (blog-gen-post-files)))
      (blog-gen-create-posts)
      (message "Created posts")
      (blog-gen-create-articles)
      (message "Created articles page")
      (blog-gen-create-tags posts)
      (message "Created tags page")
      (blog-gen-create-about)
      (message "Created about page")
      (blog-gen-create-home)
      (message "Created home page"))
    (when prod
      (when (blog-gen-file-contains-p "localhost")
        (error "localhost references in file"))))
  (defun blog-gen-file-contains-p(s) nil)
  (global-set-key (kbd "C-c C-g") #'blog-gen-publish)

Create new blog post

A template for creating a new blog post.

  (defun blog-gen-new-post()
    (interactive)
    (insert "#+title: TBD
#+summary: TBD
#+publish-date: 2018-01-31
#+export_file_name: ../../yyyy/TBD
#+tags: TBD
#+option: num:nil"))

Tangled elisp file

All the functions described above are tangled into ~/.emacs.d/lisp/blog-gen.el. A require in .emacs will make this available for use.

;;; blog-gen.el --- Static site generator in emacs and org-mode

;; Copyright (C) 2017-2018 Praki Prakash

;; Author: Praki Prakash
;; Maintainer: Praki Prakash
;; Created: 2017-12-31
;; Keywords: languages
;; Homepage: https://MonadicT.gihub.io

;; This file is not part of GNU Emacs.
;;; blog-gen.el --- Static site generator in emacs and org-mode

;; Copyright (C) 2017-2018 Praki Prakash

;; Author: Praki Prakash
;; Maintainer: Praki Prakash
;; Created: 2017-12-31
;; Keywords: languages
;; Homepage: https://MonadicT.gihub.io

;; This file is not part of GNU Emacs.
;;; Dependencies ----------------------
(require 'cl)
(require 'subr-x)
(require 's)
(require 'yatl)

;;; Variables
(require 'ox-html)

 ;;; Variables and options

(defgroup org-export-blog nil
  "Options specific to RSS export back-end."
  :tag "Org Blog"
  :group 'org-export
  :version "24.4"
  :package-version '(Org . "9.0"))

(defcustom blog-gen-publish-url "https://MonadicT.github.io"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-title "MonadicT"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-tagline "I see dead objects!"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-author "Praki Prakash"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-copyright-message "Copyright &copy; 2014-%s, Praki Prakash"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-style-file "blog-style.css"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-banner-file "banner.org"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-footer-file "footer.org"
  "???"
  :group 'org-export-blog
  :type 'string)

(defcustom blog-gen-root-dir
  "~/projects/MonadicT.github.io/"
  "")
(defcustom blog-gen-posts-dir
  "_resources/posts/"
  "")
(setq blog-gen-local t)

;;; Functions ------------------------
(defmacro assert-equal (actual expected)
  `(when (not (equal ,actual ,expected))
     (error "Actual: %s expected: %s"
            (format "%s" ',actual)
            (format "%s" ',expected))))
(defun blog-gen-get-all-keywords()
  (org-element-map
      (org-element-parse-buffer 'element)
      'keyword
    (lambda (kw)
      (cons (org-element-property :key kw)
            (org-element-property :value kw)))))
(defun blog-gen-get-keyword-value(keywords key &optional default-value)
  (if-let ((kw-value (cdr (assoc-ignore-case key keywords))))
      kw-value
    default-value))
(defun blog-gen-post-details (f)
  (with-temp-buffer
    (find-file f)
    (let* ((kws (blog-gen-get-all-keywords))
           (details (make-hash-table  :test #'equal))
           (export-file-name (blog-gen-get-keyword-value kws "export_file_name" nil))
           (href (or export-file-name "")))
      (while (string-match "^\\.\\./" href)
        (setq href (substring href 3)))
      (puthash "post-file" f details)
      (puthash "title" (blog-gen-get-keyword-value kws "title" "") details)
      (puthash "summary" (blog-gen-get-keyword-value kws "summary" "") details)
      (puthash "publish-date" (blog-gen-get-keyword-value kws "publish-date" nil) details)
      (puthash "export_file_name" export-file-name details)
      (puthash "tags" (blog-gen-get-keyword-value kws "tags" "") details)
      (puthash "target" (blog-gen-get-keyword-value kws "target" "") details)
      (puthash "href" (concat (blog-gen-base-url) href) details)
      (unless (string-match "blog-generator.org" f) (kill-buffer))
      details)))
(defun blog-gen-post-files()
  (let* ((posts-dir (concat blog-gen-root-dir "/" blog-gen-posts-dir))
         (org-files (directory-files posts-dir t "[a-ZA-Z0-9_-]*\\.org$"))
         (org-files
          (seq-remove
           (lambda (f)
             (or (string-match "index.org$" f)
                 (string-match "about.org$" f)
                 (string-match "sitemap.org$" f)))
           org-files)))
    (mapcar
     #'blog-gen-post-details
     org-files)))
(defun blog-gen-published-posts (posts)
  (seq-filter (lambda (p) (gethash "export_file_name" p)) posts))
(defun blog-gen-order-posts(posts)
  (seq-sort (lambda (a b) (string> (gethash "publish-date" a) (gethash "publish-date" b))) posts))
(defmacro html-export(&rest content)
  `(progn (insert "#+BEGIN_EXPORT html\n")
          (insert ,@content)
          (insert "\n#+END_EXPORT\n\n")))
(defun blog-gen-create-posts()
  (let ((posts (blog-gen-published-posts
                (blog-gen-post-files))))
    (mapcar
     (lambda (post)
       (let ((post-file (gethash "post-file" post))
             (export-file-name (gethash "export_file_name" post)))
         (message (concat "exporting" post-file "to" export-file-name))
         (when export-file-name
           (with-temp-buffer
             (find-file post-file)
             (org-export-to-file 'blog export-file-name)
             (kill-buffer)))))
     posts)))
  (defun blog-gen-create-articles()
    (with-temp-buffer
      (find-file (concat blog-gen-root-dir "index.org"))
      (erase-buffer)
      (insert "#+title: MonadictT\n")
      (insert "#+options: num:nil html-style:nil\n")
      (insert "#+HTML_HEAD: <link href=\"https://fonts.googleapis.com/css?family=Cormorant+Garamond|Roboto\" rel=\"stylesheet\">\n")
      (insert "#+HTML_HEAD_EXTRA: <style>* {font-family: 'Roboto';}</style>\n")
      (insert "#+HTML_HEAD_EXTRA: <style>pre {font-family: 'Segoe Print';}</style>\n")

      (insert "* Posts\n")
      (let ((posts (blog-gen-order-posts
                    (blog-gen-published-posts
                     (blog-gen-post-files)))))
        (mapcar
         (lambda (post)
           (let* ((title (gethash "title" post))
                  (summary (gethash "summary"  post))
                  (export-file-name (gethash "export_file_name"  post))
                  (export-file-name
                   (let ((href export-file-name))
                     (while (string-match "^\\.\\./" href)
                       (setq href (substring href 3)))
                     href))
                  (publish-date (gethash "publish-date" post))
                  (l (list (make-symbol (format "a@href=\"/%s\"" export-file-name)) title)))
             (html-export
              (yatl-html-frag
               (div.post-title
                (eval (yatl-compile-fn l)))))
             (html-export
              (yatl-html-frag
               (div.post-summary summary)) "\n\n")
             (html-export
              (yatl-html-frag
               (div.post-publish-date "Published: " publish-date)) "\n")))
         posts))
      (save-buffer)
      (let ((out-file (concat blog-gen-root-dir "articles.html")))
        (when (file-exists-p out-file)
          (delete-file out-file))
        (org-export-to-file 'blog out-file))
      (delete-file (concat blog-gen-root-dir "index.org"))))
;;(blog-gen-create-articles)
(defun create-tag-post-map(posts)
  (let ((tag-post-map (make-hash-table :test 'equal)))
    (mapcar
     (lambda(post)
       (let ((title (gethash "title" post))
             (tags (mapcar #'s-trim (split-string (gethash "tags" post) "[,]+"))))
         (mapcar
          (lambda (tag)
            (when (not (string-empty-p tag))
              (puthash tag (cons post (gethash tag tag-post-map '())) tag-post-map)))
          tags)))
     posts)
    tag-post-map))
(defun sorted-keys(tag-post-map)
  (let ((keys '()))
    (maphash
     (lambda (k v) (setq keys (cons k keys)))
     tag-post-map)
    (seq-sort (lambda (a b) (string-lessp (upcase a) (upcase b))) keys)))
(defun blog-gen-create-tags(posts)
  (let* ((tag-post-map (create-tag-post-map posts))
         (tags  (sorted-keys tag-post-map)))
    (with-temp-buffer
      (find-file "tags.org")
      (erase-buffer)
      (insert "#+title: Tags\n")
      (insert "#+options: num:nil\n")
      (insert "* Tags\n")
      (mapcar
       (lambda (tag)
         (let ((post-list (gethash tag tag-post-map)))
           (insert "** " tag "\n")
           (mapcar
            (lambda (post)
              (princ post)
              (let ((title (gethash "title" post))
                    (href  (gethash "href" post)))
                (insert "- [[" href "][" title "]]\n")))
            post-list)))
       tags)
      (write-file "tags.org")
      (let ((out-file (concat blog-gen-root-dir "tags.html")))
        (org-export-to-file 'blog out-file))
      (kill-buffer))))


;;  (setq x (blog-gen-post-files))

;;  (blog-gen-create-tags x)

;;  (require 'blog-gen)
(defun blog-gen-create-about()
  (with-temp-buffer
    (find-file (concat
                blog-gen-root-dir
                blog-gen-posts-dir
                "about.org"))
    (next-line) ;; When on line 1, org export throws a weird error
    (org-export-to-file 'blog (concat blog-gen-root-dir "about.html") nil )
    (kill-buffer)))

(blog-gen-create-about)
(defun blog-gen-create-home()
  (copy-file
   (concat blog-gen-root-dir "articles.html")
   (concat blog-gen-root-dir "index.html") t))
  (defun blog-gen-publish(prod)
    (interactive "P")
(message "Blog generattion started")
    (if prod (setq blog-gen-local nil) (setq blog-gen-local t))
    (message (format "Local Gen %S" blog-gen-local))
    (let ((posts (blog-gen-post-files)))
      (blog-gen-create-posts)
      (message "Created posts")
      (blog-gen-create-articles)
      (message "Created articles page")
      (blog-gen-create-tags posts)
      (message "Created tags page")
      (blog-gen-create-about)
      (message "Created about page")
      (blog-gen-create-home)
      (message "Created home page"))
    (when prod
      (when (blog-gen-file-contains-p "localhost")
        (error "localhost references in file"))))
  (defun blog-gen-file-contains-p(s) nil)
  (global-set-key (kbd "C-c C-g") #'blog-gen-publish)
  (defun blog-gen-new-post()
    (interactive)
    (insert "#+title: TBD
#+summary: TBD
#+publish-date: 2018-01-31
#+export_file_name: ../../yyyy/TBD
#+tags: TBD
#+option: num:nil"))

(provide 'blog-gen)
;;; blog-gen.el ends here