---
title: "Polymorphic Website"
summary: "Every page is one file, read as a page, a canvas, plain text or markdown."
date: 2026-10-25
links:
  - "file"
---

<!-- DRAFT: Axel to rewrite -->

## Problem

A website is read by browsers, terminals, scripts, crawlers and language-model agents. The usual approach serves HTML and adds exports such as RSS or an API. Each export is a copy of the content, and copies drift.

## Approach

Content is there to be accessed, and there is no reason for it not to be available everywhere. Each page is one markdown file, and each interface is a renderer over that file.

```mermaid
flowchart LR
  F[(about.md)] --> N{negotiate}
  N -->|browser| P[Page, HTML, 65,517 bytes]
  N -->|view=canvas| C[Canvas, a world over the page]
  N -->|curl or .txt| T[Terminal, 80 columns, 533 bytes]
  N -->|.md or Accept: text/markdown| A[Agent, markdown, 519 bytes]
  F --> L[llms.txt index]
```

## Solution

One function selects the renderer from the request.

```mermaid
flowchart TB
  R[request] --> S{ends in .md or .txt?}
  S -->|yes| SA[agent or terminal]
  S -->|no| Q{view= in the URL?}
  Q -->|yes| QV[that view, and remember it]
  Q -->|no| K{a remembered view?}
  K -->|yes| KV[that view]
  K -->|no| AC{asks for text/markdown?}
  AC -->|yes| AG[agent]
  AC -->|no| UA{a terminal client?}
  UA -->|yes| TE[terminal]
  UA -->|no| PG[page]
```

A `.md` or `.txt` suffix is checked first. A view chosen with `view=` is kept in a cookie for a year. The User-Agent check runs last, so `curl aesv.io` returns text with no flag.

```js
const BY_SUFFIX = { md: 'agent', txt: 'terminal' }
function pick({ suffix, query, cookies, accept, ua }) {
  return BY_SUFFIX[suffix]
      ?? (VIEWS.includes(query.view) && query.view)
      || (VIEWS.includes(cookies.view) && cookies.view)
      || (/text\/markdown/i.test(accept) && 'agent')
      || (TERMINAL_UA.test(ua) && 'terminal')
      || 'page'
}
reply.header('vary', 'Accept, User-Agent, Cookie').send(RENDER[pick(req)](file))
```

Every response sends `Vary: Accept, User-Agent, Cookie`, so a shared cache never serves curl's text to a browser.

```sh
$ curl aesv.io/about          # terminal: wrapped text, links as numbered notes
$ curl aesv.io/about.md       # agent: the file, with a small YAML head
$ open https://aesv.io/about  # page
```

Every device offers all four interfaces, except the watch, which offers the page and the canvas. Phones also get the terminal view. It costs nothing to have it. On a first visit, the welcome and the interface chooser open as two dialogs over the home page, so that crawlers and Google see the homepage data.

## Outcome

Response sizes for `/about` by client:

| Client                         | Body                     |
| ------------------------------ | ------------------------ |
| curl                           | 533 bytes, text          |
| `/about.md`                    | 519 bytes, markdown      |
| A browser                      | 65,517 bytes, HTML       |
| A browser asking for markdown  | 519 bytes, markdown      |
| A browser that chose terminal  | 533 bytes, text          |

The home page is 6,821 bytes to curl and 93,920 bytes to a browser. Usage per interface was not measured.

## Applicability

The design fits any site that people and programs both read, such as documentation, public services and timetables. HTTP content negotiation already serves several representations at one URL.
