From 10bfc916c96bd39e9a5028c05c41ead369167307 Mon Sep 17 00:00:00 2001 From: Utkarsh Sengar Date: Sat, 22 Aug 2026 23:36:52 -0700 Subject: [PATCH 1/3] landing: redesign the fold around evidence instead of instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old landing was a left-aligned document on pure white with the HTTP memo as its hero. Held next to the pages that do this well (portradar, zerolang, subtitlegenerator) it read as unstyled rather than restrained. An earlier attempt at fixing that by subtraction - removing the accent word, the subhead, the motion, the shadows - made it worse, because austerity is not the same thing as professionalism. Those pages are heavily designed; they just don't spend the budget on slop vocabulary. So this is additive: - A tinted page (#F4F5F6) with a dot texture, so white surfaces have something to sit on. Single biggest contributor to "looks designed". - Centred composition, and a real nav with one solid action. - Display type up to 66px carrying the claim, all black. No accent- coloured word: that is the most templated move in this category, and none of the reference pages do it. - A short subhead that states the mechanic. Researched lengths from the pages worth copying: Vercel 5-word headline, Linear 8 + 13, Resend 8 + 12. This lands at 8 + 13. The old lede was 45 words. - Evidence above the fold. A real drop in an iframe (/p/ztx4J9P/raw, which does not bump view_count so the homepage doesn't inflate that drop's counter), plus the marks of the agents the CLI already detects. - The prompt slab loses its fake traffic lights, and its duplicate copy button. One affordance, no window chrome. The HTTP memo is retired from this page and replaced by the real thing: a trimmed but literal GET /api/onboard response. The old block rendered to:/from:/re: as if they were HTTP headers. They are RFC 5322 email headers - so the component built to signal protocol-seriousness was showing invented ones, which the engineers most worth impressing would notice. The endpoint returns 13.4 KB across 21 sections; showing it beats drawing it. Copy is rewritten throughout to drop insider language. "Mints a new version" and "slug stays put" are phrases nobody outside this repo says. New file src/views/logos.ts holds the brand marks, inlined so the page makes no third-party request. Only the four whose identity was verified against the source SVG are included: Claude, Cursor, OpenAI (for Codex), Cline - all from simple-icons 16.28.0, CC0-1.0. Aider and Devin are not in that set, and the "amp" slug there is Google AMP rather than Sourcegraph's Amp, so those three are named in prose instead. A wrong logo reads worse than no logo and these are other companies' marks. Scope: landing only. /verify and /p/:slug still use pageHead() and httpMemo() from chrome.ts, and every new rule is scoped to body.landing or to a class only this page emits, so those two pages are byte-identical. Verified: typecheck clean; wrangler dev returns 200 with all five marks rendered and no old-chrome markup; /verify, the viewer and /style.css still 200; true 360px and 768px viewports (rendered inside sized iframes, because headless Chrome clamps its own window to 500px) show no page-level horizontal overflow. This bends DESIGN.md §1, §3, §5.2, §6 and §7 - see the block comment in styles.ts. The doc needs amending if this direction is accepted; not touched here so it doesn't get rewritten for a design that may not ship. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --- src/styles.ts | 257 ++++++++++++++++++++++++ src/views/landing.ts | 457 ++++++++++++++----------------------------- src/views/logos.ts | 30 +++ 3 files changed, 438 insertions(+), 306 deletions(-) create mode 100644 src/views/logos.ts diff --git a/src/styles.ts b/src/styles.ts index 8f03770..ae91751 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1162,6 +1162,263 @@ iframe.canvas { border: 0; width: 100%; background: #fff; flex: 1; } .gate { margin-top: 36px; max-width: 100%; padding: 0 8px; } .gate-input { font-size: 18px; } } + +/* ══════════════════════════════════════════════════════════════════ + LANDING REDESIGN + + Everything here is scoped to body.landing (or to classes only the + landing emits) so /verify and /p/:slug keep the existing chrome + untouched. The old .page-head / details.req / .prompt / .hero rules + above are still live for those two pages. + + Why this exists: the previous landing was a left-aligned document on + pure white with the HTTP memo as its hero. Against the pages that do + this well it read as unstyled rather than restrained. The moves that + changed that, roughly in order of effect: + + 1. A tinted page with a dot texture, so white surfaces have + something to sit on. + 2. Centred composition and a real nav with one solid action. + 3. Display type carrying the claim, all black - no accent-coloured + word, which is the most templated move in this category. + 4. A short subhead that states the mechanic. Researched lengths: + Vercel 5-word headline, Linear 8 + 13, Resend 8 + 12. + 5. Evidence above the fold - a real drop in an iframe, plus the + marks of the agents the CLI already detects. + + This bends DESIGN.md §1, §3 (pure white, no gradients), §5.2 (memo + as hero), §6 (720px left-aligned single column) and §7 (no marketing + prose above the fold). DESIGN.md needs amending if this ships. + ══════════════════════════════════════════════════════════════════ */ + +body.landing { + --page: #F4F5F6; + --card: #FFFFFF; + --lshell: 1080px; + background: var(--page); + /* Texture, not decoration: it renders as dots, not a colour wash. + The one gradient on the page. */ + background-image: radial-gradient(circle, rgba(10,10,10,.055) 1px, transparent 1.1px); + background-size: 24px 24px; +} +/* the shared 880px <main> does not apply to this layout */ +body.landing main { max-width: none; margin: 0; padding: 0; } + +/* ---------- nav ---------- */ +.lnav { + position: sticky; top: 0; z-index: 20; + background: rgba(244,245,246,.92); + border-bottom: 1px solid var(--rule); +} +.lnav .in { + max-width: var(--lshell); margin: 0 auto; + padding: 14px 28px; + display: flex; align-items: center; gap: 22px; +} +.lnav .wm { + font-family: var(--mono); font-weight: 500; font-size: 14.5px; + letter-spacing: -0.01em; color: var(--ink); text-decoration: none; +} +.lnav .wm::before { content: "<"; color: var(--red); } +.lnav .wm::after { content: ">"; color: var(--red); } +.lnav .links { display: flex; gap: 20px; margin-left: 14px; } +.lnav .links a { + font-size: 14px; color: var(--ink-soft); + text-decoration: none; transition: color 0.12s; +} +.lnav .links a:hover { color: var(--ink); } +.lnav .right { margin-left: auto; display: flex; align-items: center; gap: 16px; } +.lnav .gh { + display: inline-flex; align-items: center; gap: 7px; + font-family: var(--mono); font-size: 12.5px; + color: var(--ink-soft); text-decoration: none; +} +.lnav .gh:hover { color: var(--ink); } +.lnav .gh svg { width: 15px; height: 15px; flex: 0 0 auto; } +.lnav .btn { + background: var(--ink); color: #fff; + font-size: 13.5px; font-weight: 500; + padding: 9px 16px; border-radius: var(--r-md); + border: 1px solid var(--ink); + text-decoration: none; white-space: nowrap; + transition: background 0.12s; +} +.lnav .btn:hover { background: #262626; } + +/* ---------- hero ---------- */ +.lhero { + max-width: var(--lshell); margin: 0 auto; + padding: 96px 28px 0; text-align: center; +} +.lhero h1 { + font-size: clamp(38px, 5.6vw, 66px); + line-height: 1.03; letter-spacing: -0.035em; + font-weight: 700; max-width: 17ch; + margin: 0 auto 22px; +} +.lhero .lede { + font-size: 19.5px; line-height: 1.52; + color: var(--ink-soft); + max-width: 60ch; margin: 0 auto 34px; +} + +/* ---------- the one action ---------- */ +/* No traffic lights. The reference set does not use fake window chrome + and it was the most toy-like thing on the old fold. */ +.lbox { + max-width: 720px; margin: 0 auto; + background: var(--code-bg); + border-radius: var(--r-lg); + overflow: hidden; text-align: left; + box-shadow: 0 1px 2px rgba(10,10,10,.06), 0 8px 24px -18px rgba(10,10,10,.28); +} +.lbox .strip { + display: flex; align-items: center; + border-bottom: 1px solid rgba(255,255,255,.08); + padding: 0 6px 0 4px; +} +.lbox .strip .t { + background: none; border: 0; cursor: pointer; + font-family: var(--mono); font-size: 12.5px; + color: rgba(239,239,239,.5); + padding: 13px 14px; + border-bottom: 2px solid transparent; margin-bottom: -1px; + transition: color 0.12s; +} +.lbox .strip .t:hover { color: rgba(239,239,239,.85); } +.lbox .strip .t.on { color: #fff; border-bottom-color: var(--red); } +.lbox .cp { + margin-left: auto; + background: rgba(255,255,255,.07); border: 0; cursor: pointer; + color: rgba(239,239,239,.72); + font-family: var(--mono); font-size: 11.5px; + padding: 6px 11px; border-radius: var(--r-sm); + transition: background 0.12s, color 0.12s; +} +.lbox .cp:hover { background: rgba(255,255,255,.13); color: #fff; } +.lbox .cp.ok { background: rgba(40,190,70,.18); color: var(--ok-on-dark); } +/* All panes share one grid cell so the slab never jumps between tabs. + That means it sizes to the tallest pane, so keep every payload to + the same number of lines or the shorter ones show dead black. */ +.lbox .body { padding: 20px 22px 22px; display: grid; } +.lbox pre { + grid-area: 1 / 1; + font-family: var(--mono); font-size: 13.5px; line-height: 1.75; + color: var(--code-fg); + white-space: pre-wrap; overflow-wrap: anywhere; + visibility: hidden; pointer-events: none; +} +.lbox pre.on { visibility: visible; pointer-events: auto; } +.lbox pre .em { color: var(--code-em); } +.lbox pre .c, .lbox pre .d { color: var(--code-dim); } +.lbox pre .a { color: var(--ok-on-dark); } + +/* ---------- works-with ---------- */ +/* No box per item. Seven outlined pills read as a tag list, and an + empty mark slot inside each one read as an unchecked checkbox. */ +.works { max-width: 860px; margin: 38px auto 0; text-align: center; } +.works-lab { font-size: 13.5px; color: var(--ink-softer); margin-bottom: 20px; } +.works-row { + display: flex; flex-wrap: wrap; justify-content: center; + align-items: center; gap: 14px 30px; +} +.works-row .lg { + display: inline-flex; align-items: center; gap: 8px; + font-size: 14.5px; font-weight: 500; + color: var(--ink-soft); white-space: nowrap; + transition: color 0.14s; +} +.works-row .lg:hover { color: var(--ink); } +.works-row .lg svg { + width: 17px; height: 17px; flex: 0 0 auto; + opacity: .62; transition: opacity 0.14s; +} +.works-row .lg:hover svg { opacity: 1; } +.works-fine { + font-family: var(--mono); font-size: 11.5px; line-height: 1.9; + color: var(--ink-softer); margin-top: 22px; +} + +/* ---------- evidence ---------- */ +.lsec { max-width: var(--lshell); margin: 0 auto; padding: 92px 28px 0; } +.lsec .eyebrow { + font-family: var(--mono); font-size: 11px; + letter-spacing: 0.1em; text-transform: uppercase; + color: var(--ink-softer); text-align: center; margin-bottom: 16px; +} +.frame { + background: var(--card); border: 1px solid var(--rule); + border-radius: var(--r-lg); overflow: hidden; + box-shadow: 0 1px 2px rgba(10,10,10,.05), 0 20px 44px -32px rgba(10,10,10,.3); +} +.frame .bar { + display: flex; align-items: center; gap: 10px; + padding: 11px 16px; border-bottom: 1px solid var(--rule-soft); + font-family: var(--mono); font-size: 12.5px; color: var(--ink-soft); +} +.frame .bar .u { color: var(--ink); font-weight: 500; } +.frame .bar .go { margin-left: auto; font-size: 11.5px; color: var(--ink-softer); text-decoration: none; } +.frame .bar .go:hover { color: var(--red); } +.shot { position: relative; } +/* The frame crops mid-sentence, which reads as broken rather than + truncated. The fade makes the cut deliberate. Functional, not + decorative — but it is a gradient, so DESIGN.md §3 needs the + carve-out if this ships. */ +.shot::after { + content: ""; position: absolute; left: 0; right: 0; bottom: 0; + height: 72px; pointer-events: none; + background: linear-gradient(to bottom, rgba(255,255,255,0), #fff 75%); +} +.frame iframe { display: block; width: 100%; height: 560px; border: 0; background: #fff; } +.lnote { text-align: center; font-size: 14px; color: var(--ink-soft); margin: 16px auto 0; max-width: 58ch; } +.lnote .inl { color: var(--red); text-decoration: none; border-bottom: 1px solid rgba(217,48,37,.3); } +.lnote .inl:hover { border-bottom-color: var(--red); } + +/* ---------- the onboard response, shown not illustrated ---------- */ +.onb { + max-width: 760px; margin: 0 auto; + background: var(--code-bg); + border-radius: var(--r-lg); overflow: hidden; + box-shadow: 0 1px 2px rgba(10,10,10,.06), 0 8px 24px -18px rgba(10,10,10,.28); +} +.onb .strip { + display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; + padding: 12px 18px; border-bottom: 1px solid rgba(255,255,255,.08); + font-family: var(--mono); font-size: 12.5px; + color: rgba(239,239,239,.9); +} +.onb .strip .m { color: var(--code-em); font-weight: 500; } +.onb .strip .meta { margin-left: auto; color: rgba(239,239,239,.42); font-size: 11.5px; } +.onb .body { padding: 16px 18px 20px; overflow-x: auto; } +.onb pre { + font-family: var(--mono); font-size: 12.5px; line-height: 1.75; + color: var(--code-fg); white-space: pre; +} +.onb .k { color: #9CDCFE; } +.onb .s { color: #CE9178; } +.onb .n { color: #B5CEA8; } +.onb .p { color: rgba(239,239,239,.45); } +.onb .c { color: var(--code-dim); font-style: normal; } + +/* ---------- capability list + examples, centred for this layout ---------- */ +body.landing .caps { max-width: 900px; margin: 0 auto; gap: 36px 52px; } +body.landing .examples { max-width: 760px; margin: 0 auto; } +body.landing .examples .cue { display: none; } +body.landing .footer-merged { max-width: 900px; margin: 70px auto 56px; } + +@media (max-width: 820px) { + .lnav .links { display: none; } + .lhero { padding-top: 56px; } + body.landing .caps { grid-template-columns: 1fr; gap: 30px; } +} +@media (max-width: 600px) { + .lnav .in, .lhero, .lsec { padding-left: 20px; padding-right: 20px; } + .lhero h1 { font-size: 34px; max-width: none; } + .lhero .lede { font-size: 17px; } + .lbox pre { font-size: 12px; } + .frame iframe { height: 300px; } + .shot::after { height: 56px; } +} `; // Cache-bust the stylesheet automatically on every CSS change. The version diff --git a/src/views/landing.ts b/src/views/landing.ts index dc8d73a..69a9dd5 100644 --- a/src/views/landing.ts +++ b/src/views/landing.ts @@ -1,96 +1,45 @@ import type { Bindings } from "../types"; -import { httpMemo, pageHead } from "./chrome"; import { STYLE_INLINE } from "../styles"; +import { AGENT_MARKS, AGENTS_WITHOUT_MARKS, GITHUB_MARK } from "./logos"; -// Curated "what people are building" list, rendered below the tool -// section. Edit this array + redeploy to rotate. Captions and `kind` -// labels are hand-curated — they don't read from the drop's stored -// title. The `kind` column on the right gives the list a visible range -// (explainer / pr writeup / design / playful / plan-spec) so it reads -// as proof-of-breadth, not just "four random links." +// Curated "a few drops people have made" list. Edit this array + redeploy +// to rotate. Captions and `kind` labels are hand-written — they don't read +// from the drop's stored title. const EXAMPLES: Array<{ slug: string; caption: string; kind: string }> = [ - { - slug: "gDMy7Vb", - caption: "how htmlbin works — an animated explainer", - kind: "explainer", - }, - { - slug: "1Wyf23j", - caption: "cross-platform gstack — pr #1111 deep dive", - kind: "pr writeup", - }, - { - slug: "ztx4J9P", - caption: "workers nav — three redesigns side by side", - kind: "design", - }, - { - slug: "i2taphP", - caption: "google logo — animation playground", - kind: "playful", - }, - { - slug: "HYmZ6DjCM", - caption: "plan: queryable drop metadata", - kind: "plan / spec", - }, + { slug: "gDMy7Vb", caption: "how htmlbin works — an animated explainer", kind: "explainer" }, + { slug: "1Wyf23j", caption: "cross-platform gstack — pr #1111 deep dive", kind: "pr writeup" }, + { slug: "ztx4J9P", caption: "workers nav — three redesigns side by side", kind: "design" }, + { slug: "i2taphP", caption: "google logo — animation playground", kind: "playful" }, + { slug: "HYmZ6DjCM", caption: "plan: queryable drop metadata", kind: "plan / spec" }, ]; -// The two prompt-tab payloads. We deliberately serve two — "one thing -// to copy" is preserved because at any moment exactly one tab is -// active, and the copy buttons read whichever one is selected. Plain -// strings here are the clipboard payloads; the visible HTML below -// hand-wires the same content with <span class="em"> accents for color. +// The drop embedded above the fold as evidence. Picked because it is light +// (the prompt slab above it is already dark), visually rich, and reads +// instantly as work somebody would actually send. `/raw` is used so the +// viewer chrome isn't nested inside our own frame, and that route does NOT +// bump view_count — see the /p/:slug/raw handler in index.ts — so embedding +// it on every homepage hit doesn't inflate this drop's counter. // -// Keep these in sync with the prompt-body HTML further down — if you -// edit one, edit the other. -const AGENT_PROMPT = `Make a delightful HTML page to explain a concept or a problem — show me what HTML can do that markdown or a flat file can't. Something visual, interactive, alive. +// If this slug is ever deleted the frame goes blank. Swap it here. +const SHOWCASE_SLUG = "ztx4J9P"; -Publish to htmlbin.dev. Credentials and API at htmlbin.dev/api/onboard.`; +// Clipboard payloads for the three tabs. Keep each visible pane to THREE +// rendered lines: the panes share one grid cell so the slab can't jump on +// tab change, which means it sizes to the tallest — and a shorter pane then +// shows dead black space underneath. +const AGENT_PROMPT = `Explain this as an HTML page — visual, not a wall of text. -// Clipboard form — paste-and-run. The visible CLI panel keeps the `$ ` -// prompt prefix and the `→ URL` result line as visual signposts, but -// neither belongs in what we copy: `$` is the shell prompt indicator, -// and `→ https://…` is example output, not a command. The echo line -// creates a tiny sample file so the publish actually succeeds — without -// it, `publish out.html` would fail with file-not-found. Comments stay: -// bash ignores `#` lines, so they're harmless on paste and useful for -// context. -const CLI_PROMPT = `# one-time — GitHub device-code, ~30s -npx @htmlbin/cli login +Publish it to htmlbin.dev. Start at htmlbin.dev/api/onboard.`; -# create a sample page and publish it -echo '<h1>hello from htmlbin</h1>' > out.html -npx @htmlbin/cli publish out.html`; +const CLI_PROMPT = `npm i -g @htmlbin/cli && htmlbin login +htmlbin publish ./plan.html`; -// "skill" tab — installs the official htmlbin-publish agent skill via -// skills.sh. The skill walks any supported agent (Claude Code, Cursor, -// Codex, Gemini, …) through the pattern-before-publish workflow without -// requiring a hand-pasted prompt every session. One install, ambient -// for the lifetime of the agent. Lives in the htmlbin-cli repo — -// skills.sh resolves the subdirectory automatically. -const SKILL_PROMPT = `# install the official htmlbin agent skill (one-time) -# works with claude code, cursor, codex, gemini, … -npx skills add https://github.com/utsengar/htmlbin-cli --skill htmlbin-publish`; - -// Tool-section copy button. Same shape as CLI_PROMPT but the global -// install path (npm i -g, then bare `htmlbin`). End-to-end paste-and-run. -const TOOL_SETUP = `npm i -g @htmlbin/cli -htmlbin login -echo '<h1>hello from htmlbin</h1>' > out.html -htmlbin publish out.html`; +const SKILL_PROMPT = `npx skills add utsengar/htmlbin-cli --skill htmlbin-publish`; export function landingPage(env: Bindings): string { const PUBLIC_URL = env.PUBLIC_URL; const HOST = stripScheme(PUBLIC_URL); - const date = new Date().toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - - // JSON-LD service schema for crawlers and agent indexers. const jsonLd = JSON.stringify({ "@context": "https://schema.org", "@type": "WebAPI", @@ -101,15 +50,20 @@ export function landingPage(env: Bindings): string { documentation: `${PUBLIC_URL}/api/onboard`, }); + const marks = AGENT_MARKS.map( + (m) => + `<span class="lg" title="${escapeAttr(m.name)}"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="${m.path}"/></svg>${escapeText(m.name)}</span>`, + ).join("\n "); + return /* html */ `<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <title>htmlbin — agent-first HTML hosting - + - + @@ -119,9 +73,9 @@ export function landingPage(env: Bindings): string { - + - + ${STYLE_INLINE} @@ -131,209 +85,136 @@ ${STYLE_INLINE} - - + -${pageHead({ verb: "GET", path: "/" })} +
- ${httpMemo({ - verb: "GET", - path: "/", - rows: [ - { k: "host", v: HOST }, - { k: "to", v: "any agent reading this" }, - { k: "from", v: `htmlbin <${HOST}>` }, - { k: "re", v: "publishing HTML to a public URL", em: true }, - { k: "date", v: date }, - { k: "accept", v: "text/agent-friendly, text/markdown, application/json" }, - ], - res: { - status: "200 OK", - ok: true, - trailing: "content-type: text/html; charset=utf-8", - }, - })} - -
-

API for agents to share HTML.

-

Agent-native, end to end.

-
-
-

↓ paste into your agent — pop open a terminal — or install the skill

+
+

Your agent writes HTML. You get a URL.

+

One human click to start. After that your agent publishes on its own.

-
-
- -
-
- - - -
- -
+
+
+ + + +
-
-
-
Make a delightful HTML page to explain a concept or a problem — show me what HTML can do that markdown or a flat file can't. Something visual, interactive, alive.
-
-Publish to htmlbin.dev. Credentials and API at htmlbin.dev/api/onboard.
-
-
-
# one-time — GitHub device-code, ~30s
-$ npx @htmlbin/cli login
-
-# create a sample page and publish it
-$ echo '<h1>hello from htmlbin</h1>' > out.html
-$ npx @htmlbin/cli publish out.html
- https://htmlbin.dev/p/aB3xK7g
-
-
-
# install the official htmlbin agent skill (one-time)
-# works with claude code, cursor, codex, gemini, …
-$ npx skills add https://github.com/utsengar/htmlbin-cli \
-    --skill htmlbin-publish
-
-# then just ask the agent
-> publish a drop to htmlbin explaining this PR
-
+
+
Explain this as an HTML page — visual, not a wall of text.
+
+Publish it to htmlbin.dev. Start at htmlbin.dev/api/onboard.
+
$ npm i -g @htmlbin/cli && htmlbin login
+$ htmlbin publish ./plan.html
+ htmlbin.dev/p/gDMy7Vb
+
# one-time
+$ npx skills add utsengar/htmlbin-cli --skill htmlbin-publish
+# then just ask your agent to publish anything
- - -

- First publish needs one human click; after that, the agent owns it. -

+
+

Works with the agent you already use

+
+ ${marks} +
+

Also ${AGENTS_WITHOUT_MARKS.join(", ")}. Auto-detected — no flags to set.

+
-
-

tool /

-

Or pop open a terminal.

-

The CLI is your one-verb shortcut to the API — versioning, tags, patterns, passcodes, all in one binary.

- -
- -# install -$ npm i -g @htmlbin/cli - -# one-time — GitHub device-code, ~30s -$ htmlbin login - -# create a sample page and publish it -$ echo '<h1>hello from htmlbin</h1>' > out.html -$ htmlbin publish out.html - https://htmlbin.dev/p/aB3xK7g +
+

what you send someone

+
+
+ ${HOST}/p/${SHOWCASE_SLUG} + · v1 + open ↗ +
+
+ +
+

A real drop, not a screenshot.

+
-

— and there's more under the hood

+
+

for the agent reading this

+
+
+ GET ${HOST}/api/onboard + one document · 21 sections +
+
+
{
+  "summary": "Agent-first HTML hosting. Drop self-contained HTML, get a public URL.",
+  "auth":    { "steps": [ 3 ] /* one of them is the human */ },
+  "publish": { "method": "POST", "url": ".../api/drops", "status": 201 },
+  "iterate": { "new_version": "PUT", "metadata_only": "PATCH" },
+  "limits":  { "max_html_bytes": 2097152 }
+  /* + spec, cli, skill, drop_shape, errors, recommendations */
+}
+
+
+

This is the response, trimmed. Read all of it.

+
+
-
versions
-

Iterate. Slug stays put.

-

Every publish mints a new version of the same drop. Pin any past one with ?v=N.

-
# republish — same slug, v2 lands -$ htmlbin publish ./out.html - /p/aB3xK7g (v2)
+

Same URL, every revision.

+

Publish again and it becomes v2. The link you already sent still works, and ?v=1 still shows the old one.

-
-
tags & queries
-

Find drops by anything.

-

Attach any string tag at publish; query your library by any combination, anytime.

-
# tag and query — any string keys -$ htmlbin publish ./out.html --tag kind=plan -$ htmlbin list --filter kind=plan
+
tags
+

Find it again later.

+

Tag a page when you publish it. Search by any combination of tags afterwards.

-
patterns
-

Pluggable templates.

-

Pre-shaped drop kinds for recurring use cases. Install the catalog or write your own.

-
# grab the official catalog -$ htmlbin patterns init -$ htmlbin patterns add pr-explainer
+

Starting points.

+

Shared structures for the pages you make often — PR write-ups, plans, roundups. Use ours or write your own.

-
passcodes
-

Share-gate any drop.

-

Public by default. Drop a passcode in front of the viewer when it shouldn't be open.

-
# gate a drop -$ htmlbin publish ./out.html \\ - --passcode hunter2
+

Not everything is public.

+

Add a passcode and the page asks for it first. A share gate, not encryption — we say so plainly.

- -
- -
- @htmlbin/cli on github - · - readme - · - node 20+
-
-

↓ a few drops people have made

- +
+

a few drops people have made

+
+ +