her.esy.fun/src/posts/posts.org

1173 lines
42 KiB
Org Mode
Raw Normal View History

2019-08-31 12:21:03 +00:00
#+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 films 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
...
<body>
<input id="light"/>
<input id="dark"/>
<div id="labels">
Change theme:
<a href="#light">Light</a>
<a href="#dark">Dark</a>
</div>
<div class="main">
ALL YOUR CONTENT HERE
</div>
</body>
#+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
"<link rel=\"stylesheet\" type=\"text/css\" href=\"" css-path "\"/>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<link rel=\"alternative\" type=\"application/rss+xml\" title=\"" rss-title "\" href=\"/archives.xml\" />"
"<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/favicon.ico\">"))
#+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
"<navigation>"
(mapconcat 'identity
(append
'("<a href=\"/index.html\">Home</a>"
"<a href=\"/archive.html\">Posts</a>"
"<a href=\"/about-me.html\">About</a>")
lst)
" | ")
"</navigation>"))
(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
"<div class=\"content\">"
(menu '("<a href=\"#postamble\">↓ bottom ↓</a>"))
"<h1>"
(format "%s" (car (plist-get info :title)))
"</h1>"
(when-let ((date (plist-get info :date)))
(format "<span class=\"article-date\">%s</span>"
(format-time-string "%Y-%m-%d"
(org-timestamp-to-time
(car date)))))
(when-let ((subtitle (car (plist-get info :subtitle))))
(format "<h2>%s</h2>" subtitle))
"</div>"))
(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)))
(let* ((obfs-email (obfuscate-html email))
(obfs-author (obfuscate-html author))
(obfs-title (obfuscate-html (get-from-info info :title)))
(full-email (format "%s &lt;%s&gt;" obfs-author obfs-email)))
(format "<div class=\"author\">Author: <a href=\"%s%s%s%s\">%s</a></div>"
(obfuscate-html "mailto:")
full-email
(obfuscate-html "?subject=yblog: ")
obfs-title
full-email))
(format "<div class=\"author\">Author: %s</div>" author)))
(when-let ((date (plist-get info :date)))
(format "<div class=\"date\">Created: %s</div>"
(format-time-string "%Y-%m-%d"
(org-timestamp-to-time
(car date)))))
(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>"))
#+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 &lt;%s&gt;" obfs-author obfs-email)))
(format "<div class=\"author\">Author: <a href=\"%s%s%s%s\">%s</a></div>"
(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 "<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
*** Image compression
:PROPERTIES:
:CUSTOM_ID: image-compression
:ID: B3AF8B04-28AF-4770-807D-5C57448D3E44
:END:
to compress images I use [[https://imagemagick.org][imagemagick]] like this:
#+begin_src bash
convert src/a.png -resize 800x800\> +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
"<link rel=\"stylesheet\" type=\"text/css\" href=\"" css-path "\"/>"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
"<link rel=\"alternative\" type=\"application/rss+xml\" title=\"" rss-title "\" href=\"/archives.xml\" />"
"<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/favicon.ico\">"))
(defun menu (lst)
"Blog menu"
(concat
"<navigation>"
(mapconcat 'identity
(append
'("<a href=\"/index.html\">Home</a>"
"<a href=\"/archive.html\">Posts</a>"
"<a href=\"/about-me.html\">About</a>")
lst)
" | ")
"</navigation>"))
(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
"<div class=\"content\">"
(menu '("<a href=\"#postamble\">↓ bottom ↓</a>"))
"<h1>"
(format "%s" (car (plist-get info :title)))
"</h1>"
(when-let ((date (plist-get info :date)))
(format "<span class=\"article-date\">%s</span>"
(format-time-string "%Y-%m-%d"
(org-timestamp-to-time
(car date)))))
(when-let ((subtitle (car (plist-get info :subtitle))))
(format "<h2>%s</h2>" subtitle))
"</div>"))
(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\">"
;; TODO install a comment system
;; (let ((url (format "%s%s" domainname (replace-regexp-in-string base-dir "" (plist-get info :input-file)))))
;; (format "<a href=\"https://comments.esy.fun/slug/%s\">comment</a>"
;; (url-hexify-string url)))
"<footer>"
(when-let ((author (get-from-info info :author)))
(if-let ((email (plist-get info :email)))
(let* ((obfs-email (obfuscate-html email))
(obfs-author (obfuscate-html author))
(obfs-title (obfuscate-html (get-from-info info :title)))
(full-email (format "%s &lt;%s&gt;" obfs-author obfs-email)))
(format "<div class=\"author\">Author: <a href=\"%s%s%s%s\">%s</a></div>"
(obfuscate-html "mailto:")
full-email
(obfuscate-html "?subject=yblog: ")
obfs-title
full-email))
(format "<div class=\"author\">Author: %s</div>" author)))
(when-let ((date (plist-get info :date)))
(format "<div class=\"date\">Created: %s</div>"
(format-time-string "%Y-%m-%d"
(org-timestamp-to-time
(car date)))))
(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 (concat "@@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: " author-name "\n"
"#+EMAIL: " author-email "\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"))
(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 "<body>")
(insert (mapconcat 'identity
'("<input type=\"radio\" id=\"light\" name=\"theme\"/>"
"<input type=\"radio\" id=\"dark\" name=\"theme\"/>"
"<input type=\"radio\" id=\"raw\" name=\"theme\"/>"
"<input type=\"radio\" id=\"darkraw\" name=\"theme\"/>"
"<div id=\"labels\">"
"<div class=\"content\">"
"Change theme: "
"<label for=\"light\">Light</label>"
"(<label for=\"raw\">raw</label>)"
" / "
"<label for=\"dark\">Dark</label>"
"(<label for=\"darkraw\">raw</label>)"
"</div>"
"</div>"
"<div class=\"main\">")
"\n"))
(goto-char (point-max))
(search-backward "</body>")
(insert "\n</div>\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 "<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