< the skull

CURL PABLOMURAD.COM
===================
2026-10-14

Open a terminal and type:

`curl pablomurad.com`

Since 14 October 2026, that command has opened my blog in the terminal. Not a
dump of the HTML you would normally get in a browser, but a separate version
with colors, posts, archives and full articles, all formatted for an 80-column
window.

The website itself hasn’t changed. Visit it in a browser and you’ll see the same
site as before.

One Windows detail: PowerShell has its own ideas about what curl means. Use
curl.exe there.

Where the idea came from
========================

I found the idea on nytpu’s blog, in a post from 18 March 2026 called “Fancy
Curl Sites [1]” (nytpu.com/gemlog/2026-03-18). nytpu had seen it on trinkey.com
and proot.party. All three sites are worth trying with curl.

The trick is wonderfully simple. The server looks at the User-Agent, notices
that the request came from curl and sends back a text file with ANSI escape
codes instead of a regular web page.

nytpu wrote the terminal home page by hand and gets the posts from a markup
language they built, which already knows how to produce terminal output. My
setup is different. The blog is a static site built with Eleventy, and its posts
come from PaCord, my own fork of Pagecord. PaCord stores and serves the posts;
it doesn’t render terminal pages.

I wanted the entire blog available this way, not just a clever home page. And I
certainly didn’t want to maintain two versions of every post.

After years on BBSes, and after building Hammer, my little BBS that you connect
to over SSH, I was unlikely to leave the idea alone.

What you see in the terminal
============================

The home page begins with the site name in ASCII art. I made it once with
figlet’s slant font and kept the result as a constant. There’s a short line
about me, followed by the twenty latest posts. Each entry includes the command
to open it:

2026-09-10 Hammer
$ curl pablomurad.com/hammer/

Under the list are shortcuts to the archive, tags, about page and RSS feed,
along with links to my profiles elsewhere.

Open a post and you get its title highlighted in the same lime green I use on
the site, followed by the date, tags and body text. Paragraphs wrap at 72
columns. Code blocks keep their original lines with four spaces of indentation.
Quotes have a bar on the left, and nested lists stay nested.

Images become numbered placeholders such as \[image: description\]. Links stay
in the prose with a number in brackets, then all the addresses appear at the
bottom, much like they do in Lynx. Related posts are listed as curl commands. If
the post also lives on Mastodon, Bluesky or Nostr, those copies appear under
“Also on”.

Tables took more work than I expected.

Some posts have tables with cells far too long for 72 columns. If a table fits,
I draw it with aligned columns. If it doesn’t, each row turns into a little
block: the first cell is bold, with the remaining cells underneath as “Header:
value”. It sounds awkward, but it’s much easier to read than a table spilling
off the side of the screen.

The archive, tag pages, ordinary pages and even the 404 page have terminal
versions. The search page, photos and newsletter form don’t. Those need a
browser, so curl still receives HTML for them.

Building it without another publishing job
==========================================

The converter lives in one file, lib/terminal.js. It’s a few hundred lines and
adds no dependencies. The site already uses cheerio to work on images and
headings, so I reused it to walk through each post’s HTML.

Paragraphs become wrapped text, strong becomes bold, em becomes italic, and code
gets cyan. The wrapping logic measures what you can actually see on screen,
ignoring ANSI escape codes because they occupy no columns. It also closes and
reopens styles when a line wraps, so a bold word doesn’t accidentally turn the
next quote bar or list marker bold.

For colors, I stuck to the basic ANSI codes and the 256-color palette.
Everything I tested understood them. The highlight uses color 191, the closest
match I could get to the site’s #d4ff3a. It’s a lime background with black
letters. Lime-colored text disappears on some light terminal themes; black on
lime works on both light and dark ones.

I also kept the layout entirely in ASCII: #, \*, | and -. The old Windows
console can make a mess of box-drawing characters, and I’d rather have something
plain that works.

Eleventy needed just one more template. It loops over the screens produced by
the converter, using the same data as the HTML pages, and writes an index.ansi
beside each index.html:

dist/hammer/index.html
dist/hammer/index.ansi

Publishing hasn’t changed at all. I write a post in PaCord, it calls the
builder, Eleventy runs, and the terminal copy comes along for free. The full
build still takes about 17 seconds on the server.

Letting nginx decide
====================

The last piece is nginx. The build generates a rules file, which I include
inside the server block. I couldn’t use map there, since that directive belongs
in the http context, but set and if were enough for this:

set $terminal\_index index.html;
set $not\_found /404.html;
if ($http\_user\_agent ~\* "^curl/") {
set $terminal\_index index.ansi;
set $not\_found /404.ansi;
}

There’s also a small exception for requests carrying the X-Original header,
which switches the index back to index.html. The idea is to keep the terminal
response where it belongs without changing what the normal site serves.

And that’s it. No separate service, no hand-written terminal pages, no extra
step whenever I publish. Just another way into the same blog.

I’ve caught myself reading my own posts through curl now. I suppose that means
the experiment worked.

Links:
  [1] https://nytpu.com/gemlog/2026-03-18


< the skull