Emacs CMS

Using Emacs as a CMS with a simple Node application using HTML templates and HTMX

Emacs CMS

Published: Fri, 02 Feb 2024 09:47:54

New Implementation

The following in this blog all remains the case for the current production version of the blog, but I have recently started a new version using Bun in Place of Node and I am using TypeScript in place of vanilla JS. Instead of using Express, I am creating a new API from scratch with the built in Bun.serve(). I also decided to replace squirrelly templates with mustache.js templates to try a different approach to HTML templates. For now I think I will stick with using Cheerio to parse the HTML for data, but perhaps I will switch that out as well as the project moves forward. I am also adding some better logic for dealing with images so I can handle different file formats. So far I am happy with this new implementation. Using Bun's built in method for sending and receiving HTTP requests is much more in keeping with the overall theme of simplicity I am trying to go for with this project. I will probably switch to this newer version when its done.

Emacs Org Mode as a CMS

I played around with a few ideas of how I would make this blog, but finally settled on a workflow where I could upload my org files to a remote server using a simple Node app, HTMX, and functionality that comes built into Emacs. I organize my notes with Denote.el, so I settled on a setup where I didn't need to leave my editor to update my blog. I will be much more liable to keep my projects blog up to date if I have an easily accessible workflow. Here is a basic outline of the steps I took to get this up and running excluding the setup of the sever itself (I used Digital Ocean and Nginx, but any VPS will do fine). The full code is here on my GitHub account.

Export Org files to the remote server

First I save the file to a blog directory on my local file system and use rsync with OK-IF-ALREADY-EXISTS set to t in order to update the blog post on my remote file system. I just export the body without any head section or doctype. If this is a new blog, I put a thumbnail for it in the blog/images directory which will be updated when I run the function.

     (defun nao/export-org-to-blog ()
      (interactive)
       (setq org-html-doctype "html5")
      (let ((filename (org-html-export-to-html nil nil nil t)))
        (copy-file (concat (file-name-sans-extension filename) ".png")
                   "/rsync:server:/path/to/images" t)
        (copy-file filename "/rsync:server/path/to/html" t)))

In order to get the file to save with the correct name, I manually set the file name using EXPORT_FILE_NAME like so #+EXPORT_FILE_NAME: ~/Documents/denote/blog/programming-blog.html. That's it on the Emacs side, not much to it.

Server side

Server side I use a simple Node app that uses Express to create a REST API that grabs the filenames, which are already in slug format, to create the list of links for the blog posts. The if/blocks are used to either make the response the full page if they navigate form outside the website, or just the content if they use internal navigation.

app.get('/blog/*', async function (req, res) {
  const slug = req.params[0];
  if (req.headers["hx-request"] === "true") {
    const path = '/path/to/blog/'
    const data = await fs.readFile(path+slug, 'utf8')
    res.send(data);
  } else {
    const route = [slug]

    const data = {
      route: route,
    }

    const html = Sqrl.render(newRoute(), data);
    res.send(html);
  }
})

I use the squirrel templates to create the different content such as the lists of links and cards. For example, you can set up links like this:

<nav>
<h2 class="post-header">Projects</h2>
    <ul>
        {{@each(it.files) => val, index}}
        <li>
            <a href="#" hx-push-url="true" hx-get="a-domain.com/blog/{{val}}" hx-target="#contentDiv" hx-swap="innerHTML">
            {{it.labels[index]}}
            </a>
        </li>
        {{/each}}
    </ul>
</nav>

This will create a list of links from the filenames and then when the links are clicked, they will swap with the content that is currently in the #contentDiv.

Images and Summaries for cards

In order to get a summary for each file I add a <p> tag with the summary class at the top of my org file and then I use cheerio to parse the html and pull out the summary text. I then just use a CSS display: none; so the summary doesn't appear in the actual blog post. To get the image I simply split the filename and replace .html with .png. If you wanted to use multiple file types it would be simple enough to create a more elaborate solution, but my screenshot app defaults to png, which is acceptable for the purposes of a simple blog.

  #+BEGIN_summary
  Using Emacs as a CMS with a simple Node application using HTML templates and HTMX
  #+END_summary

  const files = await fs.readdir(path);

  for (file of files) {
    const img = file.split(".");
    const data = await fs.readFile(path+file, 'utf8');
    links.push(file);
    titles.push(makeTitle(file));
    images.push(img[0] + ".png")
    const $ = cheerio.load(data);
    summaries.push($('div.summary p').text());
  }

Problems with this Workflow

The major issue with this workflow is that I don't have a lot of control the HTML that org export creates. It is possible to customize it further, but it requires quite a bit of customization. As is stands you would most likely need to write custom CSS to target the classes that org export creates, which are named well and extensive enough to do what you need. If you want to make use of bootstrap or another CSS framework, there are probably much easier ways to produce a blog than this, but if you don't mind writing CSS, its a pretty simple elegant solution.

Future plans for the Project

Aside from tweaking the CSS to get it work better on different screen sizes and improve the overall look and feel, I would like to add some logic to add a default thumbnail if I don't specific one by putting it in the blog directory. As the blog grows I might also try to implement a directory structure that would be reflected on the UI by nesting the posts under categories, JavaScript, Rust, Ideas, etc.

System Crafters Web Ring

Messing around with computers and coding since I was 8. Now getting paid to do what I love.