<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>vmac.ch (full articles only)</title><link>https://vmac.ch/</link><description>Recent articles on vmac.ch</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 24 Apr 2026 09:22:28 +0200</lastBuildDate><atom:link href="https://vmac.ch/index.xml" rel="self" type="application/rss+xml"/><item><title>How to slim down your Hugo homepage into an archive and a live page</title><link>https://vmac.ch/posts/2026-04-04-split-hugo-homepage/</link><pubDate>Sat, 04 Apr 2026 16:40:00 +0200</pubDate><guid>https://vmac.ch/posts/2026-04-04-split-hugo-homepage/</guid><description>&lt;p>My homepage deploy script stopped working at the end of January. The Hugo content folder is now over 4 GB in size and this causes quite long build times. But the bigger issue is the checkout time and the actual time required to deploy the final page after the GitHub Action build has completed. The whole issue is made more complicated as I’m unable to run Hugo directly on my webhost. So each time I trigger a new build the build process copies the entire public folder, which takes up to 30 minutes. And then it often runs into a caching or timeout issue on my webhost, leading to the page not updating for up to 12 hours. I never found out what the actual issue was. So it was time to find a solution for these problems. I had already run into this situation a year ago and managed to patch it, but I was never happy with the overall build process. This time I want to fix the problems once and for all.&lt;/p>
&lt;p>For this I need to fix the following issues:&lt;/p>
&lt;ul>
&lt;li>Drastically reduce repository/content folder size and thereby shorten build and deploy times&lt;/li>
&lt;li>Fix the timeout/caching on deploy, so the builds are stable again&lt;/li>
&lt;li>Speed up the deploy step: Ideally only upload what is new&lt;/li>
&lt;/ul>
&lt;p>One obvious solution for my webhost would be to not use Hugo at all. I learned that Hugo works best when the build happens on the same system where the webserver lives. But that was not an option I was interested in. I like the Hugo setup, and I did not want to migrate the full site with all posts.&lt;/p>
&lt;h2 id="split-the-hugo-site-into-archive-and-main">Split the Hugo Site into Archive and Main&lt;/h2>
&lt;p>Having already tried to optimize the size of the content folder, I knew this would not help much. I could gain some time by doing another slimming round, but since it only took around eight months to end up in the same situation again, this would not be a long-term solution. If I invest the effort again, I want it to result in something sustainable.&lt;/p>
&lt;p>So the solution I came to was to split my page into two Hugo instances. This is not a process Hugo supports out of the box, but the structure of my page made it possible to implement.&lt;/p>
&lt;p>I kept the old repository (the big one) and it is now the home of my archive-page Hugo instance. And I created a new repository and Hugo instance which contains the active part of my homepage (main-page). I only ever post to the &lt;em>main-page&lt;/em> going forward. So there are no build scripts needed for the &lt;em>archive-page&lt;/em>, so I moved the GitHub Actions over to the new repository.&lt;/p>
&lt;p>&lt;img src="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/folder-structure.png" alt="">&lt;/p>
&lt;p>I implemented the following changes in both Hugo instances but the &lt;em>archive-page&lt;/em> was only built and uploaded once to the webhost. And the assumption is that the new instance can override all the index pages and if not the webserver will serve the one from the initial archive build.&lt;/p>
&lt;h3 id="pagination">Pagination&lt;/h3>
&lt;p>For this new setup to work correctly I could no longer use Hugo’s native pagination. The native pagination just splits after every N posts, and this causes the posts to move to different pages each time I post anything new. The main requirement in pagination I needed for the new system is that the generated pages are stable over time / new builds. So I can build the pages for the archive once and later just add on for the new posts. Never needing to change or overwrite the older pages.&lt;/p>
&lt;p>For this I switched to a date based pagination system (one page per month). My homepage now shows the last 20 posts by default (configurable via a custom config &lt;code>archive_rolling_window&lt;/code>) and after that we get one page per month for which we have posts. And to not have a totally empty index page on my blog I moved the 20 newest posts from the archive page into the new setup. So the split is not directly on the end of 2025.&lt;/p>
&lt;p>To correctly create the archive page I needed to add a new category to the content directory in the &lt;em>archive-page&lt;/em> only. I generated this folder structure once, one &lt;code>_index.md&lt;/code> file per month for which we have posts. And a special layout which then renders the archive months overview pages. This step was needed as there is no simple way to output multiple page structures from a single Hugo category and I did not want to destroy the regular posts rendering in the &lt;em>archive-page&lt;/em>.&lt;/p>
&lt;p>&lt;img src="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/archive.png" alt="">&lt;/p>
&lt;p>The partial renders month-based archive pagination with a fixed sliding window of five entries. It builds a list of months from archived content, validates them against existing pages, and sorts them in descending order. It determines the current month from the URL and centers the navigation window around it. Additionally, it provides navigation controls to jump to the newest (HOME), previous, next, and oldest archive entries.&lt;/p>
&lt;p>&lt;img src="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/pagination-control.png" alt="">&lt;/p>
&lt;p>Here is an example on how the pagination partial is used (in this case for the &lt;code>articles&lt;/code> index page):&lt;/p>
&lt;pre>
&lt;code class="language-gohtml">&amp;lt;section&amp;gt;
{{ partial "archive-month-navigation.html" (dict "archived" $archived "rollingWindow" $rollingWindow "context" . "basePath" "archive/articles" "homePath" "/articles.html" "extraMonths" (slice
"2023/12"
"2023/11"
"2023/10"
"2023/09"
"2023/08"
"2023/07"
"2023/06"
)) }}
&amp;lt;/section&amp;gt;&lt;/code>
&lt;/pre>
&lt;p>The implementation handles sparse data, missing months, boundary conditions, and even allows virtual months via its configuration which is used to preseed it with data in cases where no posts are present in the &lt;em>main-page&lt;/em> and all data is in the &lt;em>archive-page&lt;/em> only. You can take a look at the full partial here: &lt;a href="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/archive-month-navigation.html.txt">archive-month-navigation.html&lt;/a>.&lt;/p>
&lt;h3 id="rel-url-shortcode">Rel URL Shortcode&lt;/h3>
&lt;p>The next problem to fix was my usage of the &lt;code>rel&lt;/code>-url-shortcode in the &lt;em>main-page&lt;/em>. This was relevant for my special pages like &lt;code>start-here&lt;/code> or the &lt;code>about&lt;/code> page. As not all posts I want to link to are present in the source now, the native shortcode fails (on build) as it is unable to find the reference. So I create my own shortcode which checks if it can use the &lt;code>rel&lt;/code> version or should just use the absolute url instead. The new shortcode is called &lt;code>smartref&lt;/code> and is a drop in replacement for the hugo one.&lt;/p>
&lt;pre>
&lt;code class="language-gohtml">{{- $archiveCutoff := site.Params.archiveCutoff | default 2026 -}}
{{- $input := .Get 0 -}}
{{- /* Normalize path */ -}}
{{- $path := strings.TrimPrefix "/" $input -}}
{{- $path = strings.TrimSuffix ".markdown" $path -}}
{{- $path = strings.TrimSuffix ".md" $path -}}
{{- /* Extract year from posts/YYYY/... */ -}}
{{- $parts := split $path "/" -}}
{{- $year := 0 -}}
{{- if ge (len $parts) 2 -}}
{{- $year = index $parts 1 | int -}}
{{- end -}}
{{- if ge $year $archiveCutoff -}}
{{- relref . (printf "%s.md" $path) -}}
{{- else -}}
{{- printf "/%s/" $path | relURL -}}
{{- end -}}&lt;/code>
&lt;/pre>
&lt;h3 id="custom-index-pages">Custom Index Pages&lt;/h3>
&lt;p>After splitting the site, my custom index pages stopped working. They rely on aggregating data across all posts, but my previous implementation assumed a single &lt;em>content/posts&lt;/em> directory. With the content now split, these indexes no longer had access to the full dataset and returned incomplete results.&lt;/p>
&lt;p>&lt;img src="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/custom-indexes.png" alt="The custom index files I use for my homepage">&lt;/p>
&lt;p>As you can see in the drawing above we have two different types of custom index pages:&lt;/p>
&lt;ul>
&lt;li>Page based on a named markdown file (like &lt;code>about.md&lt;/code> or &lt;code>reading.md&lt;/code>), these files use some custom shortcodes I made which find the relevant data from the &lt;code>content/posts&lt;/code> directory.&lt;/li>
&lt;li>Custom indexes with special layouts (like the main blog index page or the articles page). These also filter the relevant data from the &lt;code>content/posts&lt;/code> directory.&lt;/li>
&lt;li>And then we have one single additional category for the projects.&lt;/li>
&lt;/ul>
&lt;p>Don&amp;rsquo;t ask why I did not use the regular Hugo tools when I first made my homepage. At some point I will need to fix this and use categories for all post types, it would make my setup so much simpler. But this was not the goal of this refactor and I did not want to have any more scope creep so I adjusted the layouts and shortcodes to work with the new setup. The main adjustment was adding support for the new pagination style. And adding support for when the rolling window is not fully populated by posts from the &lt;em>main-page&lt;/em> (this happens in the articles list), but also in the about and reading sections.&lt;/p>
&lt;p>The main shortcode I use is called &lt;code>tagarticles&lt;/code> (&lt;a href="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/tagarticles.html.txt">tagarticles.html&lt;/a>) and it returns a styled list of posts with the given tag. This is used like here (an example from the &lt;code>reading.md&lt;/code> page):&lt;/p>
&lt;pre>
&lt;code class="language-gohtml">## End of year reading summary
{{&lt; tagarticles "books-read" >}}
[
{"url":"/posts/2022/2022-12-31-books-in-2021/","title":"Books read in 2022","date":"2022-12-31T15:41:00+01:00"},
{"url":"/posts/2021/2021-12-31-books-in-2021/","title":"Books read in 2021","date":"2021-12-31T11:32:00+01:00"},
{"url":"/posts/2020/2020-12-31-books-in-2020/","title":"Books read in 2020","date":"2020-12-31T17:39:00+01:00"},
{"url":"/posts/2019/2019-12-31-books-in-2019/","title":"Books read in 2019","date":"2019-12-31T16:34:00+02:00"},
{"url":"/posts/2018/2018-12-31-books-in-2018/","title":"Books read in 2018","date":"2018-12-31T12:42:00+00:00"}
]
{{&lt; /tagarticles >}}&lt;/code>
&lt;/pre>
&lt;p>The relevant part is that the body of the shortcode contains an array of archived posts which are not present on the &lt;em>main-page&lt;/em> anymore, and the shortcode combines these with the posts from the &lt;code>content/posts&lt;/code> directory into a single list, which is then sorted by publish date. Duplicate data is not an issue here, as we assume that archived posts are not returned by the regular Hugo mechanism.&lt;/p>
&lt;h2 id="improve-deployment-process">Improve Deployment Process&lt;/h2>
&lt;p>&lt;img src="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/flow.png" alt="">&lt;/p>
&lt;p>The next optimization is the GitHub workflow (&lt;a href="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/build.yml.txt">build.yml&lt;/a>). Publishing via Git is no longer an option due to caching and timeout issues on my webhost. I was never able to fully diagnose the exact cause, but as the site grew larger, Git-based deployments became unreliable and would often fail or time out. Since Hugo always builds the entire site from scratch, the amount of data uploaded to the webhost on each deploy grew too big. What I needed instead was an iterative build process that only updates the changed parts of the site. So my build script is split into six main steps:&lt;/p>
&lt;ol>
&lt;li>Checkout Repository&lt;/li>
&lt;li>Update archive pages&lt;/li>
&lt;li>Detect files which are new and need to be uploaded later (saved to &lt;code>changed_files.txt&lt;/code>)&lt;/li>
&lt;li>Hugo Build and commit step (I still commit the built page to my repository)&lt;/li>
&lt;li>Upload changes files via &lt;code>lftp&lt;/code> to my webhost&lt;/li>
&lt;li>Notify Micro.blog about the update&lt;/li>
&lt;/ol>
&lt;p>Please note that the Hugo build itself is still the same (a full rebuild of the &lt;em>main-page&lt;/em>) but around that I have additional logic to identify the relevant changes. The important part and the special sauce of my new setup is the second, third and fifth steps:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Update archive tree&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">run&lt;/span>: |&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ./scripts/update-archive-dirs.sh&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The called script (&lt;a href="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/update-archive-dirs.sh.txt">update-archive-dirs.sh&lt;/a>) checks all posts in &lt;code>content/posts&lt;/code> and updates the tree structure in &lt;em>content/archive&lt;/em> so that Hugo knows which archive index pages it needs to generate.&lt;/p>
&lt;p>&lt;strong>Important&lt;/strong>: This step only creates archive index pages for the &lt;em>main-page&lt;/em>. The necessary archive index pages for the archive-page must already exist. I created these manually and uploaded them to the webhost once. Without this, the pagination for the archive section would not work.&lt;/p>
&lt;p>This script is therefore the second part required to get the month-based pagination working.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># -------------------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Detect files that will change&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># -------------------------------------------------&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Detect files to upload&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">run&lt;/span>: |&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ./scripts/detect-changed-public.sh --debug &amp;gt; changed_files.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;Files detected:&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> cat changed_files.txt&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For the uploading we first create a temporary file (&lt;code>lftp.txt&lt;/code>) with a script for lftp:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Build lftp script&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">run&lt;/span>: |&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;open -u \&amp;#34;$FTP_USER\&amp;#34;,\&amp;#34;$FTP_PASS\&amp;#34; \&amp;#34;$FTP_HOST\&amp;#34;&amp;#34; &amp;gt; lftp.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;set ftp:ssl-force true&amp;#34; &amp;gt;&amp;gt; lftp.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;set cmd:trace true&amp;#34; &amp;gt;&amp;gt; lftp.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> while IFS= read -r file; do
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> dir=$(dirname &amp;#34;$file&amp;#34;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;mkdir -p \&amp;#34;$FTP_BASE_PATH/$dir\&amp;#34;&amp;#34; &amp;gt;&amp;gt; lftp.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;put \&amp;#34;public/$file\&amp;#34; -o \&amp;#34;$FTP_BASE_PATH/$file\&amp;#34;&amp;#34; &amp;gt;&amp;gt; lftp.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> done &amp;lt; changed_files.txt
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> echo &amp;#34;quit&amp;#34; &amp;gt;&amp;gt; lftp.txt&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which we then give to the &lt;code>glazrtom/ftp-action@v3.0.0&lt;/code> action which does the actual uploading:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">Upload via FTP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">uses&lt;/span>: &lt;span style="color:#ae81ff">glazrtom/ftp-action@v3.0.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">with&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">host&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.FTP_HOST }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">user&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.FTP_USER }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">password&lt;/span>: &lt;span style="color:#ae81ff">${{ secrets.FTP_PASS }}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;source lftp.txt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">options&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">localDir&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">remoteDir&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We must set the &lt;code>options&lt;/code>, &lt;code>localDir&lt;/code>, and &lt;code>remoteDir&lt;/code> to empty strings so that the script works correctly. All arguments need to be set inside the script.&lt;/p>
&lt;p>We also need to specify the &lt;code>host&lt;/code>, &lt;code>user&lt;/code>, and &lt;code>password&lt;/code> twice. The GitHub Action requires these parameters, but it does not reuse them inside the script itself. As a result, the script runs without an active connection unless we explicitly reconnect. Therefore, the first line in the script (&lt;code>open ...&lt;/code>) establishes the actual connection to the webhost.&lt;/p>
&lt;p>The last part is the bash script used to determine the delta of changed files. The script uses a heuristic to detect which files need to be uploaded. This works for all posts I have written so far, although I have already had to adjust it a couple of times as new edge cases appeared.&lt;/p>
&lt;p>This approach works well because my microposts and regular posts follow a strict structure. I generate them via a custom app, so the layout is always consistent.&lt;/p>
&lt;p>The script first detects changes in the &lt;code>content/posts&lt;/code> directory. It then compares a set of hardcoded output files against the state of the previous build (&lt;code>.hugo-extra-output-manifest&lt;/code>). Additionally, some files are always included in each build (defined in the &lt;code>GLOBAL_OUTPUTS&lt;/code> array).&lt;/p>
&lt;p>I do not show the full script here (it is quite long), but you can download it here: &lt;a href="https://vmac.ch/posts/2026-04-04-split-hugo-homepage/assets/detect-changed-public.sh.txt">detect-changed-public.sh&lt;/a>. The most relevant part is the &lt;code>EXTRA_OUTPUTS&lt;/code> array, which defines expected output files in the &lt;code>public&lt;/code> folder along with rules that determine when they should be considered changed:&lt;/p>
&lt;ul>
&lt;li>Content change (tracked via &lt;code>.hugo-extra-output-manifest&lt;/code>)&lt;/li>
&lt;li>New posts in &lt;code>content/posts&lt;/code> matching specific tags&lt;/li>
&lt;li>New posts in &lt;code>content/posts&lt;/code> matching specific categories&lt;/li>
&lt;li>New &lt;code>_index.md&lt;/code> files in &lt;code>content/archive/**&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Any of these rules will cause the file to be emitted as changed and included in the upload.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>GLOBAL_OUTPUTS&lt;span style="color:#f92672">=(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;index.html&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;index.xml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;sitemap.xml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>EXTRA_OUTPUTS&lt;span style="color:#f92672">=(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;reading/index.html|content/reading.md|tags=book-start,books-read,review|categories=book-start&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;start_here/index.html|content/start_here.md&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;index.html,index.xml|content/archive/**/_index.md|include-parents&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="summary">Summary&lt;/h2>
&lt;p>With all of these changes I was able to get the build time to under 1 minute, and so far I have not noticed any strange caching artefacts on my webhost. By adding the XML-RPC ping call to the GitHub Action, it now also notifies Micro.blog about updates directly, so new posts are visible there immediately.&lt;/p>
&lt;p>It took a while to come up with this solution, and if you are at the beginning of your Hugo journey, I would strongly recommend using Hugo’s taxonomy system properly and organizing your posts by category—not like I did. Keeping all posts in a single directory is not worth going against the system. It would have made splitting the homepage into two Hugo instances much simpler and avoided a lot of the custom logic (around my index pages) I had to introduce.&lt;/p>
&lt;p>Perhaps I will do a full refactor at some point, but not right now. At the moment I’m just glad I got the build times down and found such a “simple” solution. Alternatively, I could also pay more for my webhost and build directly on there.&lt;/p></description></item><item><title>Books read in 2025</title><link>https://vmac.ch/posts/2025-12-31-books-read-in-2025/</link><pubDate>Wed, 31 Dec 2025 09:11:00 +0100</pubDate><guid>https://vmac.ch/posts/2025-12-31-books-read-in-2025/</guid><description>&lt;p>📚 My reading year of 2025.&lt;/p>
&lt;p>The year is coming to an end again, and it is time for another summary of the books I’ve read. This year was strange. My reading time and motivation were way down, with the one exception of reading the &lt;em>Mother of Learning&lt;/em> series. I still struggle with reading nonfiction. It just takes a lot of energy, and energy is something I don’t have much of these days (especially when working way too much for three quarters of a year).&lt;/p>
&lt;p>I thought about my reading goals for next year, and I decided I won’t set any goals for 2026. I will find books to read (my TBR stack of physical books is at least 40 cm high), and there are always interesting new books to discover. But I don’t want to put any pressure on myself to reach a goal.&lt;/p>
&lt;p>So I hope for an interesting book year next year.&lt;/p>
&lt;h2 id="the-best-books-i-read-of-2025">The best books (I read) of 2025&lt;/h2>
&lt;h3 id="in-the-fiction-category-i-have-one-recommendation">In the fiction category, I have one recommendation&lt;/h3>
&lt;p>I got this series recommended by a friend and could not stop reading it once I started. Thanks, Marco ;-). This series is the perfect combination of what I like in fiction books: a nice magic system and a main protagonist finding all the loopholes in it.&lt;/p>
&lt;div style="display: flex; width: 1030px; flex-wrap: wrap;padding-bottom:0px; margin-left: -25%;">
&lt;div style="display:flex; flex-direction:column;">
&lt;div style="display: flex; flex-direction:row; width: 100%; padding-bottom:0px">
&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-05-29-bookblog-mother-of-learning-arc-1-1748548378.jpg" alt="Mother of Learning: ARC 1" style="width:250px; padding-left:10px; coverobject-fit:cover;">
&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-06-06-bookblog-mother-of-learning-arc-2-1749200235.jpg" alt="Mother of Learning: ARC 2" style="width:250px; padding-left:10px; coverobject-fit:cover;">
&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-06-13-bookblog-mother-of-learning-arc-3-1749826614.jpg" alt="Mother of Learning: ARC 3" style="width:250px; padding-left:10px; coverobject-fit:cover;">
&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-06-24-bookblog-mother-of-learning-arc-4-1750736785.jpg" alt="Mother of Learning: ARC 4" style="width:250px; padding-left:10px; coverobject-fit:cover;">
&lt;/div>
&lt;p style="margin-top: 2px;margin-bottom:0px;font-size:0.7em;text-align:center;">&lt;a href="https://www.amazon.de/-/en/dp/B0CHSJ19J9">Mother of Learning 1-4&lt;/a> by nobody103&lt;/p>
&lt;/div>
&lt;/div>
&lt;h3 id="on-the-nonfiction-side-there-is-also-one">On the nonfiction side, there is also one&lt;/h3>
&lt;p>I got interested in biology this year, so it is not surprising that I liked this book. This book answered quite a few of the questions I had about how cells work and how life itself works.&lt;/p>
&lt;div style="display: flex; width: 629px; flex-wrap: wrap;padding-bottom:0px">
&lt;div style="width:100%; display:flex; flex-direction:column;">
&lt;img style="width:50%;height:92%; padding-left:25%;paddding-right:25%" src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-09-18-bookblog-how-life-works-a-user-s-1758193363.jpg" alt="How Life Works: A User’s Guide to the New Biology" />
&lt;p style="margin-top: 2px;margin-bottom:0px;font-size:0.7em;text-align:center;">&lt;a href="https://www.amazon.de/gp/product/B0C742CGCG">How Life Works: A User’s Guide to the New Biology&lt;/a> by Philip Ball&lt;/p>
&lt;/div>
&lt;/div>
&lt;h2 id="reading-statistics">Reading statistics&lt;/h2>
&lt;p>I set out this year with the plan to read 24 books—one fiction and one nonfiction per month. I managed to read 17 books, but only four of them were nonfiction. As I wrote in the introduction, it was a hard year for reading, as I worked a lot and had a lot of other things on my plate. There just was not enough energy for reading.&lt;/p>
&lt;h2 id="plans-for-next-year">Plans for next year&lt;/h2>
&lt;p>No fixed plan for next year. One book a month is what I can realistically expect to be reading right now.&lt;/p>
&lt;h2 id="list-of-all-books-in-2025">List of all books in 2025&lt;/h2>
&lt;style>
.masonry {
column-count: 4; /* Number of columns */
column-gap: 10px; /* Space between columns */
width: 1500px;
margin-left: -50%;
}
@media (max-width: 1440px) {
.masonry {
column-count: 4; /* Reduce column count for smaller screens */
width: 100%;
margin-left: 0;
}
}
@media (max-width: 768px) {
.masonry {
column-count: 2; /* Reduce column count for smaller screens */
width: 100%;
margin-left: 0;
}
}
.item {
margin-bottom: 10px; /* Space between rows */
break-inside: avoid; /* Avoid breaking images across columns */
}
.item img {
width: 100%;
height: auto; /* Maintain aspect ratio */
display: block;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* shrink only first column items */
.item.first-column img {
aspect-ratio: 2 / 2.45; /* taller number = shorter image */
height: auto;
object-fit: contain;
}
/* optional: slightly tighter spacing */
.item.first-column {
margin-bottom: 6px;
}
&lt;/style>
&lt;div class="masonry">
&lt;div class="item first-column">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-01-01-bookblog-anders-nicht-falsch-1735740019.jpg" alt="Anders nicht falsch">&lt;/div>
&lt;div class="item first-column">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-01-10-bookblog-woodwalkers-1-carags-verwandlung-1736529015.jpg" alt="Carags Verwandlung">&lt;/div>
&lt;div class="item first-column">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-02-02-bookblog-liberation-2053-2054-1738473133.jpg" alt="Liberation 2053-2054">&lt;/div>
&lt;div class="item first-column">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-03-23-bookblog-the-space-between-worlds-ashtown-1742692249.jpg" alt="The Space Between Worlds">&lt;/div>
&lt;div class="item first-column">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-04-03-bookblog-duly-noted-extend-your-mind-1743691279.jpg" alt="Duly Noted: Extend Your Mind through Connected Notes">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-03-01-bookblog-this-inevitable-ruin-dungeon-crawler-1740819748.jpg" alt="This Inevitable Ruin">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-04-28-bookblog-careless-people-the-explosive-memoir-1745830560.jpg" alt="Careless People: The explosive memoir that Meta doesn't want you to read">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-05-07-bookblog-not-till-we-are-lost-1746638839.jpg" alt="Not Till We Are Lost">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-05-29-bookblog-mother-of-learning-arc-1-1748548378.jpg" alt="Mother of Learning: ARC 1">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-06-06-bookblog-mother-of-learning-arc-2-1749200235.jpg" alt="Mother of Learning: ARC 2">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-06-13-bookblog-mother-of-learning-arc-3-1749826614.jpg" alt="Mother of Learning: ARC 3">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-06-24-bookblog-mother-of-learning-arc-4-1750736785.jpg" alt="Mother of Learning: ARC 4">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-07-03-bookblog-guild-of-tokens-nyc-questing-1751525800.jpg" alt="Guild of Tokens">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-07-26-bookblog-the-cat-and-the-warlock-1753547579.jpg" alt="The Cat And The Warlock">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-09-18-bookblog-how-life-works-a-user-s-1758193363.jpg" alt="How Life Works: A User’s Guide to the New Biology">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-09-23-bookblog-shatter-me-tiktok-made-me-1758615491.jpg" alt="Shatter Me">&lt;/div>
&lt;div class="item">&lt;img src="https://vmac.ch/posts/2025-12-31-books-read-in-2025/covers/2025-12-22-bookblog-children-of-time-the-children-1766390249.jpg" alt="Children of Time">&lt;/div>
&lt;/div>
&lt;ul>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/3905574977">Anders nicht falsch&lt;/a> by Maria Zimmermann&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B01FV8Y52Y">Carags Verwandlung&lt;/a> by Katja Brandis&lt;/li>
&lt;li>&lt;a href="https://www.amazon.com/gp/product/B0D5TJLLLP">Liberation 2053-2054&lt;/a> by Lee Schneider&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0DJWKWV8W">This Inevitable Ruin&lt;/a> by Matt Dinniman&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B084D9VSV1">The Space Between Worlds&lt;/a> by Micaiah Johnson&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0CN7NT41X">Duly Noted: Extend Your Mind through Connected Notes&lt;/a> by Jorge Arango&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0DJWKWV8W">This Inevitable Ruin&lt;/a> by Matt Dinniman&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0DZD18WRP">Careless People: The explosive memoir that Meta doesn&amp;rsquo;t want you to read&lt;/a> by Sarah Wynn-Williams&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0DQ4LGLHY">Not Till We Are Lost&lt;/a> by Dennis E. Taylor&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B09M2R6QLF">Mother of Learning: ARC 1&lt;/a> by nobody103&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0B11L9TWP">Mother of Learning: ARC 2&lt;/a> by nobody103&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0BCGMW45C">Mother of Learning: ARC 3&lt;/a> by nobody103&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0BWSCYLRQ">Mother of Learning: ARC 4&lt;/a> by nobody103&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B07STPH2F5">Guild of Tokens&lt;/a> by Jon Auerbach&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0FHWZXJKQ">The Cat And The Warlock&lt;/a> by Serg Koren&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B0C742CGCG">How Life Works: A User’s Guide to the New Biology&lt;/a> by Philip Ball&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B08F38Y1LM">Shatter Me&lt;/a> by Tahereh Mafi&lt;/li>
&lt;li>&lt;a href="https://www.amazon.de/gp/product/B00SN93AHU">Children of Time&lt;/a> by Adrian Tchaikovsky&lt;/li>
&lt;/ul>
&lt;h3 id="still-reading-right-now">Still reading right now&lt;/h3>
&lt;p>None&lt;/p>
&lt;h2 id="authors">Authors&lt;/h2>
&lt;ul>
&lt;li>Adrian Tchaikovsky&lt;/li>
&lt;li>Dennis E. Taylor&lt;/li>
&lt;li>Jon Auerbach&lt;/li>
&lt;li>Jorge Arango&lt;/li>
&lt;li>Katja Brandis&lt;/li>
&lt;li>Lee Schneider&lt;/li>
&lt;li>Maria Zimmermann&lt;/li>
&lt;li>Matt Dinniman&lt;/li>
&lt;li>Micaiah Johnson&lt;/li>
&lt;li>nobody103&lt;/li>
&lt;li>Philip Ball&lt;/li>
&lt;li>Sarah Wynn-Williams&lt;/li>
&lt;li>Serg Koren&lt;/li>
&lt;li>Tahereh Mafi&lt;/li>
&lt;/ul></description></item></channel></rss>