Building extensions
An extension is a folder. It can add content types, page blocks, modules, a whole template, data models with admin screens, web routes and background reactions to events. Server code is JavaScript in a sandbox. There is no Node, no npm and no build tool to install.
Start one
From a folder that has an extensions directory:
sophia new app my-app # also: block, component, template
sophia check extensions/my-app # add --json for a machine-readable report
check validates the manifest, parses every template and compiles server.js. Then switch the extension on for a site under Extensions, or with the extension_enable tool.
The folder
extension.json the manifest
templates/*.html Jinja templates; names are "<extension>:<path>"
server.js optional server code
css/, js/, img/ sources bundled by "sophia build" (minified and fingerprinted)
After editing, run sophia check and sophia build, then restart or press Reload extensions.
The manifest
Every key is optional except name:
{
"name": "tickets",
"title": "Tickets",
"version": "0.1.0",
"kind": "app",
"description": "A small support desk.",
"server": "server.js",
"permissions": ["data", "mail"],
"collections": [{ "key": "faq", "label": "FAQs", "fields": [{ "name": "answer", "label": "Answer", "type": "richtext" }] }],
"models": [{ "key": "ticket", "label": "Tickets", "title_field": "subject",
"fields": [{ "name": "subject", "label": "Subject", "type": "text", "required": true }] }],
"blocks": [{ "type": "promo", "label": "Promo", "template": "blocks/promo.html", "fields": [] }],
"modules": [{ "type": "hours", "label": "Hours", "template": "modules/hours.html", "fields": [] }],
"admin": [{ "key": "queue", "label": "Support queue", "perm": "content.edit" }],
"settings": [{ "name": "notify", "label": "Email me at", "type": "email" }]
}
Field types: text, textarea, richtext, number, toggle, select, image, file, url, date, color, code, email, secret, list (with its own fields) and item (a link to another entry). Saved values are always cleaned against the fields.
sophia schema prints the full JSON Schema of the manifest.
Templates
Templates use Jinja syntax. They see the site, the page, menus, the current entry or list, block and module settings, and the template's own settings as theme. Helpers include position(), asset(), link(), t() for translated strings, render_blocks(), and the filters excerpt, date, time12, tel and money.
Server code
sx.route("GET", "/open", (req) => ({ json: sx.data.list("ticket", { where: { status: "open" } }) }));
sx.route("POST", "/new", (req) => {
if (!sx.rateLimit("ip:" + req.ip, 5, 3600)) return { status: 429, text: "Slow down." };
sx.data.create("ticket", { subject: String(req.form.subject || "").slice(0, 200), status: "open" });
return { redirect: "/x/tickets/thanks" };
});
sx.admin("queue", (req) => ({ html: sx.render("queue.html", { rows: sx.data.list("ticket", {}).items }) }));
sx.on("record.saved", (event) => sx.log("saved", event));
Routes answer at /x/<extension>/<path>. A route returns { json }, { html }, { text }, { page: { title, html } } (wrapped in the site's layout) or { redirect }, with optional status and headers.
The request carries method, path, params, query, body, json, form, headers, ip, the signed-in site account and the site.
What code may call depends on its declared permissions:
| Call | Permission |
|---|---|
sx.data.list / get / create / update / remove | data |
sx.content.list / get | content.read |
sx.content.draft (an editor publishes it) | content.write |
sx.accounts.get / list / setRoles | accounts |
sx.http.fetch(url, ...) | http:<host> for each host |
sx.mail.send({ to, subject, text }) | mail |
Always available: sx.settings(), sx.site(), sx.render(), sx.escape(), sx.log(), sx.rateLimit() and sx.requireRole(req, "staff"). Each call runs fresh with a memory and time limit (64 MB, 3 seconds by default).
Live page regions
Parts of a page can update from the server without a reload, with no framework and no build step. Include the core script and mark the region:
<div id="queue" data-sx-live="/x/tickets/queue" data-sx-every="15"></div>
<form data-sx-target="#queue" method="post" action="/x/tickets/close">...</form>
<a data-sx-target="#detail" href="/x/tickets/item/4">Open</a>
<script src="{{ asset('core:live.js') }}" defer></script>
data-sx-live loads the region from a route that returns { html }, and data-sx-every refreshes it every so many seconds while the tab is visible. A form or link with data-sx-target sends through fetch and swaps the answer into the target. data-sx-swap="outer" replaces the element itself. The route sees req.live as true, so one route can answer a whole page or just the part.
Rules worth keeping
- Escape anything a visitor typed with
sx.escapebefore putting it in HTML. - Declare only the permissions you use. Secrets go in settings fields of type
secret. - Content written from code is saved as a draft. People publish.
Packing and signing
sophia keygen my.key
sophia pack extensions/my-app --key my.key
sophia trust <public key> # on the install that will accept it
sophia install packages/my-app-0.1.0.sxp
A package installs only when a key that install trusts signed it.
The help desk extension that ships with Sophia Stack X is a complete example of an app with models, admin screens, routes and email.