online lostsh.github.io rebuilding-the-blog | 2026-09-02 rfc 3514: evil bit not set
post 05 2026-09-02 bash | html 1496 days

Rebuilding the blog, by hand

Bash | HTMLpost-mortem

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:

from the 2022 post
╔═════════════╗
║ 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:

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:

emitted order, last generated file
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.

updateIndex.sh deleted from the git log 89 + 29 lines
lines deleted118
generator runs, ever14
first run2022-07-25
last run2022-07-29
runs since0
thumbnails it could emit0 of 9

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.