Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Every page on the site is a `.md` file in the root of this repository. There is
## How a page is served

- `/` is `index.md`. `/manifesto` and `/manifesto.md` are both `manifesto.md`. A missing page is `404.md` with a 404 status.
- A browser (anything that sends `Accept: text/html`) gets `text/plain`, so the source displays inline instead of downloading. Everything else gets `text/markdown`.
- A browser (anything that sends `Accept: text/html`) gets the verbatim source in a `<pre>`, with Markdown links and bare URLs made clickable, because plain text in a browser has no links. Nothing is rendered. Everything else gets raw `text/markdown`.
- `/<page>.html` redirects to the readm3.com render of that page.
- Every response carries `Access-Control-Allow-Origin: *`, so a reader on any origin can fetch a page. That is what the **html** link at the bottom of each page relies on: it hands the file to [readm3.com](https://readm3.com), which renders it in the browser.
- Only `.md` files are served. Dotfiles, the server, this README's neighbours in `.github`: none of it is reachable.
- `www.` redirects to the apex.
Expand Down
2 changes: 1 addition & 1 deletion index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ More documents land here as they get written.

Every page is a Markdown file and nothing else. No HTML, no JavaScript, no build step.

Read it here as text, fetch it with curl, pipe it through readm3, or click **html** at the bottom of any page to render it with readm3.com.
In a browser you see the source as written, links included. Fetch it with curl, pipe it through readm3, or click **html** at the bottom of any page to render it with readm3.com. Swapping `.md` for `.html` in the address does the same.

curl https://fleetsysops.com/manifesto.md
curl -s https://fleetsysops.com/manifesto.md | npx @profullstack/readm3 --print
Expand Down
30 changes: 26 additions & 4 deletions server.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { handle, resolve, root, SITE } from "./server.ts";
import { browserPage, handle, resolve, root, SITE } from "./server.ts";

const get = (path: string, init: RequestInit & { host?: string } = {}) => {
const { host, ...rest } = init;
Expand All @@ -20,11 +20,30 @@ describe("the Markdown server", () => {
expect(await response.text()).toStartWith("# Fleet SysOps");
});

test("gives a browser text/plain so the source displays instead of downloading", async () => {
test("gives a browser the verbatim source in a <pre> with the links clickable", async () => {
const response = get("/manifesto.md", { headers: { accept: "text/html,application/xhtml+xml,*/*;q=0.8" } });
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/plain; charset=utf-8");
expect(await response.text()).toContain("We test in prod.");
expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
const html = await response.text();
expect(html).toContain("<pre>");
expect(html).toContain("- We test in prod.");
expect(html).toContain('<a href="https://readm3.com/viewer?url=https://fleetsysops.com/manifesto.md">[html](https://readm3.com/viewer?url=https://fleetsysops.com/manifesto.md)</a>');
expect(html).toContain("<title>manifesto.md · fleetsysops.com</title>");
});

test("escapes the source in the browser view and links bare URLs", () => {
const html = browserPage("# T\n\n<script>alert(1)</script> see https://example.com/a?b=1 and [x](/y.md).", "t.md");
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
expect(html).toContain('<a href="https://example.com/a?b=1">https://example.com/a?b=1</a>');
expect(html).toContain('<a href="/y.md">[x](/y.md)</a>');
});

test("redirects /<page>.html to the readm3.com render, and 404s an unknown one", async () => {
const response = get("/manifesto.html");
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("https://readm3.com/viewer?url=https://fleetsysops.com/manifesto.md");
expect(get("/nope.html").status).toBe(404);
});

test("resolves an extensionless path to the .md file and strips a trailing slash", async () => {
Expand All @@ -45,6 +64,9 @@ describe("the Markdown server", () => {
expect(response.status).toBe(404);
expect(response.headers.get("content-type")).toBe("text/markdown; charset=utf-8");
expect(await response.text()).toStartWith("# Not found");
const browser = get("/nope", { headers: { accept: "text/html" } });
expect(browser.status).toBe(404);
expect(await browser.text()).toContain("# Not found");
});

test("never serves anything that is not a Markdown file", () => {
Expand Down
70 changes: 56 additions & 14 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
*
* Every document is a `.md` file in this directory, served as text. `/` is `index.md`,
* `/manifesto` is `manifesto.md`, and a missing page is `404.md` with a 404 status.
* Browsers get `text/plain` so they show the source instead of offering a download;
* everything else (curl, agents, readm3.com fetching a page to render it) gets
* `text/markdown`. Every response allows any origin, which is what lets the `html`
* link at the bottom of each page hand the file to readm3.com.
* Browsers get the exact source in a `<pre>`, with every link clickable, because a
* browser shows plain text as dead characters and the `html` link at the bottom of
* each page has to be a link. Everything else (curl, agents, readm3.com fetching a
* page to render it) gets raw `text/markdown`. `/<page>.html` redirects to the
* readm3.com render. Every response allows any origin, which is what lets readm3.com
* fetch the file.
*
* Nothing that is not a Markdown file is ever served.
*/
import { existsSync, readFileSync, statSync } from "node:fs";
import { join, normalize, sep } from "node:path";
import { join, normalize, relative, sep } from "node:path";

export const root = import.meta.dir;
export const SITE = (process.env.SITE_URL ?? "https://fleetsysops.com").replace(/\/$/, "");
Expand All @@ -34,10 +36,44 @@ export function resolve(pathname: string): string | null {
return null;
}

/** Browsers announce text/html; they get plain text so the Markdown displays inline. */
/** A browser announces text/html; everything else is a program that wants the Markdown. */
function isBrowser(request: Request): boolean {
return (request.headers.get("accept") ?? "").includes("text/html");
}

function contentType(request: Request): string {
const accept = request.headers.get("accept") ?? "";
return accept.includes("text/html") ? "text/plain; charset=utf-8" : "text/markdown; charset=utf-8";
return isBrowser(request) ? "text/html; charset=utf-8" : "text/markdown; charset=utf-8";
}

const escape = (text: string) => text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!);

/**
* The browser view: the Markdown source, verbatim, in a <pre>, with Markdown links and
* bare URLs wrapped in anchors so they can be clicked. Nothing is rendered; a reader
* who wants rendering follows the html link.
*/
export function browserPage(source: string, name: string): string {
const link = /\[([^\]\n]+)\]\(((?:https?:\/\/|\/)[^\s)]+)\)|https?:\/\/[^\s<>)"']+/g;
let html = "";
let last = 0;
for (const match of source.matchAll(link)) {
html += escape(source.slice(last, match.index));
const href = match[2] ?? match[0];
html += `<a href="${escape(href)}">${escape(match[0])}</a>`;
last = match.index + match[0].length;
}
html += escape(source.slice(last));
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escape(name)} · fleetsysops.com</title>
<style>
:root{color-scheme:light dark}
body{margin:0;background:#fff;color:#111}
pre{margin:0;padding:24px 16px;white-space:pre-wrap;overflow-wrap:anywhere;font:15px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;max-width:80ch}
a{color:inherit;text-decoration:underline;text-underline-offset:3px}
@media (prefers-color-scheme:dark){body{background:#0d1117;color:#e6edf3}}
</style></head><body><pre>${html}</pre></body></html>
`;
}

function headers(request: Request, extra: Record<string, string> = {}): Headers {
Expand Down Expand Up @@ -68,14 +104,20 @@ export function handle(request: Request): Response {
return Response.redirect(`${url.origin}${url.pathname.replace(/\/+$/, "")}${url.search}`, 308);
}

const file = resolve(url.pathname);
if (file) {
const body = request.method === "HEAD" ? null : readFileSync(file);
return new Response(body, { status: 200, headers: headers(request) });
if (url.pathname.endsWith(".html")) {
const page = url.pathname.slice(0, -5);
if (resolve(`${page}.md`)) return Response.redirect(`https://readm3.com/viewer?url=${SITE}${page}.md`, 302);
}

const file = resolve(url.pathname);
if (file) return respond(request, readFileSync(file, "utf8"), 200, file);
const missing = join(root, "404.md");
const body = existsSync(missing) ? readFileSync(missing) : "not found\n";
return new Response(request.method === "HEAD" ? null : body, { status: 404, headers: headers(request, { "cache-control": "no-store" }) });
return respond(request, existsSync(missing) ? readFileSync(missing, "utf8") : "not found\n", 404, missing, { "cache-control": "no-store" });
}

function respond(request: Request, source: string, status: number, file: string, extra: Record<string, string> = {}): Response {
const body = isBrowser(request) ? browserPage(source, relative(root, file)) : source;
return new Response(request.method === "HEAD" ? null : body, { status, headers: headers(request, extra) });
}

if (import.meta.main) {
Expand Down
Loading