Friday Fixes: Vanishing Tables, an Overdue Homelab, and the Apostrophe That Broke the Build
Two unrelated problems this week, both from the same instinct: don’t guess at a root cause, and don’t touch anything load-bearing without checking what it actually holds first.
Part 1: The Tables That Vanished on Substack
The recent post on Meta’s AI strategy shipped with four tables. They render fine on this site. On the Substack mirror, all four are simply gone — no broken layout, no placeholder, nothing in the HTML at all.
First instinct was to blame this site’s own RSS pipeline — some markdown-to-HTML step mangling table syntax on the way into the feed. Pulling the live feed output directly ruled that out fast: the <content:encoded> block for that post had all four tables present as correct, valid <table> HTML, exactly what the markdown renderer should produce. The bug wasn’t here.
The actual cause is a platform limitation, not a feed bug: Substack’s post editor has no table block at all, and its importer strips <table> markup entirely when converting external HTML into its internal format. Every publisher syndicating tables into Substack via RSS hits this — WordPress, Ghost, Medium included. Not something specific to one feed or one post.
Rather than guess at a fix, I built two one-off variants of the actual post and compared them side by side before touching any pipeline code:
- Tables as images. Run the post through the real rendering pipeline, screenshot each table, splice the PNGs back in. Crisp, faithful to the real data, no overflow. Trade-off: no text selection, needs alt text.
- Tables degraded to plain text in a code block. Fine for narrow tables. Fell apart completely on the post’s widest table — six columns comparing open-weight model contenders — which wrapped illegibly at the width a newsletter code block actually gets.
Images won outright once both were sitting next to each other.
Before building a second image-rendering path from scratch, I checked whether the site’s existing “share as image” feature — the button that lets a reader download a branded PNG of a table or code block — could just be reused. It could, almost entirely. The table parser, renderer, height calculator, and dark-theme branding all transferred directly. The one real gap: that route was built for a single browser click, not a stable URL an external importer or email client could fetch on its own.
The shipped fix extracted the shared rendering logic into its own module, added a new GET route that serves a stable, cacheable image for “table N of post X,” and taught the RSS-to-HTML converter an opt-in mode that swaps every table for one of these images — but only on the feed Substack actually reads. The real feed, the one actual RSS readers consume, is untouched. Real tables stay real tables for anything that can render them.
The gotcha: the new route 401’d on the first test. The site’s middleware protects every API route behind admin auth by default, with an allowlist for public exceptions — and that allowlist checked for an exact match on the old route’s path. The new nested route didn’t match, so it fell straight through to the auth gate, which Substack’s importer obviously can’t pass. A one-line fix, widening an exact match to a prefix match, but the kind of bug that would have shipped a feature that failed for every single external caller if it hadn’t been tested end to end before merging.
Part 2: A Homelab Overdue for a Version Check
Separately, the homelab hadn’t had a systematic update pass in a while. Not broken, just not current — and “still works” isn’t the same thing as “current.” This is the same instinct as running a scheduled security scan, just pointed at version numbers instead of vulnerabilities: something worth doing on a cadence, and something very easy to keep skipping if nobody’s a full-time sysadmin.
The approach: inventory everything first, read-only, then work through updates in risk order. Low-risk items — routine OS package patches, most of them years of accumulated Ubuntu point releases — went through immediately. Anything touching the box’s own access path, or its core job, got flagged and confirmed before touching it.
That list ended up covering the Coder control plane itself, the Docker daemon underneath every container on the box (including the one this very session was running in), Tailscale (the only network path back into the machine), the Cloudflare tunnel daemon, a stale Home Assistant instance, and a two-month-old build of llama.cpp.
| Tool | Old Version | New Version | Summary |
|---|---|---|---|
| Coder | 2.35.2 | 2.36.0 | Control plane restart; this session’s own container survived it |
| OS packages | various | latest | Routine Ubuntu point releases (NetworkManager, GNOME, Kerberos libs, libinput, linux-firmware, Chrome, Node patch) |
| NVIDIA driver | 590-open (orphaned) + 595-open | 595-open only | Cleanup went sideways, then got fixed — see below |
| Docker Engine | 29.6.2 | 29.7.2 | Daemon restart bounced every container on the box, all recovered |
| Tailscale | 1.98.9 | 1.102.2 | The only network path back into the box — verified reconnect immediately after |
| cloudflared | 2026.3.0 | 2026.8.2 | Tunnel re-registered cleanly after restart |
| Home Assistant | 2026.7.3 | 2026.8.1 | Config backed up first; retired entirely shortly after (see below) |
| llama.cpp | 773 commits behind | latest | Held for explicit confirmation first — see below |
Three things stood out.
The NVIDIA near-miss. Clearing out an orphaned old driver package with a routine apt autoremove took the active driver metapackage down with it — it had been flagged as auto-installed too, and autoremove doesn’t ask twice before taking dependencies along with the thing you actually meant to remove. The GPU never actually went dark; the loaded kernel modules were untouched the whole time. But it’s a good reminder that “clean up this one orphaned package” and “let autoremove figure out what’s safe” are not the same instruction, and it’s worth checking what autoremove is about to take before confirming it.
The one I stopped to ask about. One of the update targets was serving a local model through a chat template file named specifically for coding agents. That was close enough to “this might be the exact thing answering right now” that it got flagged instead of rolled into the rest of the pass on the same momentum. Turned out this session was running on a cloud-hosted model, not the local one — so the rebuild, which meant stopping the service, pulling 773 commits, and a full CUDA rebuild, was safe to run outright. Worth internalizing the check itself regardless of the answer: assume your own inference path is something you don’t touch by default, until you’ve actually confirmed otherwise.
The Home Assistant retirement. A second, unrelated Home Assistant instance had been running in Docker on this box since May — separate from the dedicated always-on box now standing in for the old SmartThings hub. Before tearing it down, the honest question was whether it held anything worth carrying over. An audit of its config turned up nothing: no automations, no scripts, no scenes, no custom dashboards, no HACS, no cloud account, nothing else on the box even referencing its port. It was default-config auto-discovery and little else. Backed up the config directory anyway — cheap insurance for a five-minute check — then removed the container, the image, and the config for good.
Part 3: The Apostrophe That Took Down the Whole Site
A completely unrelated production incident, same week: a Vercel deploy of this site failed outright, dropping straight into a YAMLException from the frontmatter parser — “can not read a block mapping entry; a multiline key may not be an implicit key” — pointing at the tags: line of one specific post. That line was fine. The build reads every post in the repo to generate the search index, so one broken file took the whole site down, not just that post’s page.
The real cause was several lines above the error: an unescaped apostrophe inside that post’s description field. YAML read the text after the apostrophe as if it were the start of a new block, and kept parsing until it hit tags: before finally giving up — which is why the error pointed at an innocent line instead of the actual malformed character. Fixed by doubling the quote, the same escaping convention this repo already uses correctly elsewhere.
After the fix, every post in the repo got parsed directly with a YAML library to confirm it was the only broken file, not a latent pattern waiting to bite the next post with an apostrophe in its description. It was the only one. One character changed, pushed straight to main since the site was actively down.
By the Numbers
- 4 tables disappeared on Substack, and 4 correctly-indexed images replaced them once the fix shipped
- 6 columns on the widest table — the one that ruled out the plain-text fallback
- 2 fix variants prototyped as one-off previews before any pipeline code was written
- 1 auth-middleware bug found mid-build: an exact-match allowlist entry that would have 401’d every external fetch of the new route
- 8 homelab components checked, 7 updated, 1 already current
- 1 GPU near-miss, caught and fixed in the same session with zero actual downtime
- 773 commits pulled in the llama.cpp rebuild, after confirming it wasn’t the model answering the session itself
- 1 stale Home Assistant instance retired, config backed up, nothing lost
- 0 lines of application code changed in the homelab pass — pure maintenance, and exactly the kind of thing worth scheduling on a recurring basis instead of waiting for something to break
- 1 unescaped apostrophe took down the entire production build
- 90 posts swept afterward to confirm it was the only one — it was