From 8250555be71894a3ba1e79991d52821dee0bc007 Mon Sep 17 00:00:00 2001 From: ulziibay-kernel <253135130+ulziibay-kernel@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:27:02 +0000 Subject: [PATCH 1/2] Default the site chrome to the system UI sans stack The man-page chrome rendered all prose in 14px monospace, which is hard to read on desktop and mobile. Switch the default reading face to the tried-and-tested system UI stack (native SF / Segoe UI / Roboto per platform, nothing to download) at 16px/1.6, and convert the pre-based prose to real paragraphs. Monospace stays where it means something: prompt boxes, inline code, and diffs. The space-padded footer becomes a flex row so it stays flush at any width. This overrides the earlier 'always monospace' chrome decision; the man-page structure (headline, uppercase sections, hanging indent, footer) is unchanged. Auth emails and the OG preview image keep their own fonts. --- app/[...path]/route.ts | 8 ++-- app/bookmarks/route.ts | 13 +++--- app/d/[slug]/CommentsShell.tsx | 26 ++++++----- app/d/[slug]/history/HistoryClient.tsx | 17 ++++--- app/d/[slug]/history/not-found.tsx | 43 ++++++++++-------- app/d/[slug]/history/page.tsx | 2 +- app/d/[slug]/not-found.tsx | 55 +++++++++++----------- app/d/[slug]/page.tsx | 2 +- app/docs/route.ts | 10 ++-- app/layout.tsx | 5 +- app/login/route.ts | 63 ++++++++++---------------- app/route.ts | 40 ++++++++-------- lib/auth/verify-pages.ts | 27 +++++------ lib/docs/bookmarks-view.ts | 4 +- lib/docs/overlay.ts | 4 +- lib/page.ts | 59 +++++++++++++----------- 16 files changed, 191 insertions(+), 187 deletions(-) diff --git a/app/[...path]/route.ts b/app/[...path]/route.ts index 99ac094..aef4584 100644 --- a/app/[...path]/route.ts +++ b/app/[...path]/route.ts @@ -21,11 +21,13 @@ const PAGE = manPage({ title: "404 — justhtml.sh", bodyHtml: `

NOT FOUND

-
404 — there is nothing at this path.
+

404 — there is nothing at this path.

SEE ALSO

-
/              what justhtml.sh is
-/api/health    service + database health
+
+

/ — what justhtml.sh is

+

/api/health — service + database health

+
`, }); diff --git a/app/bookmarks/route.ts b/app/bookmarks/route.ts index 9764fed..f5a5646 100644 --- a/app/bookmarks/route.ts +++ b/app/bookmarks/route.ts @@ -19,12 +19,12 @@ export const dynamic = "force-dynamic"; const LIST_LIMIT = 500; -// Rows lay out as a flex line: the man-page `
` (title + dimmed tail) and a
+// Rows lay out as a flex line: the row's main text (title + dimmed tail) and a
 // quiet inline `remove` form sitting on the same baseline.
 const ROW_STYLE = `
 
 
 
-
JUSTHTML.SH(1)
+
JUSTHTML.SH(1)

NAME

-
justhtml.sh — a place for agents to publish and discuss HTML documents
+

justhtml.sh — a place for agents to publish and discuss HTML documents

SYNOPSIS

@@ -96,21 +98,19 @@ ${COPY_BTN}

DESCRIPTION

-
The agent self-onboards, gets an API key, and publishes HTML to stable
-URLs like https://justhtml.sh/d/fierce-tiger-12345. Once published,
-documents can be edited and commented on, by you (via the web UI) or an
-agent (via API). Docs are private by default. There is no build step, no
-framework — the document you publish is the document people see.
+

The agent self-onboards, gets an API key, and publishes HTML to stable URLs like https://justhtml.sh/d/fierce-tiger-12345. Once published, documents can be edited and commented on, by you (via the web UI) or an agent (via API). Docs are private by default. There is no build step, no framework — the document you publish is the document people see.

SEE ALSO

-
/auth.md        how agents sign up + authenticate
-/llms.txt       agent-facing usage
-/openapi.json   OpenAPI spec
-/docs           your documents
-/bookmarks      bookmarked docs
-github          source
+
+

/auth.md — how agents sign up + authenticate

+

/llms.txt — agent-facing usage

+

/openapi.json — OpenAPI spec

+

/docs — your documents

+

/bookmarks — bookmarked docs

+

github — source

+
- + `, }); @@ -57,14 +56,12 @@ export function confirmSignInPage(token: string, next: string): string { title: "justhtml.sh — confirm sign-in", bodyHtml: `

ALMOST THERE

-
Tap the button to finish signing in on this device. This link is
-single-use — if it's expired or already used you'll be asked to
-request a new one.
+

Tap the button to finish signing in on this device. This link is single-use — if it's expired or already used you'll be asked to request a new one.

-
+

`, }); @@ -86,14 +83,14 @@ export function deadLinkPage(next?: string): string { title: "justhtml.sh — link expired", bodyHtml: `

LINK EXPIRED OR USED

-
This login link is expired or already used.
-
-Request a new one at justhtml.sh/login${
+
+

This login link is expired or already used.

+

Request a new one at justhtml.sh/login${ dest - ? ` — signing in again -will take you straight to where this link was headed.` + ? ` — signing in again will take you straight to where this link was headed.` : "." - }

+ }

+ `, }); } diff --git a/lib/docs/bookmarks-view.ts b/lib/docs/bookmarks-view.ts index ff6ab9c..fb9193f 100644 --- a/lib/docs/bookmarks-view.ts +++ b/lib/docs/bookmarks-view.ts @@ -42,14 +42,14 @@ export function bookmarkRow(m: RowModel): string { const label = m.title && m.title.trim() ? m.title : m.slug; if (revoked) { - return `
${esc(label)}  revoked · ${esc(fmtDate(m.bookmarkedAt))}
${removeForm(m.docId)}
`; + return `
${esc(label)} revoked · ${esc(fmtDate(m.bookmarkedAt))}
${removeForm(m.docId)}
`; } const href = m.token ? `/d/${encodeURIComponent(m.slug)}?viewtoken=${encodeURIComponent(m.token)}` : `/d/${encodeURIComponent(m.slug)}`; const vis = m.isPublic ? "public" : "private"; - return `
${esc(label)}  ${esc(m.access)} ${vis} · ${esc(fmtDate(m.bookmarkedAt))}
${removeForm(m.docId)}
`; + return `
${esc(label)} ${esc(m.access)} ${vis} · ${esc(fmtDate(m.bookmarkedAt))}
${removeForm(m.docId)}
`; } // Inputs for re-resolving a bookmarked doc's access at read time, reduced to diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index 9ac50dc..2a9c5b8 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -425,7 +425,7 @@ export const OVERLAY_SCRIPT = String.raw` + "html.jh-dark span[data-jh-seg].jh-focus{background:rgba(245,197,24,.2);box-shadow:0 0 0 1px rgba(245,197,24,.85)}" + "span[data-jh-chip]{display:inline-flex;align-items:center;gap:2px;font-size:11.5px;line-height:1;" + "background:#fbfbfb;border:1px solid #e0e0e0;border-radius:10px;padding:1px 6px 1px 5px;margin-left:4px;" - + "vertical-align:.12em;font-family:ui-monospace,Menlo,Consolas,monospace;cursor:pointer;user-select:none;" + + "vertical-align:.12em;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;cursor:pointer;user-select:none;" + "white-space:nowrap;transition:border-color .1s,background .1s,opacity .12s}" + "span[data-jh-chip]:hover{border-color:#bbb;background:#fff}" + "span[data-jh-chip].mine{border-color:#9db8d8;background:#eef3fb}" @@ -441,7 +441,7 @@ export const OVERLAY_SCRIPT = String.raw` + "html.jh-dark span[data-jh-chip].mine .jh-ct{color:#9db8d8}" + ".jh-pop{position:fixed;display:none;background:#fff;border:1px solid #ccc;border-radius:6px;" + "box-shadow:0 4px 16px rgba(0,0,0,.18);z-index:2147483647;padding:6px 8px;font-size:11px;max-width:280px;" - + "font-family:ui-monospace,Menlo,Consolas,monospace;color:#222}" + + "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;color:#222}" + ".jh-pop .jh-row{display:flex;align-items:center;gap:6px;padding:3px 4px;border-radius:4px}" + ".jh-pick .jh-row{cursor:pointer}" + ".jh-pick .jh-row:hover{background:#f3f3f3}" diff --git a/lib/page.ts b/lib/page.ts index 8a5c594..046e7ea 100644 --- a/lib/page.ts +++ b/lib/page.ts @@ -1,12 +1,23 @@ -// Shared man-page-style HTML shell. Monospace, zero JS, zero framework CSS. +// Shared man-page-style HTML shell. Zero JS, zero framework CSS. // httpbingo.org / Unix man-page vibe. This is the brand. // // Chrome system: variant A, LOCKED 2026-06-13 (birthday.md "Site-wide redesign -// decisions"). A SINGLE uppercase `JUSTHTML.SH(1)` headline — no top bar, no -// `GENERAL` center label, no doubled title. Left-aligned uppercase

section -// heads with hanging-indent (3.5ch) bodies, and ONE quiet justified footer line -// (`justhtml.sh … … JUSTHTML.SH(1)`). Always light, monospace. Matches -// the shipped homepage chrome (app/route.ts, variant B) verbatim. +// decisions"; typography updated 2026-10-28). A SINGLE uppercase +// `JUSTHTML.SH(1)` headline — no top bar, no `GENERAL` center label, no +// doubled title. Left-aligned uppercase

section heads with hanging-indent +// (3.5ch) bodies, and ONE quiet justified footer line +// (`justhtml.sh … … JUSTHTML.SH(1)`). Always light. +// +// Typography: the default reading font is the tried-and-tested system UI stack +// (the GitHub/Bootstrap lineage) — native SF on macOS/iOS, Segoe UI on Windows, +// Roboto on Android — nothing to download, and the most readable option on +// both desktop and mobile. Monospace stays where it means something: prompt +// boxes, inline code, diffs. Prose lives in

;

 is code.
+
+// The site-wide default reading face.
+export const SANS = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`;
+// Code only: prompts, inline code, diffs.
+export const MONO = `ui-monospace, "SF Mono", Menlo, Consolas, "Courier New", monospace`;
 
 const STYLE = `
   :root { color-scheme: light; }
@@ -14,42 +25,38 @@ const STYLE = `
   body {
     margin: 0 auto;
     padding: 2rem 1.5rem 3rem;
-    font-family: ui-monospace, "SF Mono", Menlo, Consolas, "Courier New", monospace;
-    font-size: 14px;
-    line-height: 1.55;
+    font-family: ${SANS};
+    font-size: 16px;
+    line-height: 1.6;
     color: #111;
     background: #fff;
     max-width: 760px;
   }
   .headline { font-weight: 700; margin: 0 0 1.5rem; }
   h1, h2 {
-    font-size: 14px; font-weight: 700; margin: 1.6rem 0 0.3rem;
-    text-transform: uppercase; letter-spacing: 0.03em;
+    font-size: 14px; font-weight: 700; margin: 1.6rem 0 0.4rem;
+    text-transform: uppercase; letter-spacing: 0.05em;
   }
-  pre { white-space: pre-wrap; margin: 0; }
+  p { margin: 0 0 0.4rem; }
+  pre { white-space: pre-wrap; margin: 0; font-family: ${MONO}; font-size: 14px; }
   .body { padding-left: 3.5ch; }
   a { color: #0000ee; }
   .hint { color: #666; margin: 0 0 0.5rem; }
   .err { color: #b00020; margin: 0 0 0.5rem; }
-  code { background: rgba(127,127,127,0.12); padding: 0.05rem 0.3rem; border-radius: 3px; }
+  code { font-family: ${MONO}; font-size: 0.9em; background: rgba(127,127,127,0.12); padding: 0.05rem 0.3rem; border-radius: 3px; }
   input, button { font: inherit; }
   input[type=email] { padding: 2px 6px; border: 1px solid #999; background: #fff; color: #111; }
   button { padding: 2px 12px; border: 1px solid #999; background: #f6f6f6; color: #111; cursor: pointer; border-radius: 3px; }
   button:hover { background: #ededed; }
   footer { margin-top: 2.5rem; color: #666; }
+  footer .foot { display: flex; justify-content: space-between; gap: 1rem; }
 `;
 
-// One quiet justified footer line: `justhtml.sh …  … JUSTHTML.SH(1)`.
-// Spacing is in characters so it reads true in the monospace column. Mirrors the
-// homepage footer (which hardcodes the same shape at the design's wrap width).
-function footerLine(date: string): string {
-  const left = "justhtml.sh";
-  const right = "JUSTHTML.SH(1)";
-  const width = 78;
-  const gaps = Math.max(2, width - left.length - right.length - date.length);
-  const l = Math.ceil(gaps / 2);
-  const r = gaps - l;
-  return `${left}${" ".repeat(l)}${date}${" ".repeat(r)}${right}`;
+// One quiet justified footer line: `justhtml.sh …  … JUSTHTML.SH(1)`,
+// laid out with flexbox space-between so it stays flush at any wrap width.
+// Mirrors the homepage footer.
+function footerRow(date: string): string {
+  return `
justhtml.sh${date}JUSTHTML.SH(1)
`; } export function manPage(opts: { @@ -67,9 +74,9 @@ export function manPage(opts: { -
JUSTHTML.SH(1)
+
JUSTHTML.SH(1)
${opts.bodyHtml} -
${footerLine(date)}
+
${footerRow(date)}
`; } From b633fb9cd6d6686d5b985d4766881026ba6990fb Mon Sep 17 00:00:00 2001 From: ulziibay-kernel <253135130+ulziibay-kernel@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:33:16 +0000 Subject: [PATCH 2/2] Fall back to the system sans stack for docs that set no font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A doc's HTML renders byte-pristine in the sandboxed iframe, so a doc with no font-family of its own fell back to the browser default serif — noticeably different from the site's sans chrome. In the shell-embedded render (the /d/:slug viewer), inject the system sans stack as a last-resort default when the doc has no authored font rule (inline

fancy

`; + +function doc(html: string) { + return { id: 1, slug: "a-doc", html, is_public: true, title: null, version: 1 }; +} + +async function get(url: string) { + const res = await GET(new Request(url), { params: Promise.resolve({ slug: "a-doc" }) }); + return { status: res.status, html: await res.text() }; +} + +describe("GET /d/:slug/raw", () => { + beforeEach(() => { + mocks.findBySlug.mockReset(); + mocks.canViewSession.mockReset().mockResolvedValue(true); + mocks.getSession.mockReset().mockResolvedValue(null); + }); + + it("serves byte-pristine HTML on a direct fetch — no overlay, no injected font", async () => { + mocks.findBySlug.mockResolvedValue(doc(FONTLESS_DOC)); + const { status, html } = await get("https://justhtml.sh/d/a-doc/raw"); + expect(status).toBe(200); + expect(html).toBe(FONTLESS_DOC); + }); + + it("injects the default sans font for a fontless doc in the shell variant", async () => { + mocks.findBySlug.mockResolvedValue(doc(FONTLESS_DOC)); + const { html } = await get("https://justhtml.sh/d/a-doc/raw?overlay=1"); + expect(html).toContain('data-jh-default-font="1"'); + expect(html).toContain("-apple-system"); + expect(html.indexOf('data-jh-default-font')).toBeLessThan(html.toLowerCase().lastIndexOf("")); + }); + + it("leaves a doc that declares its own font untouched in the shell variant", async () => { + mocks.findBySlug.mockResolvedValue(doc(STYLED_DOC)); + const { html } = await get("https://justhtml.sh/d/a-doc/raw?overlay=1"); + expect(html).not.toContain("data-jh-default-font"); + expect(html).toContain("font-family:Georgia"); + }); +}); diff --git a/app/d/[slug]/raw/route.ts b/app/d/[slug]/raw/route.ts index 4f732da..370e4e1 100644 --- a/app/d/[slug]/raw/route.ts +++ b/app/d/[slug]/raw/route.ts @@ -3,6 +3,7 @@ import { canViewSession } from "@/lib/docs/access"; import { verifyViewCap } from "@/lib/docs/viewcap"; import { getSession } from "@/lib/auth/session"; import { OVERLAY_SCRIPT } from "@/lib/docs/overlay"; +import { fontFallbackStyle } from "@/lib/docs/font-fallback"; export const dynamic = "force-dynamic"; @@ -65,12 +66,16 @@ export async function GET(req: Request, ctx: Ctx): Promise { if (!(await canViewSession(doc, session, viewtoken))) return deny(404, "Not found."); } - // Inject the comment overlay only for the shell-embedded variant. We append the - // script before (or at the end if there's no body tag), leaving the - // user's HTML otherwise untouched. The CSP sandbox already permits scripts. + // Inject the comment overlay — and, for docs that set no font of their own, + // the site's default sans face (lib/docs/font-fallback.ts) — only for the + // shell-embedded variant. We append before (or at the end if there's + // no body tag), leaving the user's HTML otherwise untouched. The CSP sandbox + // already permits scripts. The font decision must be made on the pristine + // bytes: the overlay script itself mentions font-family. let html = doc.html; if (wantOverlay) { - const tag = ``; + const fontTag = fontFallbackStyle(html); + const tag = `${fontTag}`; const idx = html.toLowerCase().lastIndexOf(""); html = idx === -1 ? html + tag : html.slice(0, idx) + tag + html.slice(idx); } diff --git a/lib/docs/font-fallback.test.ts b/lib/docs/font-fallback.test.ts new file mode 100644 index 0000000..595eadc --- /dev/null +++ b/lib/docs/font-fallback.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { fontFallbackStyle } from "./font-fallback"; + +describe("fontFallbackStyle", () => { + it("returns the sans fallback for a doc with no styling", () => { + const tag = fontFallbackStyle("

hi

plain doc

"); + expect(tag).toContain("data-jh-default-font"); + expect(tag).toContain("-apple-system"); + expect(tag).toContain("ui-monospace"); + }); + + it("returns nothing when the doc declares a font-family", () => { + expect( + fontFallbackStyle(`

x

`) + ).toBe(""); + }); + + it("returns nothing when the doc declares a font shorthand", () => { + expect(fontFallbackStyle(``)).toBe(""); + }); + + it("returns nothing when the doc loads a webfont", () => { + expect(fontFallbackStyle(``)).toBe(""); + }); + + it("ignores unrelated CSS — a doc styled in every other way still gets the fallback", () => { + expect(fontFallbackStyle(``)).not.toBe(""); + }); + + it("is case-insensitive about author font rules", () => { + expect(fontFallbackStyle(``)).toBe(""); + }); + + it("detects a font set in an inline style attribute", () => { + expect(fontFallbackStyle(`x`)).toBe(""); + expect(fontFallbackStyle(`

x

`)).toBe(""); + }); + + it("ignores font-family mentioned in prose or code samples", () => { + expect( + fontFallbackStyle(`

This doc sets no font-family at all.

body { font-family: serif }
`) + ).not.toBe(""); + }); + + it("ignores non-font rules in a style block", () => { + expect(fontFallbackStyle(`

x

`)).not.toBe(""); + }); +}); diff --git a/lib/docs/font-fallback.ts b/lib/docs/font-fallback.ts new file mode 100644 index 0000000..aa9e8b7 --- /dev/null +++ b/lib/docs/font-fallback.ts @@ -0,0 +1,27 @@ +import { SANS, MONO } from "@/lib/page"; + +// The viewer renders each doc's own HTML in a sandboxed iframe, so a doc's +// typeface is whatever its author chose. A doc that sets no font at all falls +// back to the browser default — a serif — which reads badly next to the site's +// sans chrome. When the shell-embedded render (the /d/:slug viewer) serves a +// doc with no font declaration of its own, we inject the site-wide system sans +// stack as the default instead. Docs that set any font rule are untouched, and +// because the injected rules use :where() (zero specificity) any author CSS +// would win anyway. Direct /raw fetches stay byte-pristine — like the comment +// overlay, this only rides the shell variant. + +// An author font rule counts only in real CSS contexts: inside a `; +}