Ivory Siege Tower

mobile construct built of thoughts and parentheses

Org-Roam-Blog Site

orb project source

Website instance definitions for the Org-Roam-Blog Engine Package.

§ Site struct

  (defun org-roam-blog--content-wrap-default (site template context)
    "Default content wrapper for ORB SITE. Renders CONTENT in TEMPLATE
    placed between `wrapper-top` and `wrapper-bottom` mustache partials.
    They must exist inside `src-template-dir' of the SITE.
    Returns a closure with a signature expected by `mustache-render' for
    \"wrapper\" tags."
    (let ((template-dirlist
           (list (org-roam-blog-site-src-template-dir site))))
      (lambda (template context)
        (let ((mustache-partial-paths template-dirlist))
          (concat
           (mustache-render "{{> wrapper-top}}" context)
           (mustache-render template context)
           (mustache-render "{{> wrapper-bottom}}" context))))))


  (defun org-roam-blog--render-default (site template context)
    "Default render function for ORB SITE. Merges CONTEXT with
  `site-top-context' and processes through wrapped TEMPLATE."
    (mustache-render
     (concat "{{#orb-site-wrapped}}" template "{{/orb-site-wrapped}}")
     (ht-merge
      context
      (org-roam-blog-site-top-context site)
      (ht ("orb-site-wrapped"
           (funcall #'org-roam-blog--content-wrap-default
                    site template context))))))
  (cl-defstruct (org-roam-blog-site (:constructor org-roam-blog-site--create)
                                    (:copier nil))
    (staging-dir        nil          :type string)
    (scratch-dir        nil          :type string)
    (src-root-dir       nil          :type string)
    (src-template-dir   nil          :type string)
    (index-ht           (ht-create)  :type hash-table)
    (top-context        (ht-create)  :type hash-table)
    (entry-registry     (ht-create)  :type hash-table)
    (content-wrap-fn    #'org-roam-blog--content-wrap-default
                        :type function)
    (render-fn          #'org-roam-blog--render-default
                        :type function))

Blog site constructor with automatic folder selection.

  (cl-defun org-roam-blog-site-create (&rest args)
    (let ((site (apply #'org-roam-blog-site--create args)))
      (unless (org-roam-blog-site-staging-dir site)
        (setf (org-roam-blog-site-staging-dir site)
              (read-directory-name "Select staging directory (output location):")))

      (unless (org-roam-blog-site-src-root-dir site)
        (setf (org-roam-blog-site-src-root-dir site)
              (read-directory-name "Select root files source directory:")))

      (unless (org-roam-blog-site-src-template-dir site)
        (setf (org-roam-blog-site-src-template-dir site)
              (read-directory-name "Select template files source directory:")))
      site))

§ Page rendering for site

Public -render func:

  (defsubst org-roam-blog-render (site template context)
    "Public shortcut to wrap and output CONTEXT to specific
  content mustache TEMPLATE for a SITE."
    (funcall (org-roam-blog-site-render-fn site)
             site template context))

Public -stage. Llooks as it will be the final method to output each page of a site.

  (defsubst org-roam-blog-stage (fname site template context &optional subdir)
    "Output rendered page as FNAME file under `scratch-dir' of the SITE.
  Extend the staging path with SUBDIR when specified.
  Pipes SITE, TEMPLATE and CONTEXT through `org-roam-blog-render'.

  Note that the `scratch-dir' is meant to be (r)sync-ed with the final
  `staging-dir' by the top-level SITE generating routines like e.g.
  `org-roam-blog-stage-site', that will create/process/sync/erase another
  `scratch-dir' (setting it to nil for the processed site). For debugging
  the output of particular pages/sections of the SITE through calls to
  `org-roam-blog-stage', set its `scratch-dir' field manually."
    (let* ((subdir (or subdir ""))
           (subdir (f-expand subdir (org-roam-blog-site-scratch-dir site)))
           (_ (f-mkdir subdir))
           (scratch-path (f-expand fname subdir)))
      (f-write-text (org-roam-blog-render site template context)
                    'utf-8 scratch-path)))

I keep the &optional subdir functionality in org-roam-blog-stage for creation of intermediate subdirectories. At the same time, large relative pathname should is available for each entry context object, see e.g. definition of org-roam-blog--relative-entry-url.

§ Index regitstrarion and staging

    (defsubst org-roam-blog-register-index (site index)
      "Register INDEX in the index-ht of the SITE,
  using slug of the INDEX as key."
      (ht-set!
       (org-roam-blog-site-index-ht site)
       (org-roam-blog-index-slug index)
       index))

A shortcut macro for index registration is defined hereby:

  (defmacro org-roam-blog-reg (site &rest kwargs)
    "Shortcut macro for immediate registration of Org Roam Blog index
  defined by KWARGS for the specified SITE."
    (let ((index (gensym "orb-index-")))
      `(let ((,index (org-roam-blog-index-create ,@kwargs)))
         (org-roam-blog-register-index ,site ,index))))

For any index context-fn should return a list of context-groups, that is - paginated entry list. The groups (or feeds, or index views) are staged with the following routine:

  (defsubst org-roam-blog-site--stage-index-groups (site index &optional entry-list)
    "Stage the grouped preview pages (feeds) for the INDEX of a SITE.
  ENTRY-LIST may come optionally pre-built."
    (if (and (org-roam-blog-index-template index)
             (org-roam-blog-index-context-fn index))
        (let ((template (org-roam-blog-index-template index))
              (context (funcall
                        (org-roam-blog-index-context-fn index)
                        index entry-list))
              (subdir (org-roam-blog-index-slug index)))
          (cl-loop for context-group in context
                   do (org-roam-blog-stage
                       (format "%s-%s.html"
                               org-roam-blog-index-filename-prefix
                               (ht-get context-group "page"))
                       site template context-group subdir))
          (org-roam-blog-stage (format "%s.html" org-roam-blog-index-filename-prefix)
                               site template (car context) subdir))
      (cl-block no-index-page-output
        (when (null (org-roam-blog-index-template index))
          (message "no template defined for %s"
                   (org-roam-blog-index-title index)))
        (when (null (org-roam-blog-index-context-fn index))
          (message "no context-fn defined for %s"
                   (org-roam-blog-index-title index))))))

The next function stages individual entry pages of an index. Similar to previous one, it iterates over a flattened collecton of context objects for an entry-list of an index.

  (defsubst org-roam-blog-site--stage-index-entries (site index &optional entry-list)
    "Stage the individual entry pages the INDEX of a SITE.
  ENTRY-LIST may come optionally pre-built."
    (if (and (org-roam-blog-index-entry-template index)
             (org-roam-blog-index-entry-context-fn index))
        (let ((template (org-roam-blog-index-entry-template index))
              (entry-context-list (org-roam-blog--build-entry-context-list index entry-list))
              (subdir
               (if-let ((index-slug (org-roam-blog-index-slug index))
                        (entry-dir (org-roam-blog-index-entry-dir index)))
                   (concat index-slug "/" entry-dir)
                 index-slug)))
          (cl-loop for (node context) in entry-context-list
                   ;;FIXME: capture counters through `org-roam-blog--build-entry-context-list'
                   ;; for counter from 1 to (length entry-context-list)
                   do (org-roam-blog-stage
                       (funcall (org-roam-blog-index-entry-fname-fn index) node)
                       site template context subdir)))
      (cl-block no-entry-pages-output
        (when (null (org-roam-blog-index-entry-template index))
          (message "no entry-template defined in %s"
                   (org-roam-blog-index-title index)))
        (when (null (org-roam-blog-index-entry-context-fn index))
          (message "no entry-context-fn defined in %s"
                   (org-roam-blog-index-title index))))))

This stages an index utilizing the previous definitions. The entry-list is built just once:

  (defsubst org-roam-blog-site--stage-index (site index)
    "Stage the INDEX of a SITE: process the feed view for entry groups
  and individual page for each entry within an INDEX."
    (let ((entry-list (org-roam-blog--index-entry-list index)))
      (org-roam-blog-site--stage-index-groups  site index entry-list)
      (org-roam-blog-site--stage-index-entries site index entry-list)))

§ Site Staging

Global Id registry for the entries. Will be used for inter-linking of related entries within the exported site.

  (defsubst org-roam-blog-site--process-registry (site)
    "Make all individual entry Id's point to their lead index
  in the SITE's `entry-registry' hash table field."
    (cl-loop
     for index in (ht-values (org-roam-blog-site-index-ht site))
     do (when (org-roam-blog-index-leading index)
          (cl-block process-registry
            (message "processing %s into global entry registry"
                     (org-roam-blog-index-title index))
            (let ((nodelist
                   (flatten-list (org-roam-blog--index-entry-list index))))
              (cl-loop
               for node in nodelist
               do (ht-set! (org-roam-blog-site-entry-registry site)
                           (org-roam-node-id node) index)))))))

Topmost routine for site staging.

  (defsubst org-roam-blog-stage-site (site) ; TODO: &optional index
      "Main staging routine for a SITE. I'm using intermediate scratch temporary
    directory and rsync (default for `org-roam-blog-local-sync-command'), in order
    to also clean up orphans from the final `org-roam-blog-site-staging-dir'."
      ;; update global context with the stamp of generation time
      (ht-set! (org-roam-blog-site-top-context site)
               "top-gen-timestamp"
               (format-time-string "%Y-%m-%d %H:%M"))
      ;; create and prepopulate scratch dir
      (setf (org-roam-blog-site-scratch-dir site)
            (make-temp-file "orb-" t))
      (shell-command
       (format "%s %s %s"
               org-roam-blog-local-sync-command
               (fname-with-tslash (org-roam-blog-site-src-root-dir site))
               (fname-no-tslash (org-roam-blog-site-scratch-dir site))))
      ;; generate global entry registry for the site
      (org-roam-blog-site--process-registry site)

      ;; set global context
      (org-roam-blog-g-entries-set (org-roam-blog-site-entry-registry site))
      (org-roam-blog-g-site-set site)              

      ;; output indexes contents for the site
      (cl-loop for index in (ht-values (org-roam-blog-site-index-ht site))
               do (cl-block stage-index
                    (message "staging index %s" (org-roam-blog-index-title index))
                    (org-roam-blog-site--stage-index site index)))

      ;; reset global context
      (org-roam-blog-g-reset)
      ;; sync with the staging directory
      (unless (f-exists? (org-roam-blog-site-staging-dir site))
        (f-mkdir (org-roam-blog-site-staging-dir site)))
      (shell-command
       (format "%s %s %s"
               org-roam-blog-local-sync-command
               (fname-with-tslash (org-roam-blog-site-scratch-dir site))
               (fname-no-tslash (org-roam-blog-site-staging-dir site))))
      ;; erase the intermediate scratch dir
      (f-delete (org-roam-blog-site-scratch-dir site) t)
      (setf (org-roam-blog-site-scratch-dir site) nil))