From 27f3bbc1ee6183968085d3b75f8a49ba99be6f81 Mon Sep 17 00:00:00 2001 From: fido-node Date: Sat, 6 Jul 2024 15:17:48 +0000 Subject: [PATCH] deploy: fido-node/org@94754dc75998e268c8c08da52bb7849876d29ba7 --- index.html | 24 +- posts.html | 90 +-- posts/about_blog.html | 114 ++-- posts/add_rss_to_blog.html | 32 +- posts/blog_index_and_tags_automation.html | 704 +++++++++++++++++++++- posts/improve_code_blocks.html | 48 +- posts/keeb.html | 118 ++-- posts/posts_preview.html | 64 +- rss.html | 102 ++-- rss.xml | 13 +- tags/@diy.html | 12 +- tags/@elisp.html | 41 +- tags/@highlightjs.html | 12 +- tags/@imagemagick.html | 12 +- tags/@keeb.html | 12 +- tags/@org-mode.html | 47 +- tags/@rss.html | 12 +- tags/@tags.html | 24 + 18 files changed, 1135 insertions(+), 346 deletions(-) create mode 100644 tags/@tags.html diff --git a/index.html b/index.html index 164194b..f5f1da5 100644 --- a/index.html +++ b/index.html @@ -3,9 +3,9 @@

avatar.jpg

-
-

 

-
+
+

 

+

Full stack engineer FP-curious | λ-affected @@ -13,9 +13,9 @@ Wanna be rustacean 🦀 and/or secops guy 🔒

-
-

Experience

-
+
+

Experience

+
  • Current position @SamsungFood. Internal tools engineer for data platform ➡ Internal tools engineer for developers.
  • @@ -24,9 +24,9 @@ Fullstack engineer.
-
-

Technologies

-
+
+

Technologies

+

Work with:

@@ -43,9 +43,9 @@ Work with:
-
-

Contacts

-
+
+

Contacts

+

Contact me via:

diff --git a/posts.html b/posts.html index aef6c44..7379121 100644 --- a/posts.html +++ b/posts.html @@ -1,11 +1,28 @@ Alex Mikhailov - Blog
-
-

Posts

-
+
+

Posts

+
-
-

Posts preview

-
+
+

Blog index and tags automation

+
+

+Let's add tags to blog posts +

+
+

+Tags are a nice and easy way to organize posts without explicit search. In the simplest way, you have a list of tags in posts, and each tag links to a page with all posts having the corresponding tag. It is also helpful to have a page with all tags available in the blog. And, of course, I don't want to maintain the list of tags manually. +Automate tags. … +

+
+

+posted on 2024-07-05 +

+
+
+
+

Posts preview

+

Add post preview for OpenGraph cards

@@ -20,9 +37,9 @@ posted on 2024-06-28

-
-

Improve code blocks

-
+
+

Improve code blocks

+

Use highlight.js for code syntax highlighting

@@ -37,9 +54,9 @@ posted on 2024-06-25

-
-

Org blog with RSS

-
+
+

Org blog with RSS

+

Let's add RSS feed to blog

@@ -54,9 +71,9 @@ posted on 2024-06-23

-
-

Org to HTML and back

-
+
+

Org to HTML and back

+

Blog post about publishing my blog with Org Mode

@@ -71,9 +88,9 @@ posted on 2024-06-22

-
-

My keyboard journey

-
+
+

My keyboard journey

+

Blog post about my keyboards

@@ -89,30 +106,33 @@ posted on 2024-06-05
-
-

Tags

-
+
+

Tags

+
-
-

@org-mode (4)

+
+

@org-mode (5)

-
-

@elisp (3)

+
+

@elisp (4)

-
-

@rss (1)

+
+

@rss (1)

-
-

@highlightjs (1)

+
+

@tags (1)

-
-

@keeb (1)

+ -
-

@diy (1)

+
+

@keeb (1)

-
-

@imagemagick (1)

+
+

@diy (1)

+
+
diff --git a/posts/about_blog.html b/posts/about_blog.html index 4711594..28577a6 100644 --- a/posts/about_blog.html +++ b/posts/about_blog.html @@ -2,44 +2,44 @@

Table of Contents

-
-

Disclaimer

-
+
+

Disclaimer

+

I'm neither proficient in Org Mode (further on "Org"), nor a good front-end engineer. I think that a simple solution is better than no solution. If you see a mistake, you can contact me via iam@fidonode.me.

-
-

What is Org?

-
+
+

What is Org?

+

Your life in plain text @@ -55,9 +55,9 @@ Everything you can do in Org is to write a text. With a special markup, of cours

-
-

Why Org Mode?

-
+
+

Why Org Mode?

+
  1. Plain text. Plain text as a data source offers significant versatility. You can read and understand what happens in org files without needing Emacs.
  2. @@ -69,16 +69,16 @@ I do not have a habit of collecting and keeping information. I believe that disc
-
-

Render Org to blog or whatever

-
+
+

Render Org to blog or whatever

+

Org already has a way to render files into HTML, allowing you to create simple HTML files with minimal styling. I'm not interesting in styling from org, so I decide to use picocss framework.

-
-

Render HTML

-
+
+

Render HTML

+

I want to change some templates here and there. I've found esxml package. It is a decent DSL for writing XML/HTML. Here is how page header and footer look in this DSL. @@ -183,9 +183,9 @@ So everything is almost done. Time to use our custom publishing function in proj

-
-

Static files

-
+
+

Static files

+

Yep, you may want to publish some photos with your blog or any other static files.

@@ -205,9 +205,9 @@ Looks self explanatory.

-
-

Whole build script

-
+
+

Whole build script

+

Here is the whole elisp script which I use to publish my blog. It have some additional quirks to work with doomscript ./build-site.el.

@@ -365,9 +365,9 @@ Here is the whole elisp script which I use to publish my blog. It have some addi
-
-

Publish through GitHub Action

-
+
+

Publish through GitHub Action

+

With all previous preparations, this step sounds simple like: emacs -Q --script ./build-site.el I've chosen a pretty standard way to publish static sites through GitHub Pages. Since I keep my Org files in a private repo, I need some additional steps to address it. I use the peaceiris/actions-gh-pages@v3 action to publish from my Org repo to the Pages repo. @@ -375,9 +375,9 @@ However, since I use Doom Emacs as my configuration framework, we n

-
-

Install Emacs

-
+
+

Install Emacs

+

If you want to run Emacs Lisp, you need the whole Emacs, at least without GUI. In a GitHub Action, you can simply run:

@@ -388,9 +388,9 @@ This way has a downside - you will install Emacs on each action run since the sy

-
-

Just bring everything

-
+
+

Just bring everything

+

I need to take extra steps since I use Doom Emacs and have my configs in Org. You may also need to install dependencies for your configuration.

@@ -426,9 +426,9 @@ Of course, I use a caching step to make the whole process faster:
-
-

BTW I use GNU Emacs

-
+
+

BTW I use GNU Emacs

+

Here's the whole publishing workflow.

@@ -507,21 +507,21 @@ jobs:
-
-

What is next

-
+
+

What is next

+

I have a plans to make posts about next features:

-
-

RSS Feed

+ -
-

Open Graph image preview

+ - diff --git a/posts/add_rss_to_blog.html b/posts/add_rss_to_blog.html index e0b0ed0..b41be96 100644 --- a/posts/add_rss_to_blog.html +++ b/posts/add_rss_to_blog.html @@ -2,34 +2,34 @@

Table of Contents

-
-

Why do you even need RSS?

-
+
+

Why do you even need RSS?

+

RSS might seem like an outdated, marginal thing. But it still has at least one benefit—you can use an RSS feed as a sitemap for search engines. Plus, it's pretty geeky.

-
-

Add RSS feed

-
+
+

Add RSS feed

+

So, what's happening here? Let's start by integrating our templating functions into the build.

-
-

Use sitemap backend in the build

-
+
+

Use sitemap backend in the build

+
(setq org-publish-project-alist
       (list
        (list "blog-rss"
@@ -59,9 +59,9 @@ How does it work? As you can see, we use the default sitemap generator from Org
 

-
-

Publishing and formatting functions

-
+
+

Publishing and formatting functions

+

We need a mandatory dependency because we don't want to mess with forming correct XML by ourselves.

diff --git a/posts/blog_index_and_tags_automation.html b/posts/blog_index_and_tags_automation.html index 905b1c1..a53e90f 100644 --- a/posts/blog_index_and_tags_automation.html +++ b/posts/blog_index_and_tags_automation.html @@ -1,13 +1,709 @@ -Blog index and tags automation

Blog index and tags automation

Let's add tags to blog posts

+Blog index and tags automation

Blog index and tags automation

Let's add tags to blog posts

+
+

Tags.

+
+

+Tags are a nice and easy way to organize posts without explicit search. In the simplest way, you have a list of tags in posts, and each tag links to a page with all posts having the corresponding tag. It is also helpful to have a page with all tags available in the blog. And, of course, I don't want to maintain the list of tags manually. +

+
+
+

Automate tags.

+
+

+Simple idea - go through all files, collect titles, dates and descriptions, render into org files. Easy peazy lemon squezy. +Let me show you parts of function body. +Prepare required pathes, list needed org files, map to collect data. +

+
(let* ((base-dir (plist-get plist :base-directory) )
+       (tag-dir (concat base-dir "/tags/"))
+       (posts-dir (concat base-dir "/posts"))
+       (post-org-files (directory-files-recursively posts-dir "\\.org$"))
+       (tag-map (make-hash-table :test 'equal))
+       (posts-list (list))
+       )
+
+

+Create folder for tags files. Than we go through each file and collect plists with data required to render tags files +

+

+(unless (file-directory-p tag-dir)
+  (make-directory tag-dir t))
 
-
-

Tag

+(dolist (file post-org-files) + (with-temp-buffer + (insert-file-contents file) + (let* ((parsed-info (list :parse-tree (org-element-parse-buffer))) + (tags (split-string (my/org-get-property "TAGS" parsed-info))) + (title (my/org-get-property "TITLE" parsed-info)) + (date (my/org-get-property "DATE" parsed-info)) + (description (my/org-get-property "DESCRIPTION" parsed-info)) + (published (my/org-get-property "PUBLISHED" parsed-info)) + (preview (with-temp-buffer + (insert-file-contents file) + (my/get-first-two-meaningful-lines))) + ) + (if (and published (string= published "true")) + (progn + (setq link-plist + (list + :title title + :description description + :preview preview + :file file + :date date)) + + (push link-plist posts-list) + + (dolist (tag tags) + (puthash tag (cons link-plist (gethash tag tag-map)) tag-map)) + + (setq posts-list + (sort posts-list + (lambda (a b) + (date-less-p (plist-get b :date) (plist-get a :date)))))))))) +
+

+Go through resulting map and render org files for each tag. +

+

+(maphash
+ (lambda (tag link-plists)
+   (let
+       ((tag-file (concat tag-dir tag ".org")))
+     (with-temp-file tag-file
+       (insert (format "#+TITLE: Tag: %s\n" tag))
+       (insert "#+OPTIONS: toc:nil\n\n")
+       (insert (format "* %s\n" tag))
+
+       (dolist (link-plist link-plists)
+	 (let*
+	     ((title (plist-get link-plist :title))
+	      (description (plist-get link-plist :description))
+	      (file (plist-get link-plist :file))
+	      (date (plist-get link-plist :date))
+	      (preview (plist-get link-plist :preview))
+	      (relative-file (file-relative-name file (file-name-directory tag-file))))
+	   (insert (format "** [[file:%s][%s]]\n" relative-file title))
+	   (insert (format "%s\n" description))
+	   (insert (format "#+BEGIN_QUOTE\n%s ...\n#+END_QUOTE\n" preview))
+	   (insert (format "%s\n" (my/date-format date))))))))
+ tag-map)
+
+

+Resulting page example: @org-mode +

+
+
+
+
+

Posts index.

+
+

+Previously, I maintained a list of posts manually. Now that I have more than 5 posts, I no longer want to do it by hand. So, after introducing tags, I've decided to automate the page with the list of posts. The same idea, almost the same data, will be integrated into the same file. Pagination has not yet been implemented and is not planned. +

+
+
+

Posts index automation.

+
+

+Here is the part of function which renders list of posts and list of tags with number of posts in each tag category. +

+
(let
+	((index-file (concat base-dir "/posts.org")))
+      (with-temp-file index-file
+	(org-mode)
+
+	(insert "#+TITLE: Alex Mikhailov - Blog\n")
+	(insert "#+AUTHOR: Aleksandr Mikhailov\n")
+	(insert "#+DESCRIPTION: Index page for my blog\n")
+	(insert "#+OPTIONS: toc:nil\n\n")
+
+	(insert "* Posts\n")
+
+	(dolist (post posts-list)
+	  (let*
+	      ((title (plist-get post :title))
+	       (description (plist-get post :description))
+	       (file (plist-get post :file))
+	       (date (plist-get post :date))
+	       (preview (plist-get post :preview))
+	       (relative-file (file-relative-name file (file-name-directory file))))
+
+	    (insert (format "** [[file:./posts/%s][%s]]\n" relative-file title))
+	    (insert (format "%s\n" description))
+	    (insert (format "#+BEGIN_QUOTE\n%s ...\n#+END_QUOTE\n" preview))
+	    (insert (format "%s\n" (my/date-format date)))
+	    )
+	  )
+
+	(insert "* Tags\n")
+	(maphash
+	 (lambda (tag posts)
+	   (insert (format "** [[file:./tags/%s.org][%s]] (%d) \n" tag tag (length posts)))          )
+	 tag-map
+	 )
+	)
+
+      )
+
+

+Here is an example of resulting page: Posts +

+
+
+
+
+

Cons.

+
+

+Now, with the way the rendering function is integrated into the process, it is called when going through each Org file found by Org-Export. This introduces O(nn) complexity. It's not ideal, but never mind, I will redo it before reaching the 100th post. +

+
+
+
+

Whole config.

+
+

+As usual, between posts, I decided to tackle a whole bunch of small tasks, so here is the entire config. I'm thinking about splitting the exporter into modules and maybe open-sourcing it properly. +A neat thing I've implemented is a way to filter out unpublished posts using a PUBLISHED property. This allows me to work on drafts without affecting the blog's current state. +

+
#+TITLE: Blog index and tags automation
+#+DATE: <2024-07-05 Fri>
+#+DESCRIPTION: Let's add tags to blog posts
+#+PUBLISHED: false
+#+TAGS: @org-mode @elisp @tags
+
+ +
;; Load the publishing system
+;; Configure environment
+;;
+(setq debug-on-error t)
+
+(let ((default-directory  (concat "~/.config/emacs/.local/straight/build-" emacs-version "/")))
+  (normal-top-level-add-subdirs-to-load-path))
+
+(add-to-list 'custom-theme-load-path
+	     (concat "~/.config/emacs/.local/straight/build-" emacs-version "/doom-themes"))
+(add-to-list 'custom-theme-load-path (concat "~/.config/emacs/.local/straight/build-" emacs-version "/base16-theme"))
+(add-to-list 'custom-theme-load-path (concat "~/.config/emacs/.local/straight/build-" emacs-version "/moe-theme"))
+
+
+(require 'xml)
+(require 'dom)
+(require 'ox-publish)
+(require 'ox-rss)
+(require 'org)
+(require 'esxml)
+;; (require 'esxml-html)
+
+;;
+;;Variables
+;;
+(setq
+ my/url "https://fidonode.me"
+ my/web-export-path "./public"
+ my/blog-src-path "./home/05 Blog"
+ my/lang-substitution-map '(("elisp" . "lisp"))
+ org-html-validation-link nil            ;; Don't show validation link
+ org-html-htmlize-output-type nil
+ org-src-fontify-natively t)
+
+;;
+;;Templates
+;;
+(defun my/footer (info)
+  `(footer ((class .  "footer"))
+    (div ((class . "container"))
+	 (hr () )
+	 (small ()
+		(p () "Alex Mikhailov")
+		(p () "Built with: "
+		   (a ((href . "https://www.gnu.org/software/emacs/")) "GNU Emacs") " "
+		   (a ((href . "https://orgmode.org/")) "Org Mode") " "
+		   (a ((href . "https://picocss.com/")) "picocss")
+		   )))))
+
+(defun my/header (info)
+  (let ((title-str (org-export-data (plist-get info :title) info)))
+    `(header ((class . "header"))
+      (div ((class . "container"))
+	   (nav ()
+		(ul ()
+		    (li ()
+			(strong () ,"Alex Mikhailov")))
+		(ul ()
+		    (li () (a ((href . "/index.html")) "About"))
+		    (li () (a ((href . "/posts.html")) "Blog"))
+		    (li () (a ((href . "/rss.xml")) "RSS"))
+		    ))))))
+
+(defun my/src-block (src-block contents info)
+  "Translate SRC-BLOCK element into HTML.
+ CONTENTS is nil. INFO is a plist holding contextual information."
+  (let* (
+	 (org-language (format "language-%s" (org-element-property :language src-block)))
+	 (language (my/replace-substrings org-language))
+	 (code (org-element-property :value src-block)))
+    (esxml-to-xml
+     `(pre ()
+       (code ((class . ,language))
+	     ,(org-html-encode-plain-text code)
+	     )))))
+
+(defun my/render-preview (file-name title description)
+  (let* ((has-imagemagick (executable-find "magick"))
+	 (full-file-path (file-truename(format "%s%s/resources/images/preview/%s.png" script-directory my/blog-src-path file-name )))
+	 (file-dir (file-name-directory full-file-path))
+	 (has-dir (file-directory-p file-dir))
+	 (has-file (file-exists-p full-file-path))
+	 (path-to-script-root (format "%shelpers" script-directory))
+	 (path-to-script (format "%s/og_image_gen.sh" path-to-script-root)))
+    (if (and has-imagemagick
+	     (and description (not (string= description ""))))
+	(progn
+	  (when (not has-file)
+	    (progn
+	      (when (not has-dir)
+		(make-directory file-dir t))
+	      (shell-command (format "bash '%s' '%s' '%s' '%s' '%s'" path-to-script title description full-file-path path-to-script-root))
+	      )
+	    ))
+      (message "Imagemagick is not installed. Preview generation skipped.")
+      )))
+
+(defun my/html-header (info)
+  (let* ((title-str (org-export-data (plist-get info :title) info))
+	 (description-str (org-export-data (plist-get info :description) info))
+	 (file-path-str (org-export-data (plist-get info :input-file) info))
+	 (base-directory-str (org-export-data (plist-get info :base-directory) info))
+	 (file-name-str (file-relative-name file-path-str (format "%s/%s" script-directory base-directory-str)))
+	 (img-link-str (format "%s/resources/images/preview/%s.png" my/url file-name-str))
+	 (has-src-blocks (my/org-has-src-blocks-p info)))
+
+    (my/render-preview file-name-str title-str description-str)
+
+    (set-text-properties 0 (length title-str) nil title-str)
+    (set-text-properties 0 (length description-str) nil description-str)
+    (set-text-properties 0 (length img-link-str) nil img-link-str)
+
+
+    `(head ()
+      (meta ((charset . "utf-8")))
+      (meta ((author . "Alex Mikhailov")))
+      (meta ((name . "viewport")
+	     (content . "width=device-width, initial-scale=1, shrink-to-fit=no")))
+      (meta ((name . "color-scheme") (content . "light dark")))
+      (meta ((http-equiv . "content-language") (content . "en-us")))
+      ;; OG block
+      (meta ((name . "description") (content .  ,description-str)))
+      (meta ((property . "og:description") (content . ,description-str)))
+      (meta ((property . "og:image") (content . ,img-link-str)))
+      (meta ((property . "og:title") (content . ,title-str)))
+
+      (meta ((name . "twitter:description") (content . ,description-str)))
+      (meta ((name . "twitter:title") (content . ,title-str)))
+      (meta ((name . "twitter:image") (content . ,img-link-str)))
+      (meta ((name . "twitter:card") (content . "summary_large_image")))
+
+      (link ((rel . "icon") (type . "image/x-icon") (href . "/resources/favicon.ico")))
+      (link ((rel . "stylesheet") (type . "text/css") (href . "/resources/css/pico.sand.min.css")))
+      (script ((defer . "true") (src . "https://umami.dokutsu.xyz/script.js") (data-website-id . "d52d9af1-0c7d-4531-84c6-0b9c2850011f")) ())
+      (title () ,title-str)
+
+      ,@(when has-src-blocks
+	  (list
+	   `(link ((id . "highlight-theme") (rel . "stylesheet") (type . "text/css")))
+	   `(script ((src . "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js")) ())
+	   `(script ((src . "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/bash.min.js")) ())
+	   `(script ((src . "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/languages/lisp.min.js")) ())
+	   `(script ((src . "/resources/js/theme-selector.js")) ())
+	   )))))
+
+(defun my/regular-template (contents info)
+  `(main ((class . "container"))
+    (raw-string ,contents)))
+
+(defun my/blog-post-template (contents info)
+  (let* ((title-str (org-export-data (plist-get info :title) info))
+	 (description-str (org-export-data (plist-get info :description) info))
+	 (tags (split-string  (my/org-get-property "TAGS" info)))
+	 (tags-html (cl-map 'list (lambda (tag)
+				    `(li () (mark () (a ((href . ,(format  "/tags/%s.html" tag)) (class . "secondary")) ,(format "%s" tag))))) tags))
+	 )
+    `(main ((class . "container blog-post"))
+      (hgroup ()
+	      (h1 () ,title-str)
+	      (p () ,description-str)
+	      (nav () (ul () (li () "Tags:") ,@tags-html))
+
+	      )
+      (raw-string ,contents)
+      )))
+
+(defun my/template (contents info)
+  (let
+      ((file-path-str (org-export-data (plist-get info :input-file) info)))
+
+    (concat
+     "<!DOCTYPE html>"
+     (esxml-to-xml
+      `(html ((lang . "en"))
+	,(my/html-header info)
+	(body ()
+	      ,(my/header info)
+	      ,(if (string-match-p "\/posts\/" file-path-str)
+		   (my/blog-post-template contents info)
+		 (my/regular-template contents info))
+	      ,(my/footer info)
+	      ))))))
+
+
+(org-export-define-derived-backend 'my-html 'html
+  :translate-alist '(
+		     (template . my/template)
+		     (src-block . my/src-block)
+		     ))
+
+(defun my/publish-to-html (plist filename pub-dir)
+  "Publish an Org file to HTML using the custom backend."
+  (org-publish-org-to 'my-html filename ".html" plist pub-dir))
+
+;;
+;;Build blog index
+;;
+
+(defun my/publish-blog-index (plist FF FFFF)
+  "Process all Org files in 'posts' and 'tags' directories, create index file, and insert links."
+
+  (let* ((base-dir (plist-get plist :base-directory) )
+	 (tag-dir (concat base-dir "/tags/"))
+	 (posts-dir (concat base-dir "/posts"))
+	 (post-org-files (directory-files-recursively posts-dir "\\.org$"))
+	 (tag-map (make-hash-table :test 'equal))
+	 (posts-list (list))
+	 )
+
+    ;; Ensure tag directory exists
+    (unless (file-directory-p tag-dir)
+      (make-directory tag-dir t))
+
+    ;; Scan all org files and collect tags
+    (dolist (file post-org-files)
+      (with-temp-buffer
+	(insert-file-contents file)
+	(let* ((parsed-info (list :parse-tree (org-element-parse-buffer)))
+	       (tags (split-string (my/org-get-property "TAGS" parsed-info)))
+	       (title (my/org-get-property "TITLE" parsed-info))
+	       (date (my/org-get-property "DATE" parsed-info))
+	       (description (my/org-get-property "DESCRIPTION" parsed-info))
+	       (published (my/org-get-property "PUBLISHED" parsed-info))
+	       (preview (with-temp-buffer
+			  (insert-file-contents file)
+			  (my/get-first-two-meaningful-lines)))
+	       )
+	  (if (and published (string= published "true"))
+	      (progn
+		(setq link-plist
+		      (list
+		       :title title
+		       :description description
+		       :preview preview
+		       :file file
+		       :date date))
+
+		(push link-plist posts-list)
+
+		(dolist (tag tags)
+		  (puthash tag (cons link-plist (gethash tag tag-map)) tag-map))
+
+		(setq posts-list
+		      (sort posts-list
+			    (lambda (a b)
+			      (date-less-p (plist-get b :date) (plist-get a :date))))))))))
+
+    (let
+	((index-file (concat base-dir "/posts.org")))
+      (with-temp-file index-file
+	(org-mode)
+
+	(insert "#+TITLE: Alex Mikhailov - Blog\n")
+	(insert "#+AUTHOR: Aleksandr Mikhailov\n")
+	(insert "#+DESCRIPTION: Index page for my blog\n")
+	(insert "#+OPTIONS: toc:nil\n\n")
+
+	(insert "* Posts\n")
+
+	(dolist (post posts-list)
+	  (let*
+	      ((title (plist-get post :title))
+	       (description (plist-get post :description))
+	       (file (plist-get post :file))
+	       (date (plist-get post :date))
+	       (preview (plist-get post :preview))
+	       (relative-file (file-relative-name file (file-name-directory file))))
+
+	    (insert (format "** [[file:./posts/%s][%s]]\n" relative-file title))
+	    (insert (format "%s\n" description))
+	    (insert (format "#+BEGIN_QUOTE\n%s ...\n#+END_QUOTE\n" preview))
+	    (insert (format "%s\n" (my/date-format date)))
+	    )
+	  )
+
+	(insert "* Tags\n")
+	(maphash
+	 (lambda (tag posts)
+	   (insert (format "** [[file:./tags/%s.org][%s]] (%d) \n" tag tag (length posts)))          )
+	 tag-map
+	 )
+	)
+
+      )
+
+    ;; Create tag files and insert links
+    (maphash
+     (lambda (tag link-plists)
+       (let
+	   ((tag-file (concat tag-dir tag ".org")))
+	 (with-temp-file tag-file
+	   (insert (format "#+TITLE: Tag: %s\n" tag))
+	   (insert "#+OPTIONS: toc:nil\n\n")
+	   (insert (format "* %s\n" tag))
+
+	   (dolist (link-plist link-plists)
+	     (let*
+		 ((title (plist-get link-plist :title))
+		  (description (plist-get link-plist :description))
+		  (file (plist-get link-plist :file))
+		  (date (plist-get link-plist :date))
+		  (preview (plist-get link-plist :preview))
+		  (relative-file (file-relative-name file (file-name-directory tag-file))))
+	       (insert (format "** [[file:%s][%s]]\n" relative-file title))
+	       (insert (format "%s\n" description))
+	       (insert (format "#+BEGIN_QUOTE\n%s ...\n#+END_QUOTE\n" preview))
+	       (insert (format "%s\n" (my/date-format date))))))))
+     tag-map)
+    )
+  )
+
+;;
+;;Sitemap/RSS
+;;
+(defun my/format-rss-feed-entry (entry style project)
+  "Format ENTRY for the RSS feed.
+ ENTRY is a file name.  STYLE is either 'list' or 'tree'.
+ PROJECT is the current 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
+	     (org-mode)
+	     (insert (format "* [[file:%s][%s]]\n" file title))
+	     (org-set-property "RSS_PERMALINK" link)
+	     (org-set-property "RSS_TITLE" title)
+	     (org-set-property "PUBDATE" date)
+	     (let ((first-two-lines (with-temp-buffer
+				      (insert-file-contents file)
+				      (buffer-substring-no-properties
+				       (point-min)
+				       (progn (forward-line 2) (point))))))
+	       (if (string-suffix-p "\n" first-two-lines)
+		   (setq first-two-lines (substring first-two-lines 0 -1)))
+	       (insert first-two-lines))
+	     (goto-char (point-max))
+	     (insert "...")
+	     (buffer-string))))
+	((eq style 'tree)
+	 ;; Return only last subdir.
+	 (file-name-nondirectory (directory-file-name entry)))
+	(t entry)))
+
+(defun my/format-rss-feed (title list)
+  "Generate RSS feed, as a string.
+ TITLE is the title of the RSS feed.  LIST is an internal
+ representation for the files to include, as returned by
+ `org-list-to-lisp'.  PROJECT is the current project."
+  (concat "#+TITLE: " title "\n"
+	  "#+STARTUP: showall \n\n"
+	  (org-list-to-subtree list 1 '(:icount "" :istart ""))))
+
+(defun my/publish-to-rss (plist filename pub-dir)
+  "Publish RSS with PLIST, only when FILENAME is 'rss.org'.
+ PUB-DIR is when the output will be placed."
+  (if (equal "rss.org" (file-name-nondirectory filename))
+      (org-rss-publish-to-rss plist filename pub-dir)))
+
+;;
+;;Helpers
+;;
+
+(defun cleanup-org-line (line)
+  "Clean up Org-mode formatting from a single LINE."
+  (let ((cleaned-line (org-no-properties line)))
+    (setq cleaned-line (replace-regexp-in-string "^\\*+ " "" cleaned-line)) ; Remove heading stars
+    (string-trim cleaned-line))) ; Remove leading/trailing whitespace
+
+(defun my/get-first-two-meaningful-lines ()
+  "Extracts the first two meaningful lines from an Org-mode buffer."
+  (interactive)
+  (save-excursion  ; Do not change the cursor position permanently
+    (goto-char (point-min))  ; Start at the beginning of the buffer
+    ;; Loop to skip comments and properties
+    (while (looking-at "^[#*]")
+      (forward-line 1))  ; Move to the next line until a meaningful line is found
+    ;; Now read and return the first two meaningful lines
+
+    (cleanup-org-line (buffer-substring-no-properties
+		       (point)
+		       (progn (forward-line 2) (point)))
+		      ))
+  )
+
+(defun date-less-p (date1 date2)
+  "Return t if DATE1 is less than DATE2.
+ DATE1 and DATE2 should be strings in the format <YYYY-MM-DD Day>."
+  (let* ((date1 (substring date1 1 11))  ; Extract the date part
+	 (date2 (substring date2 1 11))
+	 (time1 (apply 'encode-time (parse-time-string (concat date1 " 00:00:00"))))
+	 (time2 (apply 'encode-time (parse-time-string (concat date2 " 00:00:00")))))
+    (time-less-p time1 time2)))
+
+(defun my/date-format (org-date)
+  "Convert an Org-mode date string ORG-DATE to a formatted date string."
+  (let* ((parsed-time (org-parse-time-string org-date))
+	 (time (apply 'encode-time parsed-time)))
+    (format-time-string "posted on %Y-%m-%d" time)))
+
+(defun my/format-date-subtitle (file project)
+  "Format the date found in FILE of PROJECT."
+  (my/date-format (org-publish-find-date file project)))
+
+(defun my/pt (var)
+  "Print the value and type of VAR."
+  (message "Value: %S, Type: %s" var (type-of var)))
+
+(defun plist-keys (plist)
+  "Return a list of keys in the property list PLIST."
+  (let (keys)
+    (while plist
+      (setq keys (cons (car plist) keys))
+      (setq plist (cddr plist)))
+    (nreverse keys)))
+
+(defvar script-directory
+  (file-name-directory (or load-file-name buffer-file-name))
+  "The directory where the current script is located.")
+
+(defun my/org-get-property (property info)
+  "Get the value of a PROPERTY in the Org document represented by INFO."
+  (org-element-map (plist-get info :parse-tree) 'keyword
+    (lambda (keyword)
+      (when (string= (org-element-property :key keyword) property)
+	(org-element-property :value keyword)))
+    nil t))
+
+(defun my/org-has-src-blocks-p (info)
+  "Return t if the Org document represented by INFO has source code blocks."
+  (org-element-map (plist-get info :parse-tree) 'src-block
+    (lambda (src-block) t)
+    nil t))
+
+(defun my/replace-substrings (input-string)
+  "Replace substrings in INPUT-STRING according to SUBSTITUTION-MAP."
+  (let ((output-string input-string))
+    (dolist (pair my/lang-substitution-map)
+      (let ((old (regexp-quote (car pair)))
+	    (new (cdr pair)))
+	(setq output-string (replace-regexp-in-string old new output-string))))
+    output-string))
+
+;;
+;;Clear folder with results
+;;
+(when (file-directory-p my/web-export-path)
+  (delete-directory my/web-export-path t))
+(mkdir my/web-export-path)
+
+
+;;
+;;Main blog configuration
+;;
+(setq org-publish-project-alist
+      (list
+       (list "blog-index"
+	     :base-directory my/blog-src-path
+	     :base-extension "org"
+	     :recursive t
+	     :publishing-directory my/web-export-path
+	     :publishing-function 'my/publish-blog-index)
+       (list "static"
+	     :base-directory my/blog-src-path
+	     :base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|pdf\\|ico\\|txt"
+	     :publishing-directory my/web-export-path
+	     :recursive t
+	     :publishing-function 'org-publish-attachment
+	     )
+       (list "blog"
+	     :recursive t
+	     :base-directory my/blog-src-path
+	     :publishing-directory my/web-export-path
+	     :publishing-function 'my/publish-to-html
+	     :html-html5-fancy t
+	     :htmlized-source t
+	     :with-author nil
+	     :with-creator t
+	     :with-toc t
+	     :section-numbers nil
+	     :time-stamp-file nil
+	     )
+       (list "blog-rss"
+	     :author "Alex M"
+	     :email "iam@fidonode.me"
+	     :base-directory my/blog-src-path
+	     :base-extension "org"
+	     :recursive t
+	     :exclude (regexp-opt '("rss.org" "index.org" "404.org" "posts.org"))
+	     :publishing-function 'my/publish-to-rss
+	     :publishing-directory my/web-export-path
+	     :rss-extension "xml"
+	     :html-link-home my/url
+	     :html-link-use-abs-url t
+	     :html-link-org-files-as-html t
+	     :auto-sitemap t
+	     :sitemap-filename "rss.org"
+	     :sitemap-title "rss"
+	     :sitemap-style 'list
+	     :sitemap-sort-files 'anti-chronologically
+	     :sitemap-function 'my/format-rss-feed
+	     :sitemap-format-entry 'my/format-rss-feed-entry)
+       ))
+
+
+;; Generate the site output
+(org-publish-all t)
+
+(message "Build complete!")
+
+
+
diff --git a/posts/improve_code_blocks.html b/posts/improve_code_blocks.html index 295b816..b5807b8 100644 --- a/posts/improve_code_blocks.html +++ b/posts/improve_code_blocks.html @@ -2,33 +2,33 @@

Table of Contents

-
-

What is the problem with default highlighting?

-
+
+

What is the problem with default highlighting?

+

Htmlize works poorly with headless publishing. It lacks extensibility, including features like line numbers, a copy button, and the ability to highlight predefined parts of the code.

-
-

Highlight.js

-
+
+

Highlight.js

+
-
-

Change code block template

-
+
+

Change code block template

+

We need to make small changes in how code blocks are rendered. By default, Org Export exports code blocks as <pre></pre>. For Highlight.js, we need <pre><code></code></pre>. Additionally, we need to set the correct language name in the class attribute. Since Highlight.js does not support elisp, I rewrite it to regular lisp using the my/replace-substring function.

@@ -69,9 +69,9 @@ CONTENTS is nil. INFO is a plist holding contextual information."
-
-

Plug Highlight.js

-
+
+

Plug Highlight.js

+

I do not want to load Highlight.js on every page, so I need to check if the initial Org file contains code blocks. Depending on this, we will render the part of the tree with JavaScript or not.

@@ -105,9 +105,9 @@ I do not want to load Highlight.js on every page, so I need to check if the init
-
-

Respect prefers-color-scheme

-
+
+

Respect prefers-color-scheme

+

Additionally, I think it's a good idea to respect the prefers-color-scheme property of the user's browser. We can switch CSS files based on this property. We should also subscribe to changes in this property to handle the edge case when it switches while reading the page.

@@ -142,9 +142,9 @@ window
-
-

Whole config

-
+
+

Whole config

+

In between posts I've switched from sxml to esxml so here is the current config.

diff --git a/posts/keeb.html b/posts/keeb.html index 584013b..85fc00f 100644 --- a/posts/keeb.html +++ b/posts/keeb.html @@ -2,51 +2,51 @@

Table of Contents

-
-

My end-game (at least I hope) keyboard

-
+
+

My end-game (at least I hope) keyboard

+
-
-

Keebs path

-
+
+

Keebs path

+

Sometimes I think about the long journey I've made with keebs. In childhood, I had decent membrane keyboards, most of which had an ergonomic profile like the MS. Not sure if it somehow affected my taste because I started my career with the simplest, cheapest board and typed countless lines of code on such keebs. Then I heard about clickity-clack mechanical keyboards and decided to try one. It was a simple Chinese keeb with a thick metal body, double-shot caps, and Cherry Brown switches. A decent thing to annoy everyone around you. I think this purchase marked my dive into mech keebs I'm not a geeky aficionado who thinks you can fix everything with a new keyboard, but I built a couple of them. I hope I've finally built the last one for quite some time.

-
-

Dactyl manuform

-
+
+

Dactyl manuform

+

Almost all of the time, I struggle with my maximalism. So I decided to build the ultimate mechanical ergonomic split keyboard and chose the Dactyl Manuform. Sounds like a crazy idea. Zero experience with QMK, zero experience with hand-wired keyboards, and zero experience in 3D printing. The last problem was the easiest one; I just asked my friend to print the bodies from PETG polymer, and Bob's your uncle. I got two pieces of rough-layered plastic with all the support structures. God, it was a nightmare to clean these prints from supports and small artifacts, but I was happy. @@ -92,9 +92,9 @@ To be honest, this keeb was ugly, and I decided that I wanted a beautiful factor

-
-

Moonlander

-
+
+

Moonlander

+

Nothing special. Ordered, paid, got it, tried it. Everything worked. Looked good. Happy year of typing. Bored. Annoyed. Too big and chunky. No concave. Quality not the best. Started planning the next one.

@@ -107,9 +107,9 @@ Nothing special. Ordered, paid, got it, tried it. Everything worked. Looked good
-
-

Custom Corne

-
+
+

Custom Corne

+

This journey started with discovering the Jian keyboard. It is a niche keeb from the Ru community focused on full support of the whole Russian layout. It was originally created by KGOH. I missed the group buy and decided that I could easily patch a Corne board with two additional keys to mimic the Jian. Interesting journey. I learned how to use KiCad, and how to export gerbers. @@ -158,9 +158,9 @@ Daily driver for ~6 months. Then the world changed, and I decided to leave my ho

-
-

Dactyl manuform again

-
+
+

Dactyl manuform again

+

Two years late I've settled down in new country and decide that I want to bring back my dactyl manuform experience.

@@ -174,13 +174,13 @@ Two years late I've settled down in new country and decide that I want to bring
-
-

Hardware

-
+
+

Hardware

+
-
-

Body

-
+
+

Body

+

I've choose to use a Ryan's generator and generate body on top of Corne preset with all keys in last row and disabled stagger for the last two columns. Generator preset The body was printed by JLC3DP (JLCPCB printing department). I've choose SLS from nylon. Print has minor artifacts; I expected better quality. @@ -216,9 +216,9 @@ Overall, I'm happy with results. I also printed bottom plates and -

Switches and caps

-
diff --git a/posts/posts_preview.html b/posts/posts_preview.html index b974a8f..420e7f7 100644 --- a/posts/posts_preview.html +++ b/posts/posts_preview.html @@ -2,31 +2,31 @@

Table of Contents

-
-

What is imagemagick

-
+
+

What is imagemagick

+

Imagemagick is a ffmpeg of the image world. You can do a lot of fun things with it. For example you can take a picture, cut corners on it, place it on top of another image, add some text and get final result. So it is looks like a good tool for making previews from code.

-
-

Basic idea of this process

-
+
+

Basic idea of this process

+

I want to hook a process of rendering post. Since this function called on each run of publishing and for each post it is a good idea to cache resulting images. I gonna simply check presence of preview image and use it as guard for running image generation. After that I gonna extract #+TITLE and #+DESCRIPTION properties from Org file. Each Org file I have, has next header:

@@ -48,9 +48,9 @@ So I with all these data I can generate my previews with simple script:
-
-

Integrate into build

-
+
+

Integrate into build

+

As I already mention I gonna skip file generation when file already here. Here is the whole function. Pretty simple. Just prepare pathes, check some dependencies, create pathes and execute script which calls imagemagick.

@@ -113,37 +113,37 @@ You can check whole function in previous post -

Whats next?

-
+
+

Whats next?

+
-
-

Tags

-
+
+

Tags

+

Show tags, show posts by tag.

-
-

Post series

-
+
+

Post series

+

Dunno how, but I'll figure out something.

-
-

Adopt/fix htmlize.el

-
+
+

Adopt/fix htmlize.el

+

I want to highlight code during publishing step.

-
-

Show more meta on posts index page.

-
+
+

Show more meta on posts index page.

+
-
-

Tag: @imagemagick

-
+ -
-

Tag: @diy

-
+ -
-

Tag: @keeb

-
+ -
-

Tag: @highlightjs

-
+ -
-

Tag: @elisp

-
+ -
-

Tag: @org-mode

-
+ -
-

Tags in posts

-
+ -
-

Posts preview

-
+ -
-

Improve code blocks

-
+ -
-

Org blog with RSS

-
+ -
-

Org to HTML and back

-
+ -
-

My keyboard journey

-
+ + diff --git a/rss.xml b/rss.xml index 8f25b2b..81109b4 100644 --- a/rss.xml +++ b/rss.xml @@ -14,8 +14,8 @@ https://fidonode.me en - Sat, 06 Jul 2024 14:43:23 +0000 - Sat, 06 Jul 2024 14:43:23 +0000 + Sat, 06 Jul 2024 15:17:44 +0000 + Sat, 06 Jul 2024 15:17:44 +0000 Emacs 27.1 Org-mode 9.3 iam@fidonode.me (Alex M) @@ -70,6 +70,15 @@ + + Tag: @tags + https://fidonode.me/tags/@tags.html + iam@fidonode.me (Alex M) + https://fidonode.me/tags/@tags.html + Sat, 06 Jul 2024 00:00:00 +0000 + + + Tag: @elisp https://fidonode.me/tags/@elisp.html diff --git a/tags/@diy.html b/tags/@diy.html index f9fc16a..26a4d18 100644 --- a/tags/@diy.html +++ b/tags/@diy.html @@ -1,11 +1,11 @@ Tag: @diy
-
-

@diy

-
+
+

@diy

+
-
-

My keyboard journey

-
+
+

My keyboard journey

+

Blog post about my keyboards

diff --git a/tags/@elisp.html b/tags/@elisp.html index 0e31ba7..e1c1093 100644 --- a/tags/@elisp.html +++ b/tags/@elisp.html @@ -1,11 +1,11 @@ Tag: @elisp
-
-

@elisp

-
+
+

@elisp

+
-
-

Posts preview

-
+
+

Posts preview

+

Add post preview for OpenGraph cards

@@ -20,9 +20,9 @@ posted on 2024-06-28

-
-

Improve code blocks

-
+
+

Improve code blocks

+

Use highlight.js for code syntax highlighting

@@ -37,9 +37,26 @@ posted on 2024-06-25

-
-

Org blog with RSS

-
+
+

Blog index and tags automation

+
+

+Let's add tags to blog posts +

+
+

+Tags are a nice and easy way to organize posts without explicit search. In the simplest way, you have a list of tags in posts, and each tag links to a page with all posts having the corresponding tag. It is also helpful to have a page with all tags available in the blog. And, of course, I don't want to maintain the list of tags manually. +Automate tags. … +

+
+

+posted on 2024-07-05 +

+
+
+
+

Org blog with RSS

+

Let's add RSS feed to blog

diff --git a/tags/@highlightjs.html b/tags/@highlightjs.html index 687b96e..97d472f 100644 --- a/tags/@highlightjs.html +++ b/tags/@highlightjs.html @@ -1,11 +1,11 @@ Tag: @highlightjs
-
-

@highlightjs

-
+
+

@highlightjs

+
-
-

Improve code blocks

-
+
+

Improve code blocks

+

Use highlight.js for code syntax highlighting

diff --git a/tags/@imagemagick.html b/tags/@imagemagick.html index fb1ea1e..cd4fd1e 100644 --- a/tags/@imagemagick.html +++ b/tags/@imagemagick.html @@ -1,11 +1,11 @@ Tag: @imagemagick
-
-

@imagemagick

-
+
+

@imagemagick

+
-
-

Posts preview

-
+
+

Posts preview

+

Add post preview for OpenGraph cards

diff --git a/tags/@keeb.html b/tags/@keeb.html index 634a61c..bc0da2b 100644 --- a/tags/@keeb.html +++ b/tags/@keeb.html @@ -1,11 +1,11 @@ Tag: @keeb
-
-

@keeb

-
+
+

@keeb

+
-
-

My keyboard journey

-
+
+

My keyboard journey

+

Blog post about my keyboards

diff --git a/tags/@org-mode.html b/tags/@org-mode.html index 2e5b983..64d1d43 100644 --- a/tags/@org-mode.html +++ b/tags/@org-mode.html @@ -1,11 +1,11 @@ Tag: @org-mode
-
-

@org-mode

-
+
+

@org-mode

+
-
-

Posts preview

-
+
+

Posts preview

+

Add post preview for OpenGraph cards

@@ -20,9 +20,9 @@ posted on 2024-06-28

-
-

Improve code blocks

-
+
+

Improve code blocks

+

Use highlight.js for code syntax highlighting

@@ -37,9 +37,26 @@ posted on 2024-06-25

-
-

Org blog with RSS

-
+
+

Blog index and tags automation

+
+

+Let's add tags to blog posts +

+
+

+Tags are a nice and easy way to organize posts without explicit search. In the simplest way, you have a list of tags in posts, and each tag links to a page with all posts having the corresponding tag. It is also helpful to have a page with all tags available in the blog. And, of course, I don't want to maintain the list of tags manually. +Automate tags. … +

+
+

+posted on 2024-07-05 +

+
+
+
+

Org blog with RSS

+

Let's add RSS feed to blog

@@ -54,9 +71,9 @@ posted on 2024-06-23

-
-

Org to HTML and back

-
+
+

Org to HTML and back

+

Blog post about publishing my blog with Org Mode

diff --git a/tags/@rss.html b/tags/@rss.html index fba979e..662f261 100644 --- a/tags/@rss.html +++ b/tags/@rss.html @@ -1,11 +1,11 @@ Tag: @rss
-
-

@rss

-
+
+

@rss

+
-
-

Org blog with RSS

-
+
+

Org blog with RSS

+

Let's add RSS feed to blog

diff --git a/tags/@tags.html b/tags/@tags.html new file mode 100644 index 0000000..d7b15bf --- /dev/null +++ b/tags/@tags.html @@ -0,0 +1,24 @@ +Tag: @tags
+
+

@tags

+
+
+
+

Blog index and tags automation

+
+

+Let's add tags to blog posts +

+
+

+Tags are a nice and easy way to organize posts without explicit search. In the simplest way, you have a list of tags in posts, and each tag links to a page with all posts having the corresponding tag. It is also helpful to have a page with all tags available in the blog. And, of course, I don't want to maintain the list of tags manually. +Automate tags. … +

+
+

+posted on 2024-07-05 +

+
+
+
+