A little walk-through the remaking of this website with emacs and org-mode.
The previous version was running under werc which was great but having been laid off recently I felt the urge to remake my website. I decided I needed a better flow and since I mostly live in emacs this would be done from emacs.
org-mode includes a publishing system that can export HTML, there are some tools to manage blogs and wikis with org but I chose to only use the base publishing feature.
First I needed to convert my existing content to org format from markdown. This was done with Pandoc.
find . -type f -name "*.md" -exec pandoc {} -f markdown -t org -o {}.org \;
After the convertion was done, I needed to lay out my filesystem, here is what I use:
λ tree
.
├── content
│ ├── pages
│ │ ├── contact.org
│ │ ├── index.org
│ │ ├── misc.org
│ │ └── now.org
│ ├── posts
│ │ ├── building-a-website-with-org-mode.org
│ │ ├── feed.org
│ │ ├── index.org
│ │ └── ...
│ ├── projects
│ │ └── index.org
│ └── static
│ ├── cargo-run.gif
│ ├── castor9.png
│ └── ...
├── Makefile
├── public_html
│ ├── contact.html
│ ├── feed.xml
│ ├── index.html
│ ├── misc.html
│ ├── now.html
│ ├── posts
│ │ ├── building-a-website-with-org-mode.html
│ │ ├── index.html
│ │ └── ...
│ ├── projects
│ │ ├── asuka.html
│ │ ├── castor9.html
│ │ └── ...
│ └── static
│ ├── cargo-run.gif
│ ├── castor9.png
│ └── ...
└── publish.el
All the org files are in content. content/pages/ is where I put the index, contact, misc and now pages. content/posts/ is where I put the blog articles. projects/ has a single .org file with all the projects. Similarly content/posts/feed.org is a single file used to generate the feed.xml RSS feed. Every generated file ends in publichtml/.
In order to manage this website I added a Makefile:
CONTENT_DIR := ./content PUBLIC_HTML_DIR := public_html IGNORE_PATTERNS := '\.git' all: watch clean: rm -rf $(PUBLIC_HTML_DIR)/* build: ## docker: build container emacs -Q --script publish.el watch: @echo "Watching $(CONTENT_DIR) for changes..." @while true; do \ make build; \ inotifywait -r -e modify,create,delete,close_write \ --exclude $(IGNORE_PATTERNS) \ $(CONTENT_DIR) ./publish.el; \ echo "Change detected, rebuilding..."; \ sleep 1; \ done serve: @echo "Server starting..." && \ $(HTTP_SERVER_CMD)python3 -m http.server -d $(PUBLIC_HTML_DIR)
The basic development flow is running `make serve` in one terminal tab and `make watch` in another. Thanks to inotifywait it rebuilds the site every time I make a change which is pretty handy.
Now onto the meat of this rebuild.
In order to build the website, I run a script called publish.el with emacs. I run emacs with -Q which starts emacs without my personal config. That means that packages are not configured and we have to add it to the publish.el.
(setq package-user-dir (expand-file-name "./.packages")) (setq package-archives '(("melpa" . "https://melpa.org/packages/") ("elpa" . "https://elpa.gnu.org/packages/"))) (package-initialize) (unless package-archive-contents (package-refresh-contents)) (package-install 'htmlize) (package-install 'ox-rss) (package-install 'rust-mode) (package-install 'ob-fsharp) (package-install 'ob-rust) (require 'ox-publish)
We install packages in .packages, this directory should be ignored by git. Then we add the melpa and elpa repositories, initialize the package management system and install some packages.
htmlize is needed to get pretty source code, ox-rss for the RSS feed generation and the rust and fsharp packages are needed in my case to add some syntax higlighting to org-babel.
Finally ox-publish is needed to generate all the html.
The website has multiple parts (posts, static pages, projects page, RSS feed and pictures) that needs to be defined in the org-publish-project-alist variable.
(setq org-publish-project-alist `(("posts" :base-directory "content/posts" :base-extension "org" :publishing-directory "public_html/posts/" :exclude "feed.org" :recursive t :with-date t :with-toc nil :section-numbers nil :html-head-extra ,jb/head-extra :htmlized-source t :publishing-function org-html-publish-to-html :org-html-preamble nil) ("projects" :base-directory "content/projects" :base-extension "org" :publishing-directory "public_html/projects/" :recursive t :with-date t :with-toc nil :section-numbers nil :html-head-extra ,jb/head-extra :htmlized-source t :publishing-function org-html-publish-to-html :org-html-preamble nil) ("pages" :base-directory "content/pages" :base-extension "org" :publishing-directory "public_html/" :recursive t :with-date t :with-toc nil :section-numbers nil :html-head-extra ,jb/head-extra :publishing-function org-html-publish-to-html :org-html-preamble nil) ("static" :base-directory "content/static" :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|html\\|mp4\\|ico" :publishing-directory "public_html/static/" :recursive t :publishing-function org-publish-attachment) ("rss" :base-directory "content/posts/" :base-extension "org" :publishing-function (org-rss-publish-to-rss) :publishing-directory "public_html/" :rss-extension "xml" :exclude ".*" :include ("feed.org") :html-link-home "https://julienblanchard.com" :html-link-use-abs-url t) ("org-site" :components ("posts" "pages" "static" "rss"))))
There are many options to choose from which are detailed here. Basically for each block we have a title, a base directory, a publishing directory, a publishing function and options. And a "org-site" block that groups all the components.
org-publish has a default template and style, in order to override it I add to redefine `org-html-template`:
(defun org-html-template (contents info) "Custom HTML template for org-publish." (concat "<!DOCTYPE html>\n" "<html lang=\"en\">\n" "<head>\n" "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" (org-html--build-meta-info info) (org-html--build-head info) "</head>\n" "<body>\n" "<div class=\"nav-container\">\n" "<div class=\"nav-wrapper\">\n" "<div class=\"nav-content\">\n" "<a href=\"/\" class=\"nav-logo\">JULIEN BLANCHARD</a>\n" "<div class=\"nav-links\">\n" "<a href=\"/posts\">POSTS</a>\n" "<a href=\"/projects\">PROJECTS</a>\n" "<a href=\"/now.html\">NOW</a>\n" "<a href=\"/misc.html\">MISC.</a>\n" "<a href=\"/contact.html\">CONTACT</a>\n" "</div>\n" "</div>\n" "</div>\n" "</div>\n" "<div class=\"container\">\n" "<div class=\"hole-container\"></div>\n" "<div class=\"page\">\n" contents "</div>\n" "<div class=\"hole-container right\"></div>\n" "</div>\n" "<div class=\"footer-container\">\n" "<div class=\"footer\">\n" "<p>Hand-made with emacs ❤</p>\n" "</div>\n" "</div>\n" "</body>\n" "</html>"))
Notice the `contents` in the middle of the template, this is where the generated content will be added.
Finally the publish.el ends with:
(org-publish-all t)
to generate everything.
The RSS feed is generated from content/posts/feed.org, the /posts index is also a single file describing all the posts. I decided to update them manually which allows me to have some kind of drafts feature, they don't appear in the index or the RSS feed until I add them. Maybe I'll automate later but since I don't post often this is okay.
This is what the feed source looks like:
#+TITLE: Julien Blanchard's Blog #+AUTHOR: Julien Blanchard #+EMAIL: julien@typed-hole.org * Revisting my Journaling habits in 2024 :PROPERTIES: :ID: ffa04848-10af-4104-969e-8634de3da571 :PUBDATE: 2024-08-28 18:12:00 :RSS_PERMALINK: posts/revisiting-my-journaling-habits.html :END: * Making a tray icon tool :PROPERTIES: :ID: 8c464a65-94c1-4a5d-bfeb-9b33a0626ec7 :PUBDATE: 2024-08-23 18:19:00 :RSS_PERMALINK: posts/making-a-tray-icon-tool.html :END: ...
with some globals at the beginning to define the title, my name and email. All posts then have a title and a properties block to define the date and link. I could add a summary of the posts as plain text below each post titles.
You can set the picture size with additional html attributes directly in the org files.
In my projects pages I moved the TOC after the picture, this done with:
* Projects #+begin_div Some of my more or less actively maintained projects. ../static/projects.jpg #+end_div ** asuka
First I disabled the TOC, then added it back where I wanted it with `#+TOC: headlines 2`.
In order to have a div with the .ascii class that I can style on my index page I used:
#+begin_div Welcome to my little corner of the Internet. #+begin_ascii some art #+end_ascii #+end_div
In order to highlight a language not supported by org-babel by default you need to install the support package. ob-fsharp and ob-rust in my case.
I then ran org-html-htmlize-generate-css to create my code.css with all the pretty colors. Cool thing is that it will recreate your emacs theme.
That's about it really, I added the website to a git repo that I just clone on my server, run make build and voilà!
What I'd like to add is updating the website with a CI hook and maybe auto-generate the RSS feed and the posts index automatically other than that I'm very satisfied with this new setup.
The source code is available here https://git.sr.ht/~julienxx/website