]> git.eshelyaron.com Git - esy-publish.git/blob - esy-publish.el
Update copyright years
[esy-publish.git] / esy-publish.el
1 ;;; esy-publish.el --- Simple Static Site Generator -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2023 Eshel Yaron
4
5 ;; Author: Eshel Yaron <me@eshelyaron.com>
6 ;; Maintainer: Eshel Yaron <me@eshelyaron.com>
7 ;; Keywords: languages extensions
8 ;; URL: http://git.eshelyaron.com/gitweb/?p=esy-publish.git
9 ;; Package-Version: 0.12.0
10 ;; Package-Requires: ((emacs "28.2"))
11
12 ;; This file is NOT part of GNU Emacs.
13
14 ;;; Commentary:
15
16 ;; Build a static websites from Org files
17
18 ;;; Code:
19
20 (require 'org)
21 (require 'ox-publish)
22 (require 'ox-html)
23 (require 'ox-texinfo)
24 (require 'org-transclusion)
25 (require 'dom)
26 (require 'xref)
27 (require 'htmlize)
28 (require 'rainbow-delimiters)
29
30 (defvar esy-publish-did-setup-p nil)
31
32 (defvar esy-publish--buffers nil)
33
34 (defvar esy-publish--publishing nil)
35
36 (defvar esy-publish--local-server-process nil)
37
38 (defvar esy-publish-elisp-directory
39   (file-name-as-directory
40    (file-truename
41     (file-name-directory
42      (expand-file-name load-file-name)))))
43
44 (defvar esy-publish-root-directory esy-publish-elisp-directory)
45
46 (defvar esy-publish-drafts-directory
47   (file-name-as-directory (expand-file-name "drafts"
48                                             esy-publish-root-directory)))
49
50 (defvar esy-publish-source-directory
51   (file-name-as-directory (expand-file-name "source"
52                                             esy-publish-root-directory)))
53
54 (defvar esy-publish-notes-source-directory
55   (file-name-as-directory (expand-file-name "notes"
56                                             esy-publish-source-directory)))
57
58 (defvar esy-publish-posts-source-directory
59   (file-name-as-directory (expand-file-name "posts"
60                                             esy-publish-source-directory)))
61
62 (defvar esy-publish-remote-directory
63   "root@direct.eshelyaron.com:/var/www/html")
64
65 (defvar esy-publish-local-directory
66   (file-name-as-directory (expand-file-name "local"
67                                             esy-publish-root-directory)))
68
69 (defvar esy-publish-local-posts-directory
70   (file-name-as-directory (expand-file-name "posts"
71                                             esy-publish-local-directory)))
72
73 (defvar esy-publish-local-man-directory
74   (file-name-as-directory (expand-file-name "man"
75                                             esy-publish-local-directory)))
76
77 (defvar esy-publish-keywords '("emacs"
78                                "prolog"
79                                "language"
80                                "politics"
81                                "ai"
82                                "lisp"))
83
84 (defvar esy-publish-notes-completion-history nil)
85
86 (defconst esy-publish-post-metadata-line
87   (concat "\n"
88           "@@html:" "<div class=\"metadata\">" "@@"
89           "Created on [{{{date}}}], "
90           "last updated [{{{modification-time(%Y-%m-%d, t)}}}]"
91           "@@html:" "</div>" "@@"))
92
93 (defun esy-publish--title-to-file-base-name (title)
94   (downcase (string-join (string-split title (rx (+ (not alnum))) t)
95                          "-")))
96 (defun esy-publish--dom-to-string (&rest doms)
97   (with-temp-buffer
98     (mapc #'dom-print doms)
99     (buffer-string)))
100
101 (defun esy-publish--file-url (file)
102   (concat
103    "https://eshelyaron.com/"
104    (let ((path (file-relative-name file esy-publish-local-directory)))
105      (if (string= (file-name-base file) "index")
106          (file-name-directory path)
107        path))))
108
109 ;;;###autoload
110 (defun esy-publish-setup ()
111   (unless esy-publish-did-setup-p
112     (dolist (cell '(("posts"         . esy-publish-insert-posts-dblock)
113                     ("notes"         . esy-publish-insert-notes-dblock)
114                     ("links-to-note" . esy-publish-insert-links-to-note-dblock)))
115       (org-dynamic-block-define (car cell) (cdr cell)))
116     (org-link-set-parameters "note"
117                              :follow             #'esy-publish-follow-note-link
118                              :export             #'esy-publish-export-note-link
119                              :store              #'esy-publish-store-note-link
120                              :complete           #'esy-publish-complete-note-link
121                              :insert-description #'esy-publish-describe-note-link
122                              :face                'esy-publish-note-link)
123     (function-put 'esy/init-step 'doc-string-elt 2)
124     (setq esy-publish-did-setup-p t)))
125
126 ;;;###autoload
127 (defun esy-publish-create-post (title subtitle description keywords)
128   (interactive (list (read-string "Post title: ")
129                      (read-string "Post subtitle: ")
130                      (read-string "Post description: ")
131                      (completing-read-multiple "Post keywords: " esy-publish-keywords)))
132   (esy-publish-setup)
133   (let* ((date (format-time-string "%F"))
134          (base (concat date "-" (esy-publish--title-to-file-base-name title)))
135          (file (expand-file-name (file-name-with-extension base "org")
136                                  esy-publish-drafts-directory)))
137     (if (file-exists-p file)
138         (error "Post already exists!")
139       (find-file file)
140       (insert
141        "#+TITLE:       " title                      "\n"
142        "#+SUBTITLE:    " subtitle                   "\n"
143        "#+DESCRIPTION: " description                "\n"
144        "#+KEYWORDS:    " (string-join keywords ",") "\n"
145        "#+DATE:        " date                       "\n"
146        esy-publish-post-metadata-line               "\n")
147       (set-buffer-modified-p nil))))
148
149 (defun org-dblock-write:posts (params)
150   (let* ((dir   (plist-get params :dir))
151          (limit (plist-get params :limit))
152          (all-posts (reverse (directory-files dir nil (rx bos digit (+ any) ".org" eos))))
153          (posts (if limit (take limit all-posts) all-posts)))
154     (dolist (post posts)
155       (let ((file (expand-file-name post dir)))
156         (insert "- ["
157                 "[file:" (file-relative-name file) "]"
158                 "[" (substring post 0 10) " ~ " (org-get-title file) "]"
159                 "]\n")))
160     (when (and limit (< limit (length all-posts)))
161       (insert "- [[file:" (file-relative-name dir) "][...older posts]]"))
162     (delete-blank-lines)))
163
164 (defun esy-publish-insert-posts-dblock (limit)
165   (interactive (list (when current-prefix-arg
166                        (prefix-numeric-value current-prefix-arg))))
167   (org-create-dblock (list :name "posts"
168                            :dir  esy-publish-posts-source-directory
169                            :limit limit))
170   (org-update-dblock))
171
172 (defun org-dblock-write:notes (params)
173   (let* ((dir   (plist-get params :dir))
174          (notes (delete "index.org"
175                         (directory-files dir nil
176                                          (rx bos alnum (+ any)
177                                              ".org" eos)))))
178     (dolist (note notes)
179       (let* ((file (expand-file-name note dir))
180              (buffer (find-file-noselect file))
181              (titles (with-current-buffer buffer
182                        (when esy-publish--publishing
183                          (push (current-buffer) esy-publish--buffers))
184                        (org-collect-keywords '("TITLE" "SUBTITLE"))))
185              (title (car (alist-get "TITLE" titles nil nil #'string=)))
186              (subtitle (car (alist-get "SUBTITLE" titles nil nil #'string=))))
187         (insert "- [[file:" (file-relative-name file) "][" title "]] :: " subtitle "\n")))
188     (delete-blank-lines)))
189
190 (defun esy-publish-insert-notes-dblock ()
191   (interactive)
192   (org-create-dblock (list :name "notes"
193                            :dir  esy-publish-notes-source-directory))
194   (org-update-dblock))
195
196 (defun org-dblock-write:links-to-note (params)
197   (let* ((dir   (plist-get params :dir))
198          (files (sort
199                  (delete-dups
200                   (mapcar #'xref-location-group
201                           (mapcar #'xref-match-item-location
202                                   (xref-matches-in-directory (rx "[[note:" (literal (file-name-base (buffer-file-name))) "][")
203                                                              "*.org"
204                                                              dir
205                                                              nil))))
206                  #'string-lessp) ))
207     (dolist (file files)
208       (let* ((full (expand-file-name file dir))
209              (titles (with-current-buffer (find-file-noselect full)
210                        (when esy-publish--publishing
211                          (push (current-buffer) esy-publish--buffers))
212                        (org-collect-keywords '("TITLE" "SUBTITLE"))))
213              (title (car (alist-get "TITLE" titles nil nil #'string=)))
214              (subtitle (car (alist-get "SUBTITLE" titles nil nil #'string=))))
215         (insert "- [[file:" (file-relative-name full) "][" title "]] :: " subtitle "\n")))
216     (delete-blank-lines)))
217
218 (defun esy-publish-insert-links-to-note-dblock ()
219   (interactive)
220   (org-create-dblock (list :name "links-to-note"
221                            :dir  esy-publish-notes-source-directory))
222   (org-update-dblock))
223
224 (defun esy-publish-follow-note-link (path arg)
225   (org-link-open-as-file
226    (expand-file-name (file-name-with-extension path "org")
227                      esy-publish-notes-source-directory)
228    arg))
229
230 (defun esy-publish-export-note-link (path description backend &optional _info)
231   (when (eq backend 'html)
232     (esy-publish--dom-to-string
233      `(a ((href . ,(concat "/notes/" path ".html"))
234           (class . "note-link")
235           (title . ,(let* ((file   (expand-file-name (file-name-with-extension path "org")
236                                                      esy-publish-notes-source-directory))
237                            (buffer (find-file-noselect file))
238                            (titles (with-current-buffer buffer
239                                      (org-collect-keywords '("TITLE" "SUBTITLE"))))
240                            (title (car (alist-get "TITLE" titles nil nil #'string=)))
241                            (subtitle (car (alist-get "SUBTITLE" titles nil nil #'string=))))
242                       (concat "Notes about " title " (" subtitle ")"))))
243          ,description))))
244
245 (defun esy-publish--note-titles ()
246   (mapcar (lambda (file)
247             (cons (org-get-title
248                    (expand-file-name file esy-publish-notes-source-directory))
249                   (file-name-base file)))
250           (delete "index.org"
251                   (directory-files esy-publish-notes-source-directory
252                                    nil (rx bos alnum (+ any)
253                                            ".org" eos)))))
254
255 (defun esy-publish-create-empty-note (title)
256   (interactive (list (read-string "Note subject: ")))
257   (let ((base (esy-publish--title-to-file-base-name title)))
258     (with-temp-buffer
259       (insert
260        "#+TITLE:       " title                                                  "\n"
261        "#+SUBTITLE:    " (read-string "Subtitle: ")                             "\n"
262        "#+DESCRIPTION: Eshel Yaron's notes about " title                        "\n"
263        "#+KEYWORDS:    " (read-string "Keywords: ")                             "\n"
264        "#+DATE:        " (format-time-string "%F")                              "\n"
265        "\n"                                                                     "\n"
266        "* References in published posts"                                        "\n"
267        "#+BEGIN: links-to-note :dir \"" esy-publish-posts-source-directory "\"" "\n"
268        "#+END:"                                                                 "\n"
269        "* References in other notes"                                            "\n"
270        "#+BEGIN: links-to-note :dir \"" esy-publish-notes-source-directory "\"" "\n"
271        "#+END:"                                                                 "\n")
272       (write-file (expand-file-name
273                    (file-name-with-extension base "org")
274                    esy-publish-notes-source-directory)))
275     base))
276
277 (defun esy-publish-complete-note-link (&optional _arg)
278   (let* ((title-name-alist (esy-publish--note-titles))
279          (max-title-width (apply #'max
280                                  (mapcar #'length
281                                          (mapcar #'car
282                                                  title-name-alist))))
283          (completion-extra-properties
284           (list
285            :annotation-function
286            (lambda (title)
287              (let ((subtitle (with-current-buffer
288                                  (find-file-noselect
289                                   (expand-file-name
290                                    (file-name-with-extension
291                                     (alist-get title
292                                                title-name-alist
293                                                nil nil
294                                                #'string=)
295                                     "org")
296                                    esy-publish-notes-source-directory))
297                                (when esy-publish--publishing
298                                  (push (current-buffer) esy-publish--buffers))
299                                (car (alist-get "SUBTITLE"
300                                                (org-collect-keywords
301                                                 '("SUBTITLE"))
302                                                nil nil #'string=)))))
303                (concat (make-string (1+ (- max-title-width
304                                            (length title)))
305                                     ?\s)
306                        subtitle)))))
307          (title (completing-read "Note: "
308                                  title-name-alist
309                                  nil 'confirm (when (use-region-p)
310                                                 (buffer-substring-no-properties
311                                                  (use-region-beginning)
312                                                  (use-region-end)))
313                                  'esy-publish-notes-completion-history))
314          (name (or (alist-get title
315                               title-name-alist
316                               nil nil
317                               #'string=)
318                    (esy-publish-create-empty-note title))))
319     (concat "note:" name)))
320
321 (defun esy-publish-describe-note-link (loc &optional _desc)
322   (org-get-title (expand-file-name (file-name-with-extension (substring loc 5) "org")
323                                    esy-publish-notes-source-directory)))
324
325 (defun esy-publish-store-note-link ()
326   (when (and (derived-mode-p 'org-mode)
327              (buffer-file-name)
328              (equal (file-name-as-directory (expand-file-name (file-name-directory (buffer-file-name))))
329                     esy-publish-notes-source-directory))
330     (let* ((note (file-name-base (buffer-file-name)))
331            (link (concat "note:" note))
332            (description (org-get-title)))
333       (org-link-store-props
334        :type "note"
335        :link link
336        :description description))))
337
338 (defface esy-publish-note-link
339   '((t :underline (:style wave) :slant italic))
340   "Face applied to \"note:\" links.")
341
342 (defun esy-publish--post-to-feed-item (file)
343   (with-current-buffer (find-file-noselect
344                         (expand-file-name
345                          file esy-publish-local-posts-directory))
346     (push (current-buffer) esy-publish--buffers)
347     (let ((dom (libxml-parse-html-region (point-min) (point-max))))
348       `(item nil
349              (title nil ,(string-join (dom-strings (car (dom-by-tag dom 'title)))))
350              (author nil ,user-full-name)
351              (category nil ,(car (string-split (dom-attr (seq-find (lambda (m) (pcase m (`(meta ((name . "keywords") . ,_)) t))) (dom-by-tag dom 'meta)) 'content) " ")))
352              (link nil ,(concat "https://eshelyaron.com/posts/" file))
353              (guid ((isPermaLink . "true")) ,(concat "https://eshelyaron.com/posts/" file))
354              (pubDate nil ,(substring file 0 10))
355              (description nil ,(format "<![CDATA[%s]]>" (esy-publish--dom-to-string (car (dom-by-id dom "content")))))))))
356
357 (defun esy-publish--finalize-sitemap (plist)
358   (let ((locs (mapcar #'esy-publish--file-url
359                       (directory-files-recursively esy-publish-local-directory
360                                                    (rx ".html" eos)))))
361     (with-temp-buffer
362       (insert "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n")
363       (dom-print `(urlset ((xlmns . "http://www.sitemaps.org/schemas/sitemap/0.9"))
364                           ,@(mapcar (lambda (loc)
365                                       `(url nil (loc nil ,loc)))
366                                     locs))
367                  t t)
368       (write-file (expand-file-name "sitemap.xml" (plist-get plist :publishing-directory))))))
369
370 (defun esy-publish--finalize-feed (plist)
371   (let ((posts (reverse (directory-files esy-publish-local-posts-directory
372                                          nil
373                                          (rx bos digit (+ any) ".html" eos)))))
374     (with-temp-buffer
375       (insert "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n")
376       (dom-print `(rss ((version . "2.0"))
377                        (channel nil
378                                 (title nil ,user-full-name)
379                                 (generator nil "GNU Emacs")
380                                 (link nil "https://eshelyaron.com")
381                                 (description nil "RSS Feed of eshelyaron.com")
382                                 (pubDate nil ,(format-time-string "%F"))
383                                 ,@(mapcar #'esy-publish--post-to-feed-item posts)))
384                  t t)
385       (write-file (expand-file-name "rss.xml" (plist-get plist :publishing-directory))))))
386
387 (defun esy-publish--transclude-config (&rest _)
388   (with-current-buffer
389       (find-file-noselect
390        (expand-file-name "esy.org" esy-publish-source-directory))
391     (org-transclusion-add-all)
392     (push (current-buffer) esy-publish--buffers)))
393
394 (defvar esy-publish-example-modes '(("lisp"   . emacs-lisp-mode)
395                                     ("prolog" . prolog-mode)))
396
397 (defun esy-publish-fontify-examples (file)
398   (interactive "fFile: ")
399   (let ((tmp (concat file ".tmp.html"))
400         (src (find-file-noselect file)))
401     (with-current-buffer src
402       (let ((dom (without-restriction
403                    (xml-remove-comments (point-min) (point-max))
404                    (set-buffer-modified-p nil)
405                    (libxml-parse-html-region (point-min) (point-max)))))
406         (dolist-with-progress-reporter (example (dom-by-class dom "example[ ]"))
407             (concat "Processing example in " file)
408           (let ((example-class (dom-attr example 'class)))
409             (when (string-match (rx "example " (group-n 1 (+ (or alnum (any "-"))))) example-class)
410               (let ((go t)
411                     (ms (match-string 1 example-class)))
412                 (dolist (r-mm esy-publish-example-modes)
413                   (if (and go (string-match (car r-mm) ms nil t))
414                       (dolist (pre (dom-by-tag example 'pre))
415                         (setcdr
416                          (cdr pre)
417                          (let ((prog-mode-hook '(rainbow-delimiters-mode))
418                                (htmlize-css-name-prefix org-html-htmlize-font-prefix)
419                                (buf (generate-new-buffer "*Example*")))
420                            (with-current-buffer buf
421                              (insert (dom-text pre))
422                              (funcall (cdr r-mm))
423                              (buffer-disable-undo (current-buffer))
424                              (buffer-enable-undo)
425                              (let* ((hb (htmlize-buffer buf))
426                                     (new (dom-children
427                                           (with-current-buffer hb
428                                             (car (dom-by-tag
429                                                   (libxml-parse-html-region
430                                                    (point-min)
431                                                    (point-max))
432                                                   'pre))))))
433                                (kill-buffer buf)
434                                (kill-buffer hb)
435                                new)))))))))))
436         (dom-add-child-before (car (dom-by-tag dom 'head))
437                               `(link ((rel . "canonical")
438                                       (href . ,(esy-publish--file-url file)))))
439         (with-temp-buffer
440           (let ((gc-cons-threshold most-positive-fixnum))
441             (with-delayed-message (1 (concat "Printing new DOM for "
442                                              file "..."))
443               (dom-print dom)))
444           (write-file tmp))))
445     (kill-buffer src)
446     (rename-file tmp file t)))
447
448 (defun esy-publish--sweep-texinfo (plist)
449   (make-directory esy-publish-local-man-directory t)
450   (let* ((in       (expand-file-name "sweep/sweep.texi" esy-publish-root-directory))
451          (no-split (expand-file-name "sweep.html"       esy-publish-local-directory))
452          (out      (expand-file-name "sweep"            esy-publish-local-man-directory))
453          (args (list "--html"
454                      "--css-ref" "../../style.css"
455                      "-c" "TREE_TRANSFORMATIONS=regenerate_master_menu"
456                      "-c" (concat "AFTER_BODY_OPEN="
457                                   (esy-publish--dom-to-string
458                                    '(div ((id . "preamble")
459                                           (class . "status"))
460                                          (nav ((id . "icon-links")
461                                                (class . "icon-links"))
462                                               (div ((class . "home-link"))
463                                                    (a ((href . "/"))
464                                                       (img ((src . "/home.svg")
465                                                             (height . "35")
466                                                             (width . "35")
467                                                             (alt . "Home")))))
468                                               (div ((class . "other-links"))
469                                                    (a ((href . "mailto:me@eshelyaron.com"))
470                                                       (img ((src . "/mail.svg")
471                                                             (height . "30")
472                                                             (width . "30")
473                                                             (alt . "Mail"))))
474                                                    " "
475                                                    (a ((href . "https://emacs.ch/@eshel")
476                                                        (rel . "me"))
477                                                       (img ((src . "/mastodon.svg")
478                                                             (height . "28")
479                                                             (width . "28")
480                                                             (alt . "Mastodon"))))
481                                                    " "
482                                                    (a ((href . "/rss.xml"))
483                                                       (img ((src . "/rss.svg")
484                                                             (height . "30")
485                                                             (width . "30")
486                                                             (alt . "RSS Feed")))))))
487                                    '(hr nil)))
488                      "-c" (concat "PRE_BODY_CLOSE="
489                                   (esy-publish--dom-to-string
490                                    '(div ((id . "postamble")
491                                           (class . "status"))
492                                          (footer ((id . "footer")
493                                                   (class . "footer"))
494                                                  (hr nil)
495                                                  "© "
496                                                  (time ((class . "copyright-year")) "2023")
497                                                  " Eshel Yaron"))))
498                      "--output")))
499     (apply #'call-process "texi2any" nil nil nil
500            (append args (list out in)))
501     (apply #'call-process "texi2any" nil nil nil
502            (cons "--no-split" (append args (list no-split in))))
503     (dolist (file (cons no-split (directory-files out t (rx ".html" eos))))
504       (esy-publish-fontify-examples file))))
505
506 (defun esy-publish--prepare-indices (&rest _)
507   (dolist (dir (list esy-publish-notes-source-directory
508                      esy-publish-posts-source-directory
509                      esy-publish-source-directory))
510     (with-current-buffer (find-file-noselect (expand-file-name "index.org" dir))
511       (org-update-all-dblocks)
512       (push (current-buffer) esy-publish--buffers))))
513
514 (defun esy-publish--prepare-notes-links (&rest _)
515   (dolist (note (delete "index.org"
516                         (directory-files esy-publish-notes-source-directory
517                                          t
518                                          (rx bos alnum (+ any)
519                                              ".org" eos))))
520     (with-current-buffer (find-file-noselect note)
521       (org-update-all-dblocks)
522       (push (current-buffer) esy-publish--buffers))))
523
524 (defun esy-publish--prepare (&rest _)
525   (esy-publish--transclude-config)
526   (esy-publish--prepare-indices)
527   (esy-publish--prepare-notes-links))
528
529 (defun esy-publish--add-canonical-tags (_plist)
530   (dolist (file (directory-files-recursively esy-publish-local-directory
531                                              (rx ".html" eos)))
532     (with-current-buffer (find-file-noselect file)
533       (goto-char (point-min))
534       (when (search-forward "<!-- insert canonical tag here -->" nil t)
535         (replace-match (format "<link rel=\"canonical\" href=\"%s\" />"
536                                (esy-publish--file-url file))
537                        nil t))
538       (basic-save-buffer)
539       (push (current-buffer) esy-publish--buffers))))
540
541 (defun esy-publish--finalize (plist)
542   (esy-publish--sweep-texinfo plist)
543   (esy-publish--finalize-feed plist)
544   (esy-publish--add-canonical-tags plist)
545   (esy-publish--finalize-sitemap plist))
546
547 ;;;###autoload
548 (defun esy-publish (&optional force)
549   (interactive "P")
550   (esy-publish-setup)
551   (let* ((org-export-with-sub-superscripts '{})
552          (org-export-with-section-numbers nil)
553          (org-export-with-toc nil)
554          (org-export-with-smart-quotes t)
555          (org-export-time-stamp-file nil)
556          (org-html-htmlize-output-type 'css)
557          (org-html-metadata-timestamp-format "%Y-%m-%d")
558          (org-time-stamp-formats '("%Y-%m-%d" . "%Y-%m-%d %H:%M"))
559          (org-confirm-babel-evaluate nil)
560          (org-src-lang-modes nil)
561          (prog-mode-hook '(rainbow-delimiters-mode))
562          (make-backup-files nil)
563          (auto-save-default nil)
564          (esy-publish--buffers nil)
565          (esy-publish--publishing t)
566          (initial-buffers (buffer-list))
567          (org-publish-project-alist
568           (list '("all" :components ("assets" "org"))
569                 (list "assets"
570                       :base-directory esy-publish-source-directory
571                       :publishing-directory esy-publish-local-directory
572                       :base-extension "svg\\|ico\\|css\\|png"
573                       :publishing-function #'org-publish-attachment)
574                 (list "org"
575                       :completion-function #'esy-publish--finalize
576                       :base-directory esy-publish-source-directory
577                       :publishing-directory esy-publish-local-directory
578                       :preparation-function #'esy-publish--prepare
579                       :completion-function #'ignore
580                       :base-extension "org"
581                       :recursive t
582                       :exclude nil
583                       :include nil
584                       :publishing-function #'org-html-publish-to-html
585                       :html-doctype "html5"
586                       :html-html5-fancy t
587                       :auto-sitemap t
588                       :sitemap-title "Sitemap for eshelyaron.com"
589                       :makeindex t
590                       :with-date t
591                       :html-head-include-default-style nil
592                       :html-head-include-scripts nil
593                       :html-use-infojs nil
594                       :html-link-org-files-as-html t
595                       :html-head       (esy-publish--dom-to-string '(link ((rel . "stylesheet")
596                                                                            (href . "/style.css")
597                                                                            (type . "text/css"))))
598                       :html-head-extra (concat (esy-publish--dom-to-string
599                                                 '(link ((rel . "alternate")
600                                                         (href . "/rss.xml")
601                                                         (type . "application/rss+xml")
602                                                         (title . "RSS feed of eshelyaron.com"))))
603                                                "\n<!-- insert canonical tag here -->")
604                       :html-preamble-format
605                       (list
606                        (list
607                         "en"
608                         (esy-publish--dom-to-string
609                          '(nav ((id . "icon-links")
610                                 (class . "icon-links"))
611                                (div ((class . "home-link"))
612                                     (a ((href . "/"))
613                                        (img ((src . "/home.svg")
614                                              (height . "35")
615                                              (width . "35")
616                                              (alt . "Home")))))
617                                (div ((class . "other-links"))
618                                     (a ((href . "mailto:me@eshelyaron.com"))
619                                        (img ((src . "/mail.svg")
620                                              (height . "30")
621                                              (width . "30")
622                                              (alt . "Mail"))))
623                                     " "
624                                     (a ((href . "https://emacs.ch/@eshel")
625                                         (rel . "me"))
626                                        (img ((src . "/mastodon.svg")
627                                              (height . "28")
628                                              (width . "28")
629                                              (alt . "Mastodon"))))
630                                     " "
631                                     (a ((href . "/rss.xml"))
632                                        (img ((src . "/rss.svg")
633                                              (height . "30")
634                                              (width . "30")
635                                              (alt . "RSS Feed"))))))
636                          '(hr nil))))
637                       :html-postamble t
638                       :html-postamble-format
639                       (list
640                        (list
641                         "en"
642                         (esy-publish--dom-to-string
643                          '(footer ((id . "footer")
644                                    (class . "footer"))
645                                   (hr nil)
646                                   "© "
647                                   (time ((class . "copyright-year")) "2023")
648                                   " %a"))))))))
649     (org-publish "all" force)
650     (dolist (buffer (seq-uniq
651                      (seq-difference esy-publish--buffers
652                                      initial-buffers)))
653       (when (buffer-live-p buffer)
654         (with-current-buffer buffer
655           (set-buffer-modified-p nil))
656         (kill-buffer buffer))))
657   (setq esy-publish--buffers nil))
658
659 ;;;###autoload
660 (defun esy-publish-to-remote ()
661   (interactive)
662   (compile (format "rsync -vrz %s %s"
663                    esy-publish-local-directory
664                    esy-publish-remote-directory)))
665
666 ;;;###autoload
667 (defun esy-publish-local-server ()
668   (interactive)
669   (unless (process-live-p esy-publish--local-server-process)
670     (let ((default-directory esy-publish-local-directory))
671       (start-process "esy-publish-local-server" nil
672                      "python3" "-m" "http.server"))))
673
674 (provide 'esy-publish)
675 ;;; esy-publish.el ends here