Making a website based of the system crafters elisp code

Hi All,

I am trying to move my existing badly running work website using wordpress to the same model as the system crafters site, so I can make posts using org mode. I have mostly managed to edit the elisp code of the site to suit my own needs.

The one thing I cannot get to work is inline images, I don’t think David really uses them on his site. I can see one org file where he did and he got round it using some in line html code (probably fine for one image).

the standard org-html-publish-to-html seems to be able to do this, but not the modified one in Davids source.

The original publish.el file is hosted here systemcrafters-site/publish.el at master - SystemCrafters/systemcrafters-site - Codeberg.org

1 Like

What’s the issue with your current setup/in your current tries? Are the <img> tags missing completely? Or do they point to the wrong resources?

Also, a link to your current Elisp code + Org file would be quite helpful to debug/investigate.

<div id="org8e5eb20" class="figure">
<p><a href="../assets/img/niceiclogo.png">../assets/img/niceiclogo.png</a>
</p>
</div>

This is the generated html code, that should have an img tag in it, to display the image inline.

#+title: G M James Electrical

G M James Electrical are based in Chatteris in Cambridgeshire and undertake electrical Design, Installation and Testing in domestic, commercial, industrial and agricultural premises. Having served many years in the electrical installation sector.  G M James Electrical have expertise in the following areas 

- Domestic  – New, Rewire, Extensions and Alterations with certification to part ‘P’ of the building regulations.
- Landlord – Electrical Installation Condition Reports (Periodic Inspection Report)
- CAT5e / CAT6 structured cabling installation and testing
- Industrial and agricultural control panel design, build and installation
- Large installation – Circuit / Submain / Main design
- BMS Installation 
- NICEIC approved contractor

#+ATTR_HTML: :width 200
[[../assets/img/niceiclogo.png]]

This is the original org file.

The image does show when clicking the hyperlink text.

Elisp code in following post

This is a shameful copy of Davids code edited for my needs. I do plan to ask him if this is ok, and to credit him on the website. (well once I know it will work for me)

;; Set the package installation directory so that packages are not stored
;; in the standard emacs.d path.
(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
			 ("melpa-stable" . "https://stable.melpa.org/packages/")
			 ("elpa" . "https://elpa.gnu.org/packages/")))

;; Initialize the package system
(package-initialize)
(unless package-archive-contents
  (package-refresh-contents))

;; Install use-package
(unless (package-installed-p 'use-package)
  (package-install 'use-package))
(require 'use-package)

;; Install dependencies
(package-install 'htmlize)

;; Load the publishing system
(require 'ox-publish)
(require 'vc-git)
;(require 'subr-x)
;(require 'cl-lib)

;; Install other dependencies
(use-package esxml
  :pin "melpa-stable"
  :ensure t)

(use-package webfeeder
  :ensure t)

;(setq org-html-validation-link nil ;; Remove validation link
;      org-html-head-include-scripts nil ;; Use our own scripts
;      org-html-head-include-defult-style nil ;; Use our own styles
;      org-html-head "<link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\" />") ;; Grab a style sheet

(setq user-full-name "Gary James")
(setq user-mail-address "gary@gmjelec.co.uk")

(defvar dw/site-url (if (string-equal (getenv "CI") "true")
                        "" ;; Don't hardcode the domain
                      "http://localhost:8080")
  "The URL for the site being generated.")

(defun dw/site-header ()
  (list `(header (@ (class "site-header"))
                 (div (@ (class "container"))
                      (div (@ (class "site-title"))
                           (img (@ (class "logo")
                                   (src ,(concat dw/site-url "/assets/img/gmj_logo.png"))
                                   (alt "G M James Electrical")))))
                 (div (@ (class "site-masthead"))
                      (div (@ (class "container"))
                           (nav (@ (class "nav"))
                                (a (@ (class "nav-link") (href "/")) "Home") " "
                                (a (@ (class "nav-link") (href "/contact/")) "Contact") " "
                                (a (@ (class "nav-link") (href "/services/")) "Services") " "
                                (a (@ (class "nav-link") (href "/projects/")) "Projects") " "
                                (a (@ (class "nav-link") (href "/contact/")) "Contact")))))))

(defun dw/site-footer ()
  (list `(footer (@ (class "site-footer"))
                 (div (@ (class "container"))
                      (div (@ (class "row"))
                           (div (@ (class "column"))
                                ;(p (a (@ (href ,(concat dw/site-url "/privacy-policy/"))) "Privacy Policy")
                                 ;  " · "
                                   (a (@ (href ,(concat dw/site-url "/credits/"))) "Credits")
                                   " · "
                                  ; (a (@ (href ,(concat dw/site-url "/rss/"))) "RSS Feeds")
                                  ; " · "
                                  ; (a (@ (rel "me") (href "https://fosstodon.org/@daviwil")) "Fediverse"))
                                (p "© 2021-2024 G M James Electrical"))
                           (div (@ (class "column align-right"))
                                (p (a (@ (href "https://www.gnu.org/software/emacs/"))
                                      (img (@ (src ,(concat dw/site-url "/assets/img/emacs.png"))
                                              (style "width: 120px")
                                              (alt "Made with Emacs")))))))))))

(defun dw/get-commit-hash ()
  "Get the short hash of the latest commit in the current repository."
  (string-trim-right
   (with-output-to-string
     (with-current-buffer standard-output
       (vc-git-command t nil nil "rev-parse" "--short" "HEAD")))))

(defun get-article-output-path (org-file pub-dir)
  (let ((article-dir (concat pub-dir
                             (downcase
                              (file-name-as-directory
                               (file-name-sans-extension
                                (file-name-nondirectory org-file)))))))

    (if (string-match "\\/index.org\\|\\/404.org$" org-file)
        pub-dir
        (progn
          (unless (file-directory-p article-dir)
            (make-directory article-dir t))
          article-dir))))

(cl-defun dw/generate-page (title
                            content
                            info
                            &key
                            (publish-date)
                            (head-extra)
                            (pre-content)
                            (exclude-header)
                            (exclude-footer))
  (concat
   "<!-- Generated from " (dw/get-commit-hash)  " on " (format-time-string "%Y-%m-%d @ %H:%M") " with " org-export-creator-string " -->\n"
   "<!DOCTYPE html>"
   (sxml-to-xml
    `(html (@ (lang "en"))
           (head
            (meta (@ (charset "utf-8")))
            (meta (@ (author "G M James Electrical - Gary James")))
            (meta (@ (name "viewport")
                     (content "width=device-width, initial-scale=1, shrink-to-fit=no")))
            (link (@ (rel "icon") (type "image/png") (href "/assets/img/favicon.png")))
            (link (@ (rel "alternative")
                     (type "application/rss+xml")
                     (title "Projects")
                     (href ,(concat dw/site-url "/rss/projects.xml"))))
            (link (@ (rel "stylesheet") (href ,(concat dw/site-url "/assets/fonts/iosevka-aile/iosevka-aile.css"))))
            (link (@ (rel "stylesheet") (href ,(concat dw/site-url "/assets/fonts/jetbrains-mono/jetbrains-mono.css"))))
            (link (@ (rel "stylesheet") (href ,(concat dw/site-url "/assets/css/code.css"))))
            (link (@ (rel "stylesheet") (href ,(concat dw/site-url "/assets/css/site.css"))))
            ;(script (@ (defer "defer")
            ;           (data-domain "gmjelec.co.uk")
            ;           (src "https://plausible.io/js/plausible.js"))
                    ;; Empty string to cause a closing </script> tag
            ;        "")
            ,(when head-extra head-extra)
            (title ,(concat title " - G M James Electrical")))
           (body ,@(unless exclude-header
                     (dw/site-header))
                 (div (@ (class "container"))
                      (div (@ (class "site-post"))
                           (h1 (@ (class "site-post-title"))
                               ,title)
                           ,(when publish-date
                              `(p (@ (class "site-post-meta")) ,publish-date))
                           ;,(if-let ((image-id (plist-get info :image)))
                           ;     (dw/embed-image image-id))
                           ,(when pre-content pre-content)
                           (div (@ (id "content"))
                                ,content)))
;                      ,(dw/embed-list-form))
                 ,@(unless exclude-footer
                     (dw/site-footer)))))))

(defun dw/format-projects-entry (entry style project)
  "Format posts with author and published data in the index page."
  (cond ((not (directory-name-p entry))
         (format "[[file:%s][%s]] - %s · %s"
                 entry
                 (org-publish-find-title entry project)
                 (car (org-publish-find-property entry :author project))
                 (format-time-string "%B %d, %Y"
                                     (org-publish-find-date entry project))))
        ((eq style 'tree) (file-name-nondirectory (directory-file-name entry)))
        (t entry)))

(defun dw/make-heading-anchor-name (headline-text)
  (thread-last headline-text
    (downcase)
    (replace-regexp-in-string " " "-")
    (replace-regexp-in-string "[^[:alnum:]_-]" "")))

(defun dw/org-html-link (link contents info)
  "Removes file extension and changes the path into lowercase file:// links."
  (when (and (string= 'file (org-element-property :type link))
             (string= "org" (file-name-extension (org-element-property :path link))))
    (org-element-put-property link :path
                              (downcase
                               (file-name-sans-extension
                                (org-element-property :path link)))))

  (let ((exported-link (org-export-custom-protocol-maybe link contents 'html info)))
    (cond
     (exported-link exported-link)
     ((equal contents nil)
      (format "<a href=\"%s\">%s</a>"
              (org-element-property :raw-link link)
              (org-element-property :raw-link link)))
     ((string-prefix-p "/" (org-element-property :raw-link link))
      (format "<a href=\"%s\">%s</a>"
              (org-element-property :raw-link link)
              contents))
     (t (org-export-with-backend 'html link contents info)))))

(defun dw/org-html-headline (headline contents info)
  (let* ((text (org-export-data (org-element-property :title headline) info))
         (level (org-export-get-relative-level headline info))
         (level (min 7 (when level (1+ level))))
         (anchor-name (dw/make-heading-anchor-name text))
         (attributes (org-element-property :ATTR_HTML headline))
         (container (org-element-property :HTML_CONTAINER headline))
         (container-class (and container (org-element-property :HTML_CONTAINER_CLASS headline))))
    (when attributes
      (setq attributes
            (format " %s" (org-html--make-attribute-string
                           (org-export-read-attribute 'attr_html `(nil
                                                                   (attr_html ,(split-string attributes))))))))
    (concat
     (when (and container (not (string= "" container)))
       (format "<%s%s>" container (if container-class (format " class=\"%s\"" container-class) "")))
     (if (not (org-export-low-level-p headline info))
         (format "<h%d%s><a id=\"%s\" class=\"anchor\" href=\"#%s\">¶</a>%s</h%d>%s"
                 level
                 (or attributes "")
                 anchor-name
                 anchor-name
                 text
                 level
                 (or contents ""))
       (concat
        (when (org-export-first-sibling-p headline info) "<ul>")
        (format "<li>%s%s</li>" text (or contents ""))
        (when (org-export-last-sibling-p headline info) "</ul>")))
     (when (and container (not (string= "" container)))
       (format "</%s>" (cl-subseq container 0 (cl-search " " container)))))))

(defun dw/org-html-template (contents info)
  (dw/generate-page (org-export-data (plist-get info :title) info)
                    contents
                    info
                    :publish-date (org-export-data (org-export-get-date info "%B %e, %Y") info)))

(org-export-define-derived-backend 'site-html 'html
  :translate-alist
  '((template . dw/org-html-template)
    (link . dw/org-html-link)
;    (src-block . dw/org-html-src-block)
;    (special-block . dw/org-html-special-block)
    (headline . dw/org-html-headline)))

(defun org-html-publish-to-html (plist filename pub-dir)
  "Publish an org file to HTML, using the FILENAME as the output directory."
  (let ((article-path (get-article-output-path filename pub-dir)))
    (cl-letf (((symbol-function 'org-export-output-file-name)
               (lambda (extension &optional subtreep pub-dir)
                 ;; The 404 page is a special case, it must be named "404.html"
                 (concat article-path
                         (if (string= (file-name-nondirectory filename) "404.org") "404" "index")
                         extension))))
      (org-publish-org-to 'site-html
                          filename
                          (concat "." (or (plist-get plist :html-extension)
                                          "html"))
                          plist
                          article-path))))

;; Define the pubishing project
(setq org-publish-project-alist
      (list
       (list "gmjelec.co.uk"
	 :recursive t
	 :base-directory "./content"
	 :publishing-directory "./public"
	 :publishing-function 'org-html-publish-to-html
	 :with-author nil ;; Author name
	 :with-creator t  ;; Show emacs and org version
	 :with-toc nil ;; Table of contents
	 :section-numbers nil ;; Section numbers
	 :time-stamp-file nil );; Time stamp of file
       '("gmjelec.co.uk:projects"
         :base-directory "./content/projects"
         :base-extension "org"
         :publishing-directory "./public/projects"
         :publishing-function org-html-publish-to-html
         :auto-sitemap t
         :sitemap-filename "../projects.org"
         :sitemap-title "Projects"
         :sitemap-format-entry dw/format-projects-entry
         :sitemap-style list
	 ;; :sitemap-function dw/news-sitemap
         :sitemap-sort-files anti-chronologically
         :with-title nil
	 :with-inline-tasks t
         :with-timestamps nil)
       '("gmjelec.co.uk:assets"
         :base-directory "./assets"
         :base-extension "css\\|js\\|svg\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|woff2\\|ttf"
         :publishing-directory "./public/assets"
         :recursive t
         :publishing-function org-publish-attachment)))

(setq org-publish-use-timestamps-flag t
      org-publish-timestamp-directory "./.org-cache/"
      org-export-with-section-numbers nil
      org-export-use-babel nil
      org-export-with-smart-quotes t
      org-export-with-sub-superscripts nil
      org-export-with-tags 'not-in-toc
      org-html-htmlize-output-type 'css
      org-html-prefer-user-labels t
      org-html-link-home dw/site-url
      org-html-link-use-abs-url t
      org-html-link-org-files-as-html t
      org-html-inline-images t
      org-html-html5-fancy t
      org-html-self-link-headlines t
      org-export-with-toc nil
      make-backup-files nil)

;; Generate the site output

(defun dw/publish ()
  "Publish the site."
  (interactive)
  
  (org-publish-all (string-equal (or (getenv "FORCE")
                                     (getenv "CI"))
                                 "true"))

  (make-directory "public/rss" t)
  (webfeeder-build "rss/projects.xml"
                   "./public"
                   dw/site-url
                   (let ((default-directory (expand-file-name "./public/")))
                     (remove "projects/index.html"
                             (directory-files-recursively "projects"
                                                          ".*\\.html$")))
                   :builder 'webfeeder-make-rss
                   :title "GMJ Electrical projects"
                   :description "G M James Electrical projects"
                   :author "Gary James")

(message "Build complete!"))

(provide 'publish)

I thought so and took the liberty to dive into David’s code. I found the issue within dw/org-html-link:

(defun dw/org-html-link (link contents info)
  …
    (cond
     (exported-link exported-link)
     ((equal contents nil)
     …

This check is too strict. [[file:$IMAGE]] links, which include inline images, usually don’t have a content, this condition wins and we end up with just <a href="$IMAGE">$IMAGE</a>. Whoops.

We need an additional check for images:

     (cond
      (exported-link exported-link)
-     ((equal contents nil)
+     ((and (equal contents nil) (not (org-export-inline-image-p link)))

Note that this might be too lenient, but the last function within the cond form, org-export-with-backend, will take care of that.

By the way, (equal contents nil) should probably be (null contents).

CC @daviwil, you might be interested in that fix.

Wow, thank you Ashraz, that works perfectly.

I really do appreciate your help. I am only a very very amateur programmer and elisp is still very difficult for me.

Thank you again for all your help, what a great community!!

1 Like

You’re welcome. In case you’re wondering how I got there before even seeing your messages with the .org file and Lisp code:

  1. I created a minimal page and a minimal exporter:
(org-export-define-derived-backend 'site-html 'html
  :translate-alist
  '((link . dw/org-html-link)))

(defun dw/org-html-link (link contents info)
  "Removes file extension and changes the path into lowercase file:// links."
  ;; kept exactly as-is
)
  1. I then ran that exporter via M-: (org-export-to-file 'site-html "Test.html") RET and inspected Test.html.
  2. I then removed the first condition, i.e. (exported-link exported-link) to see the effects. It still didn’t show the image, so I reverted the removal.
  3. I then removed the next condition form (the one I changed above) completely. The image showed up.
  4. I then used C-h f org-link and scrolled through the functions to check whether there is something and found org-html-link. I skimmed its definition and found ;; Image file. This told me about org-export-inline-image-p
  5. I added the org-export-inline-image-p check and verified that it worked.
2 Likes

Thanks for the thorough investigation! If you have time, can you send a PR for that? Otherwise I’ll fix it there the next time I’m in the code.

The code is GPL licensed, you are perfectly welcome to take the site code and adapt it to your needs!

Thank you daviwil. The website us up at gmjelec.co.uk - I have credited you in the credits link. Please let me know if there are any links in there you would rather not see.

Thank you for all your help and videos. I would not even be using emacs if it was not for your Emacs from scratch content.

Looks great! I like the customization tweaks you made to it. Nice logo, too!

It’s really cool to see people from other trades using Emacs :slight_smile:

I use emacs a lot for work. I sync org files for notes from my phone to my PC when out and about so they are at home when I got to my PC. I also use org files and output them to PDF for job specifications for customers. I use mu4e everyday for my email client too.

Just goes to show tho, that emacs can be extremely useful even if your not a programmer.

I do use it for programing, if only on an amateur basis, there is a blitz-basic amiga major mode that I use a lot. Also I am trying to learn python at the moment, which again I use emacs for.