Skip to content

Publish your first site

This tutorial picks up where Run gotifacts locally left off: you have a running instance and you’re logged in to the portal as an admin. Now you’ll publish a page and watch it appear on its own subdomain.

Publishing happens on the ingest plane, which is authenticated by an API key (never by your browser session). Mint one with the CLI inside the running container:

Terminal window
docker compose exec gotifacts \
gotifacts keys create --name tutorial --grant "claude:publish"

The command prints the token once — copy it. It looks like gtf_…. Save it:

Terminal window
export GOTIFACTS_API_KEY=gtf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The grant claude:publish means: may publish to the claude group subtree, and nothing else. More on permissions.

Create a self-contained HTML file:

index.html
<!doctype html>
<html>
<head><meta charset="utf-8" /><title>Hello gotifacts</title></head>
<body style="font-family: system-ui; padding: 4rem; text-align: center">
<h1>👋 My first gotifacts site</h1>
<p>Published over the ingest API.</p>
</body>
</html>

The publish request carries a small JSON meta part identifying the site:

Terminal window
printf '{"group":"claude","slug":"hello","title":"Hello gotifacts"}' > meta.json

group + slug decide the URL. Here the site will live at hello.claude.gotifacts.localhost — see the URL ⇄ path convention.

Send both parts to the ingest endpoint:

Terminal window
curl -fsS \
-H "Authorization: Bearer ${GOTIFACTS_API_KEY}" \
-F 'meta=<meta.json;type=application/json' \
-F 'index=@index.html;type=text/html' \
http://gotifacts.localhost/ingest/sites

You get back JSON with the public URL:

{ "url": "http://hello.claude.gotifacts.localhost", "group": "claude", "slug": "hello", "updated_at": "..." }

Open http://hello.claude.gotifacts.localhost — there’s your page. Refresh the portal at http://gotifacts.localhost and you’ll see the site listed, with a live thumbnail.

Publishing is idempotent: the same group/slug replaces the site. Edit index.html, run the Step 4 command again, and the URL updates in place.

  • The ingest plane is key-authenticated and separate from the portal.
  • A grant confines a key to a group subtree.
  • group + slug deterministically map to a subdomain.