diff --git a/src/styles.ts b/src/styles.ts index 8f03770..90321cc 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1162,6 +1162,314 @@ 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
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); } + +/* ---------- the primary action ---------- */ +/* The page previously had no CTA verb at all: the only action was a + small copy pill inside the code box chrome. For a page whose whole + job is starting the device-code flow — and which sits at the top of a + funnel that loses ~41% at /verify — that was the weakest element on + it. Copying the prompt IS the conversion event, so it gets a real + button with a real verb, and the in-chrome pill is gone so there is + still only one copy affordance. */ +.lcta { + display: inline-flex; align-items: center; gap: 9px; + margin: 22px auto 0; + background: var(--red); color: #fff; + border: 1px solid var(--red); + font-size: 14.5px; font-weight: 500; + padding: 12px 22px; border-radius: var(--r-md); + cursor: pointer; + transition: background 0.12s, border-color 0.12s, transform 0.04s; +} +.lcta:hover { background: var(--red-press); border-color: var(--red-press); } +.lcta:active { transform: translateY(1px); } +.lcta.ok { background: var(--ok); border-color: var(--ok); } +.lcta svg { width: 15px; height: 15px; flex: 0 0 auto; } +.lcta-fine { + font-family: var(--mono); font-size: 11.5px; + color: var(--ink-softer); margin: 12px 0 0; +} + +/* ---------- 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: 18px; +} + +/* ---------- evidence ---------- */ +.lsec { max-width: var(--lshell); margin: 0 auto; padding: 92px 28px 0; } +/* The evidence block reads as part of the fold rather than a new + section, and starting it higher shows more of the embedded page. */ +.lsec.lsec-tight { padding-top: 48px; } +.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 .ver { color: var(--ink-softer); } +/* Was 11.5px in the faintest ink on the page, floated hard right by + margin-left:auto — so it sat far from the version it acts on and read + as decoration rather than a control. Now it follows the version, wears + the accent, and carries an underline so it is legible as a link. */ +.frame .bar .go { + margin-left: 6px; + color: var(--red); text-decoration: none; + border-bottom: 1px solid var(--red-bg-stroke); + transition: border-color 0.12s; +} +.frame .bar .go:hover { border-bottom-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; } +/* Bottom spacing is padding, not margin, on purpose. As a margin it + collapsed through main (padding:0) and out of body, so it landed + outside the tinted background box and html's white showed through as + a strip under the footer. Padding cannot collapse. */ +body.landing .footer-merged { + max-width: 900px; + margin: 70px auto 0; + padding-bottom: 56px; +} +/* Belt and braces: if any future last-child margin escapes again, the + canvas is still the landing surface rather than white. */ +body.landing { min-height: 100vh; } + +@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..27c5ee2 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 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 '

hello from htmlbin

' > 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 '

hello from htmlbin

' > 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) => + `${escapeText(m.name)}`, + ).join("\n "); + return /* html */ ` htmlbin — agent-first HTML hosting - + - + @@ -119,9 +73,9 @@ export function landingPage(env: Bindings): string { - + - + ${STYLE_INLINE} @@ -131,209 +85,157 @@ ${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

+
+

Send your agent's work as a link, not a file.

+

A URL that survives every revision. Free, no signup, works with any agent.

-
-
- -
-
- - - -
- -
+
+
+ + +
-
-
-
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
- +

Free · no signup · one human click to start

-

- 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 the live page ↗ +
+
+ +
+

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)
+

Revise without breaking the link.

+

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 anything you published.

+

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
+

Start from a real structure.

+

Shared shapes 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
+

Keep some pages private.

+

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 pages people have published

+
+ +