Static Personal Website with Org Mode
Static Personal Website with Org Mode
Created:
Abstract
In this article I will show you how I built this site using:
You can find the source code of this madness in here.
As you can see it is hosted into Github pages, therefore it's a static webpage. However, we can get some backend features using Org and Elisp. Of course, it's anything like a project who has a real backend available, and some times we would need to do some proper hacks.
Keep reading to discover the pitfalls and advantages of this approach.
Content
In this section, I will run you through the main features of the site, inserting links to the code where necessary.
Features
Autogenerated HTML and Org-mode typing comfort
One of the biggest advantages of choosing org-mode as a technology is that you will get access to the comfort of writing a blog using a markdown language. Then, it looks clean and comes with a lot of features (integrated into Emacs):
- It's easy to manipulate: tables, dates, links
- Organize your text in sections
- Create lists like this one and so on
Basically you can do what you could do in a .md
file and
more.
That's great!!! But what do you want at the end of the day is the well-known trinity: HTML, CSS, JS. So, we need a way to generate at least HTML from that. Luckily, org-mode comes with a native export system to HTML, with its own CSS too! However, the outcome is far from being modern.
How to make things better? For sure I don't want to mess up with the CSS: I'm not a frontend dev, plus I learned a long time ago the pain of web designing. The only other option is to steal it! Fortunately, I have a go-to option: Bootstrap.
Anyway, Bootstrap comes with its own set of rules and structure.
Aligning the native autogenerated HTML code to that is still
very hard, but apparently someone else had my same idea and he
created an org extension that generates a Bootstrap-friendly HTML
output. This extension is called ox-twbs and it provides a
command (org-twbs-export-to-html
) to compile the org to HTML.
In this way I'm writing Org text right now and what you see it's the final result. I got the responsiveness and all the styling (almost) for free, icons included (+ fontawesome for the homepage).
Article List and Blog Dynamic Content
I already told you that this site is static, then how can we achieve some dynamic behavior?
The idea here is to take advantage of the org compilation to execute some Elisp code (call it server-side if you wish). This code will create some Js to be embedded into the page itself to serve as data. Later, on the javascript client-side, we will use those variables to render the data into the page.
Using this method we can access other HTML files, such as this article page, manipulate their content, and generate our data for the targeted page. Two examples are The Blog page and the Article List page.
Both occasions, there are two Js arrays, called
htmlArticles
and htmlArticlesPaths
. The former contains a
subset of the HTML structure (Title and Abstract) of the
Articles, and the latter contains the relative path of them.
Starting from those arrays we can zip it and create the list of
articles: minimal in the case of the Article List page and with
some preview in the case of the Blog page.
In addition, in order to achieve some simple sorting, we just need to store the date into the articles filenames, as you can see directly from the URL of this page.
What described before is achieved by the Elisp code into the file templates.org(omitted for brevity) and then integrated into the page in this way:
html: <ul id="pagination" class="pagination pagination-lg"></ul>
After that, we need to add some custom js to treat those variables and populate the empty article tags:
// Pagination 0 based const htmlArticlesPaginated = htmlArticles.slice(page, page + perPage); const htmlArticlesPathsPaginated = htmlArticlesPaths.slice(page, page + perPage); var articleDivs = $("[data-include]").map(function() { return this.id; }); var articlesZip = []; var articlesElementsZip = []; for (var i = 0; i < htmlArticlesPaginated.length; i++) { articlesZip.push([htmlArticlesPaginated[i], htmlArticlesPathsPaginated[i]]); } var htmlArticlesTitle = articlesZip.map(function(tuple) { const [articleContent, articlePath] = tuple; return $($.parseHTML(articleContent)).find("#Article").wrap(function (){ return "<a href='" + articlePath + "'></a>" }).parent(); }); var htmlArticlesAbstract = htmlArticlesPaginated.map(function(articleContent) { return $($.parseHTML(articleContent)).find("#outline-container-ArticleAbstract"); }); for (var i = 0; i < htmlArticlesPaginated.length; i++) { articlesElementsZip.push([articleDivs[i], htmlArticlesTitle[i], htmlArticlesAbstract[i]]); } articlesElementsZip.forEach(function(tuple) { const [element, title, abstract] = tuple; $("#" + element).html($('<div>').append(title).append(abstract)); }); #+end_src #+call: templates.org:inline-js(blk="blog_populateArticles")
Obviously, all of this came at a cost, in particular:
- In case of an article addition/deletion/renaming, we need to recompile both pages. This could be a nasty problem if we would need to add multiple dynamic behaviors to several pages. A possible workaround could be to trigger the whole site compilation from a single page, like the root page of the site. It might still take quite some time and this leads to the next point.
- Not scalable compilation time: if we are going to have hundreds of articles it might take a long time and space (size of js variables). A workaround could be to split the articles into multiple folders, by year or with an archive folder, then keep the dynamic nature only to a subset of the articles.
Pagination
Pagination is no different from the previous section, in fact, It takes advantage of the same data.
If you take a look closely at the previous snippet you will see
two additional variables, called page
and perPage
, used to
slice the starting data arrays to the specific selected
page. Both of those variables are defined in another snippet, in
particular, the page
is selected from the URL query string,
meanwhile, the perPage
is fixed to 5.
Here you can see the complete js code snippet for the pagination:
const page = ((new URLSearchParams(window.location.search).get('page') || 1) - 1) * 5; const perPage = 5; for (var i = 0; i < (htmlArticles.length/perPage); i++) { var active = ""; if ((page/perPage) == i) { active = "class='active'"; } $("#pagination").append( '<li ' + active + '><a href="' + window.location.href.split('?')[0] + '?page=' + (i+1) + '">' + (i+1) + '</a></li>' ); } SRC CALL: templates.org:inline-js(blk="pagination")
Conclusions
In conclusion, DON'T DO WHAT I HAVE DONE. I'm sure there are several solutions that are more convenient than the one I implemented here, like Wordpress, Blogspot, Blogger…
Anyway, even if I have to re-implement several well-known frameworks, I also gain these benefits:
- I have the chance to learn org better
- I am free from a specific framework
- It's fun (for me)