Build and deploy a site
Write your site yourself — any design, any stack — with Era as its backend: products, prices, stock, checkout, orders, accounts, posts, forms and bookings. Declare what the owner may change in era.theme.json, publish to getera.site, and the owner edits it in Era's theme editor.
Era is the backend of your site, not a site builder. The design and the code are yours — plain HTML, Astro, a Next.js static export, anything that produces files — and in Era desktop your own Claude or Codex writes it from scratch for the brand. Era holds the business: products, prices, stock and photos, the basket and Era's hosted checkout, orders, customer accounts, blog and news, forms and bookings. Everything else the owner should be able to change without code — the hero line, opening hours, a section's photo, the order of the sections — you declare in era.theme.json, the way a Shopify theme declares its sections. The owner then edits those in Era's theme editor, next to a live preview, and the site shows the change within half a minute.
write the site → era theme check → era deploy → the owner edits in Era → era theme pull before you change the codeYou need the era CLI signed in to your workspace and the Website module on. Publishing and importing a theme need site:manage.
Put the business in Era
era sites list --json # the site's id is its publishable key
era run catalog.create_item --data '{"name":"Ethiopia Guji 250 g","kind":"product","fulfilment":"shipping","priceAmount":42000,"inShop":true}'
curl -s "https://app.getera.io/api/site/v1/SITE_ID/products" # what a visitor seesProducts, prices (in minor units: 42000 is ₺420.00), stock and product photos live in Era; the owner changes them in Products and your site follows. Posts are written in Website → Blog / News, forms in Forms.
Write the site, with Era as its backend
Load the kit once — as a script (era-site.js, from @getera/storefront/site) or as a package:
<script src="/era-site.js" data-site="SITE_ID" data-api="https://app.getera.io" defer></script>import { createStorefront } from "@getera/storefront"
const era = createStorefront({ siteId: "SITE_ID" })
const { items } = await era.products.list({ category: "coffee" })
await era.cart.add(items[0].variantId!, 1)
await era.checkout.redirect({ returnUrl: `${location.origin}/thanks` })Render the products at build time too, so the page is complete and indexable before any script runs.
Declare what the owner may change
era.theme.json at the site's root lists sections; each has typed settings, optional repeatable blocks, and optionally a live source (Era posts, products, a form, services):
{
"name": "Köşe Kavurma",
"locales": ["tr", "en"],
"sections": [
{ "id": "hero", "label": { "tr": "Giriş", "en": "Hero" },
"settings": [
{ "id": "title", "type": "text", "label": { "tr": "Başlık", "en": "Headline" }, "default": { "tr": "Moda'da kendi kavurduğumuz kahve", "en": "Coffee we roast ourselves, in Moda" } },
{ "id": "image", "type": "image", "label": { "tr": "Görsel", "en": "Image" }, "default": { "url": "/img/hero.avif", "alt": "The roaster at dawn" } },
{ "id": "cta", "type": "link", "label": { "tr": "Düğme", "en": "Button" }, "default": { "tr": { "label": "Kahvelerimiz", "href": "#coffee" }, "en": { "label": "Our coffee", "href": "#coffee" } } } ] },
{ "id": "hours", "label": { "tr": "Çalışma saatleri", "en": "Opening hours" },
"blocks": { "max": 7, "settings": [ { "id": "label", "type": "text", "label": "Days" }, { "id": "value", "type": "text", "label": "Hours" } ],
"default": { "tr": [ { "label": "Her gün", "value": "08:00–20:00" } ] } } },
{ "id": "news", "label": { "tr": "Haberler", "en": "News" }, "source": { "type": "posts", "kind": "news" },
"settings": [ { "id": "count", "type": "number", "label": "How many", "default": 3, "min": 1, "max": 6 } ] }
],
"layout": { "order": ["hero", "hours", "news"] }
}| Setting type | Value | Per language |
|---|---|---|
text, textarea | a string (textarea: an empty line starts a paragraph) | yes |
link | { "label", "href" } | yes |
image | { "url" or "fileId", "alt" } | shared |
number, checkbox, select, color, url | a number, true/false, an option, a colour, a URL | shared |
json | anything | yes |
Set "localized": true or false on a setting to override. A section's blocks become one list the owner adds rows to; layout.order is the starting order the owner may change (and hide sections).
Then bind the page to it. The text you write is the fallback; Era's value replaces it:
<section data-era-section="hero">
<h1 data-era="hero.title">Moda'da kendi kavurduğumuz kahve</h1>
<img data-era="hero.image" src="/img/hero.avif" alt="The roaster at dawn">
<a data-era="hero.cta" href="#coffee">Kahvelerimiz</a>
</section>
<ul data-era="hours.blocks">
<template><li><span data-era-field="label"></span> <span data-era-field="value"></span></li></template>
<li>Her gün 08:00–20:00</li>
</ul>
<div data-era-posts="news" data-era-limit="news.count" data-era-href="/news/{slug}">
<template><article><h3 data-era-field="title"></h3><p data-era-field="excerpt"></p><time data-era-field="date"></time></article></template>
</div>
<meta name="description" data-era-attr="content:seo.description">
<div data-era-show="announcement.on">…</div>Keys are <section>.<setting> (<section>.blocks for blocks). Keep them stable: that is where the owner's edits live.
era theme check # validates era.theme.json against Era, writes nothingPublish
era deploy dist --site SITE_ID # or set siteId in era.config.jsonTheme imported (version 1: 14 new settings, 0 kept as the owner set them).
Uploading 23 files…
Live: https://koshe.getera.siteera deploy imports era.theme.json (from the folder or its parent) and uploads the files to POST /api/v1/sites/{siteId}/deploy. Every deploy is a version you can roll back to in Website → Deploys.
The owner edits it in Era
Website → Content, or Design → theme editor: the sections in the site's order, each with its settings and blocks, beside a live preview (computer and phone, every language). Saving writes the values; the edge binds them into every page on the next request (cached for at most 30 seconds), so there is no rebuild and no flash of old text. Products, prices and posts are changed where they live in Era.
Change the code later without losing the owner's edits
era theme pull # era.theme.values.json: what the owner has set
# change the code freely; add settings with defaults; keep existing keys
era deploy distA re-import keeps every value the owner changed and adds new settings with their defaults. Settings you removed from the manifest are reported and kept, unless you pass era theme import --prune.
How the owner's changes reach the site
| Where the site runs | How it stays current |
|---|---|
Deployed by Era (*.getera.site, your domain) | The edge binds the values into the HTML at request time — no rebuild. |
| Anywhere else, plain or SPA | era-site.js / era.bind() fetches GET /api/site/v1/{siteId}/content (edge-cached, ETag) and fills the bindings. |
| A static generator that bakes text at build time | Add a webhook for site.content_changed in Settings → Webhooks pointing at your build hook (Netlify, Vercel, Cloudflare Pages); it fires on every save. |
const content = await era.content() // { version, entries: { "hero.title": { kind, value, values } } }
const title = await era.content("hero.title") // the value in the page's language
const settings = await era.settings() // company, contact, socials, logo, languagesThe storefront kit
In the browser, a site talks to Era with @getera/storefront. Its key is the site's own id: it can do only what a visitor of your published site can do — read what the site shows, fill the basket, check out, submit forms, book — and it answers only your site's own addresses (its *.getera.site address, its verified domains, and localhost while developing). Never put an API key in a site.
| Area | Calls |
|---|---|
| Catalogue | products.list({ q, category, collection, brand, sort, page, pageSize }), products.get(slug), search(q), categories(), collections() |
| Basket | cart.add(variantId, qty), cart.set(…), cart.remove(variantId), cart.get(), cart.clear() |
| Checkout | checkout.url({ returnUrl }), checkout.redirect(…), checkout.result(), orders.get(token), paymentMethods() |
| Owner's content | content(), content(key), settings(), bind(root) |
| Posts and pages | posts.list({ kind: "blog" | "news" }), posts.get(slug), pages() |
| Forms | forms.get(slug), forms.submit(slug, values, { honeypot, startedAt }) |
| Booking | booking.services(), booking.slots(serviceId, date), booking.book(input) (signed-in customer) |
| Customer accounts | customer.requestCode(email), customer.verify(email, code), customer.orders(), customer.bookings(), customer.signOut() |
The kit never sends an amount: prices, discounts and totals are always worked out by Era.
Hosted checkout
await era.checkout.redirect({ returnUrl: `${location.origin}/thanks` })
// On /thanks:
const done = era.checkout.result() // { orderToken, number, status, paid }
const order = done && (await era.orders.get(done.orderToken))Era's checkout collects the details, delivery and payment with your connected payment app, places the order (see How orders work) and returns the shopper to returnUrl?order=…&number=…&status=…. The return address must be on one of your site's own hosts.
Your own domain
era domains add shop.example.com --site SITE_IDEra shows the DNS records to add in Website → Domains; once your DNS points at Era the domain verifies by itself and serves the live version.
Templates
era templates lists Era's 31 code templates and era init --template <name> copies one into a folder — useful as inspiration or when someone asks for a template. A site written for the brand from scratch is the default.
Also from the API
| Call | Does |
|---|---|
POST /api/v1/actions/site.import_theme | Register era.theme.json (dryRun, prune) |
POST /api/v1/actions/site.set_content | Change one entry ({ siteId, key, value: { "tr": … } }) |
GET /api/v1/site_contents?site_id=… | The owner's current values |
GET /api/site/v1/{siteId}/content · /content/{key} · /settings | What a visitor's page reads (public, cached) |
POST /api/v1/sites/{siteId}/deploy | Publish a built site: { files: [{ path, content (base64) }] } |
GET /api/v1/sites/{siteId}/deploys · POST …/deploys/{id}/rollback · DELETE /api/v1/sites/{siteId}/deploy | Versions, roll back, take down |
For anything a visitor must not do — syncing stock from a supplier, exporting orders — use @getera/sdk with an API key on a server, never in the site's browser code.