Website Preview and Deploy

Menu

Introduction

These functions complement Org Mode publishing features by adding a local preview, using elnode or webrick, and a deploy function, using rsync.

The functions can be invoked with:

  • website/server-start
  • website/deploy-project

These functions are useful if you define Org Mode projects in which files are published locally and deployed with an external tool.

For the local preview to work, you also need to ensure links are relative, so that they work independent of the URL base.

Installation

Use:

  • M-x org-babel-load-file FILE

or, if you prefer:

  • Tangle this file with M-x org-babel-tangle. This generates an Emacs Lisp file with the required code.
  • Load this file, with either (require 'avm-org-publish-support) or (load "avm-org-publish-support.el")

which is equivalent to the command above.

Usage

Add the following properties to the specification of an Org Mode project (those stored in org-publish-project-alist):

  • :port port used to serve a local copy of the website (default to 5000)
  • :local-dir local directory where the website is published1
  • :deploy-dir directory where the website is deployed
  • :no-sources whether Org mode files have to be excluded from the deployment. (This is useful when you publish projects in the same directory of your sources and you do not wat to publish the sources.)

Setters and Getters

These function get the information required for previewing, building, and deploying. (They isolate setters and getters in their functions.)

(defun website/get-local-dir (project)
  "Where do we find the HTML files for <project>?"
  (plist-get (cdr (assoc project org-publish-project-alist)) :local-dir))

(defun website/get-preview-dir (project)
  "Where do we find the preview dir for <project>?"
  (plist-get (cdr (assoc project org-publish-project-alist)) :preview-dir))

(defun website/get-deploy-dir (project)
  "Where do we push the HTML files for <project>?"
  (plist-get (cdr (assoc project org-publish-project-alist)) :deploy-dir))

(defun website/get-port (project)
  "Port used for local preview? (Answer: 5000 if not set explicitly)"
  (or
   (plist-get (cdr (assoc project org-publish-project-alist)) :port)
   5000))

(defun website/exclude-sources? (project)
  "Whether we want to exclude org sources from the deploy.
This is useful when the project is published in the same
directory of your Org mode files and you do not want to publish
them in the remote directory."
  (plist-get (cdr (assoc project org-publish-project-alist)) :no-sources))

Building

This is syntactic sugar, since org-mode already provides this function.

(defun website/build-project (project-name &optional force async)
  "Build a website (invoke org-publish)"
  (interactive
   (list (completing-read "Publish project: " org-publish-project-alist nil t)
         current-prefix-arg))
  (org-publish project-name force async))

Previewing

Define two functions which start and stop a webserver serving the files of a project.

Customize the variable website/server to use the webserver of your choice, include elnode, if you want. (For the developers, the elnode tutorial provides the example code for starting and stopping the server.)

These functions can be safely moved to the Emacs initialization file, especially when there is more than one project:

; (require 'elnode nil t) ;; required is you want to use elnode

;;https://gist.github.com/willurd/5720255
(defvar website/server "ruby -run -ehttpd {dir} -p{port}"
  "*Webserver to use for previewing.

It can either be the symbol 'elnode, in which case the Emacs Lisp
elnode server will be used, or a command specification, which
will be instantiated with the required port and the directory and
started with start-process.

In order to use the directory and the port specified in the
project, use the special symbols {dir} and {port}.

Examples of commands include:

  cd {dir}; python -m http.server {port}
  ruby -run -ehttpd {dir} -p{port}")

(defun website/start-server (project-name)
  "Ask for a project name and start previewing it"
  (interactive
   (list (completing-read "Start local server for project: "
                          org-publish-project-alist nil t)))
  (let* ( (dir (website/get-preview-dir project-name))
          (port (website/get-port project-name)) )
    (if (not dir)
        (message "Property :local-dir is not set for this project. Aborting")
      (if (equal website/server 'elnode)
          (elnode-start 
           (elnode-webserver-handler-maker dir)
           :port port 
           :host "localhost")
        (let ( (command (website/instantiate-command website/server port dir)) )
          (start-process-shell-command "webserver" (format "*webserver %s*" project-name) command)))
      (message "Started serving directory %s on port %s" dir port))))

(defun website/instantiate-command (command port dir)
  "Generate the command to launch a webser, based on the value of website/server"
  (replace-regexp-in-string "{dir}" dir
                            (replace-regexp-in-string "{port}" (format "%s" port) command)))

(defalias 'website/preview-project 'website/start-server)
(defalias 'website/run-server 'website/start-server)

(defun website/stop-server (project-name)
  "Stop previewing a project, given its name"
  (interactive
   (list (completing-read "Stop local server for project: "
                          org-publish-project-alist nil t)))
  (let ( (port (website/get-port project-name)) )
    (progn
      (if (equal website/server 'elnode)
          (elnode-stop port)
        (kill-buffer (concat (format "*webserver %s*" project-name))))
      (message "Stopped serving project %s" project-name))))

Deploying

Define a function which invokes the deploy command for the project. All project specifications are stored in website/alist.

The code for deploying has been taken from: https://stackoverflow.com/questions/1453956/which-shell-command-in-emacs-lisp. A more structured solution probably uses call-process, but I did not want to have to manage shell expansions.

This function can be safely moved to the Emacs initialization file, especially when there is more than one project:

(defun website/deploy-project (project-name)
  (interactive
   (list (completing-read "Deploy the following project: "
                          org-publish-project-alist nil t)))
  (let* ( (local-dir (website/get-local-dir project-name))
          (deploy-dir (website/get-deploy-dir project-name))
          (exclude-sources (website/exclude-sources? project-name))
          (buffer (get-buffer-create (concat "*rsync-buffer for " project-name "*"))) )
    (if (and deploy-dir local-dir)
        (progn
          (display-buffer buffer)
          (start-process "process-name"
                         buffer
                         "/usr/bin/rsync"
                         "-crvz"
                         "--exclude=*~"
                         "--exclude=.git"
                         "--exclude=_*"
                         (if exclude-sources "--exclude=*.org" "--include=*.org")
                         "--delete"
                         "--delete-excluded"
                         (file-name-as-directory local-dir) ; add a final slash (otherwise local-dir might be created on the server instead)
                         deploy-dir))
      (message "No deployment command or no local dir specified for %s" project-name))))

What do we provide?

(provide 'website/management)

[fn:1 Notice that this partially overlaps with the :publishing-directory property you need to set for Org Mode publishing to work. However, it simplifies quite a bit the specification of where the HTML files can be found for local previewing the website.]

Footnotes:

1

1