<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>emre şahin's digital garden 🍃 - static site generator</title>
    <link>https://emresahin.net/tags/static-site-generator/</link>
    <description>Posts in the static site generator tag</description>
    <language>en</language>
    <managingEditor>contact@emresahin.net (Emre Şahin)</managingEditor>
    <lastBuildDate>Tue, 15 Sep 2026 19:46:32 +0000</lastBuildDate>
    <atom:link href="https://emresahin.net/tags/static-site-generator/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>devlog 22</title>
      <published>2025-02-03T10:24:41+00:00</published>
      <updated>2025-02-03T10:24:41+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 03 Feb 2025 10:24:41 +0000</pubDate>
      <link>https://emresahin.net/devlog-22/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-22/</guid>
      <description>🐢 We need to find a theme for the blog. The current one breaks Nushell output tables because the code blocks are too narrow. 🐇 https://www.getzola.org/themes/pico/ is an option, but I don’t like its header. 🦊 Minimal Dark from the same author looks better: https://kuznetsov17.github.io/minimal-da...</description>
      <category>Digital Garden</category>
      <category>Development</category>
      <category>Theme</category>
      <category>Zola</category>
      <category>Anemone</category>
      <category>mdBook</category>
      <category>Nushell</category>
      <category>Netlify</category>
      <category>Syntax Highlighting</category>
      <category>Static Site Generator</category>
      <content:encoded><![CDATA[<p>🐢 We need to find a theme for the blog. The <a href="https://www.getzola.org/themes/anemone/">current
one</a> breaks Nushell output tables
because the code blocks are too narrow.</p>
<p>🐇 https://www.getzola.org/themes/pico/ is an option, but I don’t like its header.</p>
<p>🦊 Minimal Dark from the same author looks better: https://kuznetsov17.github.io/minimal-dark/notes/note1/</p>
<p>🐇 https://www.getzola.org/themes/no-style-please/ is also an option.</p>
<p>🦊 https://halve-z.netlify.app/posts/information/ looks interesting, but there is too much screen estate for the left bar.</p>
<p>🐢 Let’s start by running the site locally first.</p>
<p>🐇 There are errors in the configuration. That’s weird, but let’s fix these.</p>
<p>🐢 Fixed errors. These are probably related to a newer version. Can we take a look at <code>netlify.toml</code> to see if it downloads the same version?</p>
<p>🐇 There are breaking changes in Zola 0.19. Let’s update and push the Netlify config to see the results.</p>
<p>🐢 We need to add language support for Nushell to prevent warnings. Take a look at how to add a syntax file to Zola.</p>
<p>🐇 I added</p>
<pre><code class="language-toml">extra_syntaxes_and_themes = ["syntaxes"]
</code></pre>
<p>to the <code>[markdown]</code> section and added a <code>syntaxes/nushell.sublime-syntax</code> file copied from https://github.com/kurokirasama/nushell_sublime_syntax. I’m getting:</p>
<pre><code>Error: Reason: Error while compiling regex '\b(?x: 7z | ?
...
s-to-gdrive | usage | ver | verify | weather | wget-all | which-cd | wifi-info | wifi-pass | xls2csv | ydx | yt-api | ytcli | ytm | z | zi)\b'
Oniguruma error: target of repeat operator is not specified
</code></pre>
<p>🐢 The syntax highlighter may be a bit buggy. Let’s try to fix this if it’s a one-off.</p>
<p>🐇 Found the bug. There is a <code>?</code> in the regex that causes it to fail. Now it compiles, and Nushell blocks are colored.</p>
<p>🐢 Cool. Let’s fix the other warnings now. <code>shell</code> and <code>console</code> are not recognized, it looks like.</p>
<p>🐇 There is only Bash listed in https://www.getzola.org/documentation/content/syntax-highlighting/.</p>
<p>🦁 What do you think about migrating to mdBook? We already maintain mdBook for XVC; what about just moving the site to mdBook?</p>
<p>🐢 I thought about this before, and the only downside is the lack of an RSS feed.</p>
<p>🦊 I found this: https://github.com/theowenyoung/mdbook-rss</p>
<p>🐢 Now, this changes everything. We can even move the <code>nedriy.at</code> site to mdBook in this case.</p>
<p>🐇 Then, let’s start working on this. The site will be a technical book site in this case.</p>
<p>🐢 Does mdBook support Nushell syntax?</p>
<p>🐇 Nushell is not in the <a href="https://rust-lang.github.io/mdBook/format/theme/syntax-highlighting.html">listed languages</a>. mdBook uses <a href="https://highlightjs.org/">highlight.js</a>, and in its <a href="https://highlightjs.readthedocs.io/en/latest/supported-languages.html">listed languages</a>, we don’t find Nu either.</p>
<p>🐢 We already added Nu support to Zola, and we can just change the theme. This is a blocker in my opinion.</p>
<p>🐇 I searched for Nushell highlight.js support, and nothing appears. I think we can just postpone until Nu has more support on this front.</p>
<p>🐢 Yes. Let’s first try this change in the non-technical blog, and we can come back to this issue. Now, we’ll update <code>console</code> and <code>shell</code> to <code>bash</code>, I think.</p>
<p>🐇 Replaced <code>shell</code> and <code>console</code> with <code>bash</code>.</p>
<p>🐢 There is a file for <code>ggplot</code> that has warnings from earlier incarnations. We also lack syntax highlighters for Vim and Tmux.</p>
<p>🐇 There is a <code>sublime-syntax</code> file for Tmux at https://raw.githubusercontent.com/gerardroche/sublime-tmux/refs/heads/master/Tmux.sublime-syntax, but do we need it for a single file?</p>
<p>🐢 Let’s set it to plain text.</p>
<p>🐇 Now we only have Mermaid warnings left.</p>
<p>🐢 There should be a diagram at https://emresahin.net/developing-a-gitignore-crate/, but it doesn’t show up. We need a shortcode to show these, like the YouTube shortcode. Now let’s get back to theme selection.</p>
<p>🐇 I tested Karzok, but it doesn’t have category and tags support.</p>
<p>🐢 And I tested https://github.com/micahkepe/radion, but the best so far is the <code>apollo</code> theme. I’m struggling to modify the index page, though. I forgot that I modified the theme’s <code>index.html</code> file. I have content in <code>/content/_index.md</code> and a modified <code>index.html</code> in <code>/themes/anemone/templates/index.html</code> to show the content, tags, categories, etc. It should be fixed now.</p>
<p>🐇 Ah, cool. Can we clean the recent duplicate pages now?</p>
<p>🐢 Yeah, let’s take a look.</p>]]></content:encoded>
    </item>
    <item>
      <title>This Site's RSS Generator</title>
      <published>2013-04-22T21:13:06+00:00</published>
      <updated>2013-04-22T21:13:06+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 22 Apr 2013 21:13:06 +0000</pubDate>
      <link>https://emresahin.net/this-sites-rss-generator/</link>
      <guid isPermaLink="true">https://emresahin.net/this-sites-rss-generator/</guid>
      <description>This is an ancient post from 2013. I’m not using any of these now. Previously with Pandoc , I was using a simple setup to create RSS feeds. Markdown files were converted to plain, headerless HTML, and they were collected together to build an XML file. The obvious drawback is that all HTML files s...</description>
      <category>Software Development</category>
      <category>Python</category>
      <category>RSS</category>
      <category>automation</category>
      <category>static site generator</category>
      <category>web development</category>
      <content:encoded><![CDATA[<p><strong>This is an ancient post from 2013. I’m not using any of these now.</strong></p>
<p>Previously with <a href="http://johnmacfarlane.net/pandoc/">Pandoc</a>, I was using a simple setup to create RSS feeds. <code>Markdown</code> files were converted to plain, headerless HTML, and they were collected together to build an XML file. The obvious drawback is that all HTML files should be generated by Pandoc; anything that doesn’t fit that route does not appear in the feeds.</p>
<p>However, when I began to use <a href="http://orgmode.org">Org Mode</a> for data analysis and other tasks, I stopped using Pandoc. Org Mode has extensive facilities for exporting into HTML and other document formats, so I would not mess with Pandoc for this.</p>
<p>I thought RSS could be produced by parsing HTML files after they are produced. This requires parsing the HTML file, but it’s simple, and there are parsers for all programming languages out there. My previous RSS generator was in Python, and I decided to modify it to fit my needs. I think producing RSS for a static HTML site is a common need, and I tried to solve this problem as simply as possible.</p>
<p>Let’s begin with the ubiquitous shebang line. This tells the system that the script is in Python.</p>
<pre><code class="language-python">#!/usr/bin/env python
</code></pre>
<p>The following are the imports for this script. Apart from <a href="http://www.dalkescientific.com/Python/PyRSS2Gen.html">PyRSS2Gen</a>, all modules are present in Python 2.7.</p>
<pre><code class="language-python">import argparse
import codecs
import os
import datetime
from HTMLParser import HTMLParser
import PyRSS2Gen as rssgen
import operator as op
import re
import subprocess as proc
</code></pre>
<p>I use <a href="https://mercurial.selenic.com">Mercurial</a> to track the site’s files. I once thought about using the Mercurial public API to check the status of files, but it proved to be overkill because only the modification time of files is necessary, and retrieving them using a standard command-line call is much simpler. Hence, I removed the following imports for the time being.</p>
<pre><code class="language-python"># from mercurial import commands as cmd
# from mercurial import hg
# from mercurial import ui as hgui
</code></pre>
<p>The following function returns a valid HTML tag string, given the tag and its attributes in a list. <code>HTMLParser</code> sends the tags in a list form, and I use this function to reconvert them to usual HTML tags.</p>
<pre><code class="language-python">def make_tag(tag, attrs):
    content_list = [ tag ]
    content_list += [ "%s=\"%s\"" % (k, v) for (k, v) in attrs]
    return "&lt;" + " ".join(content_list) + "&gt;"
</code></pre>
<p><code>TitleBodyExtractor</code> is an <code>HTMLParser</code> subclass. It collects the body of a page in a string and also keeps the title. These two are the only requirements. It might be possible to parse meta tags to get publish date and author information as well, but I prefer to keep simple things simple.</p>
<pre><code class="language-python">class TitleBodyExtractor(HTMLParser):

    def __init__(self):
        HTMLParser.__init__(self)
        self.in_body = False
        self.in_title = False
        self.body = ""
        self.title = ""


    def handle_data(self, data):
        if self.in_body:
            self.body += data
        if self.in_title:
            self.title += data

    def handle_starttag(self, tag, attrs):
        if self.in_body:
            self.body += make_tag(tag, attrs)

        if tag == "body":
            self.in_body = True
        if tag == "title":
            self.in_title = True

    def handle_endtag(self, tag):
        if tag == "body":
            self.in_body = False

        if tag == "title":
            self.in_title = False

        if self.in_body:
            self.body += "&lt;/%s&gt;" % (tag)
</code></pre>
<p>Getting contents of a file in <em>UTF-8</em> encoding is a common task. The following two functions retrieve and store the contents in UTF-8 using the <code>codecs</code> module.</p>
<pre><code class="language-python">def get_content(filename):
    f = codecs.open(filename, "r", "utf-8")
    cont = f.read()
    f.close()
    return cont

def write_content(filename, content):
    f = codecs.open(filename, "w", "utf-8")
    f.write(content)
    f.close()
</code></pre>
<p>Mercurial allows running commands for a repository outside of that repository with the <code>-R</code> command-line switch. However, it requires the exact path of the repository and does not accept a child path. The following function finds the repository path of a file by recursively checking whether parent paths contain an <code>.hg/</code> directory.</p>
<pre><code class="language-python">def get_repo_path(dir):
    if dir == "/" or dir == "":
        return ""
    if os.path.exists(os.path.join(dir, ".hg")):
        return dir
    else:
        return get_repo_path(os.path.dirname(dir))
</code></pre>
<p>The <code>FileObject</code> class keeps the required data of an HTML file. It stores the path, modification time, body, and title.</p>
<pre><code class="language-python">class FileObject:
    def __init__(self, path, mtime):
        self.path = path
        self.mtime = int(mtime)
        self._body = ""
        self._title = ""

    def parse(self):
        content = get_content(self.path)
        tbe = TitleBodyExtractor()
        tbe.feed(content)
        self._body = tbe.body
        self._title = tbe.title

    def body(self):
        if self._body == "":
            self.parse()
        return self._body

    def title(self):
        if self._title == "":
            self.parse()
        return self._title

    def __str__(self):
        return str(self.path) + " " + str(self.mtime)
</code></pre>
<p>The modification time of a file should be retrieved from the Mercurial repository. The following function calls <code>hg log</code> with a specific template, then parses the date to get the last commit time of a file. If the file is not registered to a repository, it simply returns the filesystem modification time.</p>
<pre><code class="language-python">def get_mtime(full_path):
    if os.path.exists(full_path):
        repo_path = get_repo_path(full_path)
        if repo_path != "":
            logcmd = "/usr/bin/hg log -R %s --template='{date|hgdate}' -l 1 %s " % (repo_path, full_path)
            # print logcmd

            proc_res = proc.check_output(logcmd, shell=True).split()
            if len(proc_res) &gt; 0:
                filetime = int(proc_res[0])
            else:
                filetime = os.path.getmtime(full_path)
        else:
            filetime = os.path.getmtime(full_path)
        return filetime
    else:
        return 0
</code></pre>
<p>We need a list of files as <code>FileObject</code> objects, given the directory name and extension. The function also takes a repository path and excludes filenames that match a given regex.</p>
<pre><code class="language-python">
    def file_list(dirname, extension, repo_path, exclude_regex=None):
        results = []
        for root, dirs, files in os.walk(dirname):
            # print "Dirs:", dirs
            for d in dirs:
                if exclude_regex == None or (not re.match(exclude_regex, d)):
                    results += file_list(os.path.join(root, d), extension, repo_path)
                else:
                    print "Skipping", d
            # print "Files:", files
            for f in files:
                if (exclude_regex == None or (not re.match(exclude_regex, f))) and f.endswith(extension):
                    fullname = os.path.join(root, f)
                    filetime = get_mtime(fullname)
                    results.append(FileObject(fullname, filetime))
        return results
</code></pre>
<p>Given a local file in the site, we need to create a link that shows the URL of that file relative to the site’s URL. The following function finds the relative path with respect to an input directory and returns the complete URL by concatenating it to the site’s URL.</p>
<pre><code class="language-python">
    def make_link(site_url, input_dir, file_path):
        rel_path = os.path.relpath(file_path, input_dir)
        return site_url + rel_path
</code></pre>
<p>Given a <code>FileObject</code> that points to an HTML file, we need a function that builds an RSS item from it. It obtains the URL of the file and fills the rest using the attributes of the <code>FileObject</code>.</p>
<pre><code class="language-python">
    def get_rss_item(file_object, input_dir, site_url):
        the_link = make_link(site_url, input_dir, file_object.path)
        item = rssgen.RSSItem(title=file_object.title(),
                              link=the_link,
                              description=file_object.body(),
                              guid=rssgen.Guid(the_link),
                              pubDate=datetime.datetime.fromtimestamp(file_object.mtime))
        return item
</code></pre>
<p>The function that creates an RSS file from the files in a given directory is the topmost function. It takes all variables that are set from the command line and returns the RSS object.</p>
<p>It first lists all files with the given extension (default being <code>.html</code>) in the input directory. Then it compares the modification time of the RSS file with the modification times of these listed files. If there is no previous RSS file or there are newer HTML files, the RSS is generated again.</p>
<p>Note that a certain amount of <em>edit time</em> can be set, so that the script doesn’t consider files as <em>new</em> if they are modified within <em>edit time</em> minutes. This can be set to prevent too frequent generation of files during an edit session.</p>
<p>To generate the RSS, each file is supplied to the previous function and an RSS item is obtained; then these are fed into the <code>RSS2</code> function of <code>PyRSS2Gen</code> to get the resulting object.</p>
<pre><code class="language-python">def generate_rss(input_dir, extension = ".html", output = "rss/rss.xml", site_title = "Title", site_description = "Description", max_items = 20, site_url = "http://example.com", edit_time=0, exclude_regex = None):
        if not site_url.endswith("/"):
            site_url += "/"
        files = file_list(input_dir, extension, get_repo_path(input_dir), exclude_regex)
        rssmtime = get_mtime(output)
        files_up = [f for f in files if f.mtime &gt;= (rssmtime + edit_time)]
        if len(files_up) &gt; 0:
            print "Before", files_up
            files_up.sort(key=lambda x: x.mtime, reverse=True)
            print "After", files_up
            rss_items = [get_rss_item(fo, input_dir, site_url) for fo in files_up[:max_items]]
            rssobj = rssgen.RSS2(title = site_title,
                                 link = site_url,
                                 description = site_description,
                                 lastBuildDate = datetime.datetime.now(),
                                 items = rss_items)
            return rssobj
        return None
</code></pre>
<p>The main function uses <code>argparse</code> to handle options. The input directory, site title, site URL, and number of items are mandatory; other options have sensible default values.</p>
<p>The function also builds the <code>exclude_regex</code> object to supply to file listings. The regex is built here from the supplied string, and all other functions use this compiled regex.</p>
<p>After generating the RSS, it writes the file with the <code>write_xml</code> function.</p>
<pre><code class="language-python">
    def main():

        parser = argparse.ArgumentParser(description='Generate RSS feed from a set of HTML files')

        parser.add_argument('--input-dir', help="input directory", required=True)
        parser.add_argument("--extension", help="file extension to collect", default=".html")
        parser.add_argument("--output", help="output filename to write the results", default="rss/rss.xml")
        parser.add_argument('--title', help="title of the RSS feed", required=True)
        parser.add_argument("--description", help="site description", default="")
        parser.add_argument("--items", help="max items included in the feed", required=True, type=int)
        parser.add_argument("--site-url", help="site url of items", required=True)
        parser.add_argument("--exclude-regex", help="regex to set skipped files", default="")
        parser.add_argument("--edit-time", help="minutes to wait before putting an item into rss", default=0, type=int)

        args = vars(parser.parse_args())

        if args["exclude_regex"] == "":
            exclude_regex = None
        else:
            exclude_regex = re.compile(args["exclude_regex"])

        rssresults = generate_rss(args["input_dir"],
                                  args["extension"],
                                  args["output"],
                                  args["title"],
                                  args["description"],
                                  args["items"],
                                  args["site_url"],
                                  args["edit_time"],
                                  exclude_regex)

        if rssresults != None:
            rssresults.write_xml(open(args["output"], "w"))



    if __name__ == "__main__":
        main()
</code></pre>
<p>You can get the resulting Python script from <code>rss-generator.py</code>.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
