Skip to main content
Back to posts

🐙 There's an octopus in the corner

niklas-heer/nheer.io

My personal website with blog posts and other information about me

1 0 Astro Updated 1 days ago

If you’re reading this on a wide screen, look at the bottom-left corner of the page. Something purple is peeking in.

That’s Inky. He’s a small octopus who lives on this site, takes “the deep web” literally, and files every tech headline as an incident.

Click him. He surfaces, says something snarky, and sometimes points at the article that prompted it. Click again and you get the next line. After a few seconds of being ignored he slides back into the wall.

On a phone the corner is empty. The layout hides him below 1,200 pixels and the animation never downloads. For this article he makes an exception: a little further down, he answers right here, whatever size your screen is.

He doesn't chat

Inky doesn’t chat and doesn’t remember you. He shuffles a list of comments, reads them out one by one, and eventually admits he’s out of ink.

The first click wakes him up. Later clicks walk through the list. Comments about the news carry a small link to the story; general jokes don’t. When the list runs out he switches to a handful of “that’s all I’ve got” lines. Twelve seconds without another click and he goes back to sleep.

That’s all he does. Try it, with the buttons or by clicking him:

The mascot, inline

Ask Inky. Here he answers on any screen.

Patch 3.5.0: Kev introduces a tiny Jev-like family of decision models built on Qwen3.5, now with fewer model-shaped excuses. Proudly small.

Comment 1 of 20.

The 20 comments this page was built with on 2026-09-21: 10 news takes, each linking to its story, and 10 general jokes. The corner mascot reads the same list. Nothing is fetched while you click.

This is the same octopus and the same list as the corner. On the live site the list changes with every build. If all you get is five jokes about Docker and the Mariana Trench, you’re looking at a build without the database, and Inky is reading the fallback lines written into the site itself.

The interesting part is where the other lines come from.

Where the lines come from

A refresh job in my homelab fetches the top five stories from Hacker News and the five newest from The New Stack and DevOps.com. It skips any URL Inky has already commented on. The rest go to a model with one request: answer in Inky’s voice.

The model is currently GPT-5.6 Luna, OpenAI’s small model, called through OpenRouter. The prompt is quite specific. Inky is a small purple octopus who lives in the deep web, takes that literally, and works as the site’s on-call announcer: he keeps the backups, files every headline as an incident, and reads it out with game-show enthusiasm. He is unsurprised rather than bitter, cheerfully bureaucratic about bad news, now and then delighted by carnage that isn’t his to fix, and a little petty about being ignored at the bottom of the page. He rotates four formats: an achievement with a useless reward, a patch note, a numbered incident report, and a plain announcement to the surface. Lines stay short. The ocean is home, not a pun quota. He mocks systems and habits, never people, and text emoticons like :3 and UwU are banned. The model returns a JSON array of strings, one per headline.

That ban has a history. The first Inky, from 1 December 2025, used Claude 3.5 Haiku and a shorter prompt: a snarky kawaii octopus with a pun quota. The commit that outlawed text emoticons also switched the model to GPT-5.1 Chat, which lasted until this week, for a reason that gets its own section below. The announcer persona is newer still; I regenerated the older general jokes so he doesn’t switch voices halfway through a click. A small script in the repository runs the real prompts on the day’s headlines through any list of models and prints the results. It stores nothing. Picking a model is a matter of taste, and of whether the reply parses.

The generated lines go into PostgreSQL with the source title and URL attached. News comments older than seven days are deleted. A smaller pool of general jokes gets topped up by two whenever it drops below ten, and trimmed when it grows past fifteen.

None of that reaches your browser as a live query. The site reads the current comments when it builds: up to ten news takes from the last week, all the general jokes, interleaved three to one. They are written into the page as data. Clicking Inky only shuffles that snapshot.

From headline to tentacle

The octopus does not fetch the news while you watch.

  1. News

    Collect a few headlines

    Five each from Hacker News, The New Stack, and DevOps.com. Already-commented URLs are skipped.

  2. Voice

    Write in Inky’s voice

    A small model via OpenRouter, GPT-5.6 Luna today, returns one snarky line per headline. Each is stored with its source link.

  3. Build

    Bake comments into the page

    The site reads the current mix at build time. The browser only shuffles what it already has.

  4. Click

    Surface, then hide again

    Wide screens get the animated octopus. Narrow ones never load him.

The production path as of the sync script and build-time reader in this repository. Builds without the database, such as local previews and test runs, use the five fallback comments instead.

If the database is missing or empty, the fallback comments take over. Live production builds refuse to ship with no comments at all.

He was drawn twice

The first Inky was a hand-built SVG: a kawaii octopus with nerd glasses, random blinking and a glow, in a component of about 1,100 lines. Five days later, on 6 December 2025, he became a Lottie animation.

Lottie is a JSON description of an After Effects animation that a small library renders as SVG in the browser. The file is called octopus v4, weighs about 188 kilobytes, and describes fifteen layers over 360 frames at 60 frames per second. The component shrank to a fifth of its size.

The peeking is CSS. At rest, the 200-pixel octopus is pushed 90 pixels off the left edge and rotated 25 degrees, so his eyes clear the wall while most of him doesn’t. Hovering or clicking slides him out and straightens him, with an easing curve that overshoots a little on the way. Reduced-motion preferences pause the animation. Below 1,200 pixels the container is hidden and the script returns before the library or the file is ever downloaded.

He had gone missing

While writing this, I checked the deployed corner in a headless browser. The animation file was requested four times. An octopus was mounted zero times.

The cause was a fix from December 2025. Astro’s view transitions swap the document on client-side navigation, so the mascot has to initialize again after each one. The script did that by also listening for the page-load event. On a fresh visit, that meant two initializations racing each other. The second one cloned the button to shed old listeners, then destroyed every animation by name, including the one the first pass had just attached to the real page. What remained was an empty box, translated 90 pixels off the left edge, quietly waiting for a click.

I don’t know for how long. Nobody said anything, which is the risk with a mascot who is designed to be easy to miss.

The fix is a flag on the button. The first pass to reach it wins, and a freshly swapped page arrives without the flag, so navigation still works. The animation loads exactly once. A browser test now opens the blog at desktop width, waits for the octopus, clicks him twice, and expects two different lines.

That wasn’t the only silence. Checking the database for this article, I found that the newest news comment was from 8 June. I can’t say exactly when the job started coming back empty, but by September the model it asked for had been retired from OpenRouter. The script read the error reply as an empty list, inserted nothing, and exited successfully. Inky had spent the summer with ten general jokes and no news, and the pipeline had no reason to complain.

Two changes. The sync now throws on a rejected model, a rejected key, or a reply without a JSON array, so a bad configuration stops the pipeline instead of passing quietly. And the model is a setting, with GPT-5.6 Luna as the default. I tried seven candidates with the real prompts on the day’s headlines. Two returned broken JSON, one took almost a minute, and Luna was fast, parsed every time, and sounded like him. A run over fifteen headlines costs a fraction of a cent. The funniest line of the afternoon came from DeepSeek, which I may yet regret not picking.

He's in the homepage terminal too

The homepage has a small terminal. inky prints one of the same comments. sudo reports the incident to Inky. rm -r is declined, on the grounds that he keeps the backups.

Same costume, same comment list. He is not a second personality there.

He reads the posts too

Since this week, Inky also has opinions about the articles. Move your mouse over a section of this post on a wide screen and he surfaces beside it with a line about what that section claims, builds or skips over. Each section gets one line, once per visit. Clicking him still gives you the news.

Those lines are not generated while you read. A small script runs once per post: it splits the article into its introduction and sections, sends each section to the same model with the same persona, and writes the answers into the post’s frontmatter next to the title and tags. They are content, reviewed like everything else in the file, and they only change when I rerun the script.

On a heckled post he also stops sitting in the corner. He follows the cursor along the left edge, so the bubble opens next to whatever you’re reading. Readers who prefer reduced motion keep him in the corner; the lines still show up there.

Why he stays in the corner

The site works if you never click him. Nothing important lives in the bubble, the animation waits for a wide screen, and the lines are a snapshot rather than a feed. Even the heckles are frontmatter. A phone reader loses a purple octopus and a few sentences of snark, nothing else.

That is also why the corner could stay empty for a while without anyone noticing, and why a summer without news went unremarked. He is optional on purpose. What has changed is that there is now a test that checks whether he showed up, and a sync that complains when he has nothing to say.