jmp2.io

Get a subdomain Raw skill OpenAPI CLI

Push a markdown file or a whole folder, get a public URL. Relative links and image assets keep working. One HTTP call, no build step, no repo.

tar czf - ./docs | curl -T - https://jmp2.io/_api/sites/handbook/tarball \
  -H "Authorization: Bearer $JMP2_TOKEN"
# -> https://<you>.jmp2.io/handbook/

Getting a token

Sign in at jmp2.io/signup with GitHub or Google, pick a subdomain, and copy the token it shows once. Store it:

mkdir -p ~/.jmp2 && chmod 700 ~/.jmp2
printf '%s' 'jmp2_live_...' > ~/.jmp2/token && chmod 600 ~/.jmp2/token
export JMP2_TOKEN=$(cat ~/.jmp2/token)

Tokens are stored hashed, so the value is shown once and cannot be recovered. Mint and revoke more from your dashboard or the API. Reads are public; the token is only ever needed for writes.

How URLs work

A document's URL is its path without the .md. That single rule is what makes relative links written for the filesystem keep resolving on the web.

Source file URL
index.md /handbook/
docs/api.md /handbook/docs/api
docs/api.md (source) /handbook/docs/api.md
docs/index.md /handbook/docs/
img/a.png /handbook/img/a.png

So [api](./api.md) renders as href="./api" and resolves correctly, and ![](./img/a.png) needs no rewriting at all. Root-relative links (/img/a.png) are rewritten to sit under the site root. index.md and README.md both map to their directory.

Publishing

A whole folder. The tarball describes the entire site, so it replaces: files deleted locally disappear from the site.

tar czf - -C ./docs . | curl -T - https://jmp2.io/_api/sites/handbook/tarball \
  -H "Authorization: Bearer $JMP2_TOKEN"

Add ?merge=1 to overlay onto the current version instead of replacing. Add ?publish=0 to stage without going live.

One file. Staging a single file inherits the rest of the live version, so this updates one page without disturbing the others:

curl -X PUT https://jmp2.io/_api/sites/handbook/files/docs/api.md \
  -H "Authorization: Bearer $JMP2_TOKEN" --data-binary @api.md
curl -X POST https://jmp2.io/_api/sites/handbook/publish \
  -H "Authorization: Bearer $JMP2_TOKEN"

A single-page site. Name the file index.md and the slug is the whole URL:

curl -X PUT https://jmp2.io/_api/sites/notes/files/index.md \
  -H "Authorization: Bearer $JMP2_TOKEN" --data-binary @notes.md
curl -X POST https://jmp2.io/_api/sites/notes/publish \
  -H "Authorization: Bearer $JMP2_TOKEN"
# -> https://<you>.jmp2.io/notes/

Visibility

Every site is one of three things. New sites are public unless you say otherwise.

State Reachable by URL Listed on <you>.jmp2.io/ Needs a password
public yes yes no
secret yes no no
secret + password yes no yes

Secret is unlisted, not private. Anyone who has the URL can still read it. Add a password when the content actually needs protecting.

# at publish time
tar czf - -C ./docs . | curl -T - \
  "https://jmp2.io/_api/sites/handbook/tarball?visibility=secret" \
  -H "Authorization: Bearer $JMP2_TOKEN"

# with a password — sent as a header, never in the URL
tar czf - -C ./docs . | curl -T - \
  https://jmp2.io/_api/sites/handbook/tarball \
  -H "Authorization: Bearer $JMP2_TOKEN" -H "X-Site-Password: hunter2"

# or change it later
curl -X POST https://jmp2.io/_api/sites/handbook/visibility \
  -H "Authorization: Bearer $JMP2_TOKEN" -H 'content-type: application/json' \
  -d '{"visibility":"secret","password":"hunter2"}'

# back to public, dropping the password
curl -X POST https://jmp2.io/_api/sites/handbook/visibility \
  -H "Authorization: Bearer $JMP2_TOKEN" -H 'content-type: application/json' \
  -d '{"visibility":"public","password":null}'

Passwords are checked with HTTP Basic auth. A username is optional: set one from your dashboard and it must match, otherwise any username is accepted. Passwords are stored as PBKDF2-SHA256 with a per-site salt, and protected pages are never written to the shared edge cache, so a cached copy can never be handed to someone who did not authenticate. Clearing a password is explicit ("password": null) so a routine publish cannot unlock a site by accident.

You can also do all of this from your dashboard: set visibility, the username and the password; add and edit documents in a plain editor; and publish by dropping a markdown file, a folder, or a .zip / .tar.gz onto the page and giving it a name. Saving there publishes a new version, so the previous one stays available for rollback.

Versions and rollback

Every publish writes a new version and flips a pointer only once it is complete, so a publish is atomic and never leaves a half-updated site. The last few versions are kept.

curl https://jmp2.io/_api/sites/handbook -H "Authorization: Bearer $JMP2_TOKEN"
curl -X POST https://jmp2.io/_api/sites/handbook/rollback \
  -H "Authorization: Bearer $JMP2_TOKEN" \
  -H 'content-type: application/json' -d '{"version": 3}'

API

Base URL https://jmp2.io/_api. Auth is Authorization: Bearer <token> on every endpoint. All responses are JSON.

Method Path Does
GET /whoami tenant, quota, bytes used
GET /sites list your sites
GET /sites/:slug one site with its version history
PUT /sites/:slug/tarball stage a tar.gz — ?merge=1, ?publish=0, ?strip=0, ?visibility=
PUT /sites/:slug/files/*path stage one file (body is the bytes)
DELETE /sites/:slug/files/*path drop one file from the staged version
POST /sites/:slug/publish make the staged version live
POST /sites/:slug/rollback {"version": N}
POST /sites/:slug/visibility `{"visibility": "public"
DELETE /sites/:slug delete a site and all its versions
GET /tokens list tokens (ids and metadata only)
POST /tokens mint another token
DELETE /tokens/:id revoke a token

A tarball upload publishes by default and returns the URL:

{ "version": 2, "files": 12, "bytes": 48210,
  "title": "Handbook", "url": "https://you.jmp2.io/handbook/", "published": true }

Slugs are 1–63 characters of [a-z0-9-]. Limits: 25 MB per upload, 10 MB per file, 2000 files, 120 writes per minute.

Things worth knowing before you are surprised

Errors

Status Means
401 token missing, revoked, or expired
403 subdomain suspended
404 no such site, token, or version
413 over quota, or past a size limit
429 rate limited — see Retry-After

CLI

curl -fsSL https://jmp2.io/cli -o /usr/local/bin/jmp2 && chmod +x /usr/local/bin/jmp2
# or anywhere on your PATH:  -o ~/.local/bin/jmp2

It is a single dependency-free bash script — read it before you run it.

jmp2 push handbook ./docs     # publish a folder
jmp2 push notes ./notes.md    # publish one file as its own site
jmp2 ls                       # list sites
jmp2 info handbook            # versions
jmp2 push handbook ./docs --secret          # unlisted
jmp2 push handbook ./docs --password hunter2  # and password protected
jmp2 secret handbook hunter2  # change an existing site
jmp2 public handbook          # list it again, dropping the password
jmp2 rollback handbook 3
jmp2 rm handbook
jmp2 tokens                   # list / token-new / token-rm <id>

It reads JMP2_TOKEN or ~/.jmp2/token, and excludes node_modules, .git, target, dist and friends from uploads — jmp2 push <slug> with no path defaults to the current directory, which is otherwise an easy way to upload a whole build tree by accident.

Install this page as a Claude Code skill

mkdir -p ~/.claude/skills/jmp2
curl -o ~/.claude/skills/jmp2/SKILL.md https://jmp2.io/skill.md

The raw source of this page is the skill — /skill.md is the same document with frontmatter, so the docs and the skill can never drift.

Self-hosting

The whole thing is one Cloudflare Worker with R2 and D1. Source and setup: github.com/c4pt0r/jmp2.io.