7d.nz

Blogging with org

[2021-07-11 dim.]

  litterate programming org elisp

Here are the explanations and the source code of the blogging engine developed for this website (7d.nz). It's build with Emacs and org-mode, creates static HTML pages and got inspired by ox-hugo.

While you reader can start reading it here, you'll probably be more at ease under Emacs especially when dwelling into the details of the code. Lucky for you, every pages of this website provides the org source its footer.

Update: [2024-06-02 dim.] Org 9.7.2

License

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Quickstart

One way to understand how it works could be to evaluate the following code under Emacs, and then start blogging right away just by modifying existing headlines or saving new ones. Even safer and perhaps inspirational will be to read it first.

Tested on emacs 28 to 30 and org 9.5 to 9.7.

Once autoexport is enabled — and you can see it by having the following expression "autoexport: enabled ?" highlighted — saving the file when the cursor will be over a section in a DONE state will export that section as a blog post.

All you need to run the static generator is a copy of this file, assuming compatible versions of the libraries. The main entry point to setup the blog is the following code block wich in turns evaluate the legit elisp blocks down below.

  ;; Skip elisp confirmation on interactive evaluation
;; and evaluate all blocks in this file.
;; Be careful if you set your own experimental ones along the way.
;; You can declare "#+begin_src emacs-lisp" to keeps elisps blocks
;; in this file not run by this function
(message "setting up org-blog auto-export mode")
(save-excursion
  (setq org-confirm-babel-evaluate
        (lambda (lang body) (not (equal "elisp" lang))))

  (condition-case err
      (while (org-next-block-if-any)
        (cond (
               (looking-at "^#\\+begin_src +\\(elisp\\) *$")
               (let (
                     (l (match-end 0)))
                 (search-forward "#+end_src")
                 (setq init-org-block (+ 1 init-org-block))
                 ;;(princ init-org-block)
                 (princ " ")
                 (eval-region l (match-beginning 0))
                 ))))

    (error () (message "%s at line %s" (error-message-string err) (line-number-at-pos)))))
;; alternatively: jump to the headline and (org-babel-execute-subtree)
(org-blog-auto-export-mode)
(add-hook 'before-save-hook 'time-stamp)

(org-display-inline-images)
(setq htmlize-force-inline-images t)
(setq org-blog-regen-toc&tags nil)

org-next-block-if-any is defined in init.el.

to export the entry point TOC & tag list upon saving:

(setq org-blog-regen-toc&tags t)

Turn it off if you're just publishing one article.

(setq org-blog-regen-toc&tags nil)

keep

org-blog-regen-toc&tags t

and

(setq org-blog-regen-tags-p nil)

to update the front page without regenerating the tags pages.

How to read this document

Enjoy the reading and good luck with the coding part. If you go into the details, you may noticed a few inconsistancies in the function prefixes (org-blog, org-weblog, blog-), as I haven't stated yet on the definitive naming.

Introduction

After having tried many of the blogging engines available from org-mode, I ended with the following list of specifications:

  • The entire blog should be kept in one file with one headline as a blog entry. The first time I read about this idea was on Arthur Malabarba's blog: Endless Parenthesis (This is also the place I landed on when I first searched init.org). Endless Parentheses uses ox-jekyll.
  • One article per top level section. While it should also be possible to manage multiple blogs, with one blog per top level section, and one article per level 2 section.
  • Only Emacs, no other dependencies, static and minimalist design.
  • The publishing process should be as fast, automatized and seamless as possible, exporting only what is needed to render one article at a time, located by the cursor position.
  • The closest thing I found to what I was looking for was Kaushal's scripter.co with ox-hugo. this blogging system is neat. It's one of the best that's been around and the org-mode source is clean. However his setup file is big. The guy is connected to dozens of social medias and I didn't start from the bare config but from his own personnal repository. Much tweaking is required to dig into hugo and go-templates (btw I don't have any proper syntax coloring for this). Hugo is a good solid blogging engine, and it certainly does provide much more than what I'll get here all by myself, but still…
  • [2023-06-13 mar.] one addendum to the specifications was recently suggested to me by email: we should define a template property, and an org-weblog-templates alist that holds all the different templates styles parameters. The next step would be to externalize the styling along with the author signature.
  • The next version of the org-weblog should invoke org-publish.

In the end, deriving the html exporter and programming it's behavior turned out to be the way to go.

The general idea activated here is a literate programming flow. That flow combines two aspects in the work:

  • the programming part, which I'll explain in detail here, since this is the place where the code is written, commented and elaborated,
  • The literate part, that is the discipline one has to keep up in order to organize ideas and thoughts and communicate them.

And now that the blog is ready, I can finally communicate more about past, present and future software projects and the many aspects of programming without bothering too much about the technicalities.

The next chapter that will also gets integrated into this blog is about collective review. That's still work in progress at the time I'm writing this [2021-12-20 lun.], and I'm getting a bit late on the topic.

Literate programming is about programming and literature. You have to consider that programming is indeed a kind of literature, and that informatics is all about text, much more than numbers: the mere existence of transistors, electricity and 0 and 1 is 99% of the time of no concern to programmers — and, as a matter of fact, to anyone else — unlike the texts they're working on.

The publishing medium matters here because the program is all about publishing online.

Structure

jumpb2.png

One headline = one post.

An org headline starts like this

*  TODO [#3] [2021-05-24 Mon 23:07] blogging with org-mode     :code:article:post:

But Many times it will be simpler than that:

* [2021-07-20 Tue] News from the front

This will produce a new post in a file named after a "slug" from the headline. If needed, that filename can be explicitly defined with the EXPORT_FILE_NAME property. This has however a drawback when using stored links, as they reference the original file, but after, in the export.

Top level entries are sometimes called "headline level 1", sometimes, "H1", even this can be confused with HTML. That behaviour can be overriden if one really needs to store the blog posts under one section.

The state (TODO/DONE) determines if a post must be published or not.

When in a DONE state (such as a square ■, in this document, according to the top directives1the top statemenents in an org file starting with #+, they would rather better be named directives rather than keywords, but it's too late now.11) every time C-x C-s is hit while the cursor is in the section, it is exported, while the summary and the index.org gets updated accordingly.

The timestamp headlines order the post chronologically in the blog. The tags are also exported for grouping and categorization. Priorities are ignored.

The index page is a list of blog entries with a short summary of the articles.

So, once the auto-export in enabled, writing in org file will export the subtree to it's own org-file, which in turns gets exported to html.

The Html backend exporter still doesn't handle the case of a few fuzzy links, so we will have to take care of that.

Pagination isn't implemented yet. A simple index increment from url /page/1 to /n would do. /page/1 might be a special case if it's also the landing page but it won't be necessarly the "recent posts" list on the front page (maybe I'll keep some on top headline, or even use the org list for this order).

UP heading

The point has to return to a headline in order to get the information about the section or sub-section we're currently writing in.

  
(defun org-up-heading (level)
  "return up to closest headline at LEVEL"
  (condition-case err
      (outline-up-heading
       (- (nth 1 (org-heading-components))
          level))
    (error () nil)))  ;; already at top level

(defun heading-components-at-level (level)
  "return the top level heading components
   of the current section. Said differently : get back to the
   root (level 1) of the current section, down to n level"
  (save-excursion
    (condition-case err
        (outline-up-heading
         (- (nth 1 (org-heading-components))
            level))
      (error () nil))  ;; already at top level
    (org-heading-components)))
(org-up-heading 1)
(org-up-heading 3)
(heading-components-at-level 1)

Date/title extraction

We separate the date (if any) and the title. When's there's no date (at least, no detected date — that is when the regexp didn't match), the post in considered as holding only a title, and will be set to the current date and time when an export occurs.

  ;; derivation of org-ts-regexp0,
;; for spaces, grouping, and text after.
;; Matches  :
;; [2021-05-25 Tue 01:03] title
;; [2021-05-25 Lun.] title
(defconst org-timestamp-and-title
  "\\(\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\) +\\([^]+0-9>
    -]+\\)?\\( +\\([0-9]\\{1,2\\}:[0-9]\\{2\\}\\)\\)?\\)] \\(.*\\)")

(defun split-datetime+title (str)
  (list :timestamp (match-string 1 str)
        :year (match-string 2 str)
        :month (match-string 3 str)
        ;; :day (match-string 4 str)
        ;; :weekday (match-string 5 str)
        :time (match-string 7 str)
        :title (match-string 8 str)))

(defun timestamp-and-title (str)
  """ on a bare string return current time + string """
  (let ((time) (current-time))
    (if (string-match org-timestamp-and-title str)
        (split-datetime+title str)
      ;; else date of export
      (list :timestamp (format-time-string "[%Y-%m-%d %a %H:%M]" time)
            :year  (format-time-string "%Y" time)
            :month (format-time-string "%m" time)
            :time  (format-time-string "%H:%M" time)
            :title str))))

(defun org-html-title-only (str)
  (if (string-match org-timestamp-and-title str)
      (split-datetime+title str)
    (str)))

Tests :

  (format-time-string "[%Y-%m-%d %H:%M]" (current-time))
(timestamp-and-title "log.el.org")
(timestamp-and-title (nth 4(heading-components-at-level 1)))
(plist-get (timestamp-and-title (nth 4 (heading-components-at-level 1))) :timestamp)
(plist-get (timestamp-and-title (nth 4 (heading-components-at-level 1))) :title)
(timestamp-and-title "[2021-07-11 Sun] blog.el.org")
(timestamp-and-title "[2021-07-11 Lun.] blog.el.org")
(timestamp-and-title "[2021-07-11 Sun 00:00] log.el.org")

Slugs

let's take a look at a Python "sluggifier". A better implementation with i18n support certainly exists in Django.

  s = s.lower()
# "[some] _ article's_title--"
# "[some]___article's_title__"

acct  = "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
wacct = "aaaaaeeeeeiiiiooooouuuunc"

for i in range(len(acct)):
    s = s.replace(acct[i], wacct[i])

s = s.replace("œ", "oe")

acct  = "-/_,:;."

for i in range(len(acct)):
    s = s.replace(acct[i], " ")

# "some___articles_title__"
# "some   articles title  "
# s = s.replace('_', ' ')

# "some   articles title  "
# "some articles title "
s = re.sub('\s+', ' ', s)

# "some articles title  "
# "some articles title"
s = s.strip()

# "some articles title"
# "some-articles-title"
s = s.replace(' ', '-')

# "[some]___article's_title__"
# "some___articles_title__"
s = re.sub('\W', '-', s)

# remove subsequent --
return s.strip('-')

intermediate functions

  (defun replace-chars-in-string (set-1 with-set-2 str)
  "Replace index-wise the character from set 1 in str by those of set 2 inside a string"
  (dotimes (i (length set-1))
    (setq str (string-replace
               (char-to-string (aref set-1 i))
               (char-to-string (aref with-set-2 i))
               str)))
  str)
  (defun replace-chars (set-1 char-2 str)
  "Replace the character from set 1 in str with char-2"
  (dotimes (i (length set-1))
    (setq str (string-replace
               (char-to-string (aref set-1 i))
               char-2
               str)))
  str)
(char-to-string 231)
  
(replace-chars-in-string
 "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
 "aaaaaeeeeeiiiiooooouuuunc"  "[some]  _   àrticle's_titlé--")


(replace-chars
 "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
 "X"  "[some]  _   àrticle's_titlé--")
;; (replace-chars-in-string
;;   "/_,:;."
;;   "       "  str)
;; (replace-regexp-in-string (regexp-quote "\s+") " " str nil 'literal)
;; (replace-regexp-in-string (regexp-quote "\s") "-" str nil 'literal)
;; (replace-chars-in-string
;;   "[](){}'"
;;   "-------" str) ;; < \W
;; (replace-regexp-in-string "\-+$" "" str nil 'literal) ;; suppress finals ---

Sluggyfier

  (defun sluggify (str)
  ;; suppress
  (replace-regexp-in-string
   "-+" "-" ;; multiple occurences
   (replace-regexp-in-string
    "^-+" "" ;; begining and
    (replace-regexp-in-string
     "-+$" "" ;; final --
     (replace-chars-in-string
      "/_,:;."
      "------"
      (replace-regexp-in-string
       (regexp-quote "\s")  "-"
       (replace-chars-in-string
        "[](){}'"
        "-------" ;; \W
        (replace-chars-in-string
         "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
         "aaaaaeeeeeiiiiooooouuuunc"  str))))))))

A slightly better option would look like so

  (defun sluggif1 (str)
  (let((replacements
        '(
          ("[](){}'" . "-")
          ("/_,:;." . "-")
          ("-+$" . "") ;; final --
          ("^-+" . "") ;; begining and
          ("-+"  . "-") ;; multiple occurences
          ;;((regexp-quote "\s") .  "-")
          ))
       (result))
    ;; \W
    (setq str(replace-chars-in-string ;; french accent
              "ãàáäâẽèéëêìíïîõòóöôùúüûñç"
              "aaaaaeeeeeiiiiooooouuuunc"  str))

    (cl-loop for (key . value) in replacements
             collect (setq str (replace-chars key value str)))))

HTML backend derivation

I'm using the standard html backend derivation file:///usr/share/emacs/28.0.5/straight/repos/org/lisp/ox-html.el negating a few options to get it to a bare minimum.

translate-alist indicates what function is in charge of translating each org elements.

Checking if a headline is already in the toc

Html ids are unique, but headlines are not. Add a number counter to differentiate identical headlines

  (defvar headline-list '() "the list of all headlines met during one export")

Generalized to this function:

  (defun append-next-numeral (string-list item)
  "add a numeral to `ITEM` present in the list `STRINGLIST`
   until it's unique, then add it to the list"
  (let( (crnt item) (i 0))
    (while (member crnt string-list)
      (setq crnt (concat item (format "-%s" (setq i (+ i 1))))))
    crnt))

(setq headline-list nil) That we use this way:

  (add-to-list 'headline-list (append-next-numeral headline-list "title"))

Headline

As described at the begining, headlines follow this structure :

*  TODO [#3] [2021-05-24 Mon 23:07] blog.el.org     :dev:article:post:

For section of level 1 and more the title and the tags needs to be exported, state and priority are discarded. Date is displayed in its own part.

Here's a derivation from org-html-headline.

  (defun exporting-article-p (info) (plist-get info :export-article))

(defun org-blog-headline (headline contents info)
  "Transcode a HEADLINE element from Org to HTML.
     CONTENTS holds the contents of the headline.  INFO is a plist
     holding contextual information."
  (unless (org-element-property :footnote-section-p headline)
    (let* ((numberedp nil) ;; don't number the section ;;(org-export-numbered-headline-p headline info))
           (numbers (org-export-get-headline-number headline info))
           (level (+ (org-export-get-relative-level headline info)
                     (1- (plist-get info :html-toplevel-hlevel))))
           (todo (and (plist-get info :with-todo-keywords)
                      (let ((todo (org-element-property :todo-keyword headline)))
                        (and todo (org-export-data todo info)))))
           (todo-type (and todo (org-element-property :todo-type headline)))
           (priority (and (plist-get info :with-priority)
                          (org-element-property :priority headline)))
           (title (org-export-data (org-element-property :title headline) info))
           (tags (and (plist-get info :with-tags)
                      (org-export-get-tags headline info)))
           (tags-elem (if tags
                          (format " <div class='tags'>%s</div>"
                                  (mapconcat (lambda(c)(format "<a href='%s.html'>%s</a>" c c))
                                             tags " "))
                        ""))
           (full-text (funcall (plist-get info :html-format-headline-function)
                               todo todo-type priority title tags info))
           (contents (or contents ""))
           (id (replace-regexp-in-string
                "['\" ]" "-"
                (or (org-element-property :CUSTOM_ID headline)
                    (org-export-get-reference headline info))
                (org-export-get-reference headline info)))
           (formatted-text
            (if (plist-get info :export-article)
                (format "<a href='#%s'>%s</a>" id full-text)
              (format "<a href='%s'>%s</a>" (sluggify full-text) full-text))))

      (setq title-num (append-next-numeral
                       headline-list
                       (replace-regexp-in-string "['\" ]" "-" title))) ;; to avoid dup ids

      (add-to-list 'headline-list title-num)
      (if (org-export-low-level-p headline info)
          ;; This is a deep sub-tree: export it as a list item.
          (let* ((html-type (if numberedp "ol" "ul")))
            (concat
             (and (org-export-first-sibling-p headline info)
                  (apply #'format "<%s class=\"org-%s\">\n"
                         (make-list 2 html-type)))
             (org-html-format-list-item
              contents (if numberedp 'ordered 'unordered)
              nil info nil
              (concat (org-html--anchor id nil nil info) formatted-text)) "\n"
             (and (org-export-last-sibling-p headline info)
                  (format "</%s>\n" html-type))))


        ;; TODO : L1 tags are handled in index.org
        ;; manage others tags here
        ;; when met, open tag.org, and insert a ref to upper entry + #headline
        ;; [2021-12-20 Mon 20:03] will do when the Missing refs are fixed

        ;; Standard headline.  Export it as a section.
        (let* ((extra-class
                (org-element-property :HTML_CONTAINER_CLASS headline))
               (headline-class
                (org-element-property :HTML_HEADLINE_CLASS headline))
               (first-content (car (org-element-contents headline))))

          ;; inner tags met in L2 sections get a reference in a tag file
          (when(and tags
                    (> level 1)
                    (plist-get info :export-article))
            ;; an article itself, not the other exports.
            ;; The variable is set for an article, not a toc, not the index
            (message "exporting inner tags")
            (blog-ref-inner-tags (org-export-output-file-name "") tags)
            (message "exporting inner tags DONE"))

          (format "<%s  class='%s'>%s%s</%s>\n" ;; id='%s'
                  (org-html--container headline info)
                  ;;(concat "section-" (org-export-get-reference headline info))
                  (concat (format "section-%d" level)
                          (and extra-class " ")
                          extra-class)

                  (if (exporting-article-p info)
                       ;; (concat  "\n<h" level " id='" title
                      ;;          "'><a href='#" title "'> " title " </a>" tags-elem "</h"level">\n")
                      ;; normal article with a ToC
                      (format "\n<h%d id='%s'><a href='#%s'> %s </a>%s</h%d>\n"
                              level
                              title-num
                              title-num
                              ;;(make-string (- level 1) ?#)  ;; # ## ###...
                              title
                              ;;tags
                              ;;(mapconcat #'identity tags " ")
                              tags-elem
                              level)

                    ;; an entry from the index or the tag list -> follow the link
                    (format "\n<h%d id='%s'><span class='timestamp'>%s</span>&nbsp;<a href='%s.html'>%s</a>%s</h%d>\n"
                            level title-num
                            (or (org-element-property :DATE headline) "")
                            (or (org-element-property :EXPORT_FILE_NAME headline)
                                (sluggify title)) ;; or
                            title
                            tags-elem
                            level))

                  (if (eq (org-element-type first-content) 'section) contents
                    (concat (org-html-section first-content "" info) contents))
                  (org-html--container headline info)))))))
  • [2021-09-02 jeu. 15:10] (C-c ok, mais unbalanced with C-x e … seems related to the pairing of "< >" along the parentheses. got to fix this.
  (setq org-export-headline-levels 4)

[2023-06-18 dim.]I see yet an other in a bug

Source code block

Add a class for prism.js.

  (defun src-code-block (src-block contents info)
  "Transcode a SRC-BLOCK element from Org to ASCII.
   CONTENTS is nil.  INFO is a plist used as a communication
   channel."
  (concat
   (format "<pre class='code line-numbers'>
<code class='language-%s'>%s</code><button class='expand'>expand</button></pre>"
           (org-element-property :language src-block)
           (replace-regexp-in-string
            "\"" "&quot;" (org-html-encode-plain-text
                           (org-element-normalize-string
                            (org-export-format-code-default src-block info)))))))

The proper way to go would be to stack the detected programming languages in the current page and insert the relevant css for each of them.

Prism current configuration : https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+awk+bash+c+css-extras+go+graphql+latex+lisp+makefile+mongodb+python+regex+rust+sql+typescript&plugins=line-highlight+line-numbers+inline-color+normalize-whitespace+toolbar+copy-to-clipboard+treeview

Total filesize: 71.3KB (84% JavaScript + 16% CSS)

This is handled fine with org-js. It needs special care to insert formatting that doesn't clutter the links in titles.

  (defun blog-html-statistics-cookie (statistics-cookie _contents _info)
  "[25/100]"
  (let ((cookie-value (org-element-property :value statistics-cookie)))
    (format "%s" cookie-value)))

Org html template head & body

Blog title is displayed in every page.

  (defcustom org-weblog-title
  "7d.nz"
  "The name of the blog" :group 'org-weblog)

<meta></meta>

  (setq org-html-validation-link "
      <a href='https://creativecommons.org/licenses/by-sa/4.0/deed.en'><img src='img/button-cc-by-sa.png' alt='cc-by-sa'></a>
      <a href='https://orgmode.org'><img src='img/button-orgmode.png' alt='orgmode'></a>
      <a href='https://validator.w3.org/check?uri=referer'><img src='img/button-html5.png' alt='html5'></a>")
  
(defun org-blog--build-meta-info (info)
  "Return meta tags for exported document.
   INFO is a plist used as a communication channel."
      (message "meta")
  (let* ((protect-string
          (lambda (str)
            (replace-regexp-in-string
             "\"" "&quot;" (org-html-encode-plain-text str))))
         ;; having possibly an additionnal element (the date)
         ;; the list is either a car with the raw title (no date)
         ;; or a list of 2 element (date + title)
         (title (or (nth 2 (plist-get info :title)) (car (plist-get info :title))))
         ;;(title (plist-get (org-export-data (plist-get info :title) info)))
         ;;(title (if (eq title nil) "???")) ;;(car(plist-get info :title))))
         (title (if (org-string-nw-p title) title " "))
         (author (nth 1 (car(org-collect-keywords '("AUTHOR")))))
         ;; There can be multiple #+author. Consider 1 here.
         ;; (author (and (plist-get info :with-author)
         ;;         (let ((auth (plist-get info :author)))
         ;;     (and auth
         ;;          ;; Return raw Org syntax, skipping non
         ;;          ;; exportable objects.
         ;;          (org-element-interpret-data
         ;;           (org-element-map auth
         ;;         (cons 'plain-text org-element-all-objects)
         ;;       'identity info))))))
         (description (plist-get info :description))
         (keywords (plist-get info :keywords))
         (charset (or (and org-html-coding-system
                           (fboundp 'coding-system-get)
                           (coding-system-get org-html-coding-system
                                              'mime-charset))
                      "utf-8")))

    (concat
     (when (plist-get info :time-stamp-file)
       (format-time-string
        (concat "<!-- "
                (plist-get info :html-metadata-timestamp-format)
                " -->\n")))
     (format
      (if (org-html-html5-p info)
          (org-html-close-tag "meta" "charset=\"%s\"" info)
        (org-html-close-tag
         "meta" "http-equiv=\"Content-Type\" content=\"text/html;charset=%s\""
         info))
      charset) "\n"
     (let ((viewport-options
            (cl-remove-if-not (lambda (cell) (org-string-nw-p (cadr cell)))
                              (plist-get info :html-viewport))))
       (and viewport-options
            (concat
             (org-html-close-tag
              "meta name='viewport'"
              (format "content='%s'"
                      (mapconcat
                       (lambda (elm) (format "%s=%s" (car elm) (cadr elm)))
                       viewport-options ", "))
              info)
             "\n")))
     (format "<title>%s%s</title>\n" org-weblog-title
             (if (s-equals? title org-weblog-title) ""
               (concat " — " title) ))
     (org-html-close-tag "meta" "name='generator' content='Org-mode'" info)
     "\n"
     (and (org-string-nw-p author)
          (concat
           (org-html-close-tag "meta"
                               (format "name='author' content='%s'"
                                       (funcall protect-string author))
                               info)
           "\n"))
     (and (org-string-nw-p description)
          (concat
           (org-html-close-tag "meta"
                               (format "name='description' content='%s'\n"
                                       (funcall protect-string description))
                               info)
           "\n"))
     (and (org-string-nw-p keywords)
          (concat
           (org-html-close-tag "meta"
                               (format "name='keywords' content='%s'"
                                       (funcall protect-string keywords))
                               info)
           "\n")))))


;; since toc is redefined
(defun org-blog-inner-template (contents info)
  "Return body of document string after HTML conversion.
        CONTENTS is the transcoded contents string.  INFO is a plist
        holding export options."
  (concat
   "<a href='/' class='homebtn'><img src='img/7dnzbw.png'/></a>"
   "<img id='toggle-theme' src='img/toggle-theme.png'>"
   ;; Table of contents.
   (let ((depth (plist-get info :with-toc)))
     (when depth (org-blog-toc depth info)))
   "<article" (when (not (exporting-article-p info)) " class='index'" ) ">"
   ;; Document contents.
   contents
   ;; Footnotes section.
   (org-html-footnote-section info)
   "</article>" ))


(defun org-blog-template (contents info)
  "Return complete document string after HTML conversion.
        CONTENTS is the transcoded contents string.  INFO is a plist
        holding export options."

  (concat
   (when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
     (let* ((xml-declaration (plist-get info :html-xml-declaration))
            (decl (or (and (stringp xml-declaration) xml-declaration)
                      (cdr (assoc (plist-get info :html-extension)
                                  xml-declaration))
                      (cdr (assoc "html" xml-declaration))
                      "")))
       (when (not (or (not decl) (string= "" decl)))
         (format "%s\n"
                 (format decl
                         (or (and org-html-coding-system
                                  (fboundp 'coding-system-get)
                                  (coding-system-get org-html-coding-system 'mime-charset))
                             "iso-8859-1"))))))
   (org-html-doctype info)
   "\n"
   (concat "<html"
           (cond ((org-html-xhtml-p info)
                  (format
                   " xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
                   (plist-get info :language) (plist-get info :language)))
                 ((org-html-html5-p info)
                  (format " lang=\"%s\"" (plist-get info :language))))
           ">\n")
   "<head>\n"
   (org-blog--build-meta-info info)
   (org-html--build-head info)
   ;;(org-html--build-mathjax-config info)
   (let ((link-up (org-trim (plist-get info :html-link-up)))
         (link-home (org-trim (plist-get info :html-link-home))) ;; todo
         (title (plist-get info :title)))
     (unless (and (string= link-up "") (string= link-home ""))
       (format (plist-get info :html-home/up-format)
               (or link-up link-home)
               (or link-home link-up))))
   ;; Preamble.
   (org-html--build-pre/postamble 'preamble info)
   "</head>\n<body>
     <div id='top-line'></div>
     <div class='modal'></div>"

   ;; Document contents.
   (let ((div (assq 'content (plist-get info :html-divs))))
     (format "<%s id=\"%s\">\n" (nth 1 div) (nth 2 div)))
   ;; Document title.
   (when (plist-get info :with-title) ;; there's necessary one (headline Lvl 1 is the title)
     (let ((title (and (plist-get info :with-title)
                       (plist-get info :title)))
           (subtitle (plist-get info :subtitle))
           (html5-fancy (org-html--html5-fancy-p info))
           (tags (plist-get info :title-tags)))

       (format "<h1 class='page-title'><a href='index.html'>%s</a></h1>" org-weblog-title)
       (when title
         (format
          (concat
           "<header" (when (exporting-article-p info) " class='article'" ) ">
            <h1 class='page-title'><a href='index.html'>
              <img src='img/7dnz.png' alt='%s'></a>
            </h1>
            </header>
            <div class='title" (when (not (exporting-article-p info)) " index" ) "'>
            <h1 class='title'>%s</h1>
            <h2 class='timestamp'>%s</h2>
            %s
           </div>")

          org-weblog-title
          (or (nth 2 title) (car title))
          (or (plist-get (nth 1(nth 1 title)) :raw-value) " ")
          (if tags
              (format " <span class='tags'>%s</span>"
                      (mapconcat
                       (lambda (c)
                         (and (not (string-empty-p c))
                              (format "<a href='%s.html'>%s</a>" c c)))
                       (split-string tags ":") " "))
            "")

          ;;(nth 1 title)'
          ;;(org-export-data title info)
          ;;(plist-get :timestamp (timestamp-and-title (org-export-data title info)))
          ;;(plist-get :raw-value (nth 1 title))
          ;;(car (plist-get info :title))
          ;;(org-export-data title info)
          (if subtitle
              (format
               (if html5-fancy
                   "<p class=\"subtitle\">%s</p>\n"
                 (concat "\n" (org-html-close-tag "br" nil info) "\n"
                         "<span class=\"subtitle\">%s</span>\n"))
               (org-export-data subtitle info))
            "")))))

   contents
   (format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
   "<footer>"
   (when (exporting-article-p info)
     (let ((o (sluggify title)))
       (format "<div class='prepostamble'>
                   <p class='orgfile'><a href='org/%s.org'>%s.org</a></p>
                   <p class='orgfile'><a href='org/%s.org.html'>%s.org.html</a></p>
                </div>" o o o o)))
   ;; Postamble.
   (org-blog--build-pre/postamble 'postamble info)


   "	<div class='footer-icons'>
      <a href='http://github.com/flintforge'><img src='img/gh-mini.png' alt='github/flintforge'></a>
      <a href='http://gitlab.com/7dnz'><img src='img/gitlab.svg' alt='gitlab/7d.nz'></a>
      <a href='./index.xml'><img src='img/rss.png' alt='rss'></a> &nbsp;
      <span id='hits'></span>
      <a href='./index.html'>
      <img class='licence' src='./img/footlogo.png' alt='7dnz'>
      </a>
      </div>"
   "</footer>"
   "<div id='bottom-line'></div>"
   ;; Possibly use the Klipse library live code blocks.
   ;; (when (plist-get info :html-klipsify-src)
   ;;   (concat "<script>" (plist-get info :html-klipse-selection-script)
   ;;           "</script><script src=\""
   ;;           org-html-klipse-js
   ;;           "\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
   ;;           org-html-klipse-css "\"/>"))
   ;; Closing document.
   "</body>\n</html>"))

The TOC highlighter is here here : ./js/script.js

The title when given to the exporter is a list of strings : TODO, priority, timestamp and title, the timestamp beeing an org-element

(⬛ [#A] (timestamp (:type inactive :raw-value [2021-07-11 Sun]
:year-start 2021 :month-start 7 :day-start 11 :hour-start nil
:minute-start nil :year-end 2021 :month-end 7 :day-end 11 :hour-end
nil :minute-end nil :begin 8 :end 25 :post-blank 1 :parent #0))
blog.el.org)

Without a date, this is what it will looks like

(⬛ [#A] blog.el.org)

customize the postamble

date, author, validation link,

  (setq org-html-postamble-format
'("en" "<p class=\"author\">Author: %a (%e)</p>
<p class=\"date\">Date: %d</p>
<p class=\"creator\">%c</p>
<p class=\"validation\">%v</p>"))
   (defun org-blog--build-pre/postamble (type info)
   "Return document preamble or postamble as a string, or nil.
TYPE is either `preamble' or `postamble', INFO is a plist used as a
communication channel."
   (let ((section (plist-get info (intern (format ":html-%s" type))))
         (spec (org-html-format-spec info)))
     (when section
       (let ((section-contents
              (if (functionp section) (funcall section info)
                (cond
                 ((stringp section) (format-spec section spec))
                 ((eq section 'auto)
                  (let ((date (cdr (assq ?d spec)))
                        (author (cdr (assq ?a spec)))
                        (email (cdr (assq ?e spec)))
                        (creator (cdr (assq ?c spec)))
                        (validation-link (cdr (assq ?v spec))))
                    (concat
                     (and (plist-get info :with-date)
                          (org-string-nw-p date)
                          (format "<p class=\"date\">%s: %s</p>\n"
                                  (org-html--translate "Date" info)
                                  date))
                     (and (plist-get info :with-author)
                          (org-string-nw-p author)
                          (format "<p class=\"author\">%s: %s</p>\n"
                                  (org-html--translate "Author" info)
                                  author))
                     (and (plist-get info :with-email)
                          (org-string-nw-p email)
                          (format "<p class=\"email\">%s: %s</p>\n"
                                  (org-html--translate "Email" info)
                                  email))
                     (and (plist-get info :time-stamp-file)
                          (format
                           "<p class=\"date\">%s: %s</p>\n"
                           (org-html--translate "Last update" info)
                           (format-time-string
                            (plist-get info :html-metadata-timestamp-format))))
                     (and (plist-get info :with-creator)
                          (org-string-nw-p creator)
                          (format "<p class=\"creator\">%s</p>\n" creator))
                     (and (org-string-nw-p validation-link)
                          (format "<p class=\"validation\">%s</p>\n"
                                  validation-link)))))
                 (t
                  (let ((formats (plist-get info (if (eq type 'preamble)
                                                     :html-preamble-format
                                                   :html-postamble-format)))
                        (language (plist-get info :language)))
                    (format-spec
                     (cadr (or (assoc-string language formats t)
                               (assoc-string "en" formats t)))
                     spec)))))))
         (let ((div (assq type (plist-get info :html-divs))))
           (when (org-string-nw-p section-contents)
             (concat
              (format "<%s id=\"%s\" class=\"%s\">\n"
                      (nth 1 div)
                      (nth 2 div)
                      org-html--pre/postamble-class)
              (org-element-normalize-string section-contents)
              (format "</%s>\n" (nth 1 div)))))))))

TOC toc

(add-to-list 'headline-list (append-next-numeral headline-list "title"))

  
(defun org-blog-toc (depth info &optional scope)
  "Build a table of contents.
     DEPTH is an integer specifying the depth of the table.  INFO is
     a plist used as a communication channel.  Optional argument SCOPE
     is an element defining the scope of the table.  Return the table
     of contents as a string, or nil if it is empty."

  (setq headline-list nil) ;; reinitialize the headline list
  (let ((toc-entries
         (mapcar (lambda (headline)
                   (cons (org-blog--format-toc-headline headline info)  ;; format headlines
                         (org-export-get-relative-level headline info)))
                 (org-export-collect-headlines info depth scope))))

    (when toc-entries
      (let ((toc (concat
                  "<a id='toc-button'>"
                  "<svg viewBox='-5 0 10 8' width='30'> <line y2='8' stroke='#000' stroke-width='10' stroke-dasharray='2 1'></line>"
                  "</svg></a>"
                  ;; (when (exporting-article-p info) "<a href='index.html'><img src='./img/7dnz.png'></a>")
                  "<nav id='text-TOC'>"
                  (org-html--toc-text toc-entries)
                  "</nav>"
                  "\n")))
        (if scope toc
          (let ((outer-tag (if (org-html--html5-fancy-p info) "nav" "div")))
            (concat (format "<%s id='TOC' class='toc'>\n" outer-tag)
                    (let ((top-level (plist-get info :html-toplevel-hlevel)))
                      ;;(format "<h%d>%s</h%d>\n" top-level (org-html--translate "" info) top-level) ;;"Table of Contents"
                      )
                    toc
                    (format "</%s>\n" outer-tag))))))))
  
(defun org-blog--format-toc-headline (headline info)
  "Return an appropriate table of contents entry for HEADLINE.
   INFO is a plist used as a communication channel."
  (let* ((headline-number (org-export-get-headline-number headline info))
         (todo (and (plist-get info :with-todo-keywords)
                    (let ((todo (org-element-property :todo-keyword headline)))
                      (and todo (org-export-data todo info)))))
         (todo-type (and todo (org-element-property :todo-type headline)))
         (priority (and (plist-get info :with-priority)
                        (org-element-property :priority headline)))
         (text  (string-replace "\"" "&quot;"
                                   (org-export-data-with-backend
                                    (org-export-get-alt-title headline info)
                                    (org-export-toc-entry-backend 'html)
                                    info)))
         (headline-x (append-next-numeral
                      headline-list
                      (replace-regexp-in-string "['\" ]" "-" text)))
         (tags (and (eq (plist-get info :with-tags) t)
                    (org-export-get-tags headline info))))

    (add-to-list 'headline-list headline-x)
    (if (plist-get info :export-article)
        (format "<a href=\"#%s\">%s</a>"
                ;; Label.
                ;; text
                headline-x
                ;;(or (org-element-property :CUSTOM_ID headline)
                ;;(org-export-get-reference headline info))
                ;; Body.
                (concat
                 ;; section numbering; todo: make it optional
                 ;; (and (not (org-export-low-level-p headline info))
                 ;;      (org-export-numbered-headline-p headline info)
                 ;;      (concat (mapconcat #'number-to-string headline-number ".")
                 ;;              ". "))
                 text
                 ;;(apply (plist-get info :html-format-headline-function)
                 ;;todo todo-type priority text tags :section-number nil)

                 ))
      (format "<a href='%s.html'>%s</a>" (sluggify text) text) ;; todo : headline with formatting

      )))

node properties

  (defun org-html-node-property (node-property _contents _info)
  "Transcode a NODE-PROPERTY element from Org to HTML.
   CONTENTS is nil.  INFO is a plist holding contextual
   information."
  (format "%s ??:%?? s"
          (org-element-property :key node-property)
          (let ((value (org-element-property :value node-property)))
            (if value (concat " " value) ""))))

footnotes / sidenotes

When the screen is large enough, (or allowing a slide on the left) fotnotes may be displayed as sidenotes 2like so, right next to the place where it's defined. We may consider distributing the note on the left or on the right of the column according to where it is in the text (closer to the right or to the left) are links rendered correctly? 22. The regular display should be kept, for proper display on tablets or prints. However forward footnote references needs to be fixed.3

bug The footnote text is considered as regular text (the same way the fontifiation occurs is emacs)

33

  (defun org-blog-sidenote (footnote-reference _contents info)
  "Transcode a FOOTNOTE-REFERENCE element from Org to HTML.
CONTENTS is nil.  INFO is a plist holding contextual information."
  (concat
   ;; Insert separator between two footnotes in a row.
   (let ((prev (org-export-get-previous-element footnote-reference info)))
     (when (eq (org-element-type prev) 'footnote-reference)
       (plist-get info :html-footnote-separator)))
   (let* ((n (org-export-get-footnote-number footnote-reference info))
          (sid (format "sfn.%d" n)) ; #id of sidenote
          (ids (format "fns.%d" n)) ; #fn to side
          (idr (format "fnr.%d" n)) ; #id of reference (bottom)
          (id (format "fn.%d%s"
                      n
                      (if (org-export-footnote-first-reference-p footnote-reference info)
                          ""
                        ".100"))))

     ; display footnote referencing a note on the side (inline) or at the bottom of page
     (concat
      "<span class='sidenote'>"
      (org-html--anchor sid n
                        (format " class='footnum' href='#fns.%d' role='doc-backlink'" n) info)
      (org-export-data
       (org-export-get-footnote-definition footnote-reference info) info)
      "</span>"
      (format
       (plist-get info :html-footnote-format)
       (org-html--anchor ids n
                         (format " class='footref fn-side' href='#sfn.%d' role='doc-backlink'" n) info))
      (format
       (plist-get info :html-footnote-format)
       (org-html--anchor idr  n
                         (format " class='footref fn-bottom' href='#fn.%d' role='doc-backlink'" n) info))
      ))))

para ?

Derived backend definition

missing sections in the translation will make the export only partial, but without any warning.

(setq org-html-html5-fancy t)
  (require 'ox-html)


(let ((org-export-before-parsing-hook '(org-ref-bbl-preprocess)))
  (org-export-define-derived-backend 'weblog 'html

  ;;: menu-entry ;; broken
   ;;'(?2 "Export to blog"
  ;; ((?f "To file" org-export-subtree-to-html)))
        ;;((?H "To temporary buffer" org-html-export-as-html))

  :options-alist
  '((:html-head-include-default-style nil "html-style" nil)
    (:html-head-include-scripts nil "html-scripts" nil)
    (:html-head-include-scripts nil "html-scripts" nil)
    (:html-doctype "HTML_DOCTYPE" nil "html5")
    (:html-html5-fancy nil t t)
    ;;(:html-postamble-format nil nil org-html-postamble-format)
    ;;(:html-self-link-headlines nil t t)
    )
  :translate-alist
  '(
    (drawer . nil) ;; not visible
    (src-block . src-code-block)
    (inner-template . org-blog-inner-template)
    (headline . org-blog-headline)
    (template . org-blog-template)
    (statistics-cookie . blog-html-statistics-cookie)
    (link . org-blog-html-link)
    (footnote-reference . org-blog-sidenote)
    (comment-block . nil) ;; this is ignored, so we use org-export-org first
    ;;(property-drawer . nil)
    ;;(paragraph . org-weblog-html-paragraph)
    ;;(node-property . org-html-node-property)
    ;;(underline . org-blog-html-underline)
    )))

Preserve nbsp

  (defun my-html-nobreak-space-filter (text backend info)
  (and (org-export-derived-backend-p backend 'weblog)
       (replace-regexp-in-string " " "&nbsp;" text)))

(add-to-list 'org-export-filter-plain-text-functions
             #'my-html-nobreak-space-filter)

paragraph

Need to override this one just to handle images differently in ToC and article. Consider using two exporters: org-article-weblog and org-toc-weblog. it's using :html-self-link-headlines as marker

Is the html5-fancy also in a similar situation ?

  (defun org-weblog-html--wrap-image (contents info &optional caption label)
  "see org-html--wrap-image in ox-html.el
   this adds minature management
  "
  (let ((html5-fancy (org-html--html5-fancy-p info)))
    (format "\n"

            ;; (if (plist-get info :html-self-link-headlines) ;; TODO and replace by ToC-gen
                (if html5-fancy
                    "<figure%s>\n%s%s\n</figure>"
                  "<div%s class=\"figure\">\n%s%s\n</div>")
            ;; ID.
            (if (org-string-nw-p label) (format " id=\"%s\"" label) "")
            ;; Contents.
            (if html5-fancy contents (format "<p>%s</p>" contents))
            ;; Caption.
            (if (not (org-string-nw-p caption)) ""
              (format (if html5-fancy "\n<figcaption>%s</figcaption>"
                        "\n<p>%s</p>")
                      caption)))))
  (defun org-weblog-html-paragraph (paragraph contents info)
  "Transcode a PARAGRAPH element from Org to HTML.
CONTENTS is the contents of the paragraph, as a string.  INFO is
the plist used as a communication channel."
  (let* ((parent (org-export-get-parent paragraph))
         (parent-type (org-element-type parent))
         (style '((footnote-definition " class=\"footpara\"")
                  (org-data " class=\"footpara\"")))
         (attributes (org-html--make-attribute-string
                      (org-export-read-attribute :attr_html paragraph)))
         (extra (or (cadr (assq parent-type style)) "")))
    (cond
     ((and (eq parent-type 'item)
           (not (org-export-get-previous-element paragraph info))
           (let ((followers (org-export-get-next-element paragraph info 2)))
             (and (not (cdr followers))
                  (memq (org-element-type (car followers)) '(nil plain-list)))))
      ;; First paragraph in an item has no tag if it is alone or
      ;; followed, at most, by a sub-list.
      contents)
     ((org-html-standalone-image-p paragraph info)
      ;; Standalone image.
      (let ((caption
             (let ((raw (org-export-data
                         (org-export-get-caption paragraph) info))
                   (org-html-standalone-image-predicate
                    #'org-html--has-caption-p))
               (if (not (org-string-nw-p raw)) raw
                 (concat "<span class=\"figure-number\">"
                         (format (org-html--translate "Figure %d:" info)
                                 (org-export-get-ordinal
                                  (org-element-map paragraph 'link
                                    #'identity info t)
                                  info nil #'org-html-standalone-image-p))
                         " </span>"
                         raw))))
            (label (org-html--reference paragraph info)))

        (org-weblog-html--wrap-image contents info caption label)))
     ;; Regular paragraph.
     (t (format "<p%s%s>\n%s</p>"
                (if (org-string-nw-p attributes)
                    (concat " " attributes) "")
                extra contents)))))

images

  (defun org-blog-html-link(link desc info)
  (if (plist-get info :export-article)
    (org-html-link link desc info)
    (org-string-nw-p desc))
  )

Style.css

./css/style.css

  (setq org-html-head"

<link rel='stylesheet' type='text/css' href='css/fonts/PT-sans.css'/>
<link rel='stylesheet' type='text/css' href='css/style.css'/>
<link rel='stylesheet' type='text/css' href='css/prism.css'/>
<script src='js/jquery.min.js' rel='preload'></script>
<script src='js/prism.js' rel='preload'></script>
<script src='js/script.js'></script>
<link rel='shortcut icon' href='favicon.gif'/>
  ")

<link rel='stylesheet' type='text/css' href='css/fonts/PT-sans.css'/> <link rel='stylesheet' type='text/css' href='css/style.css'/> <link rel='stylesheet' type='text/css' href='css/prism.css'/> <script src='js/jquery.min.js' rel='preload'></script> <script src='js/prism.js' rel='preload'></script> <script src='js/script.js'></script> <link rel='shortcut icon' href='favicon.gif'/>

#+endexample

For memo, here is a <head>

  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5">
<meta name="referrer" content="no-referrer">

<script src='script.js'>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#ffc40d" />
<meta name="theme-color" content="#ffffff" />

<link href="https://github.com/7d.nz/" rel="me">

<meta property="og:title" content="" />
<meta property="og:description"
      content=" …
               " />
<meta property="og:type" content="article" />
<meta property="og:url" content="" />

<meta property="article:published_time" content="2020-11-02T00:00:00&#43;01:00"/>
<meta property="article:modified_time" content="2020-11-02T00:00:00&#43;01:00"/>

<meta name="twitter:card" content="summary"/><meta name="twitter:image" content="/android-chrome-256x256.png"/>
<meta name="twitter:title" content=""/>
<meta name="twitter:description" content=""/>
<meta name="DC.Creator" content="Phil.Estival"/>

• Export to html: entry point

parachute.png

note that org-entry-get looks into org-special-properties first.

(org-entry-get nil "FILE" t)
(org-entry-get nil "TODO" t)
(org-entry-get nil "ALLTAGS" t)

If an interactive export of the entire buffer is desired :

  (defun org-blog-export-to-html
    (&optional async subtreep visible-only body-only ext-plist)
  "Export current buffer to a HTML file for a blog page.

   If narrowing is active in the current buffer, only export its
   narrowed part.

   If a region is active, export that region.

   A non-nil optional argument ASYNC means the process should happen
   asynchronously.  The resulting file should be accessible through
   the `org-export-stack' interface.

   When optional argument SUBTREEP is non-nil, export the sub-tree
   at point, extracting information from the headline properties
   first.

   When optional argument VISIBLE-ONLY is non-nil, don't export
   contents of hidden elements.

   When optional argument BODY-ONLY is non-nil, only write code
   between \"<body>\" and \"</body>\" tags.

   EXT-PLIST, when provided, is a property list with external
   parameters overriding Org default settings, but still inferior to
   file-local settings.

   Return output file's name."
    (message "export-to")
  (interactive)
  (let* (
         (filename (or (org-entry-get nil "EXPORT_FILE_NAME" t)
                       (sluggify (plist-get (timestamp-and-title (nth 4 (heading-components-at-level 1))) :title) )))

         ;;(org-export-coding-system org-html-coding-system)
    )

    (message "[ox-blog] exporting to html file %s.html" filename)

    ;;(message "TAGS : %s" (org-entry-get nil "ALLTAGS" t))
    ;; here the tags display fine
    ;; in the template, there's only the first L2 section
    ;; find a way to STUFF a property transmitted along the way

    (org-export-to-file 'weblog (concat filename ".html")
      async subtreep visible-only body-only ext-plist
    )))

Export subtree

This is from ox-hugo.el

  ;; For Org <= 9.1, `org-get-tags' returned a list of tags *only* at
;; the current heading, while `org-get-tags-at' returned inherited
;; tags too.
(with-no-warnings
  (if (fboundp #'org--get-local-tags)   ;If using Org 9.2+
      (defalias 'org-blog--get-tags 'org-get-tags)
    (defalias 'org-blog--get-tags 'org-get-tags-at)))

Now, we aren't using any of these, but they point out intresting display feature that may be introduced later on.

  (defun org-hugo--selective-property-inheritance ()
  "Return a list of properties that should be inherited."
  (let ((prop-list '(;;"HUGO_FRONT_MATTER_FORMAT"
                     ;;"HUGO_PREFER_HYPHEN_IN_TAGS"
                     ;;"HUGO_PRESERVE_FILLING"
                     ;;"HUGO_DELETE_TRAILING_WS"
                     ;;"HUGO_ALLOW_SPACES_IN_TAGS"
                     ;;"HUGO_BLACKFRIDAY"
                     ;;"HUGO_SECTION"
                     ;;"HUGO_SECTION*"
                     ;;"HUGO_BUNDLE"
                     ;;"HUGO_BASE_DIR"
                     ;;"HUGO_CODE_FENCE"
                     ;;"HUGO_MENU"
                     ;;"HUGO_CUSTOM_FRONT_MATTER"
                     ;;"HUGO_DRAFT"
                     ;;"HUGO_ISCJKLANGUAGE"
                     ;;"KEYWORDS"
                     ;;"HUGO_MARKUP"
                     ;;"HUGO_OUTPUTS"
                     ;;"HUGO_TAGS"
                     ;;"HUGO_CATEGORIES"
                     ;;"HUGO_SERIES"
                     ;;"HUGO_TYPE"
                     ;;"HUGO_LAYOUT"
                     ;;"HUGO_WEIGHT"
                     ;;"HUGO_RESOURCES"
                     ;;"HUGO_FRONT_MATTER_KEY_REPLACE"
                     ;;"HUGO_DATE_FORMAT"
                     ;;"HUGO_WITH_LOCALE"
                     ;;"HUGO_LOCALE"
                     ;;"HUGO_PAIRED_SHORTCODES"
                     "DATE" ;Useful for inheriting same date to same posts in different languages
                     ;;"HUGO_PUBLISHDATE"
                     ;;"HUGO_EXPIRYDATE"
                     ;;"HUGO_LASTMOD"
                     ;;"HUGO_SLUG" ;Useful for inheriting same slug to same posts in different languages
                     ;;"HUGO_PANDOC_CITATIONS"
                     ;;"BIBLIOGRAPHY"
                     ;;"HUGO_AUTO_SET_LASTMOD"
                     "AUTHOR")))
    (mapcar (lambda (str)
              (concat "EXPORT_" str))
            prop-list)))

ox-hugo.el::3904

Renamed it so there's no confusion if ox-blog is also active:

  (defun org-blog--get-element-path (element info)
  "Return the section path of ELEMENT.
INFO is a plist holding export options."
  (let ((root (or (org-export-get-node-property :EXPORT_HUGO_SECTION element :inherited)
                  (plist-get info :hugo-section)))
        (filename (org-export-get-node-property :EXPORT_FILE_NAME element :inherited))
        (current-element element)
        fragment fragments)
    ;; Iterate over all parents of current-element, and collect
    ;; section path fragments.
    (while (and current-element
                (not (org-export-get-node-property :EXPORT_HUGO_SECTION current-element nil)))
      ;; Add the :EXPORT_HUGO_SECTION* value to the fragment list.
      (when (setq fragment (org-export-get-node-property :EXPORT_HUGO_SECTION* current-element nil))
        (push fragment fragments))
      (setq current-element (org-element-property :parent current-element)))
    ;; Return the root section, section fragments and filename
    ;; concatenated.
    (concat
     (file-name-as-directory root)
     (mapconcat #'file-name-as-directory fragments "")
     filename)))
  (defun org-blog--get-pre-processed-buffer ()
  "Returns a pre-processed copy of the current buffer.
   Internal links to other subtrees are converted to external
   links."
  (let* ((buffer (generate-new-buffer (concat "*Ox-hugo Pre-processed " (buffer-name) " *")))
         ;; Create an abstract syntax tree (AST) of the Org document
         ;; in the current buffer.
         (ast (org-element-parse-buffer))
         ;;(org-use-property-inheritance (org-hugo--selective-property-inheritance))
         (info (org-combine-plists
                (list :parse-tree ast)
                (org-export--get-export-attributes 'hugo)
                (org-export--get-buffer-attributes)
                (org-export-get-environment 'hugo)))
         (local-variables (buffer-local-variables))
         (bound-variables (org-export--list-bound-variables))
         vars)
    (with-current-buffer buffer
      (let ((inhibit-modification-hooks t)
            (org-mode-hook nil)
            (org-inhibit-startup t))

        (org-mode)
        ;; Copy specific buffer local variables and variables set
        ;; through BIND keywords.
        (dolist (entry local-variables vars)
          (when (consp entry)
            (let ((var (car entry))
                  (val (cdr entry)))
              (and (not (memq var org-export-ignored-local-variables))
                   (or (memq var
                             '(default-directory
                                buffer-file-name
                                buffer-file-coding-system))
                       (assq var bound-variables)
                       (string-match "^\\(org-\\|orgtbl-\\)"
                                     (symbol-name var)))
                   ;; Skip unreadable values, as they cannot be
                   ;; sent to external process.
                   (or (not val) (ignore-errors (read (format "%S" val))))
                   (push (set (make-local-variable var) val) vars)))))

        ;; Process all link elements in the AST.
        (org-element-map ast 'link
          (lambda (link)
            (let ((type (org-element-property :type link)))
              (when (member type '("custom-id" "id" "fuzzy"))
                (let* ((raw-link (org-element-property :raw-link link))
                       (destination (if (string= type "fuzzy")
                                        (org-export-resolve-fuzzy-link link info)
                                      (org-export-resolve-id-link link info)))
                       (source-path (org-blog--get-element-path link info)) ;; see above
                       (destination-path (org-blog--get-element-path destination info))
                       (destination-type (org-element-type destination)))
                  ;; (message "[ox-hugo pre process DBG] destination type: %s" destination-type)

                  ;; Change the link if it points to a valid
                  ;; destination outside the subtree.
                  (unless (equal source-path destination-path)
                    (let ((link-desc (org-element-contents link))
                          (link-copy (org-element-copy link)))
                      ;; (message "[ox-hugo pre process DBG] link desc: %s" link-desc)
                      (apply #'org-element-adopt-elements link-copy link-desc)
                      (org-element-put-property link-copy :type "file")
                      (org-element-put-property
                       link-copy :path
                       (cond
                        ;; If the destination is a heading with the
                        ;; :EXPORT_FILE_NAME property defined, the
                        ;; link should point to the file (without
                        ;; anchor).
                        ((org-element-property :EXPORT_FILE_NAME destination)
                         (concat destination-path ".org"))
                        ;; Hugo only supports anchors to headlines, so
                        ;; if a "fuzzy" type link points to anything
                        ;; else than a headline, it should point to
                        ;; the file.
                        ((and (string= type "fuzzy")
                              (not (string-prefix-p "*" raw-link)))
                         (concat destination-path ".org"))
                        ;; In "custom-id" type links, the raw-link
                        ;; matches the anchor of the destination.
                        ((string= type "custom-id")
                         (concat destination-path ".org::" raw-link))
                        ;; In "id" and "fuzzy" type links, the anchor
                        ;; of the destination is derived from the
                        ;; :CUSTOM_ID property or the title.
                        (t
                         (let ((anchor (org-hugo--get-anchor destination info)))
                           (concat destination-path ".org::#" anchor)))))
                      ;; If the link destination is a heading and if
                      ;; user hasn't set the link description, set the
                      ;; description to the destination heading title.
                      (when (and (null link-desc)
                                 (equal 'headline destination-type))
                        (let ((headline-title
                               (org-export-data-with-backend
                                (org-element-property :title destination) 'ascii info)))
                          ;; (message "[ox-hugo pre process DBG] destination heading: %s" headline-title)
                          (org-element-set-contents link-copy headline-title)))
                      (org-element-set-element link link-copy))))))))

        ;; Workaround to prevent exporting of empty special blocks.
        (org-element-map ast 'special-block
          (lambda (block)
            (when (null (org-element-contents block))
              (org-element-adopt-elements block ""))))

        ;; Turn the AST with updated links into an Org document.
        (insert (org-element-interpret-data ast))
        (set-buffer-modified-p nil)))
    buffer))

UUID to keep track of sections. I tried not to, but this is unavoidable in the end.

  (defun uuid ()
  (md5 (format "%s%s%s%s%s%s%s%s%s"
               (user-uid)
               (emacs-pid)
               (system-name)
               (user-full-name)
               (current-time)
               (emacs-uptime)
               (garbage-collect)
               ;;(buffer-string)
               (random)
               (recent-keys))))
  (defcustom org-blog-regen-toc&tags t
  "is the toc and tags updated upon saving ?
this can be long and should be turned off to speed things up"
  :group 'org-blog
)

(setq org-blog-regen-toc&tags nil) (setq org-blog-regen-toc&tags t)

org-blog-regen-toc&tags
  
  (defun org-export-subtree-to-html (&optional async visible-only all-subtrees)
    "Export the current subtree to a html post.
  inspired by the ox-hugo exporter.

A non-nil optional argument ASYNC means the process should happen
asynchronously.  The resulting file should be accessible through the
`org-export-stack' interface.
When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.

When optional argument ALL-SUBTREES is non-nil, print the
subtree-number being exported.

- If point is under a valid Hugo post subtree, export it, and
  also return the exported file name.
- or, if point is not under a valid Hugo post subtree, but one exists
  elsewhere in the Org file, do not export anything, but still
  return t.
- Else, return nil."

    (setq headline-list '()) ;; flush the list of headlines (prevent dups ids)
    (setq narrowed (buffer-narrowed-p))

    ;; (dolist (fn '(org-babel-exp-src-block write-region)
    ;;    (advice-add fn :around #'org-hugo--advice-silence-messages)))

    ;; Publish only the current subtree
    (save-restriction
      (save-excursion
        (ignore-errors
          ;;(org-back-to-heading :invisible-ok)

          (condition-case err
              (outline-up-heading
               (+ -1(nth 1 (org-heading-components))))
            (error () nil))  ;; already at top level
          (when (not narrowed)
            (org-narrow-to-subtree)))

        (let ((subtree (org-element-at-point)))
          (if (not (member (nth 2 (org-heading-components)) org-done-keywords)) ;; todo state in the DONE set
              (progn
                (widen)
                (message "section is not in a DONE state (%s)" (org-heading-components)))
            ;; then proceed ..
            ;(message "subtree ???")
            (let* ((info (org-combine-plists
                          (org-export--get-export-attributes
                           'blog subtree visible-only)
                          (org-export--get-buffer-attributes)
                          (org-export-get-environment 'blog subtree)))
                   (id (save-excursion (org-up-heading 1) (org-entry-get nil "ID")))
                   (export-file-name (save-excursion (org-up-heading 1)
                                                     (org-entry-get nil "EXPORT_FILE_NAME")))
                   (is-commented (org-element-property :commentedp subtree))
                   (dir default-directory)

                   (time-&-title (timestamp-and-title
                                  (org-element-property :title subtree)))
                   (title (plist-get time-&-title :title))
                   (title-tags (org-entry-get nil "ALLTAGS" t))
                   (exclude-tags (plist-get info :exclude-tags)) ;; list of exclusion tags
                   (is-excluded (seq-intersection ;; is this section excluded ?
                                 '(split-string (or title-tags "") ":")
                                 '(exclude-tags)))
                   )

              (cond
               (is-commented
                (message "[ox-weblog] `%s' was not exported as that subtree is commented"
                         title))
               (is-excluded
                (message "[ox-weblog] `%s' was not exported as it is tagged with an exclude tag `%s'"
                         title matched-exclude-tag))
               (t
                (if all-subtrees
                    (progn
                      (setq org-blog--subtree-count (1+ org-blog--subtree-count))
                      (message "[ox-weblog] %d/ exporting `%s' .." org-blog--subtree-count title))
                  (message "[ox-weblog] exporting `%s' .." title))

                ;; keep track of the post by setting an ID if there was none
                (when (not id)
                  (save-excursion
                    (setq id (uuid))
                    (message "insert new id %s" id)
                    (org-up-heading 1)
                    (org-entry-put nil "ID" id)
                    ))

                ;; Do the buffer pre-processing only if the user is
                ;; exporting only the current post subtree.
                (let*((current-outline-path (org-get-outline-path :with-self))
                      (buffer (org-blog--get-pre-processed-buffer)) ;;  TODO  verify this (links conversion)
                      (sourcefile (f-filename (buffer-file-name))) ;; this file
                      (filename (or (org-entry-get nil "EXPORT_FILE_NAME" t)
                                    (sluggify title)) )
                      (file.org (concat filename ".org"))
                      (org/file.org (concat"./org/"  file.org))
                      ;;(tmp/file.org (concat"/tmp/"  file.org))
                      (file.html (concat filename ".html"))
                      (article-title (concat org-weblog-title " :: " title)))


                  (message "htmlizing")
                  (cd dir)
                  ;;(with-current-buffer buffer
                  (with-current-buffer (org-org-export-as-org)
                    ;;(kill-buffer filename)
                    ;;(uniquify-rename-buffer title)
                    (outline-show-all)

                    (if (eq t(compare-strings (concat filename ".org") nil nil
                                              sourcefile nil nil))
                        (message
                         "[ox-blog] org post export ( source file (%s) and post title (%s) share
                               the same name. Would overwrite the former. Skipping."
                         sourcefile filename)
                      ;;(progn
                      ;;(when (get-buffer file.org) (kill-buffer file.org))
                      ;; otherwise write file throws an error and they'll stack buffer<1><2>..
                      ;;(org-org-export-as-org))
                      (write-file org/file.org))
                    ;;(message "wrote %s" org/file.org)

                    (cd dir)
                    (with-current-buffer (htmlize-buffer) ;; why not the same dir ?
                      (rename-buffer article-title)
                      (write-file (concat org/file.org ".html") nil)
                      (kill-buffer)
                      )

                    (kill-buffer))

                  ;;(message ">>> %s %s %s" dir filename title)
                  (with-current-buffer buffer
                    ;;(org-next-visible-heading) << [BUG] wants to save the buffer...
                    ;; (goto-char (point-max)) ;; ...hence
                    ;; (org-up-heading 1) ;; bibliography is exported too
                    (org-up-heading 1)

                    (message "[ox-blog] exporting to html > %s.html" filename)
                    (setq ret
                          ;; the tags are retrieved fine until here
                          ;; but inside the template,
                          ;; calling for (org-entry-get nil "ALLTAGS" t) or returning to top header
                          ;; will only get the first L2 section.
                          ;; Pass the tags headline here, into the ext-plist.
                          (org-export-to-file 'weblog file.html
                            async :subtreep visible-only nil
                            (list :title-tags title-tags :export-article t )))

                    (message "exported %s %s (%s)" filename title title-tags)

                    (cd dir)
                    ;; negate for export only (no index update)
                    (when org-blog-regen-toc&tags
                      (save-excursion
                        (blog-toc-insert-headline-and-summary
                         filename time-&-title title-tags id export-file-name)))

                    (message "killing buffer %s" file.org)
                    ;;(kill-buffer file.org)
                    (message "subtree exported")
                    )
                  (message "killing buffer %s" buffer)
                  (kill-buffer buffer)
                  (other-window 1)
                  )))
              ret))))))

  ;; (dolist (fn '(org-babel-exp-src-block write-region)
  ;;    (advice-remove fn #'org-hugo--advice-silence-messages)))

  ;; ((when (not narrowed)
  ;;    (widen));; but should do it only if it wasn't narrowed in the first place
  ;;  )

Elementary styles

  (setq org-html-text-markup-alist
  '((bold . "<b>%s</b>")
    (code . "<code class='code'>%s</code>")
    (italic . "<i>%s</i>")
    (strike-through . "<del>%s</del>")
    (underline . "<u>%s</u>")
    (verbatim . "<code class='verbatim'>%s</code>")))

Parts from ox-hugo

save hook, export

  
(defun org-blog-export-to-html-after-save ()
  "Function for `after-save-hook' to run `org-export-subtree-to-html'.
Need to check what goes on when Org Capture in progress."
  (unless (eq real-this-command 'org-capture-finalize)
        (save-excursion
      (org-export-subtree-to-html))))
          ;;;(org-export-blog))))

;; which we can shortcut to the export-subtree


;;;###autoload
(define-minor-mode org-blog-auto-export-mode
  "Toggle auto exporting the subtree upon saving"
  :global nil
  :lighter "autoexport"
  :keymap org-mode-map
  ;; :keymap '(("C-x M-b t" . (setq org-blog-regen-tags-p (not org-blog-regen-toc&tags)))
  ;;          ("C-x M-b T" . (setq org-blog-regen-toc&tags (not org-blog-regen-toc&tags))))

  (if org-blog-auto-export-mode
      ;; mode is enabled
      (add-hook 'after-save-hook #'org-blog-export-to-html-after-save :append :local)
    ;; mode is disabled
    (remove-hook 'after-save-hook #'org-blog-export-to-html-after-save :local)))


;; export valid subtree

(defun org-blog--get-valid-subtree ()
  "Return the Org element for a valid blog post subtree.
The condition to check validity is that the STATE of the
entry is in a DONE state.

As this function is intended to be called inside a valid
post subtree, doing so also moves the point to the beginning of
the heading of that subtree.

Return nil if a valid post subtree is not found.  The point
will be moved in this case too."

  (if (member (nth 3 (org-element-at-point)) org-done-keywords)
      (cl-return entry)
    (cl-return nil)))

this is the entry point when in a headline to be exported. And which can be hooked up to be saved.

[2021-06-02 mer. 00:39] got it working for the export to md. It asks for the target filename.

  • shoud be auto set with the slug
  • remove the filename/target assoc
  • next, give it the derived html backend.
  • use the tags to build indexes/Table of contents (which is an org file)

(org-export-subtree-to-html) (profiler-report) (profiler-stop)

Change the target/publishing directory

Here's how ox-hugo does:

  ;;;; Publication Directory
(defun org-hugo--get-pub-dir (info)
  "Return the post publication directory path.

The publication directory is created if it does not exist.

INFO is a plist used as a communication channel."
  (let* ((base-dir (if (plist-get info :hugo-base-dir)
                       (file-name-as-directory (plist-get info :hugo-base-dir))
                     (user-error "It is mandatory to set the HUGO_BASE_DIR property")))
         (content-dir "content/")
         (section-path (org-hugo--get-section-path info))
         (bundle-dir (let ((bundle-path (or ;Hugo bundle set in the post subtree gets higher precedence
                                         (org-hugo--entry-get-concat nil "EXPORT_HUGO_BUNDLE" "/")
                                         (plist-get info :hugo-bundle)))) ;This is mainly to support per-file flow
                       (if bundle-path
                           (file-name-as-directory bundle-path)
                         "")))
         (pub-dir (let ((dir (concat base-dir content-dir section-path bundle-dir)))
                    (make-directory dir :parents) ;Create the directory if it does not exist
                    dir)))
    (file-truename pub-dir)))
  (defun org-hugo-export-to-md (&optional async subtreep visible-only)
  "Export current buffer to a Hugo-compatible Markdown file.

If narrowing is active in the current buffer, only export its
narrowed part.

If a region is active, export that region.

A non-nil optional argument ASYNC means the process should happen
asynchronously.  The resulting file should be accessible through
the `org-export-stack' interface.

When optional argument SUBTREEP is non-nil, export the sub-tree
at point, extracting information from the headline properties
first.

When optional argument VISIBLE-ONLY is non-nil, don't export
contents of hidden elements.

Return output file's name."
  (interactive)
  (org-hugo--before-export-function subtreep)
  ;; Allow certain `ox-hugo' properties to be inherited.  It is
  ;; important to set the `org-use-property-inheritance' before
  ;; setting the `info' var so that properties like
  ;; EXPORT_HUGO_SECTION get inherited.
  (let* ((org-use-property-inheritance (org-hugo--selective-property-inheritance))
     (info (org-combine-plists
        (org-export--get-export-attributes
         'hugo subtreep visible-only)
        (org-export--get-buffer-attributes)
        (org-export-get-environment 'hugo subtreep)))
     (pub-dir (org-hugo--get-pub-dir info))
     (outfile (org-export-output-file-name ".md" subtreep pub-dir)))
  ;; (message "[org-hugo-export-to-md DBG] section-dir = %s" section-dir)
  (prog1
    (org-export-to-file 'hugo outfile async subtreep visible-only)
    (org-hugo--after-export-function info outfile))))

• auto-export notice

Add a highlight when autoexport: enabled ? is active

This doesn't work in comment and won't override an existing formating (like a strike) nor a comment

  
(defgroup org-web-blog nil
 "Eye candy for Org weblog"
 :tag "Org Web Blog"
 :group 'org-appearance)

(defface blog-auto-export-enabled '((t :inherit default :foreground "chartreuse" :weight "bold"))
  "Face used to mention auto export is enabled"
  :group 'org-web-blog)

(defface greenout '((t :inherit default :foreground "chartreuse" :background "chartreuse"))
  "Face blackout the ongoing interogation"
  :group 'org-web-blog)

;; (defface blackout '((t :inherit default :foreground "#081020" :background "#081020"))
;;   "Face blackout the ongoing interogation"
;;   :group 'org-web-blog)

(add-hook 'org-blog-auto-export-mode-hook
 (lambda ()
   (font-lock-add-keywords nil
     '(("\\<autoexport: \\(enabled\\)" 1 'blog-auto-export-enabled prepend)
       ("\\<enabled \\(\\?\\)" 1 'greenout prepend)
       ))
   (font-lock-fontify-buffer)
   ))

;;
;;(setq 'org-blog-auto-export-mode-hook nil)
;;(font-lock-refresh-defaults)
;; to restore defaults

Tags and summary

First attempts

with org-elements.el. There's some doc on worg but really, the sources of org say everything.

The complex way

Reparse elements. They are cons with (type, properties, &rest elements)

  
(defun org-element-mapper (p)
  (cl-case (org-element-type p)
     ;;('paragraph  (org-element-mapper (org-element-contents p)))
     ('paragraph  (mapconcat #'identity (mapcar #'org-element-mapper (org-element-contents p)) ""))
     ('bold (format "*%s*" (car (org-element-contents p))))
     ('italic (format "/%s/"  (car (org-element-contents p))))
     ('verbatim (format "=%s= "  (org-element-property :value p)))
     ('plain-text (format "%s" p))
     ('link (org-element-property :raw-link p))
     ('src-block (format "\n#+src\n%s" (org-element-contents p)))
     (t
       ;;(if (consp p) p
       ;; (mapcar #'org-element-mapper p)
        (format "<%s> >%s [%s] " (org-element-type p) (type-of p) p )))))
  (org-element-map (org-element-parse-buffer) 'paragraph
  (lambda (paragraph)
    (let ((parent (org-element-property :parent paragraph)))
      (and (eq (org-element-type parent) 'section)
           (let ((first-child (car (org-element-contents parent))))
             (eq first-child paragraph))
           ;; Return value.
           paragraph))))
  (org-element-map (org-element-parse-buffer) 'paragraph
  (lambda (paragraph)
      (let ((parent (org-element-property :parent paragraph)))
        (and (eq (org-element-type parent) 'section)
             (let ((first-child (car (org-element-contents parent))))
               (eq first-child paragraph))
             ;; Return value.
             (org-element-mapper paragraph)))))

other attempts

  (org-element-map (org-element-parse-buffer ) 'paragraph
  (lambda (paragraph)
    (let ((parent (org-element-property :parent paragraph)))
      (and (eq (org-element-type parent) 'section)
           (let ((first-child (car (org-element-contents parent))))
             (eq first-child paragraph))
           ;; Return value.
           (org-element-interpret-data paragraph)))))
  
(org-element-map
  (org-element-parse-buffer 'object t)
  #'(paragraph)
  ;;#'org-element-mapper
  #'org-element-interpret-data
  nil nil )
  
(save-excursion
   (org-up-heading 1)
   (org-narrow-to-subtree)
   (let* ((tree (org-element-parse-buffer 'object t)))
      (org-element-map tree '(paragraph)
        ;;#'identity
          (lambda (p) (org-element-contents p))
         nil nil t )))
  
(save-excursion
  (let* (
         (tree (org-element-parse-buffer 'object t))
         )
    (org-element-map ;; tree
        tree
        '(paragraph bold)
      ;;#'identity
      ;;'org-element-type
      (lambda (p) (car (org-element-contents p)))
      nil nil nil t
      ;(lambda (p) (car (org-element-contents p))
      ))))

The easy way : org-interpret data

Getting only the paragraph

  
(save-excursion
  (mapconcat #'identity
             (org-element-map
                 (org-element-parse-buffer 'object t)
                 '(paragraph)
               ;; #'org-element-mapper
               'org-element-interpret-data
               nil nil )
             " "))

Summary

  
(defgroup org-weblog nil "the not so easy org blog" :version 26.1)

(defcustom org-blog-words-in-summary
  100 "maximum number of words in a blog entry summary" :group 'org-weblog)

Gettings visible paragraphs

  (mapconcat #'identity (org-element-map (org-element-parse-buffer 'object t)
                          '(paragraph)
                        #'org-element-interpret-data
                        nil nil ) " ")

Take the begining of the text following the headline to make a summary displayed in ./index.html.

  (defun subtree-summary ()
  (save-excursion
    (save-restriction
      (org-up-heading 1)
      (org-narrow-to-subtree)
      (org-show-all)
      (setq blog-org-structure
            (org-element-map
                (org-element-parse-buffer 'object t)
                'paragraph #'org-element-interpret-data))
      (or (org-entry-get nil "SUMMARY" t)
          (with-temp-buffer ;;file "/tmp/t"
            (insert
             (mapconcat #'identity
                        blog-org-structure
                        " "))
            (goto-char (point-min))
            (setq num 0)
            (while (and
                    (< num org-blog-words-in-summary)
                    (not (eq "summarystop" (word-at-point)))) ;; [2021-11-29 Mon 15:20] nope doesn't match
              (forward-word) ;; -strictly to ignore a comment line
              (setq num (+ 1 num)))
            (concat (buffer-substring-no-properties (point-min) (point)) "...")))
      )))

Young and unaware programmer as I once was, I did code my own string library in C++. I met many bugs while doing (double frees among other niceties…) learned the usage of operators and put them everywhere.

While coding it, I realized that a high level programming language or a clever compiler would make it possible to create an editor able to behave equally in an interactive mode and in a program: functions behaviour could be tested interactively on samples; strings operators and memory buffers would need to be 100% correct.

We don't create the index.org file, there should already be one (for now) with the following structure, so that org-insert-heading-after-current works. That's mostly because I'm going fast on that section

index.org

  #+TITLE: 7d.nz
#+EMAIL: pe@7d.nz
#+RSS_FEED_URL: https://7d.nz

./index.html is the default website's landing page Open index.org and run (org-export-to-file 'weblog "index.html") to change the ToC.

  (defcustom org-blog-toc-entry-file "index"
  "the index file holding the main ToC entry with all articles titles and summary" :group 'blaxorg)

and perhaps later paginated

  (defun blog-toc-insert-headline-and-summary
    (filename ttitle tags id export-filename)
  "add a new entry in index.org"
  (message "updating %s :: %s"  org-blog-toc-entry-file ".org" id)
  (let((summary (subtree-summary))
       (current (current-buffer))
       (index·org (concat org-blog-toc-entry-file ".org"))
       (index·html (concat org-blog-toc-entry-file ".html"))
       )

    ;; the form (with-temp-buffer ... insert-file)
    ;; is the only one I found to open, write and return
    ;; correctly from excursion
    (with-current-buffer(find-file index·org)

      (goto-char (point-min))
      ;; look for an entry already holding the id. If so, cut it.
      (while (and (outline-next-heading)
                  (if (equal (org-entry-get nil "ID") id)
                      (progn (org-cut-subtree) nil)
                    t)))

      (org-insert-heading)   ;; insert heading, date, title, tags and summmary
      (insert (concat
               (plist-get ttitle :title) "  " tags "\n"
               summary
               "\n"
               ))

      ;; #+OPTIONS: prop:t should  takes care of that
      ;; (#+OPTIONS: prop:t doesn't seem to work)
      (org-entry-put nil "ID" id) ;; insert id prop
      (when export-filename
        (org-entry-put nil "EXPORT_FILE_NAME" export-filename))
      (org-entry-put nil "DATE" (plist-get ttitle :timestamp))
      (mark-whole-buffer)       ;; sort
      (org-sort-entries nil ?R nil nil "DATE") ;; most recent first
      (deactivate-mark)
      ;; non async for now so we can debug a few things
      (org-export-to-file 'weblog index·html) ;; nil nil nil nil
      ;;'(:html-self-link-headlines t)) ;; dblcheck that option plz
      (write-file index·org)
      (when org-blog-regen-tags-p (org-blog-export-toc-tags))
      (org-delete-keyword "TITLE") ;; that's a bit hacky. Better style it with a display:none
      (org-insert-keyword "TITLE" " ")
      )
    (if (get-buffer index·org) ;; refresh buffer *index.org* if it's open
        (with-current-buffer index·org (revert-buffer t t)))))
  (blog-toc-insert-headline-and-summary
   "test.org"
   (timestamp-and-title (nth 4(heading-components-at-level 1)))
   (nth 5 (heading-components-at-level 1)) 0 "ex.org")

ox-rss

  (use-package ox-rss)
(let(
     (index·org (concat org-blog-toc-entry-file ".org"))
     )
  (with-current-buffer(find-file index·org)
    (org-delete-keyword "TITLE")
    (org-insert-keyword "TITLE" org-weblog-title)
    (org-rss-export-to-rss)
    (org-delete-keyword "TITLE")
    (org-insert-keyword "TITLE" " ")))

or C-e r r from index.org.

Look for an entry with a given property (id)

There is only one entry to a given article in the index. In a sophisticated solution, those files are linked in a merkel tree by consecutive revision. Only one sha1 would then be required to retrieve an entire branch and history, but this is not git nor venti.

  (while (and (outline-next-heading)
            (if (equal (org-entry-get nil "ID") id)
                (progn (org-cut-subtree) nil)
              t)))

To sort main ToC entries by time

  (mark-whole-buffer)
(org-sort-entries nil ?T) ;; most recent first

To prevent duplicates

When exporting a section and before writing the org file, the export is triggered by title or body change. a title change also implies, if there's no EXPORT_FILE_NAME, a filename change but the text body might have stayed the same. In wich case, we ought to simply rename the file.

Grepping for :ID: will also point duplicates.

The following is an idea to identify and book-keep exports.

check the body sha1 isn't found in the dir.sig file : the body is the post section past the first headline. If no such file exists, create it and append the sha1 of the text-body followed by filename

Register it in a dir.sig file. This is where to look for :

  • filenames. They're unique as they are in the same directory.
  • sha1 of text body. unique.

If one or the other is found twice, adjust accordingly.

  • body changes : proceed, update the sha1
  • title changes: move filename, update title.

If there's only an EXPORT_FILE_NAME change, this is considering as both a change in body and title.

If there are both changes in body and title then we can no longer keep track of the change. We could still introduce a section Id in the properties, but I'm trying to avoid to clutter the sources with extra data. The sha1 may go in the listing and summary. Here is a good place to mention we could map org-data onto something more racidal : folders.

  (defun bookkeep-export-buffer()
   (goto-line 2)
   (sha1 buffer (point) (point-max)))

In the end, it's a far more complicated solution than preserving an ID property. Let's move on.

Tags

list of tags in file

  (defun org-buffer-tags()
  (let (tags)
    (org-with-point-at (point-min)
      (while (outline-next-heading)
        ;; should be same level, but there's only L1 headlines in index.org
        (let ((curnt-tags (nth 5 (org-heading-components))))
          (when curnt-tags
            (mapcar (lambda (v) (setq tags (append tags (list v))))
                    (org-split-string curnt-tags ":"))))))
    (seq-uniq tags)))

(org-buffer-tags)

but there's already (org-get-buffer-tags). (mapcar 'car (org-get-buffer-tags)).

Org export to org

  (defun blog-export-to-org-tag(tag)
  "export only selected tag as <tag>.org
   good care is advised with tags and filenames if they're not in their own folders"
  (setq org-export-select-tags '(tag))
  (org-org-export-to-org)
  (setq org-export-select-tags '("export"))) ;; restore export settings

Need to set an export file name, so look for it, remove it if there's one and set that one instead. The cursor mark can stays after colon.

#+EXPORT_FILE_NAME: dev.org

Then Call org-kill-line, insert new name, continue.

To get the export file name: file:///usr/local/share/emacs/lisp/org/ox.el:6417

  
(org-with-point-at (point-min)
  (catch :found
    (let ((case-fold-search t))
      (while (re-search-forward
              "^[ \t]*#\\+EXPORT_FILE_NAME:[ \t]+\\S-" nil t)
        (let ((element (org-element-at-point)))
          (when (eq 'keyword (org-element-type element))
            (throw :found
                   (org-element-property :value element))))))))

From where I derived this generic function

  
(defun org-search-keyword(keyword)
  (org-with-point-at (point-min)
    (catch :found
      (let ((case-fold-search t))
        (while (re-search-forward
                (concat "^[ \t]*#\\+" keyword ":[ \t]+\\S-")
                nil t)
          (let ((element (org-element-at-point)))
            (when (eq 'keyword (org-element-type element))
              (throw :found
                     (org-element-property :value element)))))))))
(org-search-keyword "EXPORT_FILE_NAME")
(org-insert-directive "EXPORT_FILE_NAME" "/tmp/t.org")

They are called #+keywords, but I think they should would better be named directives.

  (defun org-insert-keyword(keyword value)
  (org-with-point-at (point-min)
    (insert (concat "#+" keyword ": " value "\n"))))
  (defun org-delete-keyword(keyword)
  "kill a line with a keyword"
  (org-with-point-at (point-min)
    ;; clean EXPORT_FILE_NAME, if there's any
    (when (org-search-keyword keyword)
      (goto-char(point-min))
      (re-search-forward
       (concat "^[ \t]*#\\+" keyword ":[ \t]+")
       nil t)
      (org-beginning-of-line)
      (org-kill-line))))

Problem here: I may need to derive the backend for headlines with EXPORT_FILE_NAME property set to correcly link the appropriate file.

Or I can completly ignore this feature and move on.

  (defun org-export-all-tags()
  (org-delete-keyword "EXPORT_FILE_NAME")
  (org-insert-keyword "EXPORT_FILE_NAME" "")
  ;;(insert (concat "#+EXPORT_FILE_NAME: \n"))
  (mapcar (lambda (tag)
            ((save-excursion
                (insert tag ".org") ;; insert new name
                (org-export-to-org-tag tag))
             (org-kill-line))
          (org-buffer-tags))
  (org-delete-keyword "EXPORT_FILE_NAME")))
  (defun org-blog-export-toc-tags()
  "for all tags at headline level 1
   produce one .org and one .html file with headlines and summaries"
  (message "generating tags files")
  (org-delete-keyword "EXPORT_FILE_NAME")
  (org-with-point-at (point-min)
    (insert (concat "#+EXPORT_FILE_NAME: \n"))
    (left-char)
    (mapcar (lambda (tag)
              (let ((org-file (concat tag ".org"))
                    (html-file (concat tag ".html")))
                (save-excursion
                  (org-kill-line)
                  (insert org-file) ;; EXPORT_FILE_NAME: <org-file>
                  (setq org-export-select-tags (list tag))
                  (org-delete-keyword "TITLE")
                  (org-insert-keyword "TITLE" tag)
                  ;;(org-org-export-to-org) ; no need
                  ;;(with-current-buffer (find-file org-file)
                  (org-export-to-file 'weblog html-file); nil nil nil nil ;'(:html-self-link-headlines nil) ;;  ))
                  (org-kill-line) ;; back to #+EXPORT_FILE_NAME: █
                  )))
            ;;(mapcar 'car (org-get-buffer-tags)) ;; where does it differ from below ?
            (org-buffer-tags))
    (setq org-export-select-tags '("export"))))

Language identifier mapping

The Syntax Highlighter uses sometimes other language identifiers for source blocks than org-mode. For example, where org-mode uses sh, Syntax Highlighter uses bash.

The mappings goes in an alist for later use.

  (defconst ox-blog-language-terms
    ("emacs-lisp" . "lisp")
    ("elisp" . "lisp")
    ("sh" . "bash")))

Exporting source code blocks

The source code exporting function, ox-blog-src-block, is modelled on org-html-src-block in org-mode/lisp/ox-html.el.

To keep things simple, the caption and label code is deleted. The language identifier is mapped using the mapping defined in the alist above. The HTML-formatting of the source code is removed, instead of org-html-format-code we use org-export-unravel-code. At last the pre formatting in angles is changed to code in brackets.

  (defun ox-blog-src-block (src-block contents info)
  "Transcode a SRC-BLOCK element from Org to HTML.
   CONTENTS holds the contents of the item.  INFO is a plist holding
   contextual information."
  (if (org-export-read-attribute :attr_html src-block :textarea)
      (org-html--textarea-block src-block)
    (let ((language-term
           (or (cdr (assoc (org-element-property :language src-block)
                           ox-blog-language-terms))
               (org-element-property :language src-block)))
          (code (car (org-export-unravel-code src-block))))
      (if (not language-term)
          (format "<pre class=\"example\"%s>\n%s</pre>" label code)
        (format "<div class=\"org-src-container\">\n%s\n</div>"
                (format "\n[code lang=\"%s\"]%s[/code]" language-term code))))))

Wrapping up and autoload

The following goes in to ./.dir-locals.el

  ((nil . ((indent-tabs-mode . nil)
         (fill-column . 70)
         (sentence-end-double-space . t)))
 ("org-sources"  . ((org-mode . ( (eval . (org-blog-auto-export-mode)))))))

This will start the org-blog-auto-export, loading the required libraries when opening files in the directory.

Less verbose message

https://scripter.co/using-emacs-advice-to-silence-messages-from-functions/

  (defun org-hugo--advice-silence-messages (orig-fun &rest args)
  "Advice function that silences all messages in ORIG-FUN."
  (let ((inhibit-message t)      ;Don't show the messages in Echo area
        (message-log-max nil))   ;Don't show the messages in the *Messages* buffer
    (apply orig-fun args)))

or multiple function :

  (dolist (fn '(org-babel-exp-src-block write-region)
            (advice-add fn :around #'org-hugo--advice-silence-messages))
  ;;(advice-add 'org-blog-export-to-html :around #'org-hugo--advice-silence-messages)

Possible improvements

Interactivity could be improved by activating impatient mode.

The publishing process isn't long. but since org-html-export-to-html can run asynchronously, the async option could initiate a new emacs process, which doesn't need to load your working init.el and freeze the running session.

So, we'll go into starting emacs with an alternate init file and deal about process management in a next chapter.

emacs -q -l export.el ...

+argument ? -eval expression? How does command line works in Emacs by the way? Do we really need an asynchronous process? What does ob-comint asyncs does? Did you enjoy the reading? Thank you, have a nice day and come back later for more.


1

the top statemenents in an org file starting with #+, they would rather better be named directives rather than keywords, but it's too late now.

2

like so, right next to the place where it's defined. We may consider distributing the note on the left or on the right of the column according to where it is in the text (closer to the right or to the left) are links rendered correctly?

3

bug The footnote text is considered as regular text (the same way the fontifiation occurs is emacs)