Zero to deployed — the 5-minute quickstart

A copy-paste recipe that goes from no account to a live page with a data-driven listing in about five minutes. It's written for both people and AI assistants: every step is a real call you can make from Claude, Cursor, ChatGPT, Claude Code, or any MCP-capable client — or straight from the HTTP API.


Step 0 — create a free account (about a minute, no card)

StaticOwl's Free plan is free forever — no card, no trial clock.

  1. Go to app.staticowl.com and sign up. You land in the admin on a Free-plan account: one site on a StaticOwl subdomain, up to 100 pages, with forms, SEO, and multi-channel output included.
  2. Connect your AI assistant. Point any MCP client at https://app.staticowl.com/api/mcp and sign in through your browser (OAuth) — there's no key to copy or paste. In Claude Code:
claude mcp add --transport http staticowl https://app.staticowl.com/api/mcp

Then run /mcp, pick staticowl, and choose Authenticate. For other clients — Claude (web and desktop), Cursor, ChatGPT — see Connect your AI assistant.

You now have StaticOwl's full tool catalog available — 352 tools across 13 toolsets. You don't need all of them at once; pick a preset (recommended is the default) as described in Connect your AI assistant.

The CMS binds your current site as your default scope on every call — you don't need to set X-Site-Id until you have two or more sites. sites_list shows what you have.


Step 1 — your site

sites_list

Returns your site and its id. (On a brand-new account with no site yet, create one with sites_create.)

types_list

Returns your content types.


Step 2 — define your own type

Skip this if the seeded types are enough for your prototype. For a product listing, add a product type:

types_create {
  name: "product",
  label: "Product",
  nodeLabel: "Product",
  keyField: "slug",
  routePattern: "/products/{slug}.html",
  fields: [
    { name: "slug",        type: "string",  required: true, unique: true },
    { name: "title",       type: "string",  required: true },
    { name: "description", type: "markdown" },
    { name: "price",       type: "number" },
    { name: "image",       type: "image" }
  ]
}

routePattern pins the exact URL shape of each record's page:

The full placeholder, encoding, and collision rules are in Routing; the field-type catalog is in Content modeling.


Step 3 — create a template

Templates are Liquid HTML. Every content field is HTML-escaped by default — the security default. To render trusted HTML (a body field, Markdown output), add a filter: | md for Markdown, | raw (or | safe) for trusted HTML, | json for a value inside a <script>.

templates_create {
  name: "product-detail",
  boundType: "product",
  content: `<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <title>{{ title }}</title>
  <style>
    body { font-family: system-ui; max-width: 720px; margin: 0 auto; padding: 40px 20px; }
    .price { font-size: 20px; color: #5A7D4D; font-weight: 700; }
  </style>
</head>
<body>
  <h1>{{ title }}</h1>
  {% if image %}{{ image | img: alt: title, width: 720 }}{% endif %}
  <div class="price">${{ price }}</div>
  <div>{{ description | md }}</div>
</body>
</html>`
}
Expression Output
{{ field }} HTML-escaped. The default — safe for text and attributes.
{{ field | md }} Markdown → HTML. For body fields.
{{ field | raw }} / {{ field | safe }} Trusted HTML, passed through as-is.
{{ field | json }} JSON, safe inside an inline <script>.
{{ url | img: alt: … }} A complete responsive <img> tag.

Full details in Template language reference.


Step 4 — add content

content_create {
  type: "product",
  fields: {
    slug: "kenya-ab",
    title: "Kenya AB",
    description: "Rich, bright, blackcurrant notes. Roasted last Tuesday.",
    price: 18.50,
    image: "https://media.staticowl.com/..."
  }
}

For images, media_upload_from_url is the assistant-friendly path: pass any public URL (a generated image, a stock photo) and StaticOwl fetches it server-side, de-duplicates by content hash, and returns a permanent CDN URL. Store that URL in the image field. See Media.

Repeat content_create for each product.


Step 5 — a data-driven listing page

{% list %} iterates content at build time. Bind a page template as your homepage and list the products:

templates_create {
  name: "home",
  boundType: "page",
  content: `<!doctype html>
<html>
<head><meta charset="UTF-8"><title>{{ title }}</title></head>
<body>
  <h1>{{ title }}</h1>
  {{ body | md }}

  <h2>Our coffees</h2>
  <div class="grid">
    {% list type:"product" order:"title" limit:12 as product %}
      <a href="/products/{{ product.slug }}.html">
        {{ product.image | img: alt: product.title, width: 400 }}
        <h3>{{ product.title }}</h3>
        <div class="price">${{ product.price }}</div>
      </a>
    {% else %}
      <p>No coffees yet — check back soon.</p>
    {% endlist %}
  </div>
</body>
</html>`
}

content_create {
  type: "page",
  fields: {
    slug: "home",
    title: "Night Owl Coffee Roasters",
    body: "Rare-varietal single-origin, roasted weekly."
  }
}

Two rules first-timers trip on:

  1. Use : for the tag's arguments, not = — type:"product", order:"title", limit:12. (It's LiquidJS keyword syntax.)
  2. Fields live on the loop variable. The default name is item; name your own with as product. Reference fields as {{ product.title }} — not top-level {{ title }}, and not {{ fields.title }}.

{% list %} supports type:, where:"field:value" (filter), order:"field" (prefix - to reverse), limit:N, as name, and an {% else %} empty branch. The full reference — including graph tags like {% similar %}, {% search %}, and {% count %} — is in Template language reference.


Step 6 — publish and build

content_publish { type: "page",    id: "page:home",        env: "dev" }
content_publish { type: "product", id: "product:kenya-ab", env: "dev" }
build_run { mode: "full", env: "dev" }

build_run returns the publicUrl and a pages[] list with every rendered path. A Free-plan site deploys to https://<slug>-<env>.preview.staticowl.com (a StaticOwl subdomain) — the URL is in the response; add your own custom domain on Starter and up.

Open it: you should see the homepage listing your products, and each product URL (/products/kenya-ab.html) rendering the detail template. When you're ready for a real environment, publish to prod and build prod — see Publishing & environments.


A few things worth knowing up front


Related content — {% similar %}

Emit a "related products" or "related posts" strip driven by meaning, not hand-picked links:

<h3>You might also like</h3>
<ul>
  {% similar limit:3 as item %}
    <li><a href="{{ item.url }}">{{ item.title }}</a></li>
  {% endsimilar %}
</ul>

With no arguments it finds items of the same type most similar to the current page. After a bulk import, materialize the index once with POST /api/maintenance/embeddings/materialize?type=product (single-record creates index automatically). Scoping, hand-picked pins, and point-in-time retrieval are all in Template language reference.


Forms — {% form %}

Real contact, lead, or newsletter forms in a couple of calls. Define the form, then drop one tag into any template:

forms_create {
  name: "contact",
  fields: [
    { name: "email",   label: "Email",   type: "email",    required: true },
    { name: "message", label: "Message", type: "textarea", required: true }
  ]
}
{% form name:"contact" submitLabel:"Send it" successMessage:"Got it — we'll reply within a day." %}

Submissions land in your site's inbox (and are exportable as CSV). The full field-type list, anti-spam behavior, and inbox reference are in Forms.


Links

Related docs

Resolved at build time via {% similar %} — cosine similarity over embeddings, not tag overlap. Zero arguments.

HTTP API
75% match
Content modeling & field types
70% match
Connect your AI assistant
69% match