Org Mode: Export HTML, copy files, and serve the results via simple-httpd so that media files work
| emacs, org
In Org Mode, when you use "Export to HTML - As HTML file and open", the resulting HTML file is loaded using a file:// URL. This means you can't load any media files. In my post about pronunciation practice, I wanted to test the playback without waiting for my 11ty-based static site generator to churn through the files.
simple-httpd lets you run a web server from Emacs. By default, the httpd-root is ~/public_html and httpd-port is 8085, but you can configure it to be somewhere else. Here I set it up to create a new temporary directory, and to delete that directory afterwards.
(use-package simple-httpd
:config
(setq httpd-root (make-temp-file "httpd" t))
:hook
(httpd-stop . my-simple-httpd-remove-temporary-root)
(kill-emacs . httpd-stop))
(defun my-simple-httpd-remove-temporary-root ()
"Remove `httpd-root' only if it's a temporary directory."
(when (file-in-directory-p httpd-root temporary-file-directory)
(delete-directory httpd-root t)))
The following code exports your Org buffer or subtree to a file in that directory, copies all the referenced local files (if they're newer) and updates the links in the HTML, and then serves it via simple-httpd. Note that it just overwrites everything without confirmation, so if you refer to files with the same name, only the last one will be kept.
(with-eval-after-load 'ox
(org-export-define-derived-backend 'my-html-served 'html
:menu-entry
'(?s "Export to HTML and Serve"
((?b "Buffer" my-org-serve--buffer)
(?s "Subtree" my-org-serve--subtree)))))
(defun my-org-serve--buffer (&optional async _subtreep visible-only body-only ext-plist)
(my-org-export-and-serve nil))
(defun my-org-serve--subtree (&optional async _subtreep visible-only body-only ext-plist)
(my-org-export-and-serve t))
;; Based on org-11ty--copy-files-and-replace-links
;; Might be a good idea to use something DOM-based instead
(defun my-html-copy-files-and-replace-links (info &optional destination-dir)
(let ((file-regexp "\\(?:src\\|href\\|poster\\)=\"\\(\\(file:\\)?.*?\\)\"")
(destination-dir (or destination-dir (file-name-directory (plist-get info :file-path))))
file-all-urls file-name beg
new-file file-re
unescaped)
(unless (file-directory-p destination-dir)
(make-directory destination-dir t))
(unless (file-directory-p destination-dir)
(error "%s is not a directory." destination-dir))
(save-excursion
(goto-char (point-min))
(while (re-search-forward file-regexp nil t)
(setq file-name (or (match-string 1) (match-string 2)))
(unless (or (string-match "^#" file-name)
(get-text-property 0 'changed file-name))
(setq file-name
(replace-regexp-in-string
"\\?.+" ""
(save-match-data (if (string-match "^file:" file-name)
(substring file-name 7)
file-name))))
(setq unescaped
(replace-regexp-in-string
"%23" "#"
file-name))
(setq new-file (concat
(if info (plist-get info :permalink) "")
(file-name-nondirectory unescaped)))
(unless (org-url-p file-name)
(let ((new-file-name (expand-file-name (file-name-nondirectory unescaped)
destination-dir)))
(condition-case err
(when (or (not (file-exists-p new-file-name))
(file-newer-than-file-p unescaped new-file-name))
(copy-file unescaped new-file-name t))
(error nil))
(when (file-exists-p new-file-name)
(save-excursion
(goto-char (point-min))
(setq file-re (concat "\\(?: src=\"\\| href=\"\\| poster=\"\\)\\(\\(?:file://\\)?" (regexp-quote file-name) "\\)"))
(while (re-search-forward file-re nil t)
(replace-match
(propertize
(save-match-data (replace-regexp-in-string "#" "%23" new-file))
'changed t)
t t nil 1)))))))))))
(defun my-org-export-and-serve (&optional subtreep)
"Export current org buffer (or subtree if SUBTREEP) to HTML and serve via simple-httpd."
(interactive "P")
(require 'simple-httpd)
(httpd-stop)
(unless httpd-root (error "Set `httpd-root'."))
(unless (file-directory-p httpd-root)
(make-directory httpd-root t))
(unless (file-directory-p httpd-root)
(error "%s is not a directory." httpd-root))
(let* ((out-file (expand-file-name (concat (file-name-base (buffer-file-name)) ".html")
httpd-root))
(html-file (org-export-to-file 'my-html-served out-file nil subtreep)))
;; Copy all the files and rewrite all the links
(with-temp-file out-file
(insert-file-contents out-file)
(my-html-copy-files-and-replace-links
`(:permalink "/") httpd-root))
(httpd-start)
(browse-url (format "http://localhost:%d/%s"
httpd-port
(file-name-nondirectory html-file)))))
Now I can use C-c C-e (org-export-dispatch), select the subtree with C-s, and use s s
to export a subtree to a webserver and have all the media files work. This took 0.46 seconds for my post on pronunciation practice and automatically opens the page in a browser window. In comparison, my 11ty static site generator took 5.18 seconds for a subset of my site (1630 files copied, 214 files generated), and I haven't yet hooked up monitoring it to Emacs, so I have to take an extra step to open the page in the browser when I think it's finished. I think exporting to HTML and serving it with simple-httpd will be much easier for simple cases like this, and then I can export to 11ty once I'm done with the basic checks.