her.esy.fun/src/posts/2019-07-04-static-org-publish.org

495 lines
18 KiB
Org Mode
Raw Normal View History

2019-07-05 21:29:02 +00:00
#+TITLE: Static blog with org-mode
#+AUTHOR: Yann Esposito
#+EMAIL: yann.esposito@gmail.com
2019-07-07 21:39:18 +00:00
#+DATE: 2019-07-04
2019-07-07 16:11:45 +00:00
#+KEYWORDS: programming, blog, org-mode
#+OPTIONS: auto-id:t
2019-07-05 21:29:02 +00:00
2019-07-21 20:11:08 +00:00
#+begin_quote
2019-07-24 08:59:48 +00:00
/tl;dr/: [[#current-solution-0a92][Full code for the impatient↓]]. Read the full blog post to have all the details.
2019-07-21 20:11:08 +00:00
#+end_quote
2019-07-21 16:03:26 +00:00
2019-07-07 16:11:45 +00:00
This is the first article using my new blog system.
2019-07-05 21:29:02 +00:00
2019-07-13 21:30:59 +00:00
Once in a while, I create a new personal website.
2019-07-07 16:11:45 +00:00
A long time ago, I used PHP for my first website.
I used include and took care of XHTML pages validation.
2019-07-13 21:30:59 +00:00
Then I used [[http://nanoc.ws][nanoc]], a ruby static website generator.
Then I switched to [[https://jaspervdj.be/hakyll/][hakyll]] because I wanted to switch to a Haskell's written
tool.
2019-07-20 19:58:48 +00:00
Now I'll try to use [[http://orgmode.org][org-mode]] directly with [[https://orgmode.org/worg/org-tutorials/org-publish-html-tutorial.html][org-publish]].
2019-07-21 16:03:26 +00:00
I am became a quite extensive emacs user now. So using org-mode and org-publish
is a bit like not having to install any 3rd party software.
2019-07-05 21:29:02 +00:00
2019-07-07 16:11:45 +00:00
* Why?
:PROPERTIES:
:CUSTOM_ID: why--8eb9
:END:
2019-07-05 21:29:02 +00:00
2019-07-07 16:11:45 +00:00
Everything started when I was hired in a place where I was given a terrible
keyboard.
After a few weeks I started to feel a lot of pain in both my wrists.
So I started to go from classical IDE to being able to use vim
correctly[fn:vim].
2019-07-05 21:29:02 +00:00
2019-07-07 16:11:45 +00:00
Then I started to work in Clojure and I heard that emacs might certainly be a
better fit for LISP dialiects.
But, I couldn't switch to an editor without vim keybindings support because they
are so great once you're used to them.
2019-07-13 21:30:59 +00:00
By chance it was about the same time that [[http://spacemacs.org][spacemacs]] appeared and I switched.
Is is really impressive how well the vim keybindings are supported.
2019-07-07 16:11:45 +00:00
Even most of the advanced vim features I used to use worked like a charm.
2019-07-05 21:29:02 +00:00
2019-07-07 16:11:45 +00:00
The first benefit of emacs is you can configure emacs with elisp.
Which unlike vimscript looks like a correct language to work with.
2019-07-05 21:29:02 +00:00
2019-07-07 16:11:45 +00:00
One unexpected benefit of emacs was [[http://orgmode.org][org-mode]].
I always heard good things about it but it took me a while to really get it and
to understand why it is so great.
2019-07-05 21:29:02 +00:00
2019-07-07 16:11:45 +00:00
If you don't know anything about org-mode, it is many things.
First imagine a Markdown but more TODO list oriented.
But along with this, emacs has a lot of helper functions to work with those
org-mode files.
2019-07-05 21:29:02 +00:00
2019-07-07 21:39:18 +00:00
One real game changer is ~org-capture~.
You can add a task quite easily while doing other work in emacs.
2019-07-07 16:11:45 +00:00
[fn:vim] I wrote this article to help people use vim: [[http://yannesposito.com/Scratch/en/blog/Learn-Vim-Progressively/][learn vim progressively]]
2019-07-21 16:03:26 +00:00
* How?
:PROPERTIES:
:CUSTOM_ID: how--831e
:END:
2019-07-21 16:03:26 +00:00
2019-07-21 20:11:08 +00:00
** Basic Blog
:PROPERTIES:
:CUSTOM_ID: basic-blog-a1fc
:END:
2019-07-25 09:10:50 +00:00
I put the need minimal code in a =.project.el.gpg= file of my blog repository.
Inspired by this [[https://francismurillo.github.io/2017-02-15-Project-Script-Loader/][blog post]] I enhanced this version to be both more user friendly
and secure (see [[#digression]]).
2019-07-21 20:11:08 +00:00
But even before that, I simply put the code in has an elisp code block of my
=index.org= that I exectued with =C-c C-c=.
I have a code block in my =index.org= page that I do not export containing the
elisp code I use to load all the information needed by =org-publish=.
2019-07-21 20:11:08 +00:00
Do not export an org-mode subtree you can simply tag it with =:noexport:=.
2019-07-21 16:03:26 +00:00
=org-publish= suppose that you'll export one html page per org file.
You need to provide a minimum set of information:
- base directory that should contain your org files
- publish directory, where you will export the html from the org files
2019-07-21 20:11:08 +00:00
A very basic publish rule would be:
#+begin_src org
,* Magic Script :noexport:
,#+begin_src elisp :results none
2019-07-21 16:03:26 +00:00
(setq org-publish-project-alist
'(("orgfiles"
:base-directory "/users/me/blog/src"
:publishing-directory "/users/me/blog/_site"
:recursive t)))
2019-07-21 20:11:08 +00:00
,#+end_src
2019-07-21 16:03:26 +00:00
#+end_src
So now, if you put org files in the base directory it will copy
recursively the tree in that base directory and will copy it to
the publishing directory exporting all org files to html files.
Nice.
If you are not familiar with emacs or orgmode, orgpublish.
This block of code set the variable named ~org-publish-project-alist~.
The ~alist~ in the end is for "associative list".
#+begin_quote
/☞/ So when you'll execute the command =org-publish= in emacs.
You will get prompted to enter the name of what to publish.
You should see a single proposition named =orgfiles=.
Once selected the export and publish will occurs.
Notice, org-publish does what you expect and do not re-export all files
each time but only the one that have changed.
#+end_quote
2019-07-21 20:11:08 +00:00
** Relative Paths
:PROPERTIES:
:CUSTOM_ID: relative-paths-d145
:END:
2019-07-21 16:03:26 +00:00
A first issue with this is that I don't want to put that in my emacs
preferences.
I would like to be able to publish my website for anyone that could clone my git
repository.
So I will use emacs projectile to find the root directory of the current
project.
#+begin_src elisp
(setq base-dir (concat (projectile-project-root) "src"))
(setq publish-dir (concat (projectile-project-root) "_site"))
(setq org-publish-project-alist
`(("orgfiles"
:base-directory ,base-dir
:publishing-directory ,publish-dir
:recursive t
:base-extension "org")))
#+end_src
That's better.
Now anyone can clone the repository.
Open my =index.org= file, and execute the code in it by =C-e C-e= and then will
be able to publish the website locally in the =_site= directory.
2019-07-21 20:11:08 +00:00
** Assets
:PROPERTIES:
:CUSTOM_ID: assets-cf7d
:END:
2019-07-21 16:03:26 +00:00
2019-07-21 20:11:08 +00:00
Generally you want a bit more features than that for publishing a blog.
2019-07-21 16:03:26 +00:00
Typically you would like to have a common template, an header, some CSS.
For that you need to work a bit more.
First, important thing, add an org publish section to publish your assets (CSS,
images, etc...)
#+begin_src elisp
(setq base-dir (concat (projectile-project-root) "src"))
(setq publish-dir (concat (projectile-project-root) "_site"))
(setq assets-dir (concat base-dir "/assets"))
(setq publish-assets-dir (concat publish-dir "/assets"))
(setq org-publish-project-alist
`(("orgfiles"
:base-directory ,base-dir
:publishing-directory ,publish-dir
:recursive t
:base-extension "org"
:exclude ".*drafts/.*")
("assets"
:base-directory ,assets-dir
:base-extension ".*"
:publishing-directory ,publish-assets-dir
:publishing-function org-publish-attachment
:recursive t)
("blog" :components ("orgfiles" "assets"))))
#+end_src
With that you will copy the assets and export org file to html.
You can do both by selecting `blog` when doing an `org-publish`.
That's better. But for a blog you also generally want to have your own CSS.
2019-07-21 16:03:26 +00:00
* Current Solution
:PROPERTIES:
:CUSTOM_ID: current-solution-0a92
2019-07-21 16:03:26 +00:00
:END:
#+begin_src elisp
2019-07-21 20:11:08 +00:00
;; Global variables
(setq domainname "https://her.esy.fun")
(setq base-dir (concat (projectile-project-root) "src"))
(setq publish-dir (concat (projectile-project-root) "_site"))
(setq assets-dir (concat base-dir "/assets"))
(setq publish-assets-dir (concat publish-dir "/assets"))
(setq rss-dir base-dir)
(setq publish-rss-dir publish-dir)
(setq css-name "minimalist.css")
2019-07-21 16:03:26 +00:00
(require 'org)
(require 'ox-publish)
(require 'ox-html)
(require 'org-element)
(require 'ox-rss)
(defun org-blog-prepare (project-plist)
"With help from `https://github.com/howardabrams/dot-files'.
Touch `index.org' to rebuilt it.
Argument `PROJECT-PLIST' contains information about the current project."
(let* ((base-directory (plist-get project-plist :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))))
(defvar org-blog-head
(concat
2019-07-21 20:11:08 +00:00
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/css/" css-name "\"/>"
2019-07-21 16:03:26 +00:00
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<link rel=\"alternative\" type=\"application/rss+xml\" title=\"Subscribe to articles\" href=\"/archives.xml\" />"
"<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/favicon.ico\">"))
(defun menu (lst)
"Blog menu"
(concat
"<navigation class=\"menu\">"
(mapconcat 'identity
(append
'("<a href=\"/index.html\">Home</a>"
"<a href=\"/archive.html\">Posts</a>")
lst)
" | ")
"</navigation>"))
(defun str-time-to-year-float (date-str)
(/ (float-time
(apply 'encode-time
(mapcar (lambda (x) (if (null x) 0 x))
(parse-time-string date-str))))
(* 365.25 24 60 60)))
(defvar blog-creation-date "2019-07-01")
2019-07-21 20:11:08 +00:00
(defun delta-date (date-str)
2019-07-21 16:03:26 +00:00
"Number of year since the begining of this blog"
(let ((y (- (str-time-to-year-float date-str)
(str-time-to-year-float blog-creation-date))))
(format "∆t=%.2f" y)))
(defun org-blog-preamble (info)
"Pre-amble for whole blog."
(concat
"<div class=\"content\">"
(menu '("<a href=\"#postamble\">↓ bottom ↓</a>"))
"<h1>"
(format "%s" (plist-get info :title))
(when-let ((date (get-from-info info :date)))
(format " - <span class=\"article-date\">%s</span>" date))
"</h1>"
"</div>"))
(defun get-from-info (info k)
(let ((i (car (plist-get info k))))
(when (and i (stringp i))
i)))
(defun rand-obfs (c)
(let ((r (% (random) 20)))
(cond ((eq 0 r) (format "%c" c))
((< 0 r 10) (format "&#%d;" c))
(t (format "&#x%X;" c)))))
(defun obfuscate-html (txt)
(apply 'concat
(mapcar 'rand-obfs txt)))
(defun org-blog-postamble (info)
"Post-amble for whole blog."
(concat
"<div class=\"content\">"
"<footer>"
(when-let ((author (get-from-info info :author)))
(if-let ((email (plist-get info :email)))
(format "<div class=\"author\">Author: <a href=\"%s%s\">%s</a></div>"
(obfuscate-html "mailto:")
(obfuscate-html email)
(obfuscate-html author))
(format "<div class=\"author\">Author: %s</div>" author)))
(when-let ((date (get-from-info info :date)))
2019-07-21 20:11:08 +00:00
(format "<div class=\"date\">Created: %s (%s)</div>" date (delta-date date)))
2019-07-21 16:03:26 +00:00
(when-let ((keywords (plist-get info :keywords)))
(format "<div class=\"keywords\">Keywords: <code>%s</code></div>" keywords))
(format "<div class=\"date\">Generated: %s</div>"
(format-time-string "%Y-%m-%d %H:%M:%S"))
(format (concat "<div class=\"creator\"> Generated with "
"<a href=\"https://www.gnu.org/software/emacs/\" target=\"_blank\" rel=\"noopener noreferrer\">Emacs %s</a>, "
"<a href=\"http://spacemacs.org\" target=\"_blank\" rel=\"noopener noreferrer\">Spacemacs %s</a>, "
"<a href=\"http://orgmode.org\" target=\"_blank\" rel=\"noopener noreferrer\">Org Mode %s</a>"
"</div>")
emacs-version spacemacs-version org-version)
"</footer>"
(menu '("<a href=\"#preamble\">↑ Top ↑</a>"))
"</div>"))
(defun org-blog-sitemap-format-entry (entry _style project)
"Return string for each ENTRY in PROJECT."
(when (s-starts-with-p "posts/" entry)
(format "@@html:<span class=\"archive-item\"><span class=\"archive-date\">@@ %s: @@html:</span>@@ [[file:%s][%s]] @@html:</span>@@"
(format-time-string "%Y-%m-%d"
(org-publish-find-date entry project))
entry
(org-publish-find-title entry project))))
(defun org-blog-sitemap-function (title list)
"Return sitemap using TITLE and LIST returned by `org-blog-sitemap-format-entry'."
(concat "#+TITLE: " title "\n"
"#+AUTHOR: Yann Esposito\n"
"#+EMAIL: yann.esposito@gmail.com\n"
"\n#+begin_archive\n"
(mapconcat (lambda (li)
(format "@@html:<li>@@ %s @@html:</li>@@" (car li)))
(seq-filter #'car (cdr list))
"\n")
"\n#+end_archive\n"))
(setq org-publish-project-alist
`(("orgfiles"
:base-directory ,base-dir
:exclude ".*drafts/.*"
:base-extension "org"
:publishing-directory ,publish-dir
:recursive t
:preparation-function org-blog-prepare
:publishing-function org-html-publish-to-html
:with-toc nil
:with-title nil
:with-date t
:section-numbers nil
:html-doctype "html5"
:html-html5-fancy t
:html-head-include-default-style nil
:html-head-include-scripts nil
:htmlized-source t
:html-head-extra ,org-blog-head
:html-preamble org-blog-preamble
:html-postamble org-blog-postamble
:auto-sitemap t
:sitemap-filename "archive.org"
:sitemap-title "Blog Posts"
:sitemap-style list
:sitemap-sort-files anti-chronologically
:sitemap-format-entry org-blog-sitemap-format-entry
:sitemap-function org-blog-sitemap-function)
("assets"
:base-directory ,assets-dir
:base-extension ".*"
:publishing-directory ,publish-assets-dir
:publishing-function org-publish-attachment
:recursive t)
("rss"
:base-directory ,rss-dir
:base-extension "org"
:html-link-home ,domainname
:html-link-use-abs-url t
:rss-extension "xml"
:publishing-directory ,publish-rss-dir
:publishing-function (org-rss-publish-to-rss)
:exclude ".*"
:include ("archive.org")
:section-numbers nil
:table-of-contents nil)
("blog" :components ("orgfiles" "assets" "rss"))))
;; add target=_blank and rel="noopener noreferrer" to all links by default
(defun my-org-export-add-target-blank-to-http-links (text backend info)
"Add target=\"_blank\" to external links."
(when (and
(org-export-derived-backend-p backend 'html)
(string-match "href=\"http[^\"]+" text)
(not (string-match "target=\"" text))
(not (string-match (concat "href=\"" domainname "[^\"]*") text)))
(string-match "<a " text)
(replace-match "<a target=\"_blank\" rel=\"noopener noreferrer\" " nil nil text)))
(add-to-list 'org-export-filter-link-functions
'my-org-export-add-target-blank-to-http-links)
#+end_src
2019-07-25 09:10:50 +00:00
* Digression
:PROPERTIES:
:CUSTOM_ID: digression
:END:
Auto loading a =.el= file when entering in a project.
It should be easy and safe.
The best solution I found.
Save your =.el= file as a =.el.gpg= file.
With =epa= emacs should encrypt the file for you using your own private key.
Great! Now we simply verify the file exists and load it only if it is encrypted
with one of you /trusted keys/.
The list of key fingerprint you trust is a configuration.
When opening a new file via projectile or dired
1. check the =.project.el.gpg= file is not already loaded for the current project
2. check the =.project.el.gpg= file exists
3. check the =.project.el.gpg= file is encrypted with a trusted key
4. decrypt and load =.project.el.gpg=
That's it.
You simply need to set the =y/trusted-gpg-key-fingerprints= variable with the
list of your own fingerprint.
You can get a list of them with =gpg --list-secret-keys=.
Now you can be happy, this is really safe, in the sense that if you clone a new
project from the internet with a =.project.el.gpg= file in it.
That file won't be run on your system.
Of course if you want to share that =.el= to other people, you need to adapt.
But for my use case, this is perfect.
2019-07-25 09:12:54 +00:00
Another use case would be to check the signature and trust people.
2019-07-25 09:10:50 +00:00
#+begin_src elisp
(defvar y/trusted-gpg-key-fingerprints
'("448E9FEF4F5B86DE79C1669B7B19A4C650D59646")
"The list of GPG fingerprint you trust when decrypting a gpg file.
You can retrieve the fingerprints of your own private keys with:
`gpg --list-secret-keys' (take care of removing the spaces when copy/pasting here)")
(defun y/get-encryption-key (file)
"given a gpg encrypted file, returns the fingerprint of they
key that encrypted it"
(string-trim-right
(shell-command-to-string
(concat
"gpg --status-fd 1 --decrypt -o /dev/null " file " 2>/dev/null"
"|grep DECRYPTION_KEY"
"|awk '{print $4}'"))))
(defun y/trusted-gpg-origin-p (file)
"Returns true if the file is encrypted with a trusted key"
(member (y/get-encryption-key file) y/trusted-gpg-key-fingerprints))
(defun y/project-el-auto-load ()
(with-eval-after-load 'projectile
(defconst y/project-file ".project.el.gpg"
"Filename looked after to be loaded and evaluated.")
(defvar y/loaded-projects (list)
"Projects that have been loaded by `y/load-project-file'.")
(defun y/load-project-file ()
"Loads the `y/project-file' for a project. This is run
once after the project is loaded signifying project setup."
(interactive)
(when (projectile-project-p)
(lexical-let* ((project-root (projectile-project-root))
(project-init-file (expand-file-name y/project-file project-root)))
(when (and (not (member project-root y/loaded-projects)) ;; project file not already loaded
(file-exists-p project-init-file) ;; project file exists
(y/trusted-gpg-origin-p project-init-file)) ;; project file is tursted
(message "Loading project init file for %s" (projectile-project-name))
(condition-case ex
(progn (load project-init-file)
(add-to-list 'y/loaded-projects project-root)
(message "%s loaded successfully" project-init-file))
('error
(message
"There was an error loading %s: %s"
project-init-file
(error-message-string ex))))))))
;; for some obscure reason there is not really a working hook
;; to be used with projectile to launch the `y/load-project-file'
;; only when switching to a new project.
;; Thus the `y/loaded-projects' state.
(add-hook 'find-file-hook #'y/load-project-file t)
(add-hook 'dired-mode-hook #'y/load-project-file t)))
#+end_src