From 0afe98086dcc98f10e7b381931ad1c0b7633e19f Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Thu, 11 Nov 2021 20:59:22 -0800 Subject: [PATCH] Integrating my publishing system using org-publish Seems like I may need to purge my web site source, or maybe use a less-intense system. --- README.org | 1 + bootstrap.org | 5 +- elisp/ox-rss.el | 414 ++++++++++++++++++++++++++++++++++++++++++ ha-org-publishing.org | 201 ++++++++++++++++++++ 4 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 elisp/ox-rss.el create mode 100644 ha-org-publishing.org diff --git a/README.org b/README.org index face674..70c8f78 100644 --- a/README.org +++ b/README.org @@ -19,6 +19,7 @@ This creates [[file:~/.emacs.d/init.el][~/.emacs.d/init.el]] that starts the pro - [[file:ha-org-word-processor.org][ha-org-word-processor.org]] :: attempts to make Org files /visually/ look like what one might see in a word processor, including turning off the colors for headers, and instead increasing their size. - [[file:ha-org-clipboard.org][ha-org-clipboard.org]] :: automatically converting HTML from a clipboard into Org-formatted content. - [[file:ha-org-journaling.org][ha-org-journaling.org]] :: for writing journal entries and tasks. + - [[file:ha-org-publishing.org][ha-org-publishing.org]] :: code for publishing my website, [[http://howardism.org][www.howardism.org]]. - [[file:ha-org-sprint.org][ha-org-sprint.org]] :: functions for working with the my Org-focused sprint files. - [[file:ha-remoting.org][ha-remoting.org]] :: my interface to systems using SSH and Vterm. - [[file:ha-feed-reader.org][ha-feed-reader.org]] :: configuration of elfeed as well as my RSS feeds. diff --git a/bootstrap.org b/bootstrap.org index 8f9caae..e185834 100644 --- a/bootstrap.org +++ b/bootstrap.org @@ -49,7 +49,8 @@ Let's get the Straight project working with =use-package=: While that enables the =:straight t= extension to =use-package=, let's just have that be the default: #+BEGIN_SRC emacs-lisp (use-package straight - :custom (straight-use-package-by-default t)) + :custom (straight-use-package-by-default t + straight-default-vc 'git)) #+END_SRC See the details in [[https://dev.to/jkreeftmeijer/emacs-package-management-with-straight-el-and-use-package-3oc8][this essay]]. ** Basic Libraries @@ -113,7 +114,7 @@ The following loads the rest of my org-mode literate files. I add them as they a "ha-org-word-processor.org" "ha-org-clipboard.org" "ha-org-journaling.org" - ;; "org-publishing.org" + "ha-org-publishing.org" "ha-org-sprint.org" "ha-capturing-notes.org" "ha-programming.org" diff --git a/elisp/ox-rss.el b/elisp/ox-rss.el new file mode 100644 index 0000000..4cdfe0e --- /dev/null +++ b/elisp/ox-rss.el @@ -0,0 +1,414 @@ +;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine + +;; Copyright (C) 2013-2015 Bastien Guerry + +;; Author: Bastien Guerry +;; Keywords: org, wp, blog, feed, rss + +;; This file is not yet part of GNU Emacs. + +;; This program is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; This library implements a RSS 2.0 back-end for Org exporter, based on +;; the `html' back-end. +;; +;; It requires Emacs 24.1 at least. +;; +;; It provides two commands for export, depending on the desired output: +;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss' +;; (as a ".xml" file). +;; +;; This backend understands two new option keywords: +;; +;; #+RSS_EXTENSION: xml +;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg +;; +;; It uses #+HTML_LINK_HOME: to set the base url of the feed. +;; +;; Exporting an Org file to RSS modifies each top-level entry by adding a +;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add +;; an ID property, later used as the guid for the feed's item. +;; +;; The top-level headline is used as the title of each RSS item unless +;; an RSS_TITLE property is set on the headline. +;; +;; You typically want to use it within a publishing project like this: +;; +;; (add-to-list +;; 'org-publish-project-alist +;; '("homepage_rss" +;; :base-directory "~/myhomepage/" +;; :base-extension "org" +;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png" +;; :html-link-home "http://lumiere.ens.fr/~guerry/" +;; :html-link-use-abs-url t +;; :rss-extension "xml" +;; :publishing-directory "/home/guerry/public_html/" +;; :publishing-function (org-rss-publish-to-rss) +;; :section-numbers nil +;; :exclude ".*" ;; To exclude all files... +;; :include ("index.org") ;; ... except index.org. +;; :table-of-contents nil)) +;; +;; ... then rsync /home/guerry/public_html/ with your server. +;; +;; By default, the permalink for a blog entry points to the headline. +;; You can specify a different one by using the :RSS_PERMALINK: +;; property within an entry. + +;;; Code: + +(require 'ox-html) +(declare-function url-encode-url "url-util" (url)) + +;;; Variables and options + +(defgroup org-export-rss nil + "Options specific to RSS export back-end." + :tag "Org RSS" + :group 'org-export + :version "24.4" + :package-version '(Org . "8.0")) + +(defcustom org-rss-image-url "http://orgmode.org/img/org-mode-unicorn-logo.png" + "The URL of the an image for the RSS feed." + :group 'org-export-rss + :type 'string) + +(defcustom org-rss-extension "xml" + "File extension for the RSS 2.0 feed." + :group 'org-export-rss + :type 'string) + +(defcustom org-rss-categories 'from-tags + "Where to extract items category information from. +The default is to extract categories from the tags of the +headlines. When set to another value, extract the category +from the :CATEGORY: property of the entry." + :group 'org-export-rss + :type '(choice + (const :tag "From tags" from-tags) + (const :tag "From the category property" from-category))) + +(defcustom org-rss-use-entry-url-as-guid t + "Use the URL for the metatag? +When nil, Org will create ids using `org-icalendar-create-uid'." + :group 'org-export-rss + :type 'boolean) + +;;; Define backend + +(org-export-define-derived-backend 'rss 'html + :menu-entry + '(?r "Export to RSS" + ((?R "As RSS buffer" + (lambda (a s v b) (org-rss-export-as-rss a s v))) + (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v))) + (?o "As RSS file and open" + (lambda (a s v b) + (if a (org-rss-export-to-rss t s v) + (org-open-file (org-rss-export-to-rss 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 + (:rss-extension "RSS_EXTENSION" nil org-rss-extension) + (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url) + (:rss-categories nil nil org-rss-categories)) + :filters-alist '((:filter-final-output . org-rss-final-function)) + :translate-alist '((headline . org-rss-headline) + (comment . (lambda (&rest args) "")) + (comment-block . (lambda (&rest args) "")) + (timestamp . (lambda (&rest args) "")) + (plain-text . org-rss-plain-text) + (section . org-rss-section) + (template . org-rss-template))) + +;;; Export functions + +;;;###autoload +(defun org-rss-export-as-rss (&optional async subtreep visible-only) + "Export current buffer to a RSS buffer. + +If narrowing is active in the current buffer, only export its +narrowed part. + +If a region is active, export that region. + +A non-nil optional argument ASYNC means the process should happen +asynchronously. The resulting buffer should be accessible +through the `org-export-stack' interface. + +When optional argument SUBTREEP is non-nil, export the sub-tree +at point, extracting information from the headline properties +first. + +When optional argument VISIBLE-ONLY is non-nil, don't export +contents of hidden elements. + +Export is done in a buffer named \"*Org RSS 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-icalendar-create-uid file 'warn-user) + (org-rss-add-pubdate-property)) + (org-export-to-buffer 'rss "*Org RSS Export*" + async subtreep visible-only nil nil (lambda () (text-mode)))) + +;;;###autoload +(defun org-rss-export-to-rss (&optional async subtreep visible-only) + "Export current buffer to a RSS file. + +If narrowing is active in the current buffer, only export its +narrowed part. + +If a region is active, export that region. + +A non-nil optional argument ASYNC means the process should happen +asynchronously. The resulting file should be accessible through +the `org-export-stack' interface. + +When optional argument SUBTREEP is non-nil, export the sub-tree +at point, extracting information from the headline properties +first. + +When optional argument VISIBLE-ONLY is non-nil, don't export +contents of hidden elements. + +Return output file's name." + (interactive) + (let ((file (buffer-file-name (buffer-base-buffer)))) + (org-icalendar-create-uid file 'warn-user) + (org-rss-add-pubdate-property)) + (let ((outfile (org-export-output-file-name + (concat "." org-rss-extension) subtreep))) + (org-export-to-file 'rss outfile async subtreep visible-only))) + +;;;###autoload +(defun org-rss-publish-to-rss (plist filename pub-dir) + "Publish an org file to RSS. + +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 + (org-icalendar-create-uid filename 'warn-user) + (org-rss-add-pubdate-property) + (write-file filename)) + (find-file filename) + (org-icalendar-create-uid filename 'warn-user) + (org-rss-add-pubdate-property) + (write-file filename) (kill-buffer))) + (org-publish-org-to + 'rss filename (concat "." org-rss-extension) plist pub-dir)) + +;;; Main transcoding functions + +(defun org-rss-headline (headline contents info) + "Transcode HEADLINE element into RSS format. +CONTENTS is the headline contents. INFO is a plist used as a +communication channel." + (unless (or (org-element-property :footnote-section-p headline) + ;; Only consider first-level headlines + (> (org-export-get-relative-level headline info) 1)) + (let* ((author (and (plist-get info :with-author) + (let ((auth (plist-get info :author))) + (and auth (org-export-data auth info))))) + (htmlext (plist-get info :html-extension)) + (hl-number (org-export-get-headline-number headline info)) + (hl-home (file-name-as-directory (plist-get info :html-link-home))) + (hl-pdir (plist-get info :publishing-directory)) + (hl-perm (org-element-property :RSS_PERMALINK headline)) + (anchor (org-export-get-reference headline info)) + (category (org-rss-plain-text + (or (org-element-property :CATEGORY headline) "") info)) + (pubdate0 (org-element-property :PUBDATE headline)) + (pubdate (let ((system-time-locale "C")) + (if pubdate0 + (format-time-string + "%a, %d %b %Y %H:%M:%S %z" + (org-time-string-to-time pubdate0))))) + (title (or (org-element-property :RSS_TITLE headline) + (replace-regexp-in-string + org-bracket-link-regexp + (lambda (m) (or (match-string 3 m) + (match-string 1 m))) + (org-element-property :raw-value headline)))) + (publink + (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm)) + (concat + (or hl-home hl-pdir) + (file-name-nondirectory + (file-name-sans-extension + (plist-get info :input-file))) "." htmlext "#" anchor))) + (guid (if org-rss-use-entry-url-as-guid + publink + (org-rss-plain-text + (or (org-element-property :ID headline) + (org-element-property :CUSTOM_ID headline) + publink) + info)))) + (if (not pubdate0) "" ;; Skip entries with no PUBDATE prop + (format + (concat + "\n" + "%s\n" + "%s\n" + "%s\n" + "%s\n" + "%s\n" + (org-rss-build-categories headline info) "\n" + "\n" + "\n") + title publink author guid pubdate contents))))) + +(defun org-rss-build-categories (headline info) + "Build categories for the RSS item." + (if (eq (plist-get info :rss-categories) 'from-tags) + (mapconcat + (lambda (c) (format "" c)) + (org-element-property :tags headline) + "\n") + (let ((c (org-element-property :CATEGORY headline))) + (format "" c)))) + +(defun org-rss-template (contents info) + "Return complete document string after RSS conversion. +CONTENTS is the transcoded contents string. INFO is a plist used +as a communication channel." + (concat + (format "" + (symbol-name org-html-coding-system)) + "\n" + "" + (org-rss-build-channel-info info) "\n" + contents + "\n" + "")) + +(defun org-rss-build-channel-info (info) + "Build the RSS channel information." + (let* ((system-time-locale "C") + (title (plist-get info :title)) + (email (org-export-data (plist-get info :email) info)) + (author (and (plist-get info :with-author) + (let ((auth (plist-get info :author))) + (and auth (org-export-data auth info))))) + (date (format-time-string "%a, %d %b %Y %H:%M:%S %z")) ;; RFC 882 + (description (org-export-data (plist-get info :description) info)) + (lang (plist-get info :language)) + (keywords (plist-get info :keywords)) + (rssext (plist-get info :rss-extension)) + (blogurl (or (plist-get info :html-link-home) + (plist-get info :publishing-directory))) + (image (url-encode-url (plist-get info :rss-image-url))) + (ifile (plist-get info :input-file)) + (publink + (concat (file-name-as-directory blogurl) + (file-name-nondirectory + (file-name-sans-extension ifile)) + "." rssext))) + (format + "\n%s + +%s + +%s +%s +%s +%s +%s (%s) + +%s +%s +%s + +" + title publink blogurl description lang date date + (concat (format "Emacs %d.%d" + emacs-major-version + emacs-minor-version) + " Org-mode " (org-version)) + email author image title blogurl))) + +(defun org-rss-section (section contents info) + "Transcode SECTION element into RSS format. +CONTENTS is the section contents. INFO is a plist used as +a communication channel." + contents) + +(defun org-rss-timestamp (timestamp contents info) + "Transcode a TIMESTAMP object from Org to RSS. +CONTENTS is nil. INFO is a plist holding contextual +information." + (org-html-encode-plain-text + (org-timestamp-translate timestamp))) + +(defun org-rss-plain-text (contents info) + "Convert plain text into RSS encoded text." + (let (output) + (setq output (org-html-encode-plain-text contents) + output (org-export-activate-smart-quotes + output :html info)))) + +;;; Filters + +(defun org-rss-final-function (contents backend info) + "Prettify the RSS output." + (with-temp-buffer + (xml-mode) + (insert contents) + (indent-region (point-min) (point-max)) + (buffer-substring-no-properties (point-min) (point-max)))) + +;;; Miscellaneous + +(defun org-rss-add-pubdate-property () + "Set the PUBDATE property for top-level headlines." + (let (msg) + (org-map-entries + (lambda () + (let* ((entry (org-element-at-point)) + (level (org-element-property :level entry))) + (when (= level 1) + (unless (org-entry-get (point) "PUBDATE") + (setq msg t) + (org-set-property + "PUBDATE" (format-time-string + (cdr org-time-stamp-formats))))))) + nil nil 'comment 'archive) + (when msg + (message "Property PUBDATE added to top-level entries in %s" + (buffer-file-name)) + (sit-for 2)))) + +(provide 'ox-rss) + +;;; ox-rss.el ends here diff --git a/ha-org-publishing.org b/ha-org-publishing.org new file mode 100644 index 0000000..a041f0c --- /dev/null +++ b/ha-org-publishing.org @@ -0,0 +1,201 @@ +#+TITLE: Publishing my Website with Org +#+AUTHOR: Howard X. Abrams +#+EMAIL: howard.abrams@gmail.com +#+DATE: 2020-12-22 +#+FILETAGS: :emacs: + +A literate programming file for publishing my website using org. + +#+BEGIN_SRC emacs-lisp :exports none +;;; org-publishing.el --- Publishing my website using org. -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2020 Howard X. Abrams +;; +;; Author: Howard X. Abrams +;; Maintainer: Howard X. Abrams +;; Created: December 22, 2020 +;; +;; This file is not part of GNU Emacs. +;; +;; *NB:* Do not edit this file. Instead, edit the original literate file at: +;; ~/other/hamacs/org-publishing.org +;; And tangle the file to recreate this one. +;; +;;; Code: +#+END_SRC +* Introduction +While the Emacs community have a plethora of options for generating a static website from org-formatted files, I keep my pretty simple, and just use the standard =org-publish= feature. + +While the following packages come with Emacs, they aren't necessarily loaded: + +#+BEGIN_SRC emacs-lisp :results silent +(require 'ox-rss) +(require 'ox-publish) +#+END_SRC + +Variable settings: + +#+BEGIN_SRC emacs-lisp +(setq org-export-with-broken-links t + org-mode-websrc-directory "/Volumes/Personal/dropbox/website" + org-mode-publishing-directory (concat (getenv "HOME") "/website-public/")) +#+END_SRC +* The Projects +I separate my /website/ into distinct projects separately built: + + - =blog-content= :: The bulk of rendering my =website= org files into HTML + - =blog-static= :: All of the assets, like images are easily copied in place + - =blog-rss= :: Regenerate the feeder files + - =org-notes= :: Optionally render a non-web site collection of notes. + +#+BEGIN_SRC emacs-lisp + (setq org-publish-project-alist + `(("all" + :components ("blog-content" "blog-static" "org-notes" "blog-rss")) + + ("blog-content" + :base-directory ,org-mode-websrc-directory + :base-extension "org" + :publishing-directory ,org-mode-publishing-directory + :recursive t + :publishing-function org-html-publish-to-html + :preparation-function org-mode-blog-prepare + :export-with-tags nil + :headline-levels 4 + :auto-preamble t + :auto-postamble nil + :auto-sitemap t + :sitemap-title "Howardisms" + :section-numbers nil + :table-of-contents nil + :with-toc nil + :with-author nil + :with-creator nil + :with-tags nil + :with-smart-quotes t + + :html-doctype "html5" + :html-html5-fancy t + ;; :html-preamble org-mode-blog-preamble + ;; :html-postamble org-mode-blog-postamble + ;; :html-postamble "
" + :html-head " + + + + \n" + :html-head-extra " + + + + " + :html-head-include-default-style nil) + + ("blog-static" + :base-directory ,org-mode-websrc-directory + :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|svg" + :publishing-directory ,org-mode-publishing-directory + :recursive t + :publishing-function org-publish-attachment) + + ("blog-rss" + :base-directory ,org-mode-websrc-directory + :base-extension "org" + :rss-image-url "http://howardism.org/img/dragon-head.png" + :publishing-directory ,org-mode-publishing-directory + :publishing-function (org-rss-publish-to-rss) + :html-link-home "http://www.howardism.org/" + :html-link-use-abs-url t + :with-toc nil + :exclude ".*" + :include ("index.org")) + + ("org-notes" + :base-directory "~/technical/" + :base-extension "org" + :publishing-directory ,(concat org-mode-publishing-directory "/notes/") + :recursive t + :publishing-function org-html-publish-to-html + :headline-levels 4 ; Just the default for this project. + :auto-preamble t + :auto-sitemap t ; Generate sitemap.org automagically... + :makeindex t + :section-numbers nil + :style " " + :table-of-contents nil + :with-author nil + :with-creator nil + :with-tags nil) + + ("org-notes-static" + :base-directory "~/technical/" + :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf" + :publishing-directory ,(concat org-mode-publishing-directory "/other/") + :recursive t + :publishing-function org-publish-attachment))) +#+END_SRC +* Including Sections +In the project definitions, I reference a =pre-= and =postamble= that allow me to inject some standard HTML file headers and footers: + +#+BEGIN_SRC emacs-lisp +(defun org-mode-blog-preamble (options) + "The function that creates the preamble top section for the blog. + OPTIONS contains the property list from the org-mode export." + (message "Preamble options: %s" (princ options)) + (let ((base-directory (plist-get options :base-directory))) + (org-babel-with-temp-filebuffer (expand-file-name "top-bar.html" base-directory) (buffer-string)))) + +(defun org-mode-blog-postamble (options) + "The function that creates the postamble, or bottom section for the blog. + OPTIONS contains the property list from the org-mode export." + (let ((base-directory (plist-get options :base-directory))) + (org-babel-with-temp-filebuffer (expand-file-name "bottom.html" base-directory) (buffer-string)))) +#+END_SRC + +Another helper function for the content of website is to make sure to update =index.org=, so that the RSS gets generated. +#+BEGIN_SRC emacs-lisp +(defun org-mode-blog-prepare (&optional options) + "`index.org' should always be exported so touch the file before publishing." + (let* ((base-directory (plist-get options :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))) +#+END_SRC +* Keybindings +Make it easy to publish all or just some of my website: +#+BEGIN_SRC emacs-lisp + (general-evil-define-key 'normal org-mode-map + :prefix "SPC m" + "p" '(:ignore t :which-key "publishing") + "p a" '("all" . org-publish-all) + "p p" '("project" . org-publish-project)) +#+END_SRC + +And let's put a /leader key/ sequence for my favorite file on my website: +#+BEGIN_SRC emacs-lisp + (ha-leader + "f h" '(:ignore t :which-key "howards") + "f h w" '("website index" . (lambda () + (find-file (expand-file-name "index.org" "~/website"))))) +#+END_SRC +* Technical Artifacts :noexport: +Let's =provide= a name so we can =require= it: + +#+BEGIN_SRC emacs-lisp :exports none +(provide 'ha-org-publishing) +;;; ha-org-publishing.el ends here +#+END_SRC + +Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~ + +#+DESCRIPTION: A literate programming version for publishing my website using org. + +#+PROPERTY: header-args:sh :tangle no +#+PROPERTY: header-args:emacs-lisp :tangle yes +#+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes + +#+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil +#+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil +#+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js