08.02.2026
How to create a simplistic blog for your terminal
Why browse when you can curl
You might already know about or have used things like wttr.in or cheat.sh.
There is a ton more you can explore on this list when it comes to accessing services from the shell (awesome-console-services) .
All those sites can be visited with curl and provide a terminal-centric experience.
Of course this nerd-sniped me enough to try this out for my own blog.
There are are three things that I need to accomplish while implementing this:
- Parse the same blog posts from sources as the original blog
- Introduce colorization not only for blog posts but also the index page
- Combine all above and put the output in a target directory
- Make the blog server seamless provide the terminal compatible pages instead of the HTML page
Parse pages for the blog
As described in a previous post the blog pages are written in Markdown with metadata provided as Front Matter which allows to add some metadata to the files, like this
---
layout: post
file_name: blog_infrastructure
title: "Maintain a simplistic blog"
description: "Hacking around to be lazy later"
date: 2023-08-07
categories: git obsidian ios
---
So this leaves me with two problems. First extract the metadata for generating the file names and the additional information like date and categories and title. After this render the whole content to a new file which has render highlights.
Using yq for metadata extraction
Taking the Front Matter example from above, let's see what yq can extract.
> yq --front-matter=extract 'Maintain a simplistic blog.md'
---
layout: post
file_name: blog_infrastructure
title: "Maintain a simplistic blog"
description: "Hacking around to be lazy later"
date: 2023-08-07
categories: git obsidian ios
Which looks pretty much like the block that is in the Markdown source.
To get the individual fields, their names can be added to the command.
> yq --front-matter=extract ".file_name" 'Maintain a simplistic blog.md'
blog_infrastructure
With this I have all at hand to generate the correct output files and use the metadata for the index page.
Render the blog post
For rendering I had two options in mind: bat (https://github.com/sharkdp/bat) or glow (https://github.com/charmbracelet/glow/).
Not because I did a great research but because both are already installed on my machine.
After tinkering around with bat first, I decided to go with glow, because I couldn't create output that contained ANSI colors to use it later.
But also with glow it was not straight forward to get the output as expected.
There are two mandatory environment variables that needs to be set when invoking the command: CLICOLOR_FORCE=1 and COLORTERM=truecolor
Which will result in "a little bit" overhead for the colors and font-style when produced than you would add manually, but maybe this is the pill to swallow when using generators.
ESC[94;1mESC[0mESC[94;1mESC[0m ESC[94;1m## ESC[0mESC[94;1mDisclaimerESC[0mESC[37mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m
ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[37m ESC[0mESC[0m
("renderable" output for the first lines which only produce an header, replace the ESC with \x1b commands mentally)
The colored index page
This is all based on the idea of using ANSI color escapes. While I was looking for more information, I stumbled over this nice stackoverflow answer, which contains even more information on text styling and navigation boiled down from the Wikipedia ANSII escape code page.
Short ANSI color command detour
But let's start simple. If you jumped to the linked answer above already, this is nothing new, but here is a quick excerpt on the usage.
Let's say you want to display a text in the terminal in red. This would be the definition of
\x1b - escape sequence (can also be \033)
31m - color for red foreground
While this is taken from the original specification which only had 8 colors. I am aiming big here and want to choose from the whole 8-bit table ;) This way the command gets a little bit more complex
\x1b - escape sequence (can also be \033)
38;5; - we want to set the foreground color
9m - basic color value for red
If you want to use even more, you can also go full 24-bit and define every value individual by using ESC[38;2;⟨r⟩;⟨g⟩;⟨b⟩m which would be ESC[38;2;255;0;0m for red.
As you have already seen in the Markdown output, it's always a good idea to make use of the reset command sequence after the color -or any other- definition: ESC[0m.
Using colors
With this, I could define a small color reference in my script.
DATE_COLOR=$'\x1b[38;5;226m'
TITLE_COLOR=$'\x1b[38;5;211m'
LINK_COLOR=$'\x1b[38;5;14m'
CATEGORIES_COLOR=$'\x1b[38;5;32m'
RST=$'\x1b[0m'
And later reference it to style my texts
format_title() {
echo -n "${TITLE_COLOR}$1${RST}"
}
post_title=$(yq --front-matter=extract ".title" "$n")
format_title "$post_title"
Automate and build
As you have already guessed by the examples, the whole build is meant to be executed in the terminal with some command line tools. The basic idea is to prepare an index page with some static content / "logo" and then loop over the Markdown sources, convert them to text files, and as a side effect extract the metadata as text for the post list. All this could be invoked by a simple shell script, but let's get a little bit nerdy here and use just here to have a nicer invocation.
My justfile looks like this
clear:
rm -rf output
build:
mkdir output
./create-header.sh > output/index
./add-posts.sh $POST_SOURCES output >> output/index
The benefit of using just is to make use of environment variables, in contrast to a Makefile.
As already mentioned, the create-header.sh is pretty much simple, so assume it just creates the index file.
The most work is done in the add-posts.sh:
#!/usr/bin/env bash
SOURCE_DIR=$1
OUTPUT_DIR=$2
DATE_COLOR=$'\x1b[38;5;226m'
TITLE_COLOR=$'\x1b[38;5;211m'
LINK_COLOR=$'\x1b[38;5;14m'
CATEGORIES_COLOR=$'\x1b[38;5;32m'
RST=$'\x1b[0m'
format_title() {
echo -n "${TITLE_COLOR}$1${RST}"
}
format_date() {
echo -n "${DATE_COLOR}$1${RST}"
}
format_link() {
echo -n "${LINK_COLOR}$1${RST}"
}
format_categories() {
echo -n "${CATEGORIES_COLOR}$1${RST}"
}
while IFS= read -r -d '' n; do
post_categories=$(yq --front-matter=extract ".categories" "$n")
post_title=$(yq --front-matter=extract ".title" "$n")
file_name=$(yq --front-matter=extract ".file_name" "$n")
post_date=$(yq --front-matter=extract ".date" "$n")
read -r -a post_cat_array <<< "$post_categories"
cat "$n" | CLICOLOR_FORCE=1 COLORTERM=truecolor glow --style dark --width 120 > $OUTPUT_DIR/$file_name
formatted_date=$(format_date $post_date)
formatted_title=$(format_title "$post_title")
formatted_link=$(format_link $file_name)
formatted_categories=$(format_categories "$post_categories")
entries+="$formatted_date - $formatted_title - /$formatted_link ($formatted_categories)"
entries+=$'\n'
done < <(find "$SOURCE_DIR" -iname "*.md" -type f -print0)
entries_sorted=$(echo -n "$entries" | sort -r)
echo "${entries_sorted}"
I iterate over all markdown files in the $SOURCE_DIR and, as explained above, extract all the metadata, convert the markdown file (the glow command invocation) and add the information to an entries list to be displayed in the index page.
This list gets sorted by date, yes this is why it's the first bit in the item string, and will be echoed to be added to the index page.
And with all of this, the text-only blog will be put into the output directory.
Serving the curl clients
The content will be uploaded into a subdirectory called curl on the web server. Now the only thing left to do is to configure the server (nginx) to serve those pages instead of the HTML version of the blog.
This is pretty much easy to do because there is a directive in nginx which allows us to check for the User-Agent provided by the client. Whenever a user now calls my blog using the an User-Agent containing "curl", the request will be rewritten to look into the curl subdirectory (of the server definition's root directory). This location will serve the content as it would serve the html pages.
location / {
if ( $http_user_agent ~* 'curl' ) {
rewrite ^/(.*)$ /curl/$1 last;
}
try_files $uri $uri/ =404;
}
location /curl {
index index;
try_files $uri $uri/ =404;
}
Calling this blog now with curl 'https://meistermeier.com' returns now
_ _ _
(_) | | (_)
_ __ ___ ___ _ ___| |_ ___ _ __ _ __ ___ ___ _ ___ _ __
| '_ ` _ \ / _ \ / __| __/ _ \ '__| '_ ` _ \ / _ \ |/ _ \ '__|
| | | | | | __/ \__ \ || __/ | | | | | | | __/ | __/ |
|_| |_| |_|\___|_|___/\__\___|_| |_| |_| |_|\___|_|\___|_|
2024-12-28 - Spring Actuator Security - /spring-actuator-security (java spring)
2024-11-01 - Enable Cypher Language Server for Neovim - /cypher_ls_neovim_part2 (neovim cypher neo4j)
2024-02-23 - Spring AI with Neo4j Vector Index - /spring-ai-neo4j (neo4j ai cypher)
2023-12-04 - Cypher Language Server support for Neovim - /cypher_ls_neovim (neovim cypher neo4j)
2023-08-07 - Maintain a simplistic blog - /blog_infrastructure (git obsidian ios)
2023-07-06 - First experiences with DuckDB (and SQL in general after a while) - /duckdb-part1 (duckdb sql)
2023-06-22 - Using Spring for GraphQL With Spring Data Neo4j - /sdn-graphql-1 (graphql spring neo4j)
and using e.g. the first link in the list, invoking curl (with -s for silent) again and passing it on to e.g. less with the -R flag to preserve colors (curl -s 'https://meistermeier.com/spring-actuator-security' | less -R) gives a readable version for consuming the content on the terminal with nice formatting.
Was it needed? Definitely not, but it was so much fun to just hack some nerdy things together. From the idea to execution it might have taken less than writing this post. And why not also add this option to your page ;)