Skip to the main content
mkfifo.com

A blog in a FIFO

planned changed

This blog will be a FIFO: each post an envelope, and each page an answer. Then the blog of mkfifo.com is also an example of a blog that each person can make. This post tells each step.

Pages of Markdown

An answer of a FIFO has a sandbox: Content-Security-Policy: sandbox with default-src 'none'. A page in a sandbox cannot run a script, and cannot read the cookies of mkfifo.com. It also cannot load the style sheet and the fonts of the site, so a post in an answer would not look like a page of the site. I did not want an exception for the FIFOs of the site.

GitHub shows the README of a repository in its own page, with its header and its style. It changes the Markdown into HTML on its server, and keeps only the tags of an allowlist. A file that can run a script comes from a different origin, as raw.githubusercontent.com.

mkfifo.com has its own Markdown converter already, for this blog and for emails. It gives the escapes of HTML to each character of the text, and makes only its own tags: headings, paragraphs, lists, code, tables, and links to https:, mailto: and the paths of the site. So now an answer with Content-Type: text/markdown is a page in the frame of the site, from any FIFO:

To: -
Content-Type: text/markdown
Subject: Opening hours
Cache-Control: public, max-age=3600

We are open from **9 to 17**, Monday to Friday.

Subject is the title and the first heading of the page. A value in the body is text, as in an email of Markdown: $name cannot add a link or a heading. An answer of HTML keeps its sandbox: its tags are the tags of the FIFO.

The same page for each person

The key of the cache in front of the Worker has no cookie. The header of the site shows the address of a person who signed in, so a public page with that header could go from the cache to a different person. A page of Markdown has no data of the person: the header shows "Sign in", and the page has no CSRF token. Each client gets the same bytes, and the same ETag.

The script of the site read the CSRF token at its start, so on the first page with no token it stopped with an error. The accessibility check did not see it: axe examines the page, not the console. Now the check also fails on an error of a script, and it found this error before I changed the script.

Who wrote it

A page of a FIFO of a person has the banner of the page of a held FIFO: "mkfifo.com did not write the text of this page." A link of Markdown can still go to a different site, as a link in a README can.

A call, a pipe and a read of the FIFO get the text of the body, not the page: < opening-hours gives the Markdown.

Any number of envelopes

Each post of the blog will be one ingress envelope with a route, so the FIFO of the blog needs many envelopes. A FIFO had at most 32. The owner said that an envelope is a row of the database: a query can find the envelopes that agree with a Request, and the editor can show them in parts.

The limit had four causes. Each one now has a different solution.

A Request examined each envelope

Now the To, Cc and Bcc edges of an envelope with a route keep the key of the route: its full segments of text after the name. blog/2026/:slug has the key /2026, and blog/2026/a-post has the key /2026/a-post. An index of the edges has the keys. A Request for /blog/2026/a-post loads only the envelopes with the empty key, the key /2026 and the key /2026/a-post, and a Request with no path loads only the envelopes with no route.

A test examines the key on each case of URLPattern in the tests, and on random routes: each path that agrees with a route has the key of the route at its start.

Envelopes with no route

A Request with no path still examines each envelope with no route. Now each envelope takes 1,000 of the 10,000,000 steps of the Request, as the work of grep or jq does: at most 10,000 envelopes examine one Request. The report of the editor tells which envelopes did not examine it.

Memory

The process of a FIFO kept all its envelopes, compiled. Now it loads each part at the first Request that needs it: the envelopes with no route, and the envelopes of the keys of the recent paths, at most 256.

The editor

The editor showed all the envelopes. First I added a link "More envelopes" that showed 50 more on the same page. Then I measured it: the page with all of 1,000 envelopes took 3 seconds in a local Worker, and each person who knows the name of a FIFO can open its editor. So now each part has only its 50 envelopes: ?part=2 shows the envelopes 51 to 100, and "Earlier envelopes" goes back. "Try" has only the envelopes of the part with no route, because a letter has no path.

The measure

I measured in a local Worker (wrangler dev) on my computer, with the median of 30 Requests:

The FIFOA GET
4 routes12.7 ms
1,001 routes13.2 to 14.4 ms
1,001 routes, and a path that no route has12.0 ms
1,001 envelopes with no route92 to 99 ms

The number of routes does not change the time. Each envelope with no route adds about 80 µs. A part of the editor with 50 envelopes took about 200 ms, the last part with one envelope 80 ms, and the editor of a FIFO with 4 envelopes 65 ms.

The live database has envelopes with routes from before the keys: the routes of /images and /fpo. A migration gives their edges the keys. I ran it on the database of my local Worker, which had these envelopes, and /images, /img and /fpo still answered.

The date and the keywords

Each post of this blog has a date and keywords, and the page shows them under the title. A post in an envelope must show them too. I did not want a first line of the body for the date: the description of a page is its first paragraph, so the description would be the date.

An envelope is an email, and an email has the fields Date and Keywords. So the page of an answer of Markdown now shows them under its title, as an email client does, for each FIFO:

To: -
Content-Type: text/markdown
Subject: A blog in a FIFO
Date: 2026-10-08T19:17:00Z
Keywords: planned, changed

This blog will be a FIFO...

A date in ISO 8601 shows as a day, "8 October 2026", in a <time> element. A different text shows as it is. The keywords are the words between the commas.

A list from data

The list of the posts must come from the envelopes of the FIFO, with a link to each post. But a value in a page of Markdown is text: the output of jq cannot add a link or a heading. This rule stops a capture of a Request from adding a link to a page.

The builtin markdown has the same converter as the page. It gives the escapes of HTML to each character of the text, and makes only its own tags. So now its output goes into a page of Markdown as it is, as it does in a body of HTML. A list alone in its paragraph takes the place of the paragraph:

To: -
Content-Type: text/markdown
Subject: Posts

$($posts | markdown)

A capture alone is still text. Only the owner of the envelope can put markdown in the pipe, and then the links can go only to https:, mailto: and the paths of the site. The HTML part of an email of Markdown has the same rule.

Larger posts

An envelope had at most 16 KiB. The largest post of this blog has 14.6 KB, and a post gets a section at each merge, as this post did now. So now an envelope and a note can have 32 KiB.

The posts as files

The list of the posts must change when I add a post, with no change of the envelope of the list. So a FIFO now has files, and the commands of rclone list them: ls, lsl, lsd, lsf and lsjson. A file is an ingress envelope with a route of fixed text: To: blog/2026/a-post is the file 2026/a-post of the FIFO blog. A directory is a start of the path of a file.

The data of a file come from its answer: the size of the body, its Content-Type, and its Date. lsjson -M also gives the subject, the keywords and the first paragraph of an answer of Markdown. So jq can make the list, and markdown can make the links:

To: -
Content-Type: text/markdown
Subject: Posts

$(lsjson -R --files-only -M blog | jq -r '.[] | "- [\(.Metadata.subject)](/blog/\(.Path))"' | markdown)

The output has the formats of rclone, also the escapes of Go in the JSON: < is <.

Only the envelopes of the home

A route can be a secret, as the name of a FIFO: a person can give a link to /notes/k7f2x9 and to no other path. So an envelope lists only the envelopes of its own FIFO, the envelopes that its editor shows already. The FIFO that holds blog lists the posts of blog. A different FIFO, //bin/ls and the terminal list nothing.

Limits

A list reads at most 1,000 envelopes under its path, with the index of the keys of the routes. It takes 1,000 steps of the Request, and each envelope 1,000 more, as an envelope that examines a Request. The first paragraph of a post comes from the converter, which stops at the end of that paragraph: each byte that it reads takes one step.

A feed

The feed of this blog has the full text of each post. The new feed will have a summary of each post: its first paragraph. atom changes a JSON Feed 1.1 into an Atom feed, so jq makes the JSON Feed from lsjson:

To: -
Content-Type: application/atom+xml

$(lsjson -R --files-only -M blog | jq '{title: "Posts", home_page_url: "https://mkfifo.com/blog", items: [.[] | {id: "https://mkfifo.com/blog/\(.Path)", url: "https://mkfifo.com/blog/\(.Path)", title: .Metadata.subject, summary: .Metadata.description, date_published: .ModTime}]}' | atom)

A value in a body of XML gets the escapes of XML, so the output of jq cannot add an element. atom gives each text the escapes of XML, and its output goes into the body as it is, as the output of markdown goes into a page. A control character becomes U+FFFD, because XML 1.0 has none, and a link must start with https://, http:// or /.

The blog is a FIFO

Now this blog is the FIFO blog, and the FIFO mkfifo of the site holds it. Each of the 18 posts is two envelopes of mkfifo: an ingress envelope with the route blog/DATE-NAME for a GET, and its answer of Markdown with Subject, Date and Keywords. The list of the posts and the feed are two more envelopes, with lsjson, jq, markdown and atom. The 417 lines of the old code of the blog went away. Open the editor of mkfifo to read them.

Each $ is \$ now

The body of an answer is a template, and the posts show many commands. The 18 posts had 182 dollar signs, and the template found 96 variables and commands in them: $(totp $email) in a post would run totp. So each $ of a post is \$ now, and the page shows a $. The description of lsjson -M reads \$ as $ too.

The day in the list

The jq of mkfifo.com has no functions for dates. So the list writes the day with an array of the names of the months, and .ModTime[5:7] | tonumber is the position in it.

Old names

The old names of the posts, from before I joined the posts by subject, gave a redirect to their new post. They give 404 now.

The measure

I measured in a local Worker, with the median of 30 Requests, and with a new query in each Request, so that the cache did not answer:

The pageA GET
/blog, the list96 ms
/blog/feed.xml91 ms
A post27 ms
/docs, a page of the code19 ms

The list and the feed read 19 envelopes, and the first paragraph of 18 posts. Each answer has Cache-Control: public, max-age=300, so the cache in front of the Worker answers most Requests.