Rebuilding the blog, by hand
The last post here went up on . This one is 1496 days later. Most of that gap is life. The part worth writing down is what was waiting when I came back to publish: a script whose job was to generate a file it could no longer generate, sitting next to that file, in a repository where nothing recorded that anything had changed.
Nothing about it had failed loudly. Every defect in it was, taken alone, the kind you notice and step over. Cosmetic. Rare. Not now. This is what each of them was, what they became together, and why deleting the whole thing was the cheapest way out. It absorbs the 2022 post that described the system as designed.
What the old system was
Static HTML and CSS on GitHub Pages. The blog index page shipped empty and a small
script built the list in the browser from blog/postsIndex.json. That JSON was
regenerated by a GitHub Action that ran on every push touching
blog/posts/**, and auto-committed the result back to the repository. The 2022
post drew it like this:
╔═════════════╗
║ github repo ╟───>─╮
╚══╦══════╤═══╝ │
╭╫╮ │ │
V║V │ ╭────┴─────╮
║ ╰─<──┤ workflow │
║ ╰──────────╯
╔══╩══════╗
║ website ║
╚═════════╝
The reasoning behind it is the one part I would write again today. From the same post:
Online you can find a number of blog systems that only consist of delegating the entire build part of the blog to an action that goes from md or yml file to generate the whole blog. This solution is very effective, but did not correspond to my approach.
That still holds, and this site still has no build step. Nothing below is an argument against that shape. It is about what happened inside it.
The generator
updateIndex.sh was 89 lines and seven shell functions. For every
.html file under blog/posts/ it scraped a title out of the first
<h2>, a description out of the first <p>, a language
list out of a <div id="languages">, a date out of the directory name,
and glued the JSON together with echo -n.
The entire HTML parser was this:
updateIndex.sh# remove html tags in the uglyest way ;p
# work only if you pipe something inside
function removeTag(){
echo $(cat /dev/stdin | cut -d'>' -f 2 | cut -d'<' -f 1)
}
# get article descritpion (the first p tag content)
function getDescription(){
echo $(cat $1 | grep "<p>" | head -n1 | removeTag)
}
Cut at the first >, keep field two; cut that at the first <,
keep field one. The comment is accurate and I am not going to pretend I did not write it.
What it means in practice is that a description survives only if the paragraph is one
unbroken run of text on a single line. I re-ran the function against four inputs:
<p>Plain sentence.</p>: works.- A paragraph containing a link: silently truncated at the
<a. - A paragraph that opens with a tag: empty string.
- A
<p>alone on its line, content below it: empty string.
The last two are not hypothetical. Run it against the exploRob page as it stood before
this rewrite and the description comes out empty, because that paragraph opens with a
<strong>. The index carries a card with a title, a thumbnail and no text
under it, and nothing anywhere says why. That is a cosmetic defect on one card. It goes on
the list and the list does not get looked at again.
Assembly was the same shape, with a worse ceiling:
updateIndex.sh# get json object out of a file
function getJsonObject(){
echo -n "{ "
echo -n "\"title\":\"$(getTitle $1)\", "
echo -n "\"date\":\"$(getDate $1)\", "
echo -n "\"url\":\"$1\", "
echo -n "\"lang\": [$(getLangArray $1)], "
echo -n "\"description\":\"$(getDescription $1)\", "
echo -n "\"picture\":\"$(getPicture $1)\""
echo -n " }"
}
Every field is interpolated straight into a JSON string literal with no escaping of any
kind. One double quote in a heading is enough. An <h2> reading
He said "hello" comes out as
"title":"He said "hello"", the file stops being JSON, the browser's
JSON.parse throws, and the blog index renders as an empty page. Not a broken
card. The whole list, gone, in every browser, until someone happens to load the page and
look.
No title the script ever ran against contained a double quote, so that one never fired in four years. A bug you cannot reach is indistinguishable, from the outside, from a bug you do not have.
And then the thumbnails:
updateIndex.sh# get url of placeholder picture or return default picture
function getPicture(){
folder=$(dirname $1)
if [ -f "$folder/img/placeholder.jpg" ]
then
echo -n "$folder/img/placeholder.jpg"
else
echo -n "../assets/img/placeholder.png"
fi
}
Two branches, two outcomes, both of them placeholders. There is no path through that function that names a real image. Hold that thought.
One more, which is the kind of thing you only find by reading the file after the fact: the
list of articles came from find, whose output order is whatever order the
directory happens to enumerate in. The index was in no order at all, not by date and not
alphabetically. The last generated file emits its nine articles in this sequence of
publication dates:
2018-11-26 2020-11-20 2020-07-01 2022-07-29 2019-05-01
2020-06-19 2022-07-26 2021-06-09 2020-02-27
The blog index rendered in that order for four years. Nobody complained, because there is no order a reader can tell is wrong when the list is nine items long.
The file it could not reproduce
The last time the workflow ran, on , it committed one line per article, like this:
blog/postsIndex.json | generated 2022-07-29{ "title":"Hangman Game", "date":"26/11/2018", "url":"./posts/2018-11-26/hang-man-game.html",
"lang": ["C99","ASCII","Python3"], "description":"Hangman's game, in C99 and Python3",
"picture":"./posts/2018-11-26/img/placeholder.jpg" },
Three years later, on , I added thumbnails to the posts and edited that JSON by hand. The same entry became:
blog/postsIndex.json | committed 2025-08-27{
"title": "Hangman Game",
"date": "26/11/2018",
"url": "./posts/2018-11-26/hang-man-game.html",
"lang": [
"C99",
"ASCII",
"Python3"
],
"description": "Hangman's game, in C99 and Python3",
"picture": "./posts/2018-11-26/img/hangman.png"
},
Two things changed there, and a single run of the generator would have undone both. The
formatting first: the script emits exactly one line per article, so a pretty-printed file
is proof on its face that a human wrote it last. Then hangman.png, which is a
real file, while getPicture, above, can only ever emit
placeholder.jpg or ../assets/img/placeholder.png. All nine
thumbnails in the committed file were set by hand, and one run would have reset all nine.
That edit is where the system turned over. Before it, postsIndex.json was
generated output and the script owned it. After it, the JSON was hand-maintained input that
the script would destroy on contact. Nothing announced the change, because nothing about it
looked like a change: same path, same filename, same description of it in the README and in
the 2022 post. The only evidence anywhere that the file had switched sides was its own
indentation.
And it happened for an ordinary reason. getPicture could only emit a
placeholder, so the thumbnails had to be patched back in by hand after any run. Patching
them back in by hand is much less work than fixing the function, once. So the patch became
the process, the function stayed on the list, and the ownership of the file moved without
anyone deciding that it should.
Nothing warned about any of this, and nothing could have. There was no check that ran the generator and diffed its output against what was committed. Nothing in the repository knew what that file was supposed to contain.
It had already stopped running
It never fired. The auto-commit ran fourteen times, all of them between
and
, and never again; they are still in the log,
all authored by lostsh-autocommit. Two commits in August 2025 touched
blog/posts/, which is the exact path the workflow triggers on, and neither
produced an auto-commit.
I did not spend long working out why, because by the time I looked the answer that mattered was already clear. What is visible is that the job ends with a hardcoded branch:
.github/workflows/update-blog-index.yml- name: Auto-commit
run: |
echo [*] Start auto-commit
git config user.name "lostsh-autocommit"
git config user.email "yohann.vernhes@gmail.com"
git add .
git commit -m "Auto commiting blog posts index"
git push origin master
echo [+] Auto-commit complete
and the 2025 commits are not on master. They are on the working branch the
site has actually been deployed from. A push step with one branch name written into it
works on one branch.
So a broken component was holding a destructive one shut, and that is the part worth sitting with. The generator was harmless for exactly as long as the workflow stayed dead. Repairing the workflow is a one-line change, obviously correct in isolation, the kind of thing you do on an afternoon spent clearing small items. It is also the action that arms everything above. Whoever eventually got to that item would have fixed CI and wiped nine curated entries in the same commit, and the commit message would have read like tidying up.
The failure was not avoided. It was deferred, and its trigger was going to be an act of maintenance.
What replaced it
Deletion. Both files are gone, the script and the workflow, and this repository now ships zero GitHub Actions. blog/postsIndex.json is the source of truth, and I edit it in the same commit as the post it describes.
The nine entries went four ways. Three of them were a hangman, a tic-tac-toe and a Pong, each about as long as a README; they are one post now instead of three. Three were rewritten and kept their URLs. One was the 2022 post about this system, and it redirects here. Two were deleted, one of them a 2021 test article whose body, in its entirety, reads "Heello hear / I just want to be free / Is it a new article ? / Triggering the workflow / This time it will work !", live and indexed for five years.
The clearout found the same pattern in the images. The tic-tac-toe post referenced no
image at all, and its img/ directory held seven photographs of the exploration
robot, byte for byte the same seven files already sitting under the exploRob post: 11 MB of
a post tree that had been under 30 MB, duplicated by a copy-paste nobody had reason to
revisit.
Publishing now is: write the HTML, add one eleven-line entry to the JSON, commit. There is no step that runs while I am not looking.
What compounds
Line them up. A description field that comes out empty on one page. A quoting bug in a path no input had ever reached. A picture field that returns the wrong constant. A list in no order. A push step naming a branch the work had left. Five items, a few minutes each, and every one of them correctly triaged as minor on the day it was found. None of them was worth stopping for, and I would triage most of them the same way again.
What they did together was change what the system was. The generator stopped being able to reproduce the file it generated. The file stopped being generated and became a hand-written source that still looked generated. The workflow stopped running and its being broken became load-bearing. At no point did anything break in a way that surfaced. The structure did not fail; it inverted, one deferred item at a time, and then ran in its inverted state for four years, because from the outside nothing about it looked different.
Small bugs are not small in the same way that small numbers are small. A minor defect that gets worked around installs the workaround, and the workaround is what carries the change in structure. The thing to watch is not the severity of the bug. It is whether a habit has grown around it, and what that habit is now quietly responsible for.
Deleting the generator was the fix here, for a file that changes a few times a year and fits on one screen. That is a fact about this file, not about generators.