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.
- 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.
- Connect your AI assistant. Point any MCP client at
https://app.staticowl.com/api/mcpand 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:
"/products/{slug}.html"→/products/kenya-ab.html"/products/{slug}/"or"/products/{slug}"→/products/kenya-ab/index.html- No
routePattern→ the default section URL (/<type>/<slug>/index.html; alanding_pageserves at/<slug>/index.html).
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:
- Use
:for the tag's arguments, not=—type:"product",order:"title",limit:12. (It's LiquidJS keyword syntax.) - Fields live on the loop variable. The default name is
item; name your own withas 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
{{ body }}shows literal<p>? That's the HTML-escape default doing its job on an HTML value. Use{{ body | md }}for Markdown or{{ body | raw }}for trusted HTML.- Use
{% elsif %}, not{% elif %}(Liquid uses the Ruby spelling). - A
markdownfield renders to HTML automatically everywhere it's read — top-level and inside{% list %}. Atextfield stays escaped; add| safeto unescape. - Two homepages? If you set both a
page/landing_pagewithslug: homeand asite.homepagepointer, thesite.homepagepointer wins for/index.html. - What you see in preview is what ships.
templates_previewruns the same renderer as the build.
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
- Connect your AI assistant — add the connector in any client, choose a toolset.
- Content modeling — field types, relationships, routes.
- Template language reference — the full Liquid surface.
- Publishing & environments — dev/prod, build modes, custom domains.
- HTTP API — the same operations from your own code.
- Sign up free: https://app.staticowl.com
- Support: support@staticowl.com