publish.el 14.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
;;; publish --- Publish org files to GitLab Pages

;;; Commentary:

;; This file takes care of exporting org files to the public directory.
;; Images and such are also exported without any processing.

;;; Code:

(require 'package)
(package-initialize)
(unless package-archive-contents
to1ne's avatar
to1ne committed
13
14
  (add-to-list 'package-archives '("nongnu" . "https://elpa.nongnu.org/nongnu/") t)
  (add-to-list 'package-archives '("melpa"  . "https://melpa.org/packages/")     t)
15
  (package-refresh-contents))
to1ne's avatar
to1ne committed
16
(dolist (pkg '(org org-contrib htmlize))
17
18
19
  (unless (package-installed-p pkg)
    (package-install pkg)))

to1ne's avatar
to1ne committed
20
(require 'cl-lib)
21
22
(require 'org)
(require 'ox-publish)
to1ne's avatar
to1ne committed
23
(require 'ox-rss)
24

to1ne's avatar
to1ne committed
25
(defvar rw-url "https://writepermission.com/"
to1ne's avatar
to1ne committed
26
27
28
29
30
  "The URL where this site will be published.")

(defvar rw-title "rw-r--r-- | writepermission.com"
  "The title of this site.")

to1ne's avatar
to1ne committed
31
32
33
34
35
36
37
(defvar rw--root
  (locate-dominating-file default-directory
                          (lambda (dir)
                            (seq-every-p
                             (lambda (file) (file-exists-p (expand-file-name file dir)))
                             '(".git" "content" "css" "elisp" "favicon.ico" "layouts" "posts"))))
  "Root directory of this project.")
38
39

(defvar rw--layouts-directory
to1ne's avatar
to1ne committed
40
  (expand-file-name "layouts" rw--root)
41
42
43
44
45
46
47
48
49
  "Directory where layouts are found.")

(defvar rw--site-attachments
  (regexp-opt '("jpg" "jpeg" "gif" "png" "svg"
                "ico" "cur" "css" "js"
                "eot" "woff" "woff2" "ttf"
                "html" "pdf"))
  "File types that are published as static files.")

to1ne's avatar
to1ne committed
50
51
52
53
54
55
(defun rw--pre/postamble-format (type)
  "Return the content for the pre/postamble of TYPE."
  `(("en" ,(with-temp-buffer
             (insert-file-contents (expand-file-name (format "%s.html" type) rw--layouts-directory))
             (buffer-string)))))

to1ne's avatar
to1ne committed
56
57
58
59
(defun rw/format-date-subtitle (file project)
  "Format the date found in FILE of PROJECT."
  (format-time-string "posted on %Y-%m-%d" (org-publish-find-date file project)))

to1ne's avatar
to1ne committed
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
(defun rw/org-html-close-tag (tag &rest attrs)
  "Return close-tag for string TAG.
ATTRS specify additional attributes."
  (concat "<" tag " "
          (mapconcat (lambda (attr)
                       (format "%s=\"%s\"" (car attr) (cadr attr)))
                     attrs
                     " ")
	  ">"))

(defun rw/html-head-extra (file project)
  "Return <meta> elements for nice unfurling on Twitter and Slack."
  (let* ((info (cdr project))
         (org-export-options-alist
          `((:title "TITLE" nil nil parse)
            (:date "DATE" nil nil parse)
            (:author "AUTHOR" nil ,(plist-get info :author) space)
            (:description "DESCRIPTION" nil nil newline)
            (:keywords "KEYWORDS" nil nil space)
            (:meta-image "META_IMAGE" nil ,(plist-get info :meta-image) nil)
            (:meta-type "META_TYPE" nil ,(plist-get info :meta-type) nil)))
         (title (org-publish-find-title file project))
         (date (org-publish-find-date file project))
         (author (org-publish-find-property file :author project))
         (description (org-publish-find-property file :description project))
         (link-home (file-name-as-directory (plist-get info :html-link-home)))
         (extension (or (plist-get info :html-extension) org-html-extension))
	 (rel-file (org-publish-file-relative-name file info))
         (full-url (concat link-home (file-name-sans-extension rel-file) "." extension))
         (image (concat link-home (org-publish-find-property file :meta-image project)))
         (favicon (concat link-home "favicon.ico"))
         (type (org-publish-find-property file :meta-type project)))
    (mapconcat 'identity
               `(,(rw/org-html-close-tag "link" '(rel icon) '(type image/x-icon) `(href ,favicon))
to1ne's avatar
to1ne committed
94
                 ,(rw/org-html-close-tag "link" '(rel alternate) '(type application/rss+xml) '(href "rss.xml") '(title "RSS feed"))
to1ne's avatar
to1ne committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
                 ,(rw/org-html-close-tag "meta" '(property og:title) `(content ,title))
                 ,(rw/org-html-close-tag "meta" '(property og:url) `(content ,full-url))
                 ,(and description
                       (rw/org-html-close-tag "meta" '(property og:description) `(content ,description)))
                 ,(rw/org-html-close-tag "meta" '(property og:image) `(content ,image))
                 ,(rw/org-html-close-tag "meta" '(property og:type) `(content ,type))
                 ,(and (equal type "article")
                       (rw/org-html-close-tag "meta" '(property article:author) `(content ,author)))
                 ,(and (equal type "article")
                       (rw/org-html-close-tag "meta" '(property article:published_time) `(content ,(format-time-string "%FT%T%z" date))))

                 ,(rw/org-html-close-tag "meta" '(property twitter:title) `(content ,title))
                 ,(rw/org-html-close-tag "meta" '(property twitter:url) `(content ,full-url))
                 ,(rw/org-html-close-tag "meta" '(property twitter:image) `(content ,image))
                 ,(and description
                       (rw/org-html-close-tag "meta" '(property twitter:description) `(content ,description)))
                 ,(and description
                       (rw/org-html-close-tag "meta" '(property twitter:card) '(content summary)))
                 )
               "\n")))

to1ne's avatar
to1ne committed
116
117
118
119
120
(defun rw/org-html-publish-to-html (plist filename pub-dir)
  "Wrapper function to publish an file to html.

PLIST contains the properties, FILENAME the source file and
  PUB-DIR the output directory."
to1ne's avatar
to1ne committed
121
122
123
124
125
126
  (let ((project (cons 'rw plist)))
    (plist-put plist :subtitle
               (rw/format-date-subtitle filename project))
    (plist-put plist :html-head-extra
               (rw/html-head-extra filename project))
    (org-html-publish-to-html plist filename pub-dir)))
to1ne's avatar
to1ne committed
127

to1ne's avatar
to1ne committed
128
(defun rw/org-html-format-headline-function (todo todo-type priority text tags info)
to1ne's avatar
to1ne committed
129
130
131
132
133
134
135
136
137
  "Format a headline with a link to itself.

This function takes six arguments:
TODO      the todo keyword (string or nil).
TODO-TYPE the type of todo (symbol: ‘todo’, ‘done’, nil)
PRIORITY  the priority of the headline (integer or nil)
TEXT      the main headline text (string).
TAGS      the tags (string or nil).
INFO      the export options (plist)."
to1ne's avatar
to1ne committed
138
139
  (let* ((headline (get-text-property 0 :parent text))
         (id (or (org-element-property :CUSTOM_ID headline)
140
                 (org-export-get-reference headline info)
to1ne's avatar
to1ne committed
141
142
143
144
145
146
                 (org-element-property :ID headline)))
         (link (if id
                   (format "<a href=\"#%s\">%s</a>" id text)
                 text)))
    (org-html-format-headline-default-function todo todo-type priority link tags info)))

to1ne's avatar
to1ne committed
147
(defun rw/org-publish-sitemap (title list)
to1ne's avatar
to1ne committed
148
  "Generate sitemap as a string, having TITLE.
149
LIST is an internal representation for the files to include, as
to1ne's avatar
to1ne committed
150
returned by `org-list-to-lisp'."
to1ne's avatar
to1ne committed
151
  (let ((filtered-list (cl-remove-if (lambda (x)
to1ne's avatar
to1ne committed
152
153
                                       (and (sequencep x) (null (car x))))
                                     list)))
to1ne's avatar
to1ne committed
154
    (concat "#+TITLE: " title "\n"
to1ne's avatar
to1ne committed
155
156
157
158
            "#+OPTIONS: title:nil\n"
            "#+META_TYPE: website\n"
            "#+DESCRIPTION: Toon Claes' personal blog\n"
            "\n#+ATTR_HTML: :class sitemap\n"
to1ne's avatar
to1ne committed
159
160
            ; TODO use org-list-to-subtree instead
            (org-list-to-org filtered-list))))
161
162
163
164
165

(defun rw/org-publish-sitemap-entry (entry style project)
  "Format for sitemap ENTRY, as a string.
ENTRY is a file name.  STYLE is the style of the sitemap.
PROJECT is the current project."
to1ne's avatar
to1ne committed
166
167
168
169
170
  (unless (equal entry "404.org")
    (format "[[file:%s][%s]] /%s/"
            entry
            (org-publish-find-title entry project)
            (rw/format-date-subtitle entry project))))
171

to1ne's avatar
to1ne committed
172
173
(defun rw/format-rss-feed-entry (entry style project)
  "Format ENTRY for the RSS feed.
to1ne's avatar
to1ne committed
174
175
ENTRY is a file name.  STYLE is either 'list' or 'tree'.
PROJECT is the current project."
to1ne's avatar
to1ne committed
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
  (cond ((not (directory-name-p entry))
         (let* ((file (org-publish--expand-file-name entry project))
                (title (org-publish-find-title entry project))
                (date (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)))
                (link (concat (file-name-sans-extension entry) ".html")))
           (with-temp-buffer
             (insert (format "* [[file:%s][%s]]\n" file title))
             (org-set-property "RSS_PERMALINK" link)
             (org-set-property "PUBDATE" date)
             (insert-file-contents file)
             (buffer-string))))
        ((eq style 'tree)
         ;; Return only last subdir.
         (file-name-nondirectory (directory-file-name entry)))
        (t entry)))

(defun rw/format-rss-feed (title list)
  "Generate RSS feed, as a string.
TITLE is the title of the RSS feed.  LIST is an internal
representation for the files to include, as returned by
`org-list-to-lisp'.  PROJECT is the current project."
  (concat "#+TITLE: " title "\n\n"
to1ne's avatar
to1ne committed
198
          (org-list-to-subtree list 1 '(:icount "" :istart ""))))
to1ne's avatar
to1ne committed
199
200

(defun rw/org-rss-publish-to-rss (plist filename pub-dir)
to1ne's avatar
to1ne committed
201
202
  "Publish RSS with PLIST, only when FILENAME is 'rss.org'.
PUB-DIR is when the output will be placed."
to1ne's avatar
to1ne committed
203
  (if (equal "rss.org" (file-name-nondirectory filename))
to1ne's avatar
to1ne committed
204
205
      (org-rss-publish-to-rss plist filename pub-dir)))

to1ne's avatar
to1ne committed
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
(defun rw/publish-redirect (plist filename pub-dir)
  "Generate redirect files from the old routes to the new.
PLIST contains the project info, FILENAME is the file to publish
and PUB-DIR the output directory."
  (let* ((regexp (org-make-options-regexp '("REDIRECT_FROM")))
         (from (with-temp-buffer
                 (insert-file-contents filename)
                 (if (re-search-forward regexp nil t)
		     (org-element-property :value (org-element-at-point))))))
    (when from
      (let* ((to-name (file-name-sans-extension (file-name-nondirectory filename)))
             (to-file (format "/%s.html" to-name))
             (from-dir (concat pub-dir from))
             (from-file (concat from-dir "index.html"))
             (other-dir (concat pub-dir to-name))
             (other-file (concat other-dir "/index.html"))
             (to (concat (file-name-sans-extension (file-name-nondirectory filename))
                         ".html"))
             (layout (plist-get plist :redirect-layout))
             (content (with-temp-buffer
                        (insert-file-contents layout)
                        (while (re-search-forward "REDIRECT_TO" nil t)
                          (replace-match to-file t t))
                        (buffer-string))))
        (make-directory from-dir t)
        (make-directory other-dir t)
        (with-temp-file from-file
          (insert content)
          (write-file other-file))))))
to1ne's avatar
to1ne committed
235

236

237
238
239
(defvar rw--publish-project-alist
      (list
       (list "blog-posts"
240
             :base-directory (expand-file-name "posts" rw--root)
241
242
             :base-extension "org"
             :recursive nil
to1ne's avatar
to1ne committed
243
             :exclude (regexp-opt '("rss.org" "index.org"))
to1ne's avatar
to1ne committed
244
             :publishing-function 'rw/org-html-publish-to-html
245
             :publishing-directory (expand-file-name "public" rw--root)
246
247
248
249
250
251
             :html-head-include-default-style nil
             :html-head-include-scripts nil
             :html-htmlized-css-url "css/style.css"
             :html-preamble-format (rw--pre/postamble-format 'preamble)
             :html-postamble t
             :html-postamble-format (rw--pre/postamble-format 'postamble)
to1ne's avatar
to1ne committed
252
             :html-format-headline-function 'rw/org-html-format-headline-function
to1ne's avatar
to1ne committed
253
254
             :html-link-home rw-url
             :html-home/up-format ""
255
256
             :auto-sitemap t
             :sitemap-filename "index.org"
to1ne's avatar
to1ne committed
257
             :sitemap-title rw-title
258
             :sitemap-style 'list
to1ne's avatar
to1ne committed
259
             :sitemap-sort-files 'anti-chronologically
to1ne's avatar
to1ne committed
260
             :sitemap-function 'rw/org-publish-sitemap
to1ne's avatar
to1ne committed
261
262
             :sitemap-format-entry 'rw/org-publish-sitemap-entry
             :author "Toon Claes"
to1ne's avatar
to1ne committed
263
             :email ""
to1ne's avatar
to1ne committed
264
265
             :meta-image "content/rw-r--r--square.png"
             :meta-type "article")
to1ne's avatar
to1ne committed
266
       (list "blog-rss"
267
             :base-directory (expand-file-name "posts" rw--root)
to1ne's avatar
to1ne committed
268
269
             :base-extension "org"
             :recursive nil
to1ne's avatar
to1ne committed
270
             :exclude (regexp-opt '("rss.org" "index.org" "404.org"))
to1ne's avatar
to1ne committed
271
             :publishing-function 'rw/org-rss-publish-to-rss
272
             :publishing-directory (expand-file-name "public" rw--root)
to1ne's avatar
to1ne committed
273
274
275
276
277
278
279
280
281
282
             :rss-extension "xml"
             :html-link-home rw-url
             :html-link-use-abs-url t
             :html-link-org-files-as-html t
             :auto-sitemap t
             :sitemap-filename "rss.org"
             :sitemap-title rw-title
             :sitemap-style 'list
             :sitemap-sort-files 'anti-chronologically
             :sitemap-function 'rw/format-rss-feed
to1ne's avatar
to1ne committed
283
284
285
             :sitemap-format-entry 'rw/format-rss-feed-entry
             :author "Toon Claes"
             :email "")
286
       (list "blog-static"
287
             :base-directory rw--root
to1ne's avatar
to1ne committed
288
             :exclude (regexp-opt '("public/" "layouts/"))
289
             :base-extension rw--site-attachments
290
             :publishing-directory (expand-file-name "public" rw--root)
291
             :publishing-function 'org-publish-attachment
to1ne's avatar
to1ne committed
292
             :recursive t)
to1ne's avatar
to1ne committed
293
       (list "blog-acme"
294
             :base-directory (expand-file-name ".well-known" rw--root)
to1ne's avatar
to1ne committed
295
             :base-extension 'any
296
             :publishing-directory (expand-file-name "public/.well-known" rw--root)
to1ne's avatar
to1ne committed
297
298
             :publishing-function 'org-publish-attachment
             :recursive t)
to1ne's avatar
to1ne committed
299
       (list "blog-redirects"
300
             :base-directory (expand-file-name "posts" rw--root)
to1ne's avatar
to1ne committed
301
302
             :base-extension "org"
             :recursive nil
to1ne's avatar
to1ne committed
303
             :exclude (regexp-opt '("rss.org" "index.org" "404.org"))
to1ne's avatar
to1ne committed
304
             :publishing-function 'rw/publish-redirect
305
306
             :publishing-directory (expand-file-name "public" rw--root)
             :redirect-layout (expand-file-name "layouts/redirect.html" rw--root))
307
       (list "site"
to1ne's avatar
to1ne committed
308
             :components '("blog-posts" "blog-rss" "blog-static" "blog-acme" "blog-redirects"))
309
310
311
312
313
314
315
316
317
318
       ))

(defun rw-publish-all ()
  "Publish the blog to HTML."
  (interactive)
  (let ((org-publish-project-alist       rw--publish-project-alist)
        (org-publish-timestamp-directory "./.timestamps/")
        (org-export-with-section-numbers nil)
        (org-export-with-smart-quotes    t)
        (org-export-with-toc             nil)
to1ne's avatar
to1ne committed
319
        (org-export-with-sub-superscripts '{})
320
321
322
323
324
325
326
327
328
329
330
331
332
        (org-html-divs '((preamble  "header" "top")
                         (content   "main"   "content")
                         (postamble "footer" "postamble")))
        (org-html-container-element         "section")
        (org-html-metadata-timestamp-format "%Y-%m-%d")
        (org-html-checkbox-type             'html)
        (org-html-html5-fancy               t)
        (org-html-validation-link           nil)
        (org-html-doctype                   "html5")
        (org-html-htmlize-output-type       'css))
    (org-publish-all)))

;;; publish.el ends here