--- name: jmp2 description: "Publish markdown files or whole folders to jmp2.io over HTTP and get a public shareable URL back. Use when the user wants to share a document, report, note, or set of docs as a link rather than a file - 'put this online', 'give me a link to this', 'publish these docs', 'host this markdown'. Covers publishing, updating, versioning and rollback, listing sites, and managing API tokens." --- # jmp2.io 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. ```sh tar czf - ./docs | curl -T - https://jmp2.io/_api/sites/handbook/tarball \ -H "Authorization: Bearer $JMP2_TOKEN" # -> https://.jmp2.io/handbook/ ``` ## Getting a token Sign in at [jmp2.io/signup](https://jmp2.io/signup) with GitHub or Google, pick a subdomain, and copy the token it shows once. Store it: ```sh 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](https://jmp2.io/account) 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. ```sh 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: ```sh 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: ```sh 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://.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 `.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. ```sh # 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](https://jmp2.io/account) 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](https://jmp2.io/account): 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. ```sh 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 ` 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"|"secret", "password": "..."|null}` | | `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: ```json { "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 - **A tarball replaces.** Files you deleted locally vanish from the site. Use `?merge=1` if that is not what you want. - **Raw HTML in markdown is escaped, not rendered.** So are `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. - **Directories redirect to a trailing slash.** `/handbook/docs` 301s to `/handbook/docs/`, without which relative links resolve one level too high. - **Unknown file types download** rather than render. - **Secret means unlisted, not private.** Without a password the URL is still publicly readable; it is simply absent from your index. - **`tar czf -` pads its output**, which some strict gzip readers reject. This handles it, so piping straight into `curl -T -` is fine. ## 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 ```sh 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. ```sh 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 ``` It reads `JMP2_TOKEN` or `~/.jmp2/token`, and excludes `node_modules`, `.git`, `target`, `dist` and friends from uploads — `jmp2 push ` 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 ```sh 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`](https://jmp2.io/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](https://github.com/c4pt0r/jmp2.io).