My Emacs Blog

Table of Contents

What Is This

I fell into the Udemy trap: buying courses and not completing them. Or arguably worse: completing a course and jumping straight into the next one. I found myself in tutorial hell, going through certain motions expecting a job to appear out of thin air. One of those motions was maintaining a blog to "share my learning". Looking back at some of those blogs, I found myself paralyzed with second hand embarrassment. So this is a redo pretending my previous abomination of a blog never existed.

The Previous Blog

The previous blog was powered by WordPress with Bluehost taking care of the hosting. I used Next.js to create a front end that was much uglier than any of the WordPress templates I could have chosen. An important lesson I learned looking back at my "minimalist" front end: reinventing the wheel isn't always the best learning method. Sometimes, gluing together things that already work can teach you just as well. The point is, I didn't need to use Next.js. At least, not in the way that I used it. If I had taken the time to style a template that presented better than the available WordPress templates, then Next.js would have made sense. But that's not what I did. I just used Next.js for the sake of using it.

The content of the blog was robotic. I was just highlighting basic things I learned. The information I presented could easily be found elsewhere and with better presentation. Nothing that I posted offered any unique insight. It was just a hollow attempt to show that I was being productive. Whenever I prepared to write a blog, it felt forced. I had blog posts on HTML, CSS, and React. When W3Schools, MDN Docs, CSS Tricks, and React docs exist, there's no reason to clutter the internet with the same information.

So, What is This?

This is where I plan to write about the things I've built, if I feel like it. Writing is something I enjoy, so when it occurs naturally and makes sense, it may live here. I've also been wanting to maximize the utility of my raspberry pi and dig deeper into some shiny objects I've been chasing. All of those ideas have converged into the creation of this blog.

Raspberry Pi

My pi had been relegated as my hub for git. I used the pi with git to organize and version control my notes and trading journal. We can have an argument here asking, "Why not just GitHub?" Because I didn't want all my random thoughts on GitHub. So I created my PiGitHub. Since it had performed admirably, it proved ready for the added responsibility of serving my blog posts. Instead of paying Bluehost, I could use my pi and self host for free. The trade off being the availability of my blog would be dependent on my internet rather than on Bluehost. For my personal blog, this felt like a fair trade.

My next step invloved chasing some more shiny objects: HTMX and Go. I had this planned out for a while. Because I already had my WordPress blog, I set this plan aside. As my Bluehost subscription was expiring, the Pi/HTMX/Go blog was on the verge of materializing. But just as I needlessly used Next.js with WordPress, I abandoned my note taking tool Obsidian. I didn't know it at the time, but that decision would leave HTMX and Go sidelined.

Emacs

I have the flaw of repeating the same mistakes disguising them as different mistakes. I learned Emacs to take over as my note taking tool. Why not just stick with Obsidian? Because procrastination can disguise itself as learning. Which is another lesson I've learned: chasing shiny objects is a form of procrastination. So whether I'm chasing a shiny object or trying to solve a problem that's already been solved, what I'm really doing is procrastinating. On the surface, chasing Emacs was no different than using Next.js without a good reason. But I think present me and future me will be using Emacs for a very long time, so maybe the effort was worthwhile. Having said all that, I didn't just bring up Emacs to say "I use Emacs by the way." The reason I bring up Emacs is because this redo blog is powered by Emacs and org-publish.

Org-Publish

I learned Emacs the same way I learned Neovim (I use Neovim by the way). I learned them both using their built in tutorials. I avoided plugins for the first three months. I still use Emacs without any plugins, but I've since evolved my Neovim experience into a lazy.nvim setup with around 20 plugins. I "vibe coded" an Emacs trading journal using Neovim, and learned a little elisp along the way. While looking into other things I could do with Emacs, I discovered org mode has a publishing management system: org-publish. All I have to do is continue writing and let Emacs convert my org files into HTML.

Emacs Config

Working on my trading journal, I learned I can make Emacs bend to my will. Could I have done the same with Obsidian? Maybe. But I've converted completely to Emacs, and I don't see myself taking notes any other way. This workflow also naturally leans into the process of writing blogs. All I had to do was follow the pattern of my trading journal and create a config file for the blog. Using org-publish docs and llm chatbots, the initial version produced HTML and CSS I was happy with:

(setq org-publish-project-alist
      `(("blog-org"
         :base-directory ,my/blog-org-dir
         :base-extension "org"
         :publishing-directory ,my/blog-public-dir
         :publishing-function org-html-publish-to-html
         :with-tags t
         :recursive t

         :auto-sitemap t
         :sitemap-filename "index.org"
         :sitemap-title "Blog Posts"
         :sitemap-sort-files anti-chronologically

         :section-numbers nil
         :with-toc nil
         :html-head "<link rel=\"stylesheet\" 
                href=\"/css/style.css\" 
                type=\"text/css\" />"
         :html-preamble nil
         :html-postamble nil)

        ("blog-static"
         :base-directory ,my/blog-static-dir
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf"
         :publishing-directory ,my/blog-public-dir
         :recursive t
         :publishing-function org-publish-attachment)

        ("blog" :components ("blog-org" "blog-static"))))

To learn more, the use of C-h v <name of variable> and C-h f <name of function> can explain each property. For example, C-h v org-publish-project-alist RET provides a ton of information starting with:

org-publish-project-alist is a variable defined in ‘ox-publish.el’.

Its value is shown below.

Association list to control publishing behavior.

Each element of the alist is a publishing project. The car of each element is a string, uniquely identifying the project. The cdr of each element is in one of the following forms: …

The Workflow

Edit in Notes

Along with the config, I have functions facilitating my workflow. The workflow involves editing the rough draft which lives in my version controlled notes. When I want to do something, I call the corresponding function. The my/copy-from-notes() function copies the rough draft from my notes to the blog directory:

(defun my/copy-from-notes()
  (interactive)
  (let* ((src (buffer-file-name))
        (dest (expand-file-name (file-name-nondirectory src) my/blog-org-dir)))
    (unless src
      (user-error "Current buffer is not visiting a file"))
    (unless (file-directory-p my/blog-org-dir)
      (make-directory my/blog-org-dir t))
    (copy-file src dest t)
    (message "Copied to blog: %s" (file-name-nondirectory src))))

Paths and Publish

Next, I wanted to be able to preview the blog as I edited it both in my notes directory and the blog directory. The first set of functions determine the proper paths and publish the current blog:

(defun my/blog-org-path ()
  (let ((filename (file-name-nondirectory (buffer-file-name))))
    (expand-file-name filename my/blog-org-dir)))

(defun my/blog-html-path ()
  (let* ((filename (file-name-nondirectory (buffer-file-name)))
         (html-name (concat (file-name-sans-extension filename) ".html")))
    (expand-file-name html-name my/blog-public-dir)))

(defun my/open-blog-copy ()
  (interactive)
  (find-file (my/blog-org-path)))

(defun my/publish-current-blog-file ()
  (interactive)
  (org-publish-current-file t))

Blog Server

I learned that if I ever planned to use custom CSS, then I would only be able to preview it by running a local server. These functions start and stop the server:

(defun my/start-blog-server()
  (unless (and my/blog-server-process
               (process-live-p my/blog-server-process))
    (setq my/blog-server-process
          (start-process "blog-server" "*blog-server*"
                         "python3" "-m" "http.server" "8080"
                         "--directory" my/blog-public-dir))
    (sleep-for 1)
    (message "Blog server started")))

(defun my/stop-blog-server()
  (interactive)
  (when (and my/blog-server-process
             (process-live-p my/blog-server-process))
    (delete-process my/blog-server-process)
    (setq my/blog-server-process nil)
    (message "Blog server stopped")))

Preview

Gluing it all together, I can preview the blog from my notes or from the blog directory

(defun my/open-blog-html ()
  (interactive)
  (let* ((html-path (my/blog-html-path))
        (html-url (concat "http://localhost:8080/"
                          (file-name-nondirectory html-path))))
    (unless (file-exists-p html-path)
      (user-error "Published HTML does not exist. Publish first."))
    (my/start-blog-server)
    (message "Opening: %s" html-url)
    (browse-url html-url)))

(defun my/preview-post-from-notes ()
  (interactive)
  (my/copy-from-notes)
  (my/open-blog-copy)
  (my/publish-current-blog-file)
  (my/open-blog-html))

(defun my/preview-post-from-source ()
  (interactive)
  (my/publish-current-blog-file)
  (my/open-blog-html))

(defun my/in-blog-source-p ()
  (and (buffer-file-name)
       (file-in-directory-p
        (buffer-file-name)
        my/blog-org-dir)))

(defun my/preview-post ()
  (interactive)
  (if (my/in-blog-source-p)
      (my/preview-post-from-source)
    (my/preview-post-from-notes)))

Send to pi

When I'm done with a blog, the final step is to send the files to the pi.

(defun my/publish-and-deploy()
  (interactive)
  (my/publish-blog)
  (shell-command 
    (format "rsync -avz --delete %s %s" 
            my/blog-public-dir 
            my/blog-pi-path)))

The my/publish-and-deploy function relies on rsync. The process is smart enough to upload files that are new or have been edited.

Flaw in the Workflow

The flaw in my workflow is that I shouldn't edit files in the blog org directory. This is because it will be out of sync with the blog in my version controlled notes. If for some reason I forget that I made changes in the blog directory and start making changes in the notes, the copy function will always overwrite the blog directory. So I have to remember to always edit the blog in my notes.

This becomes an issue when I preview a blog. The displayed buffer will switch to the blog directory, and I've already caught myself editing the "wrong" file forgetting to switch to the notes buffer. There is much room for improvement.

Grappling with CSS

Learning web development taught me that I hate CSS. The only way I can articulate the hatred is by explaining I enjoy the proccess of creating logic, running into a wall, and eventually crashing through the wall. The logic can be some kind of frontend user interaction or some type of backend logic. I don't experience this same form of satisfaction when I finally center a div. The level headed approach would involve becoming more intimate with CSS.

I choose the lazy approach. Using solutions like tailwind or bootstrap still involve understanding CSS. Because this blog isn't trying to win any beauty contests and prefers speed, I went with Pico.css. It's more bloated than Water.css or Simple.css, but it offers robust customization if I want to go down that path in the future. At the time of writing, this blog uses the classeless approach. It provides more styling than org-publish gives out of the box.

More Pi configuration

nginx

While the initial plan was to learn and use Go for my server, my llm companions convinced me that nginx was worth learning and had great synergy with the Emacs blog workflow. Trying to adjust to my new way of thinking: glue working products together, nginx was the perfect choice. Also, I have many projects planned where it will be Go time. But for this project, all I had to do was install nginx on the raspberry pi and configure /etc/nginx/sites-available/default.

ufw

The operating system on my raspberry pi is debian, so I use ufw for the firewall. I ran a few command line arguments for the setup.

sudo ufw default deny incoming
sudo ufw default allow outgoing

sudo ufw allow from 192.168.0.0/24 to any port 22
sudo ufw allow 80
sudo ufw allow 443

Port Forwarding

After setting up the firewall with ufw, I had to configure the ports through my router. When I was at this stage, I thought I was nearly done. I was also afflicted with a sense of dread. Was I just providing a convenient backdoor for hackers? I still have this sense of dread, but the best way to learn is the hard way.

Static IP

I configured my router so that the pi always had the same ip. This is where I learned about local and public IPs. Of course, this learning involved failure and frustration.

In my local network, the pi is always 192.168.0.33. Previously, I achieved this by editing /etc/dhcpcd.conf. However, I learned that things had changed after I updated debian to trixie. Trixie uses NetworkManager so my previous method of editing the dhcpcd file was useless. One of my options was to use nmcli to set a static local IP.

Or, I could use a more reliable method by configuring my router some more. After finding my pi's mac address, I used it to set up a DHCP reservation through my router. My previous configuration relied on the pi to coordinate with the router to reserve an IP. Now, the router knows to reserve an IP for the pi and does all the work.

Porkbun DDNS

So I thought that was it. All I needed to do was purchase a domain from porkbun, and point it to my pi. This is when I learned about local and public IPs. The configuration I had just done only set a static IP for my local network. The domain I was about to purchase would serve as a pointer to my public IP. If my public IP is constantly changing, then my domain would end up pointing to the old IP. While I had set a static IP for my local network, Xfinity (my ISP) controls my public IP. I learned they don't provide static public IPs for residential plans.

Some solutions to this problem are cloudflare and duckdns. The solution this blog uses is ddns through porkbun. Initially, I was going to use a python script and use systemd to periodically update my public IP with porkbun if it ever changed. I again chose the lazy route and used ddclient. This wasn't a painless process. My llm friends and I had to edit a file to finally get ddclient set up. Long story short, I had to edit a url in the source code:

sudo sed -i 's|https://porkbun.com/api/json/v3|https://api.porkbun.com/api/json/v3|g' /usr/bin/ddclient

Finally, I set up Certbot to automate the HTTPS certificate renewal process. This whole process showed my how powerful grep and sed can be. I need to read the man pages and start using them more. It also showed me that I have so much to learn.

Too long, didn't read

  1. Create Emacs org-publish configuration
  2. Use something like rsync to transfer your local blog files
  3. Use something like a raspberry pi to recieve the blog files
  4. Set up nginx where your blog files are hosted
  5. Set firewall permissions for port 80 and 443
  6. Set up a dhcp reservation through your router for the machine hosting your blog
  7. Set up a dynamic dns solution using something like ddclient

Going Forward

I'm not sure there will be any more blogs. This could be it. I have a lot of projects planned, so I hope it leads to a lot of writing. For now, hello world.