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/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.
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
 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.
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/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.
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}'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.
?merge=1 if that is not what you want.javascript: links.
Uploaded .html files are served as text/plain on purpose — every subdomain
here is somebody's own origin, and nothing user-supplied is returned as HTML./handbook/docs 301s to
/handbook/docs/, without which relative links resolve one level too high.tar czf - pads its output, which some strict gzip readers reject. This
handles it, so piping straight into curl -T - is fine.| 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 |
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/jmp2It 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.
mkdir -p ~/.claude/skills/jmp2
curl -o ~/.claude/skills/jmp2/SKILL.md https://jmp2.io/skill.mdThe 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.
The whole thing is one Cloudflare Worker with R2 and D1. Source and setup: github.com/c4pt0r/jmp2.io.