diff --git a/project.el b/project.el index 7495769..c83af5e 100644 --- a/project.el +++ b/project.el @@ -1,16 +1,22 @@ ;; sign it with ;; gpg --local-user yann@esposito.host --output project.el.sig --detach-sign project.el -(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 "/")) -(setq publish-assets-dir (concat publish-dir "/")) -(setq rss-dir base-dir) -(setq rss-title "Subscribe to articles") -(setq publish-rss-dir publish-dir) -(setq css-path "/css/minimalist.css") -(setq author-name "Yann Esposito") -(setq author-email "yann@esposito.host") +(defvar domainname "https://her.esy.fun") +(defvar base-dir (concat (projectile-project-root) "src")) +(defvar publish-dir (concat (projectile-project-root) "_site")) +(defvar assets-dir (concat base-dir "/")) +(defvar publish-assets-dir (concat publish-dir "/")) +(defvar posts-dir (concat base-dir "/posts")) +(defvar posts-publish-dir (concat publish-dir "/posts")) +(defvar micro-dir (concat base-dir "/micro")) +(defvar micro-publish-dir (concat publish-dir "/micro")) +(defvar rss-dir base-dir) +(defvar rss-title "Subscribe to articles") +(defvar posts-descr "Articles") +(defvar micro-descr "Short micro blog entries à la twitter/mastodon") +(defvar publish-rss-dir publish-dir) +(defvar css-path "/css/minimalist.css") +(defvar author-name "Yann Esposito") +(defvar author-email "yann@esposito.host") (require 'org) (require 'ox-publish) @@ -26,7 +32,7 @@ (concat "" "" - "" + "" "")) (defun menu (lst) @@ -36,7 +42,7 @@ (mapconcat 'identity (append '("Home" - "Posts" + "Posts" "Slides" "About") lst) @@ -131,26 +137,31 @@ (defun org-blog-sitemap-format-entry (entry _style project) "Return string for each ENTRY in PROJECT." - (when (s-starts-with-p "posts/" entry) - (format (concat "@@html:" - "@@ %s: @@html:@@" - " [[file:%s][%s]]" - " @@html:@@") - (format-time-string "%Y-%m-%d" (org-publish-find-date entry project)) - entry - (org-publish-find-title entry project)))) + (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) + (org-id-get-create) + (insert-file-contents file) + (buffer-string)))) + ((eq style 'tree) + (file-name-nondirectory (directory-file-name entry))))) -(defun org-blog-sitemap-function (title list) +(defun org-blog-sitemap-fn-descr (descr title list) "Return sitemap using TITLE and LIST returned by `org-blog-sitemap-format-entry'." (concat "#+TITLE: " title "\n" "#+AUTHOR: " author-name "\n" "#+EMAIL: " author-email "\n" - "\n#+begin_archive\n" + "#+DESCRIPTION: " descr "\n" (mapconcat (lambda (li) - (format "@@html:
  • @@ %s @@html:
  • @@" (car li))) + (format "* %s" (car li))) (seq-filter #'car (cdr list)) - "\n") - "\n#+end_archive\n")) + "\n"))) (defun org-blog-publish-to-html (plist filename pub-dir) "Same as `org-html-publish-to-html' but modifies html before finishing." @@ -204,16 +215,20 @@ Return output file name." dst-file)) (copy-file filename dst-file t))))) +(defalias 'org-blog-posts-sitemap-fn + (apply-partially 'org-blog-sitemap-fn-descr posts-descr)) + +(defalias 'org-blog-micro-sitemap-fn + (apply-partially 'org-blog-sitemap-fn-descr micro-descr)) + (setq org-publish-project-alist `(("orgfiles" :base-directory ,base-dir - :exclude ".*drafts/.*" + :exclude ".*(drafts|posts|micro)/.*" :base-extension "org" :publishing-directory ,publish-dir - :recursive t :publishing-function org-blog-publish-to-html - :with-toc nil :with-title nil :with-date t @@ -226,14 +241,59 @@ Return output file name." :html-head-extra ,org-blog-head :html-preamble org-blog-preamble :html-postamble org-blog-postamble + :auto-sitemap nil) + ("posts" + :base-directory ,posts-dir + :base-extension "org" + :publishing-directory ,posts-publish-dir + :recursive t + :publishing-function org-blog-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-filename "posts.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) + :sitemap-function org-blog-posts-sitemap-fn) + + ("micro" + :base-directory ,micro-dir + :base-extension "org" + :publishing-directory ,micro-publish-dir + :recursive t + :publishing-function org-blog-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 "micro.org" + :sitemap-title "Micro Blog Posts" + :sitemap-style list + :sitemap-sort-files anti-chronologically + :sitemap-format-entry org-blog-sitemap-format-entry + :sitemap-function org-blog-micro-sitemap-fn) ("assets" :base-directory ,assets-dir @@ -252,11 +312,11 @@ Return output file name." :publishing-directory ,publish-rss-dir :publishing-function (org-rss-publish-to-rss) :exclude ".*" - :include ("archive.org") + :include ("posts/posts.org" "micro/micro.org") :section-numbers nil :table-of-contents nil) - ("blog" :components ("orgfiles" "assets" "rss")))) + ("blog" :components ("orgfiles" "posts" "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) diff --git a/src/index.org b/src/index.org index d569fda..74d5029 100644 --- a/src/index.org +++ b/src/index.org @@ -8,7 +8,6 @@ #+OPTIONS: H:5 #+STARTUP: showeverything - Welcome to my personal website. - [[file:archive.org][articles]] diff --git a/src/micro/ping.org b/src/micro/ping.org new file mode 100644 index 0000000..7ddca83 --- /dev/null +++ b/src/micro/ping.org @@ -0,0 +1,11 @@ +#+TITLE: Ping +#+KEYWORDS: social +#+AUTHOR: Yann Esposito +#+EMAIL: yann@esposito.host +#+DESCRIPTION: +#+LANGUAGE: en +#+LANG: en +#+OPTIONS: H:5 auto-id:t +#+STARTUP: showeverything + +Ping! diff --git a/src/posts/new-blog.org b/src/posts/new-blog.org index 8852cf6..ad75d27 100644 --- a/src/posts/new-blog.org +++ b/src/posts/new-blog.org @@ -1,5 +1,5 @@ #+TITLE: New Blog -#+SUBTITLE: Meta Post (not really related to Donal Knuth) +#+SUBTITLE: Meta Post (not really related to Donald Knuth) #+AUTHOR: Yann Esposito #+EMAIL: yann@esposito.host #+DATE: [2019-08-17 Sat] diff --git a/src/posts/posts.org b/src/posts/posts.org new file mode 100644 index 0000000..1e71644 --- /dev/null +++ b/src/posts/posts.org @@ -0,0 +1,1172 @@ +#+TITLE: Blog Posts +#+AUTHOR: Yann Esposito +#+EMAIL: yann@esposito.host +#+DESCRIPTION: Articles +* * [[file:/Users/yaesposi/y/her.esy.fun/src/posts/project-el/index.org][Autoload Script by project]] +:PROPERTIES: +:RSS_PERMALINK: project-el/index.html +:PUBDATE: 2019-08-17 +:ID: FB591D7B-8153-44F5-8D40-3F8C83858529 +:CUSTOM_ID: ----file--users-yaesposi-y-her-esy-fun-src-posts-project-el-index-org--autoload-script-by-project-- +:END: +#+TITLE: Autoload Script by project +#+SUBTITLE: fast, secure, easy autoload +#+AUTHOR: Yann Esposito +#+EMAIL: yann@esposito.host +#+DATE: [2019-08-17 Sat] +#+KEYWORDS: programming, blog, org-mode +#+OPTIONS: auto-id:t + +#+begin_quote +/tl;dr/: A script that use projectile and GPG to securely load +an emacs lisp script when opening a new project. + +Check the [[#solution]] section to get the code. +#+end_quote + +* Problem + :PROPERTIES: + :CUSTOM_ID: problem + :ID: BD82DF52-CF0F-4E40-8E3A-AA04479E3071 + :PUBDATE: <2019-08-31 Sat 14:19> + :END: + +When providing a repository containing only org files of my blog. +I also wanted to provide everything necessary for users to be able to publish my +website. +Emacs, org-publish mostly assume you should put all those details in a +centralised place in your =~/.emacs.d/.init.el= file. + +The main principle is quite simple. + +1. When opening a new file in a project, check for the presence of a + =project.el= and =project.el.sig= file at the root directory of the + project. +2. Check the file was signed with by a trusted fingerprint. +3. Load the file. + +Other solutions I found on the internet asked you each time you enter in a +project if you trust the file or not. +This was both quite annoying and insecure as it is kind of easy to type 'y' +instead of 'n' and to load 3rd party script. + +Note that checking who signed a file with an external signature is not as +straightforward as it should be: + +#+begin_src elisp +(defun auto-load-project/get-sign-key (file) + "Return the fingerprint of they key that signed FILE. + +To sign a file you should used + +`gpg --local-user my@email --output project.el.sig --detach-sign project.el`" + (string-trim-right + (shell-command-to-string + (concat + (format "gpg --status-fd 1 --verify %s.sig %s 2>/dev/null " file file) + "|grep VALIDSIG" + "|awk '{print $3}'")))) +#+end_src + +- The `--status-fd` should provide more script friendly output. + GPG provide localized output by default which are therefore hard to use in + script (for grep for example). + +We use =projectile= to detect the project-root and when we are in a new project. +Unfortunately the =projectile-after-switch-project-hooks= doesn't work as I +expected. +So I use the hooks =find-file-hook= and =dired-mode-hook= to try to load the +file. +In order not to load the code each time, I need to keep a local state of project +already loaded. + +So now, each time I modify the =project.el= I sign it with the following +command line: + +#+begin_src bash +gpg --local-user my@email --output project.el.sig --detach-sign project.el +#+end_src + +* Solution + :PROPERTIES: + :CUSTOM_ID: solution + :ID: DC21FA06-1531-4944-855A-B61EBA1681B1 + :PUBDATE: <2019-08-31 Sat 14:19> + :END: + +The project is hosted here: https://gitlab.esy.fun/yogsototh/auto-load-project-el + +You can setup the emacs package in spacemacs with: + +#+begin_src elisp + ;; ... + dotspacemacs-additional-packages + '((auto-load-project :location + (recipe + :fetcher git + :url "https://gitlab.esy.fun/yogsototh/auto-load-project-el" + :files ("auto-load-project.el")))) + ;; ... + (defun dotspacemacs/user-config () + ;; ... + (require 'auto-load-project) + (setq auto-load-project/trusted-gpg-key-fingerprints + '("0000000000000000000000000000000000000000" ;; figerprint of trusted key 1 + "1111111111111111111111111111111111111111" + "2222222222222222222222222222222222222222" + ))) + ;; ... +#+end_src + +The full current code should be easy to follow if you have basic notions +of eLISP: + +#+begin_src elisp +(require 'projectile) + +(defvar auto-load-project/trusted-gpg-key-fingerprints + '() + "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 auto-load-project/get-sign-key (file) + "Return the fingerprint of they key that signed FILE. + +To sign a file you should used + +`gpg --local-user my@email --output project.el.sig --detach-sign project.el`" + (string-trim-right + (shell-command-to-string + (concat + (format "gpg --status-fd 1 --verify %s.sig %s 2>/dev/null " file file) + "|grep VALIDSIG" + "|awk '{print $3}'")))) + +(defun auto-load-project/trusted-gpg-origin-p (file) + "Return non-nil if the FILE is encrypted with a trusted key." + (member (auto-load-project/get-sign-key file) + auto-load-project/trusted-gpg-key-fingerprints)) + +(defconst auto-load-project/project-file "project.el" + "Project configuration file name.") + +(defvar auto-load-project/loaded-projects (list) + "Projects that have been loaded by `auto-load-project/load'.") + +(defun auto-load-project/load () + "Loads the `auto-load-project/project-file' for a project. +This is run once the project is loaded signifying project setup." + (interactive) + (when (projectile-project-p) + (lexical-let* ((current-project-root (projectile-project-root)) + (project-init-file (expand-file-name auto-load-project/project-file current-project-root)) + (project-sign-file (concat project-init-file ".sig"))) + (when (and (not (member current-project-root auto-load-project/loaded-projects)) + (file-exists-p project-init-file) + (file-exists-p project-sign-file) + (auto-load-project/trusted-gpg-origin-p project-init-file)) + (message "Loading project init file for %s" (projectile-project-name)) + (condition-case ex + (progn (load project-init-file) + (add-to-list 'auto-load-project/loaded-projects current-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)))))))) + +(add-hook 'find-file-hook #'auto-load-project/load t) +(add-hook 'dired-mode-hook #'auto-load-project/load t) + +(provide 'auto-load-project) +#+end_src + +* * [[file:/Users/yaesposi/y/her.esy.fun/src/posts/troll-2/index.org][Troll 2]] +:PROPERTIES: +:RSS_PERMALINK: troll-2/index.html +:PUBDATE: 2019-08-17 +:ID: 4CF9E289-E019-4403-8F61-9F7FBC3AB052 +:CUSTOM_ID: ----file--users-yaesposi-y-her-esy-fun-src-posts-troll-2-index-org--troll-2-- +:END: +#+Title: Troll 2 +#+Subtitle: How a terrible movie can be entertaining +#+Author: Yann Esposito +#+Email: yann@esposito.host +#+Date: [2019-08-17 Sat] +#+KEYWORDS: movie +#+DESCRIPTION: +#+LANGUAGE: en +#+LANG: en +#+OPTIONS: H:5 auto-id:t +#+STARTUP: showeverything + +#+begin_notes +I watched what may be the worse movie of all time and I still enjoyed +greatly the show. +#+end_notes + +I wanted to watch an horror teen movie I saw when I was a kid; Troll. +During my searches, I discovered there is another movie named "Troll 2". +I thought that it is certainly the sequel. + +I took a look to the imdb note, and it was very bad (2.9 / 10). +But, hey, I decided to watch it. +Here is an overview of my watching experience. + +* The watching +:PROPERTIES: +:CUSTOM_ID: the-watching +:ID: F1444E4E-88F3-44EA-8E3B-5A313A558964 +:PUBDATE: <2019-08-31 Sat 14:19> +:END: + +The synopsis of Troll 2 is a family living in a city that exchange its +house to go to the village of Nilbog "to live like farmers"... +The son of the family see his dead grandfather. +The grandpa warn his grandson against Goblins. +Those creatures make you eat green things that transform you and then they +eat you. +Of course, nobody listen to the boy that does not want to go to Nilbog, +which is infested by goblins that can take Human appearance. + +Troll 2 is bad on most criterion you use to measure the quality of a movie. + +During the first minutes of the movie, you get to see the costume of the +goblins. +Those costume looks very bad and cheap. +So much you can only find them not terrorizing but funny and ridiculous. + +#+CAPTION: One goblin during the introduction scene of Troll 2 +#+NAME: fig:troll-2-intro +#+ATTR_HTML: A goblin +[[./Troll-2-intro.jpg]] + +Soon after that, you realize the acting of all actors is extremely bad. +In fact, it is so bad, you might not believe me how bad it is. +To give you an idea, the only equal bad acting I ever witnessed was while +looking at amateurs first Youtube movies trying to follow a scenario. +Apparently most actors were amateurs, it was their first and last movie. + +#+CAPTION: One particularly terrible acting scene +#+NAME: fig:bad-acting +#+ATTR_HTML: A bad acting demonstration +[[file:bad-acting.png]] + +The dialog are, really something... +For example the expression "clusters of hemorrhoid" is used in a non ironic +dialog. + +The scenario is terrible. +For example, most of the thing occurring suffer from terrible plot holes or +terrible mistakes that make everything hard to believe even if you accept +the premises of a world were Goblins would exist. +For example, the grandfather ghost can stop time for 30 seconds for no +reason at all. + +The realization is a series of basic mistakes. +Actors are not in the same places after all plan cut for example. +Some filmed scene feel so wrong. + +I forgot to give a word about the music. +It is like the director choose the worst music to go along each scene. + +The first ending is really, quite surprising. +They win against the monsters with, what I believe was a failed attempt at +humor. +It misses the point so bad, that the irony still make it funny. + +#+CAPTION: Our hero save the day by urinating on the table. His family is frozen for 30s said grandpa, they were for 70s. +#+NAME: fig:prevent-eating +#+ATTR_HTML: Eliott prevents his family to eat the food by urinating on the table +[[./prevent-eating-scene.jpg]] + +Of course, the very last scene is a classical so terrible cliché. +To let you close the experience in awe. + +But there is a bonus, the cherry on the cake. +During all the movie, it is *never* question of a Troll at all. +*There is not Troll in Troll 2*. + +Still, it was quite entertaining. + +# LocalWords: Nilbog cliché + +* After the movie +:PROPERTIES: +:CUSTOM_ID: after-the-movie +:ID: 44290CAD-7467-4D01-AB15-9A2D5A01B6C5 +:PUBDATE: <2019-08-31 Sat 14:19> +:END: + +What is really interesting though is that I really enjoyed the watch. +It was so bad, that I couldn't enter in the movie. +I analyzed the movie, and saw all its failures and noticed everything wrong +about it. +In the end, the experience was still quite enjoyable. +For example, there are some attempts at humor, but almost all of them fail +terribly. +I didn't laugh about the joke, but I did nonetheless by thinking about how +bad that joke was. + +Once going to [[https://www.imdb.com/title/tt0105643/][imdb]], I discovered I wasn't alone in loving that terrible +movie. +Now, I don't know if I should give that movie a 1 or 2 stars or 8 to 9 +stars because it was so entertaining. + +Since then, I learned a few anecdotes about that movie. + +It was realized in America by Claudio Fragasso who didn't speak fluent +English. +He and his wife were apparently irritated by many of her friends turning +vegetarian. +He brought the film crew over with him from Italy. +None of them spoke English either. + +#+begin_quote +“The cast had few experienced actors, and was primarily assembled from +residents of nearby towns who responded to an open casting call. + +George Hardy was a dentist with no acting experience who showed up for fun, +hoping to be cast as an extra, only to be given one of the film’s largest +speaking roles. + +Don Packard, who played the store owner, was actually a resident at a +nearby mental hospital, and was cast for—and filmed—his role while on a day +trip; after recovering and being released from the hospital, he recalled +that he had no idea what was happening around him, and that his disturbed +“performance” in the film was not acting”. +#+end_quote + +Also that movie is so bad, there is a documentary about that movie named: +[[https://www.imdb.com/title/tt1144539]["Best Worst Movie"]]. + + + +* * [[file:/Users/yaesposi/y/her.esy.fun/src/posts/new-blog.org][New Blog]] +:PROPERTIES: +:RSS_PERMALINK: new-blog.html +:PUBDATE: 2019-08-17 +:ID: 27E9B039-E66E-4419-96BD-6235EA38954A +:CUSTOM_ID: ----file--users-yaesposi-y-her-esy-fun-src-posts-new-blog-org--new-blog-- +:END: +#+TITLE: New Blog +#+SUBTITLE: Meta Post (not really related to Donald Knuth) +#+AUTHOR: Yann Esposito +#+EMAIL: yann@esposito.host +#+DATE: [2019-08-17 Sat] +#+KEYWORDS: programming, blog, org-mode, web, css +#+OPTIONS: auto-id:t + +#+begin_notes +tl;dr: The first blog post of a blog should certainly be about the blog +itself to provide a feeling of self-reference. +#+end_notes + +* Peaceful and Respectful Website +:PROPERTIES: +:CUSTOM_ID: peaceful-and-respectful-website +:ID: 9C715987-0E8C-49CB-8165-94C730989B91 +:PUBDATE: <2019-08-31 Sat 14:19> +:END: + +There is a trend about website being quite less accessible, using more and +more resources, adding trackers, popups, videos, animations, big js +frameworks, etc... + +I wanted a more peaceful and respectful website. + +That website was created with the following constraints in mind by order of +priority: + +1. *Respect Privacy*; no tracker of any sort (no ads, no google analytics, no + referrer for all external links, etc...) +2. *nearly no javascript*; no js at all except for a single exception, + pages containing Math formula are displayed using mathjax. That means + that event the CSS theme switcher does not use javascript. +3. *Accessible*; should be easy to read on a text browser so people with + disabilities could easily consume it +4. *nerdy*; should feel mostly like markdown text in a terminal and source + code should be syntax highlighted. +5. *theme switchable*; support your preferred light/dark theme by default + but you can change it if you want. +6. *rss*; you should be able to get informed when I add a new blog post. +7. *frugal*; try to minimize the resources needed to visit my website; no + javascript, no web-font, not too much CSS magic, not much images or really + compressed one. + +Some of the constraints are straightforward to get, some not. + +You can also check that not using more resources one of the theme of my +website looks quite more classical and modern that my preferred ones. + +** Respect Privacy +:PROPERTIES: + :CUSTOM_ID: respect-privacy + :ID: 085ADDF5-D910-4D0E-BA26-F07060D89E37 + :END: +The one should be easy, simply not put any 3rd party inclusion in my website. +So, no external CSS in my headers, no link to any image I do not host myself. +No 3rd party javascript. + +** Javascript Free +:PROPERTIES: + :CUSTOM_ID: javascript-free + :ID: 882ED09A-B7E9-4AD9-AB15-9C817EE000B3 + :END: +I do not really see why a content oriented website should need to execute javascript. + +** Accessible +:PROPERTIES: +:CUSTOM_ID: disability-friendly +:ID: 854BF110-84CF-45D3-99A4-4F7473BFD6DC +:END: +A good way to check that a website is friendly to disabled people is by +looking at it with a text browser. +If you open most website today you see that at the top of the page is +crippled with a numerous number of links/metas info used for javascript +tricks, login/logout buttons, etc... +The website should only contain, a pretty minimal menu to navigate, and the +content. + +** Nerdy +:PROPERTIES: +:CUSTOM_ID: nerdy +:ID: 08E2BE72-C7FD-4E86-92BF-1DB97B9A6F44 +:END: +The feel of the website should be nerdy, it should look like reading a +terminal or emacs. +It should almost feel the same as if you were using a text-browser. +For sensible people, I added a "modern" theme that should better suit +modern eye, still the first design should always be the terminal looking +one. + +** Theme switchable +:PROPERTIES: +:CUSTOM_ID: theme-switchable +:ID: B2061EBE-C79C-49B8-AC57-5FA5D6961F21 +:END: +Even if you are not used to disability friendly browser. +The website should try to guess your preferred way to consume my website. +Recently we dark/light themes were integrated as a new CSS feature. +This website should propose your apparently preferred theme. +But you could also change it manually. + +** RSS +:PROPERTIES: +:CUSTOM_ID: rss +:ID: F21DD644-92A1-4BEA-A3AC-4AB8BA03A60A +:END: +This is another layer that help you consume my website as you prefer. +You should at least be informed a new article has been published. + +** Frugal +:PROPERTIES: +:CUSTOM_ID: frugal +:ID: 29FFEBDC-B978-44E3-9C33-CB6302632037 +:END: +This one is a bit tricky. +It would mean, that visiting my website should not consume much resources. +Mainly, this would prevent using heavy medias as much as possible. +So, no video, no animated gif, no image if possible or very compressed small one. +So I have a script that convert all images to maximize site to `800x800` +and use at max 16 colors. On my current example image the size goes from 3.1MB to 88KB. + +* How +:PROPERTIES: +:CUSTOM_ID: how +:ID: 21EA558C-C9BB-45F2-8F17-B544DBA6AC44 +:PUBDATE: <2019-08-31 Sat 14:19> +:END: +** CSS +:PROPERTIES: +:CUSTOM_ID: css +:ID: B117AFAE-BBA4-4DFB-B171-5B60B4E67412 +:END: +Regarding CSS, I always found that the default text display by navigator is +terrible. +So just to "fix" a minimal CSS to have something bearable it takes me about +120 lines of CSS. + +By fixing I mean things like using a fixed line width for text (there is an +optimal range to improve legibility). +Also having correct list indentation, line-height and font-size. +Table displaying correctly. + +Then I have about 90 lines of CSS to make my HTML look like text source of +a markdown. + +Then I set a few CSS rules to handle the ids and classes added by +org-export as instead of using the ubiquitous Markdown, I prefer greatly to +use org mode files. +I need 60 lines of CSS for them. + +In order to handle color themes (5 at the time of writing those lines) I +use almost 350 line to handle those. + +*** CSS Theme selection +:PROPERTIES: +:CUSTOM_ID: css-theme-selection +:ID: 4061F98F-1AA4-441A-A073-6E666A4B1AA1 +:END: + +One thing that wasn't straightforward while writing the CSS was to provide +an interactive theme selector without any javascript involved. +That theme switcher is really the limit I can concede to modern standards +because it is CSS only. + +The trick is to provide one top-level element per theme at the beginning of +the body of the HTML. +Then hide those elements (I chose inputs). +Finally provide a set of anchor links. + +#+begin_src html + ... + + + +
    + Change theme: + Light + Dark +
    +
    + ALL YOUR CONTENT HERE +
    + +#+end_src + +Then use the /sibling/ CSS selector =~=. +Then put all your content in a div of class =.main= for example. +Finally in the CSS you can write things like: + +#+begin_src css + /* hide all radio button that are not inside another div of body */ + body > input { + display: none; + } + :root { + --light-color: #fff; + --dark-color: #000; + } + input#light:target ~ .main { + background-color: var(--light-color); + color: var(--dark-color); + } + input#dark:target ~ .main { + background-color: var(--dark-color); + color: var(--light-color); + } +#+end_src + +I previously used checkbox inputs but using URL fragment feels better. + +** Blog Engine - org-mode with org-publish +:PROPERTIES: +:CUSTOM_ID: blog-engine---org-mode-with-org-publish +:ID: 674088E6-C71B-4C48-B785-43BE8C4A2ADB +:END: +So publishing a website is something that could go from. +Write your own HTML each time. +But this is quite tedious, so we generally all use a website generator. +The next thing with the minimal possible amount of work is using org-mode +with org-publish. +Because a website is mostly, export all of file in org-mode format (easier +to write and manipulate than raw HTML) to HTML. + +In fact, there are numerous details that make this task not this straightforward. +You want: + +1. from a tree of org-mode files, generate an equivalent file tree of HTML + files generated from the org-mode files. This is the main purpose of org-publish. +2. We also want to set specific headers, a CSS file, a favicon, link to RSS + file, mobile friendly directives. +3. Have common header/footer (preamble, postamble) if possible with a menu. +4. An archive page with a list of posts. +5. Generate an RSS file +6. Niceties: + - obfuscate your email to prevent spam + - link to your email with a link to the current page integrated in the body/subject + - replace your external link to open in a new tab securely (=noopener / noreferrer=). + - compress images during publishing + +Also, a single detail make using org-publish a bit awkward compared to +classical other classical static website generators; it is designed to be +set in you full emacs configuration. +But I wanted to be able to clone my git repository and be able to generate +my website locally even if I clone it on different directories. + +So I created a package just for that: [[file:project-el/index.org][Autoload eLISP file in projects]]. + +*** Tree of files +:PROPERTIES: +:CUSTOM_ID: tree-of-files +:ID: 9025C6FA-C1DE-4194-AE19-A1E735FDF6FD +:END: + +There is a first pass that use =projectile= emacs package to detect the +current root file of the project and provide a list of absolute paths. + +Then you set the associative list =org-publish-project-alist= with many +straightforward details. +The source directory, the destination directory, but also, file to exclude, +a function used to transform org files to HTML, etc... + +#+begin_src elisp + (setq domainname "https://john.doe") + (setq base-dir (concat (projectile-project-root) "src")) + (setq publish-dir (concat (projectile-project-root) "_site")) + (setq assets-dir (concat base-dir "/")) + (setq publish-assets-dir (concat publish-dir "/")) + (setq rss-dir base-dir) + (setq rss-title "Subscribe to articles") + (setq publish-rss-dir publish-dir) + (setq publish-rss-img (concat domainname "/rss.png")) + (setq css-path "/css/minimalist.css") + (setq author-name "John Doe") + (setq author-email "john@doe.com") + + (require 'org) + (require 'ox-publish) + (require 'ox-html) + (require 'org-element) + (require 'ox-rss) + + (setq org-link-file-path-type 'relative) + (setq org-publish-timestamp-directory + (concat (projectile-project-root) "_cache/")) + + (setq org-publish-project-alist + `(("orgfiles" + :base-directory ,base-dir + :exclude ".*drafts/.*" + :base-extension "org" + :publishing-directory ,publish-dir + :recursive t + :publishing-function org-blog-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 ".*" + :exclude ".*\.org$" + :publishing-directory ,publish-assets-dir + :publishing-function org-blog-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" + :rss-image-url ,publish-rss-img + :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")))) +#+end_src +*** HTML Headers +:PROPERTIES: +:CUSTOM_ID: html-headers +:ID: 97990AD3-218F-46E5-A5CD-7DCDDDF34653 +:END: + +I set the header to provide a link to the RSS file, the CSS, the favicon +and viewport directive for mobile browsers. + +#+begin_src elisp + (defvar org-blog-head + (concat + "" + "" + "" + "")) + +#+end_src +*** Preamble & Postamble +:PROPERTIES: +:CUSTOM_ID: preamble---postamble +:ID: 43B0C7DD-4CC8-4A6D-8E72-D8C3A242D927 +:END: + +So I put a menu in both the preamble and postamble. +The postamble contains a lot of details about the article, author, email, +date, etc... + +#+begin_src elisp +(defun menu (lst) + "Blog menu" + (concat + "" + (mapconcat 'identity + (append + '("Home" + "Posts" + "About") + lst) + " | ") + "")) + + (defun get-from-info (info k) + (let ((i (car (plist-get info k)))) + (when (and i (stringp i)) + i))) + + (defun org-blog-preamble (info) + "Pre-amble for whole blog." + (concat + "
    " + (menu '("↓ bottom ↓")) + "

    " + (format "%s" (car (plist-get info :title))) + "

    " + (when-let ((date (plist-get info :date))) + (format "%s" + (format-time-string "%Y-%m-%d" + (org-timestamp-to-time + (car date))))) + (when-let ((subtitle (car (plist-get info :subtitle)))) + (format "

    %s

    " subtitle)) + "
    ")) + + (defun org-blog-postamble (info) + "Post-amble for whole blog." + (concat + "
    " + "" + (menu '("↑ Top ↑")) + "
    ")) +#+end_src +*** Obfuscate email +:PROPERTIES: +:CUSTOM_ID: obfuscate-email +:ID: CF4CD280-7091-4ADB-969E-5D566D382F87 +:END: + +A simple function to obfuscate HTML by using hexadecimal and octal notation. + +#+begin_src elisp + (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))) +#+end_src +*** Specific email subject +:PROPERTIES: +:CUSTOM_ID: specific-email-subject +:ID: A137ADD3-F02F-4CEC-948C-75088D8276B4 +:END: + +You can set a subject to an email when you click on it by writing a link +that looks like: + +#+begin_example +mailto:john@doe.com?subject=the-subject +#+end_example + +Of course most of it is obfuscated. + +#+begin_src elisp + (let* ((obfs-email (obfuscate-html email)) + (obfs-author (obfuscate-html author)) + (obfs-title (obfuscate-html (get-from-info info :title))) + (full-email (format "%s <%s>" obfs-author obfs-email))) + (format "
    Author: %s
    " + (obfuscate-html "mailto:") + full-email + (obfuscate-html "?subject=yblog: ") + obfs-title + full-email)) +#+end_src +*** Nice external links +:PROPERTIES: +:CUSTOM_ID: nice-external-links +:ID: 89B2526A-3315-460D-995E-A243C07F260D +:END: + +Also, why not fix our external link (see [[https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/][this article]] as reference): + +#+begin_src elisp + ;; 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 " +dither -colors 16 -depth 4 dest/a.png +#+end_src + +For example: + + +#+CAPTION: Compressed Image +#+NAME: fig:compressed-image +#+ATTR_HTML: A Compressed Image +[[../img/a.png]] + +I compress automatically images during publishing with: + +#+begin_src elisp + (defun org-blog-publish-attachment (plist filename pub-dir) + "Publish a file with no transformation of any kind. + 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. + Take care of minimizing the pictures using imagemagick. + Return output file name." + (unless (file-directory-p pub-dir) + (make-directory pub-dir t)) + (or (equal (expand-file-name (file-name-directory filename)) + (file-name-as-directory (expand-file-name pub-dir))) + (let ((dst-file (expand-file-name (file-name-nondirectory filename) pub-dir))) + (if (string-match-p ".*\\.\\(png\\|jpg\\|gif\\)$" filename) + (shell-command + (format "convert %s -resize 800x800\\> +dither -colors 16 -depth 4 %s" + filename + dst-file)) + (copy-file filename dst-file t))))) +#+end_src +*** Full code +:PROPERTIES: +:CUSTOM_ID: full-code +:ID: 53F10956-E515-4DDB-B861-76189C60EDB8 +:END: + +Here is the full code: + +#+begin_src elisp + (setq domainname "https://john.doe") + (setq base-dir (concat (projectile-project-root) "src")) + (setq publish-dir (concat (projectile-project-root) "_site")) + (setq assets-dir (concat base-dir "/")) + (setq publish-assets-dir (concat publish-dir "/")) + (setq rss-dir base-dir) + (setq rss-title "Subscribe to articles") + (setq publish-rss-dir publish-dir) + (setq publish-rss-img (concat domainname "/rss.png")) + (setq css-path "/css/minimalist.css") + (setq author-name "John Doe") + (setq author-email "john@doe.com") + + (require 'org) + (require 'ox-publish) + (require 'ox-html) + (require 'org-element) + (require 'ox-rss) + + (setq org-link-file-path-type 'relative) + (setq org-publish-timestamp-directory + (concat (projectile-project-root) "_cache/")) + + (defvar org-blog-head + (concat + "" + "" + "" + "")) + + (defun menu (lst) + "Blog menu" + (concat + "" + (mapconcat 'identity + (append + '("Home" + "Posts" + "About") + lst) + " | ") + "")) + + (defun get-from-info (info k) + (let ((i (car (plist-get info k)))) + (when (and i (stringp i)) + i))) + + (defun org-blog-preamble (info) + "Pre-amble for whole blog." + (concat + "
    " + (menu '("↓ bottom ↓")) + "

    " + (format "%s" (car (plist-get info :title))) + "

    " + (when-let ((date (plist-get info :date))) + (format "%s" + (format-time-string "%Y-%m-%d" + (org-timestamp-to-time + (car date))))) + (when-let ((subtitle (car (plist-get info :subtitle)))) + (format "

    %s

    " subtitle)) + "
    ")) + + (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 + "
    " + ;; TODO install a comment system + ;; (let ((url (format "%s%s" domainname (replace-regexp-in-string base-dir "" (plist-get info :input-file))))) + ;; (format "comment" + ;; (url-hexify-string url))) + "" + (menu '("↑ Top ↑")) + "
    ")) + + (defun org-blog-sitemap-format-entry (entry _style project) + "Return string for each ENTRY in PROJECT." + (when (s-starts-with-p "posts/" entry) + (format (concat "@@html:" + "@@ %s: @@html:@@" + " [[file:%s][%s]]" + " @@html:@@") + (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: " author-name "\n" + "#+EMAIL: " author-email "\n" + "\n#+begin_archive\n" + (mapconcat (lambda (li) + (format "@@html:
  • @@ %s @@html:
  • @@" (car li))) + (seq-filter #'car (cdr list)) + "\n") + "\n#+end_archive\n")) + + (defun org-blog-publish-to-html (plist filename pub-dir) + "Same as `org-html-publish-to-html' but modifies html before finishing." + (let ((file-path (org-html-publish-to-html plist filename pub-dir))) + (with-current-buffer (find-file-noselect file-path) + (goto-char (point-min)) + (search-forward "") + (insert (mapconcat 'identity + '("" + "" + "" + "" + "
    " + "
    " + "Change theme: " + "" + "()" + " / " + "" + "()" + "
    " + "
    " + "
    ") + "\n")) + (goto-char (point-max)) + (search-backward "") + (insert "\n
    \n") + (save-buffer) + (kill-buffer)) + file-path)) + +(defun org-blog-publish-attachment (plist filename pub-dir) + "Publish a file with no transformation of any kind. + 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. + Take care of minimizing the pictures using imagemagick. + Return output file name." + (unless (file-directory-p pub-dir) + (make-directory pub-dir t)) + (or (equal (expand-file-name (file-name-directory filename)) + (file-name-as-directory (expand-file-name pub-dir))) + (let ((dst-file (expand-file-name (file-name-nondirectory filename) pub-dir))) + (if (string-match-p ".*\\.\\(png\\|jpg\\|gif\\)$" filename) + (shell-command + (format "convert %s -resize 800x800\\> +dither -colors 16 -depth 4 %s" + filename + dst-file)) + (copy-file filename dst-file t))))) + + (setq org-publish-project-alist + `(("orgfiles" + :base-directory ,base-dir + :exclude ".*drafts/.*" + :base-extension "org" + :publishing-directory ,publish-dir + + :recursive t + :publishing-function org-blog-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 ".*" + :exclude ".*\.org$" + :publishing-directory ,publish-assets-dir + :publishing-function org-blog-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" + :rss-image-url ,publish-rss-img + :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 "