OpenArtifacts

openartifacts.sh / blog

Publish a multi-file site (and get relative paths right)

2026-09-07 · tutorials · md

A static site is rarely one HTML string. It is an index.html, a stylesheet, a script, images, maybe a custom 404.html, and a small pile of paths that only work if the browser resolves them the same way you did locally. OpenArtifacts publishes that pile as one immutable version and gives the artifact one stable URL.

The important word is one. A publish is a complete snapshot, not a patch. If version two omits css/app.css, that file is not borrowed from version one.

Build a boring directory

Start with this shape:

site/
  index.html
  404.html
  css/app.css
  js/app.js
  images/mark.svg

Inside index.html, link from the document to the asset:

<link rel="stylesheet" href="css/app.css">
<script type="module" src="js/app.js"></script>
<img src="images/mark.svg" alt="">

Use relative paths. In production the artifact has its own subdomain. In local development it is served under /site/<host>/. A path such as css/app.css works in both places. A leading slash such as /css/app.css escapes the artifact's directory in local development and asks the router itself for the file.

The same rule applies inside CSS. From css/app.css, an image in the sibling top-level images directory is ../images/mark.svg, not /images/mark.svg.

Publish every file in one request

Create a token if you do not have one:

curl -sX POST https://api.openartifacts.sh/v1/signup

Then send each file as a multipart part. The part's filename is its path inside the site:

curl -sX POST https://api.openartifacts.sh/v1/publish \
  -H "Authorization: Bearer $OA_TOKEN" \
  -F slug=field-notes \
  -F 'files=@site/index.html;filename=index.html' \
  -F 'files=@site/404.html;filename=404.html' \
  -F 'files=@site/css/app.css;filename=css/app.css' \
  -F 'files=@site/js/app.js;filename=js/app.js' \
  -F 'files=@site/images/mark.svg;filename=images/mark.svg'

The response names what actually landed:

{
  "url": "https://field-notes-k3p7.openartifactsusercontent.com",
  "slug": "field-notes",
  "version": 1,
  "activated": true,
  "files": ["404.html", "css/app.css", "images/mark.svg", "index.html", "js/app.js"],
  "total_bytes": 18421
}

Do not confuse a form field name with a path. In the documented multipart form, setting filename=css/app.css is what keeps the directory.

Paths must be relative and may not traverse with ... One publish may contain at most 50 files and 10 MB.

Treat a version as a complete site

The public contract is simple: every publish creates a numbered version containing exactly the files in that request. Activating it changes what the stable URL serves. It does not merge the new upload into the previous version.

That gives an agent a useful rule: assemble and validate the whole output directory first, then publish it once. If an asset is intentionally removed, omit it. If an asset should remain, include it again. The publish response's sorted files list and total_bytes are a cheap deployment receipt; version detail also reports file_count. Compare them with what you meant to send before activating a staged version.

Visitors see one activated version at a time. The practical benefit is that HTML and its assets change together, while an older complete version remains available for rollback.

Stage before changing the live pointer

Publishing the same slug through /v1/publish creates and activates the next version. For a deliberate staging step, post to the versions route with activate=false:

curl -sX POST \
  'https://api.openartifacts.sh/v1/artifacts/field-notes/versions?activate=false' \
  -H "Authorization: Bearer $OA_TOKEN" \
  -F 'files=@site/index.html;filename=index.html' \
  -F 'files=@site/css/app.css;filename=css/app.css' \
  -F 'files=@site/js/app.js;filename=js/app.js' \
  -F 'files=@site/images/mark.svg;filename=images/mark.svg' \
  -F 'files=@site/404.html;filename=404.html'

That records version 2 but leaves version 1 live. There is no public preview URL for an inactive version today; staging lets automation inspect the API result and version inventory before activation.

curl -s https://api.openartifacts.sh/v1/artifacts/field-notes \
  -H "Authorization: Bearer $OA_TOKEN"

curl -sX POST \
  https://api.openartifacts.sh/v1/artifacts/field-notes/activate \
  -H "Authorization: Bearer $OA_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"version":2}'

Activation uses the integer version from the version list. Rollback is the identical call with an earlier number:

curl -sX POST \
  https://api.openartifacts.sh/v1/artifacts/field-notes/activate \
  -H "Authorization: Bearer $OA_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"version":1}'

The hostname never changes. Only the selected version changes.

Know the public serving rules

Extensionless links are supported: a request for /about can resolve to an uploaded /about.html or /about/index.html. A supplied /404.html is returned with an actual 404 status for unknown paths. Those conveniences do not fix broken asset references, which is why relative paths remain the important part of this tutorial.

This design is why OpenArtifacts is a better fit for agent-built static sites than a deployment flow that asks the agent to configure a build project, repository, and framework adapter. The artifact is the files. The API makes the snapshot boundary explicit, keeps version history, and rolls back with one call. If conversational tool use is a better fit than multipart curl, the same model is exposed through the MCP server.

You can try all of this without a signup form or card. The provisional token works immediately; show its claim URL to a human within 30 minutes. Claiming keeps the stable URL and unlocks 100 free publishes each month.

Your agent can have a URL before you finish this sentence — free, no signup form.

curl -X POST https://api.openartifacts.sh/v1/signup
curl -X POST https://api.openartifacts.sh/v1/publish \
  -H "Authorization: Bearer oa_sk_..." -F slug=my-page -F files=@index.html

Or hand it the whole flow at once: the skill. Claiming your account unlocks 100 free publishes a month.


← all posts · openartifacts.sh · skill.md