Creating a blog

Table of Contents

Introduction

I have been blogging by manually exporting my org-mode files into HTML with a custom CSS file and then SCP-ing the generated HTML file into my SDF personal page. While this works fine, The page doesn't have any SSL certificate, which just makes my website look bad. So I have recently decided to switch my blog over to Codeberg and utilize their pages facility.

The implementation

After some research, I've found out that Codeberg pages doesn't have any support for static site generators like Github pages does, But I am not deterred by this. I have devised a plan to use Codeberg effectively.

Firstly, I have created a private repository with all my articles on it (in org-mode format of course), I also have the scripts necessary to perform this on there, which are

  • An Emacs lisp script, and
  • A shell script

Emacs Lisp script

The Emacs lisp script actually exports the Emacs Org-files into a HTML directory.

I will go over each (interesting) part one by one.

Cleaning public

(message "Cleaning public/")
(shell-command "rm public/*")
(message "Done!"

This block just removes the old public directory, this is done to ensure that no files that were "removed" or "drafted" stay public.

Customizing HTML output

;; Customize the HTML output
(setq org-html-postamble t                    ; use org-html-export-format
      org-html-head-include-scripts nil       ; Use our own scripts
      org-html-head-include-default-style nil ; Use our own styles
      org-confirm-babel-evaluate nil          ; Don't ask permission for evaluating source blocks
      org-html-head "<link rel=\"stylesheet\" href=\"style.css\" />"
      user-full-name "tusharhero"
      user-mail-address "tusharhero@sdf.org"
)

I would say the most interesting part is the line where I disable prompt for asking permission before evaluating the babel source blocks, that is because, I am using emacs-lisp org-babel source blocks to generate text here and there, particularly I am using it right now to copy code from actual files during build time. I won't go over how exactly that is done, because I am still figuring it out.

Defining publishing project

;; Define the publishing project
(setq org-publish-project-alist
      (list
       (list "org-site:main"
             :recursive t
             :base-directory "./content"
             :publishing-function 'org-html-publish-to-html
             :publishing-directory "./public"
             :exclude-tags '("draft" "noexport")
             :exclude "draft*"
             :with-author  t
             :with-creator t
             :with-toc t
             :with-email t
             :html-link-home "/"
             :section-numbers nil
             :time-stamp-file t)))

Now, all of this is pretty standard, I think you can just guess what each part does, but the most interesting part is :html-home/up-format option, it allows to put the navigation bar in the general HTML, even though I could have done by defining a completely new element, but I decided that reusing old functionality is probably best here.

CSS minifier at home

As you can see in Styling, my CSS is a bit too large (by my standards). So I decided to minify it a bit… and this is what I wrote

(message "Starting the minification of CSS...")
(save-excursion
  (find-file "public/style.css")

  ;; Remove comments
  (replace-regexp-buffer
   (rx "/*"
       (zero-or-more (or (not (any "*")) (seq "*" (not (any "/")))))
       (one-or-more "*") "/")
   "")

  ;; Remove new lines
  (replace-regexp-buffer (rx "\n") "")

  ;; Replace multple spaces
  (replace-regexp-buffer  (rx (>= 2 "\s")) " ")

  ;; Remove some useless spaces
  (dolist (symbol (list "{" "}" ";" "," ">" "~" "-"))
    (replace-regexp-buffer (format "%s " symbol) (format "%s" symbol))
    (replace-regexp-buffer (format " %s" symbol) (format "%s" symbol))
   )
  (replace-regexp-buffer  (rx ": ") ":")

  (save-buffer))

Here is the definition of replace-regexp-buffer (I know, should make it a function).

(defmacro replace-regexp-buffer (regexp to-string)
  "Replace things macthing REGEXP with TO-STRING in all of the buffer."
  `(progn
     (goto-char (point-min))
     (while (re-search-forward ,regexp nil t)
       (replace-match ,to-string nil nil))))

It is a very basic minifer it doesn't really do anything complicated but I have managed to save up to 2.4KB using it, which is pretty good.

Full script

Here is the script, (It's licensed under the GPLv3 Copyright @ tusharhero 2024, although most of it has been taken from this tutorial.)

;;; build-script.el --- Build Script for website  -*- lexical-binding: t; after-save-hook: (lambda nil (shell-command "./build.sh")); -*-
;;; Commentary:
;; Licensed under GPLv3 Copyright @ tusharhero 2024
;;; Code:

(message "Cleaning public/")
(shell-command "rm public/*")
(message "Done!")

(message "Getting dependencies, may take some time...")

;; Add melpa
(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))

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

;; Install dependencies
(package-install 'htmlize)

;; Load the publishing system
(require 'ox-publish)
(message "Done!")


(message "Setting customizations...")
;; Customize the HTML output
(setq org-html-postamble t                    ; use org-html-export-format
      org-html-head-include-scripts nil       ; Use our own scripts
      org-html-head-include-default-style nil ; Use our own styles
      org-confirm-babel-evaluate nil          ; Don't ask permission for evaluating source blocks
      org-html-head "<link rel=\"stylesheet\" href=\"style.css\" />"
      user-full-name "tusharhero"
      user-mail-address "tusharhero@sdf.org"
)

(setq org-html-postamble-format
      '(("en"
"<p class=\"author\">Author: %a (%e)</p>
<p class=\"license\">
<a href=\"/\">tusharhero's pages</a> is licensed under <a href=\"https://creativecommons.org/licenses/by-nd/4.0/deed.en/\">CC BY-ND 4.0</a>
</p>
<p class=\"date\">Date: %d</p>
<p class=\"build-date\">Site built at: %C</p>
<p class=\"creator\">%c</p>")))


(setq org-html-home/up-format
      "<div class=\"navbar\" id=\"org-div-home-and-up\">
<a accesskey=\"H\" %s href=\"%s\">tusharhero's pages</a>
<span class=\"craftering\">
  <a class=\"arrow\" href=\"https://craftering.systemcrafters.net/@tusharhero/previous\">←</a>
  <a href=\"https://craftering.systemcrafters.net/\">craftering</a>
  <a class=\"arrow\" href=\"https://craftering.systemcrafters.net/@tusharhero/next\">→</a>
</span>
</div>"
)
;; Define the publishing project
(setq org-publish-project-alist
      (list
       (list "org-site:main"
             :recursive t
             :base-directory "./content"
             :publishing-function 'org-html-publish-to-html
             :publishing-directory "./public"
             :exclude-tags '("draft" "noexport")
             :exclude "draft*"
             :with-author  t
             :with-creator t
             :with-toc t
             :with-email t
             :html-link-home "/"
             :section-numbers nil
             :time-stamp-file t)))

(message "Complete!")

(message "Actually building the files...")
(org-publish-all t)
(message "Complete!")

(message "Copying CSS file over to public directory...")
(copy-file "style.css" "public/style.css")
(message "Done!")

(message "Copying favicon file over to public directory...")
(copy-file "favicon.ico" "public/favicon.ico")
(message "Done!")

(defmacro replace-regexp-buffer (regexp to-string)
  "Replace things macthing REGEXP with TO-STRING in all of the buffer."
  `(progn
     (goto-char (point-min))
     (while (re-search-forward ,regexp nil t)
       (replace-match ,to-string nil nil))))

(defmacro get-file-size (filename)
  "Get the size of FILENAME."
  `(progn (file-attribute-size (file-attributes ,filename))))

(message "Starting the minification of CSS...")
(save-excursion
  (find-file "public/style.css")

  ;; Remove comments
  (replace-regexp-buffer
   (rx "/*"
       (zero-or-more (or (not (any "*")) (seq "*" (not (any "/")))))
       (one-or-more "*") "/")
   "")

  ;; Remove new lines
  (replace-regexp-buffer (rx "\n") "")

  ;; Replace multple spaces
  (replace-regexp-buffer  (rx (>= 2 "\s")) " ")

  ;; Remove some useless spaces
  (dolist (symbol (list "{" "}" ";" "," ">" "~" "-"))
    (replace-regexp-buffer (format "%s " symbol) (format "%s" symbol))
    (replace-regexp-buffer (format " %s" symbol) (format "%s" symbol))
   )
  (replace-regexp-buffer  (rx ": ") ":")

  (save-buffer))

(message "Done, saved %s in the CSS!"
         (file-size-human-readable
          (- (get-file-size "style.css") (get-file-size "public/style.css"))))

(provide 'build-site)
;;; build-site.el ends here

The shell script

But commit-pushing twice is a bit repetitive, and things that are repetitive should be scripted away. So here is the script, It's licensed under the GPLv3 Copyright @ tusharhero 2024

This script checks if there are any new changes in the remote (in this case my private repository containing the sources of all the articles in org format), if there are it pulls and runs the build.sh script after which it makes a commit in the pages repository and pushes it.

I run this script every hour on my server using Systemd timers.

#!/bin/sh
git fetch
DIFFNUMBER=$(git diff origin/master | wc -l | sed -e 's/ .*//')
echo $DIFFNUMBER

if [ $DIFFNUMBER = 0 ]
then echo "Nope! Nothing on the remote!"
     exit
fi

echo "We seem to have new content!"
git pull

./build.sh

cd public
git add .
git commit -am "Automated Content Update"
git push

Styling

For styling I have this custom CSS file, its not really anything super interesting so I won't bother explaining anything.

/*CSS for my articles!*/
/*You may use it anywhere*/

body {
    margin: auto;
    width: 50rem; max-width: 100%;
    padding: 0.5rem; padding-top: 1.5rem;
    background: black;
    color: lawngreen;
    font-size: 1.4rem;
    font-family: monospace;
}

html::-webkit-scrollbar-track,
pre::-webkit-scrollbar-track {
  background-color: black;
}

html::-webkit-scrollbar,
pre::-webkit-scrollbar {
  width: 0.5rem;
}

html::-webkit-scrollbar-thumb,
pre::-webkit-scrollbar-thumb {
    border-radius: 0.2rem;
    background-color: lime;
}

.title {
  font-weight: bold;
  color: mediumspringgreen;
}

:is(h1, h2, h3, h4, h5, h6) {
    font-weight: bold;
    color: springgreen;
}

a,
a:visited {
    cursor: pointer;
    color: lightgreen;
    transition: 0.2s;
    text-decoration: none;
}

a:hover,
a:visited:hover {
    font-size: 1.1em;
    padding: 0.2em;
    color: black;
    background-color: lightgreen;
}

::selection {
    color: lightsalmon;
}

p.verse {
  font-family: "Times New Roman", Times, serif;
  font-size: 1em;
  font-style: italic;
  margin-bottom: 10px;
}

code {
    color: lightgreen;
    background: #171717;
    border: solid 0.01em green; border-radius: 0.2em;
}

img {
    overflow-x: scroll;
    scrollbar-width: none;
    max-width: 100%;
    height: auto;
    margin: 20px 0;
}

.navbar {
    position: fixed; top: 0; 
    width: inherit; max-width: inherit;
    padding-top: 0em; padding-bottom: 0.2em;
    background: #171717;
    border-bottom: solid lime 0.1em; border-radius: 0.1em;
    list-style-type: none;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: baseline;
    z-index: 1;
}
.navbar > a
{
    text-decoration: green wavy underline;
    font-size: 0.8em;
}

.navbar > a :: hover
{
    font-size: 0.8em;
}

.craftering
{
    display: flex;
    align-items: baseline;
}

.craftering > .arrow
{
    font-size: 1em;
}


.status {
    margin-top: auto;
    padding: 0.1em;
    border: solid lime 0.1em; border-radius: 0.1em;
    font-family: monospace;
}

/* Org-mode stuff (Copied and modified from org output)*/

  #content { max-width: 60em; margin: auto; }
  .title  { text-align: center;
      margin-bottom: .2em; }
  .subtitle { text-align: center;
      font-size: medium;
      font-weight: bold;
      margin-top:0; }
  .todo   { font-family: monospace; color: #f00; }
  .done   { font-family: monospace; color: #008000; }
  .priority { font-family: monospace; color: #ffa500; }
  .tag    { background-color: #eee; font-family: monospace;
      padding: 2px; font-size: 80%; font-weight: normal; }
  .timestamp { color: #bebebe; }
  .timestamp-kwd { color: #5f9ea0; }
  .org-right  { margin-left: auto; margin-right: 0px;  text-align: right; }
  .org-left   { margin-left: 0px;  margin-right: auto; text-align: left; }
  .org-center { margin-left: auto; margin-right: auto; text-align: center; }
  .underline { text-decoration: underline; }
  #postamble p, #preamble p { font-size: 90%; margin: .2em; }
  p.verse { margin-left: 3%; }
  pre {
      border: 1px solid #e6e6e6;
      border-radius: 3px;
      padding: 8pt;
      font-family: monospace;
      overflow: auto;
      margin: 1.2em;
  }
  pre.src {
      position: relative;
      overflow: auto;
  }
  pre.src:before {
      display: none;
      position: absolute;
      top: -8px;
      right: 12px;
      padding: 3px;
      color: #555;
      background-color: #f2f2f299;
  }
  pre.src:hover:before { display: inline; margin-top: 14px;}
  /* Languages per Org manual */
  pre.src-asymptote:before { content: 'Asymptote'; }
  pre.src-awk:before { content: 'Awk'; }
  pre.src-authinfo::before { content: 'Authinfo'; }
  pre.src-C:before { content: 'C'; }
  /* pre.src-C++ doesn't work in CSS */
  pre.src-clojure:before { content: 'Clojure'; }
  pre.src-css:before { content: 'CSS'; }
  pre.src-D:before { content: 'D'; }
  pre.src-ditaa:before { content: 'ditaa'; }
  pre.src-dot:before { content: 'Graphviz'; }
  pre.src-calc:before { content: 'Emacs Calc'; }
  pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
  pre.src-fortran:before { content: 'Fortran'; }
  pre.src-gnuplot:before { content: 'gnuplot'; }
  pre.src-haskell:before { content: 'Haskell'; }
  pre.src-hledger:before { content: 'hledger'; }
  pre.src-java:before { content: 'Java'; }
  pre.src-js:before { content: 'Javascript'; }
  pre.src-latex:before { content: 'LaTeX'; }
  pre.src-ledger:before { content: 'Ledger'; }
  pre.src-lisp:before { content: 'Lisp'; }
  pre.src-lilypond:before { content: 'Lilypond'; }
  pre.src-lua:before { content: 'Lua'; }
  pre.src-matlab:before { content: 'MATLAB'; }
  pre.src-mscgen:before { content: 'Mscgen'; }
  pre.src-ocaml:before { content: 'Objective Caml'; }
  pre.src-octave:before { content: 'Octave'; }
  pre.src-org:before { content: 'Org mode'; }
  pre.src-oz:before { content: 'OZ'; }
  pre.src-plantuml:before { content: 'Plantuml'; }
  pre.src-processing:before { content: 'Processing.js'; }
  pre.src-python:before { content: 'Python'; }
  pre.src-R:before { content: 'R'; }
  pre.src-ruby:before { content: 'Ruby'; }
  pre.src-sass:before { content: 'Sass'; }
  pre.src-scheme:before { content: 'Scheme'; }
  pre.src-screen:before { content: 'Gnu Screen'; }
  pre.src-sed:before { content: 'Sed'; }
  pre.src-sh:before { content: 'shell'; }
  pre.src-sql:before { content: 'SQL'; }
  pre.src-sqlite:before { content: 'SQLite'; }
  /* additional languages in org.el's org-babel-load-languages alist */
  pre.src-forth:before { content: 'Forth'; }
  pre.src-io:before { content: 'IO'; }
  pre.src-J:before { content: 'J'; }
  pre.src-makefile:before { content: 'Makefile'; }
  pre.src-maxima:before { content: 'Maxima'; }
  pre.src-perl:before { content: 'Perl'; }
  pre.src-picolisp:before { content: 'Pico Lisp'; }
  pre.src-scala:before { content: 'Scala'; }
  pre.src-shell:before { content: 'Shell Script'; }
  pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
  /* additional language identifiers per "defun org-babel-execute"
       in ob-*.el */
  pre.src-cpp:before  { content: 'C++'; }
  pre.src-abc:before  { content: 'ABC'; }
  pre.src-coq:before  { content: 'Coq'; }
  pre.src-groovy:before  { content: 'Groovy'; }
  /* additional language identifiers from org-babel-shell-names in
     ob-shell.el: ob-shell is the only babel language using a lambda to put
     the execution function name together. */
  pre.src-bash:before  { content: 'bash'; }
  pre.src-csh:before  { content: 'csh'; }
  pre.src-ash:before  { content: 'ash'; }
  pre.src-dash:before  { content: 'dash'; }
  pre.src-ksh:before  { content: 'ksh'; }
  pre.src-mksh:before  { content: 'mksh'; }
  pre.src-posh:before  { content: 'posh'; }
  /* Additional Emacs modes also supported by the LaTeX listings package */
  pre.src-ada:before { content: 'Ada'; }
  pre.src-asm:before { content: 'Assembler'; }
  pre.src-caml:before { content: 'Caml'; }
  pre.src-delphi:before { content: 'Delphi'; }
  pre.src-html:before { content: 'HTML'; }
  pre.src-idl:before { content: 'IDL'; }
  pre.src-mercury:before { content: 'Mercury'; }
  pre.src-metapost:before { content: 'MetaPost'; }
  pre.src-modula-2:before { content: 'Modula-2'; }
  pre.src-pascal:before { content: 'Pascal'; }
  pre.src-ps:before { content: 'PostScript'; }
  pre.src-prolog:before { content: 'Prolog'; }
  pre.src-simula:before { content: 'Simula'; }
  pre.src-tcl:before { content: 'tcl'; }
  pre.src-tex:before { content: 'TeX'; }
  pre.src-plain-tex:before { content: 'Plain TeX'; }
  pre.src-verilog:before { content: 'Verilog'; }
  pre.src-vhdl:before { content: 'VHDL'; }
  pre.src-xml:before { content: 'XML'; }
  pre.src-nxml:before { content: 'XML'; }
  /* add a generic configuration mode; LaTeX export needs an additional
     (add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
  pre.src-conf:before { content: 'Configuration File'; }

  table { border-collapse:collapse; }
  caption.t-above { caption-side: top; }
  caption.t-bottom { caption-side: bottom; }
  td, th { vertical-align:top;  }
  th.org-right  { text-align: center;  }
  th.org-left   { text-align: center;   }
  th.org-center { text-align: center; }
  td.org-right  { text-align: right;  }
  td.org-left   { text-align: left;   }
  td.org-center { text-align: center; }
  dt { font-weight: bold; }
  .footpara { display: inline; }
  .footdef  { margin-bottom: 1em; }
  .figure { padding: 1em; }
  .figure p { text-align: center; }
  .equation-container {
      display: table;
      text-align: center;
      width: 100%;
  }
  .equation {
      vertical-align: middle;
  }
  .equation-label {
      display: table-cell;
      text-align: right;
      vertical-align: middle;
  }
  .inlinetask {
      padding: 10px;
      border: 2px solid #808080;
      margin: 10px;
      background: #ffffcc;
  }
  textarea { overflow-x: auto; }
  .linenr { font-size: smaller }
  .code-highlighted { background-color: #ffff00; }
  .org-info-js_info-navigation { border-style: none; }
  #org-info-js_console-label
    { font-size: 10px; font-weight: bold; white-space: nowrap; }
  .org-info-js_search-highlight
    { background-color: #ffff00; color: #000000; font-weight: bold; }
  .org-svg { }


/* Local Variables: */
/* after-save-hook: (lambda () (copy-file "style.css" "public/style.css" t)) */
/* End: */

Future plans

These are the future plans:

  • [X] Rewrite the style sheet
  • [ ] Make the index "interesting"

Author: tusharhero (tusharhero@sdf.org)

tusharhero's pages is licensed under CC BY-ND 4.0

Date: 2024-03-23 Sat 20:44

Site built at: 2024-04-19 Fri 06:44

Emacs 29.0.50 (Org mode 9.5.5)