| name | sui-development |
| description | Guide for developing web applications using Yao SUI framework. Use when building SUI pages, components, templates, handling data binding, event handling, backend scripts, routing, i18n, or integrating with CUI. Trigger when user mentions SUI, Yao web development, page templates, or frontend/backend integration in Yao applications. |
SUI Development Guide
SUI (Simple UI) is Yao's built-in web framework for building server-rendered pages with reactive components.
Core Concepts
- File-based routing - Directory structure defines URL routes
- Page = Component - Every page can be used as a component
- Server-side rendering - Templates rendered on server with data binding
- Progressive enhancement - Frontend scripts add interactivity
Directory Structure
/suis/<sui>/templates/<template>/
├── __document.html # Global document wrapper
├── __data.json # Global data ($global)
├── __assets/ # Static assets
├── __locales/ # i18n files
└── pages/
└── <page>/ # Route = folder name
├── <page>.html # Template
├── <page>.css # Styles (auto-scoped)
├── <page>.ts # Frontend script
├── <page>.json # Page data config
├── <page>.config # Page settings (guard, cache, SEO)
└── <page>.backend.ts # Server-side logic
Build Commands
yao sui build <sui> <template>
yao sui build <sui> <template> -D
yao sui watch <sui> <template>
yao sui trans <sui> <template>
Template Syntax
Data Interpolation
{{ name }}
{{ user.email }}
{{ title ?? 'Default' }}
{{ price * quantity }}
{{ count > 0 ? 'Yes' : 'No' }}
Conditionals
<div s:if="{{ isActive }}">Active</div>
<div s:elif="{{ isPending }}">Pending</div>
<div s:else>Unknown</div>
<div s:if="{{ isAdmin && isActive }}">Admin Panel</div>
Loops
<li s:for="{{ items }}" s:for-item="item" s:for-index="i">
{{ i + 1 }}. {{ item.name }}
</li>
<div s:for="{{ users }}" s:for-item="user" s:if="{{ user.active }}">
{{ user.name }}
</div>
Attribute Binding
<a href="{{ '/user/' + id }}">Link</a>
<button s:attr-disabled="{{ !valid }}">Submit</button>
<div class="base {{ isActive ? 'active' : '' }}">Content</div>
<div ...props></div>
Built-in Variables
| Variable | Description | Example |
|---|
$param | Route parameters | {{ $param.id }} |
$query | URL query params | {{ $query.search }} |
$payload | POST body | {{ $payload.name }} |
$global | Global data | {{ $global.title }} |
$theme | Current theme | {{ $theme }} |
$locale | Current locale | {{ $locale }} |
$auth | OAuth info (when guarded) | {{ $auth.user_id }} |
Built-in Functions
{{ P_('models.user.Find', id) }}
{{ True(user) }}
{{ Empty(items) }}
{{ len(items) }}
{{ filter(items, .active) }}
Data Binding
Page Data (<page>.json)
{
"title": "Static value",
"$users": "models.user.Get",
"$user": {
"process": "models.user.Find",
"args": ["$param.id"]
},
"$items": {
"process": "@GetItems",
"args": ["active", 10]
}
}
- Keys with
$ prefix call processes
@MethodName calls backend script functions
- Use
$param, $query, $payload in args
Backend Script (<page>.backend.ts)
There are three types of backend functions, each with different auth mechanisms:
interface AuthorizedInfo {
user_id?: string;
email?: string;
team_id?: string;
owner_id?: string;
}
declare function Authorized(): AuthorizedInfo | null;
function BeforeRender(request: Request): Record<string, any> {
const id = request.params.id;
return {
user: Process("models.user.Find", id),
items: Process("models.item.Get", { limit: 10 }),
};
}
function ApiGetUsers(): any[] {
const auth = Authorized();
if (!auth?.user_id) throw new Error("unauthorized");
return Process("models.user.Get", {
wheres: [{ column: "user_id", value: auth.user_id }],
});
}
function GetRecord(request: Request): any {
return Process("models.record.Find", request.params.id);
}
Important: $param is NOT available in backend scripts. Use request.params.
CRITICAL: ApiXxx functions called via $Backend().Call do NOT receive a request
parameter. Use the Authorized() global function for user identity. See
references/backend-api.md for the full explanation and Go runtime source references.
Components
Every page is a component. Use is attribute or <import> to embed:
<div is="/card" title="My Card">
<p>Content</p>
</div>
<import s:as="Card" s:from="/card" />
<Card title="My Card"><p>Content</p></Card>
Component Structure
/card/card.html:
<div class="card">
<h3>{{ title }}</h3>
<div class="body"><children></children></div>
</div>
Named Slots
<div class="modal">
<div class="header"><slot name="header"></slot></div>
<div class="body"><children></children></div>
<div class="footer"><slot name="footer"></slot></div>
</div>
<div is="/modal">
<slot name="header"><h2>Title</h2></slot>
<p>Body content</p>
<slot name="footer"><button>OK</button></slot>
</div>
Props Access
Frontend (card.ts):
import { Component } from "@yao/sui";
const self = this as Component;
const title = self.props.Get("title");
const allProps = self.props.List();
Backend (card.backend.ts):
function BeforeRender(request: Request, props: Record<string, any>): Record<string, any> {
const userId = props.userId;
return { user: Process("models.user.Find", userId) };
}
Event Handling
Event Binding
<button s:on-click="HandleClick">Click</button>
<input s:on-input="HandleInput" />
<form s:on-submit="HandleSubmit">...</form>
<button s:on-click="DeleteItem" s:data-id="{{ item.id }}" s:json-item="{{ item }}">
Delete
</button>
Event Handler
import { $Backend, Component, EventData } from "@yao/sui";
const self = this as Component;
self.DeleteItem = async (event: Event, data: EventData) => {
const id = data.id;
const item = data.item;
await $Backend().Call("DeleteItem", id);
(event.target as HTMLElement).closest(".item")?.remove();
};
self.HandleSubmit = async (event: Event) => {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
await $Backend().Call("Submit", Object.fromEntries(formData));
};
State Management
const self = this as Component;
self.state.Set("count", 0);
self.watch = {
count: (value: number) => {
self.root.querySelector(".count")!.textContent = String(value);
},
};
self.Increment = () => {
const count = self.state.Get("count") || 0;
self.state.Set("count", count + 1);
};
Frontend API
Backend Calls
import { $Backend } from "@yao/sui";
const users = await $Backend().Call("GetUsers");
const user = await $Backend().Call("GetUser", 123);
const item = await $Backend().Call("CreateItem", "Widget", 9.99);
Note: $Backend().Call sends only your explicit arguments to the backend ApiXxx
function. Authentication info is injected into the V8 context by the guard system and
accessed via the Authorized() global function — not via a request parameter.
Component Query
const card = $$("#my-card");
card.toggle();
card.state.Set("expanded", true);
const button = component.find("button");
const title = component.query(".title");
Render API
<div s:render="userList" class="user-list"></div>
self.RefreshUsers = async () => {
const users = await $Backend().Call("GetUsers");
await self.render("userList", { users });
};
OpenAPI Client
const api = new OpenAPI({ baseURL: "/api" });
const response = await api.Get<User[]>("/users");
if (!api.IsError(response)) {
console.log(response.data);
}
await api.Post<User>("/users", { name: "John" });
Page Configuration
<page>.config:
{
"title": "Page Title",
"guard": "oauth:/login",
"cache": 3600,
"dataCache": 300,
"api": {
"defaultGuard": "oauth",
"guards": {
"PublicMethod": "-"
}
},
"seo": {
"title": "SEO Title",
"description": "Meta description"
}
}
Guards
| Guard | Description |
|---|
oauth | OAuth 2.1 (recommended) |
bearer-jwt | Bearer token JWT |
cookie-jwt | Cookie-based JWT |
- | Public (no auth) |
Guard Scope (CRITICAL — Two Separate Systems)
The guard and api.defaultGuard fields serve different purposes:
| Config Field | Protects | When It Runs | Sets Auth For |
|---|
"guard" | Page render (HTML response) | When browser loads the page | request.authorized, $auth in templates |
"api.defaultGuard" | $Backend().Call API calls | When frontend calls $Backend().Call(...) | Authorized() global function in V8 |
You MUST set both if your page has backend API methods that need authentication:
{
"guard": "oauth:/login",
"api": { "defaultGuard": "oauth" }
}
Common mistake: Only setting "guard" but not "api". The page renders correctly
(user sees the page), but clicking a button that calls $Backend().Call("CreateItem")
fails with unauthorized because apiGuard() has no config and skips authentication.
The guard value supports an optional redirect suffix separated by ::
"oauth" — returns 403 JSON error if not authenticated
"oauth:/login" — redirects to /login if not authenticated (for page guard only)
Content Negotiation — Markdown Output
SUI pages can serve raw Markdown (or other formats) instead of HTML when requested with
a .md URL suffix or Accept: text/markdown header. This lets AI agents consume the
same URLs humans visit, but receive structured text instead of rendered HTML.
How it works:
- Request comes in:
GET /docs/en-us/getting-started/what-is-yao-agents.md
- Middleware detects
.md suffix → strips it, sets content_type=markdown on the context
- SUI loads the page, parses config, finds
"markdown" handler
- Calls the handler (backend.ts method or Yao process) → returns raw text
- Response:
200 text/markdown; charset=utf-8 with the raw content
Config — using a backend.ts method (preferred):
{
"title": "Documentation",
"guard": "-",
"markdown": {
"method": "Markdown"
}
}
The method calls a function exported from the page's own backend.ts. The function
receives the same request object as Content / Catalog / BeforeRender:
function Markdown(request: any): string {
const id = request.params?.id || "";
return fs.ReadFile(mdxPath);
}
Config — using a global Yao process (fallback):
{
"markdown": {
"process": "scripts.docs.RawMarkdown",
"in": ["$param.id"]
}
}
in supports $param.*, $query.*, $header.* placeholders (same as http.yao).
Rules:
- If config has no
markdown field → .md requests return 404
method takes priority over process when both are set
- Handler must return a
string (or []byte)
- Normal HTML requests (no
.md suffix, no Accept: text/markdown) are unaffected
- The same pattern can be extended for other formats (e.g.
"json" field in the future)
Routing
Dynamic Routes
Use [param] in folder names:
| Structure | URL | Access |
|---|
/users/[id]/ | /users/123 | {{ $param.id }} |
/[category]/[id]/ | /news/456 | {{ $param.category }} |
URL Rewriting (app.yao)
{
"public": {
"rewrite": [
{ "^\\/assets\\/(.*)$": "/assets/$1" },
{ "^\\/users\\/([^\\/]+)$": "/users/[id].sui" },
{ "^\\/(.*)$": "/$1.sui" }
]
}
}
Internationalization
How It Works (Build Pipeline)
SUI i18n is server-side rendered. The build process:
- Scans HTML for
s:trans elements and '::text' / __m("text") markers
- Extracts text, assigns auto-generated keys like
trans_<route>_1, trans_<route>_2, ...
(route slashes become underscores: route features/hero → key prefix trans_features_hero)
- For each key, looks up translations via two mechanisms (in order):
a.
keys: — direct mapping trans_<route>_N: translated text (primary, used at runtime)
b. messages: — original text → translated text lookup (used during build to populate keys:)
- Writes compiled locale files to
public/.locales/<locale>/<route>.yml with resolved keys: map
(the messages: block is stripped from compiled output — only keys: survives)
- At request time, reads the
locale cookie, loads the compiled locale, and replaces text
The locale cookie must use lowercase values: zh-cn, en-us, ja, etc.
Translation Markers
Three ways to mark translatable content:
<span s:trans>Hello World</span>
<p s:trans>This entire paragraph will be translated.</p>
<button s:trans>Submit</button>
<span>{{ '::Welcome' }}</span>
const message = __m("Welcome back");
const label = T("Settings");
Important: T() and __m() can only be called inside functions, not at module top-level.
The SUI translation runtime is not initialized when top-level constants are evaluated.
Wrong — T() at top level:
const LABEL = T("Hello");
Correct — T() inside a function:
function init() {
const label = T("Hello");
}
CRITICAL: T() arguments MUST be string literals, not variables.
The build system uses a regex (transFuncRe) to statically scan .ts/.js source for
T("...") and __m("...") calls. Only literal string arguments are detected and registered
as ScriptMessages in the build output. Variable arguments are invisible to the scanner,
so the translation will be missing at runtime.
The build pipeline for script translations:
BuildScripts reads the .ts file source
translateScript regex-matches T("literal") → creates Translation{Type: "script"}
MergeTranslations looks up the literal in messages: from the source locale YAML to get the translated value, then stores it in ScriptMessages
writeLocaleFiles clears Messages but keeps ScriptMessages in the build output (public/.locales/)
- At runtime,
parser.Locale() reads the build output → ScriptMessages is injected into __sui_locale
T("literal") at runtime looks up __sui_locale["literal"] and returns the translation
Wrong — variable argument (translation silently missing):
const DOWNLOADS = { macos: { labelKey: "Download for macOS", url: "..." } };
function init() {
const info = DOWNLOADS["macos"];
label.textContent = T(info.labelKey);
}
Correct — string literal arguments:
function downloadLabel(os: string): string {
if (os === "windows") return T("Download for Windows");
if (os === "linux") return T("Download for Linux");
return T("Download for macOS");
}
Also required: The English source string (e.g. "Download for macOS") must exist as a
key in the messages: section of the page-level source locale file (e.g.
__locales/zh-cn/features.yml) with its translation as the value. This is how
MergeTranslations resolves the translated text for ScriptMessages.
Important: s:trans captures the trimmed text content of the element as the lookup key.
The key in messages: must match exactly (case-sensitive, whitespace-trimmed).
Locale File Structure
Locale files live under the template root __locales/ directory, organized by locale:
__locales/
├── en-us/
│ ├── __global.yml # Shared translations (header, footer, etc.)
│ ├── index.yml # Page-specific: /index
│ ├── features.yml # Page-specific: /features (parent page itself)
│ ├── features/ # Sub-component locale files
│ │ ├── hero.yml # For component at /features/hero
│ │ ├── agents.yml # For component at /features/agents
│ │ └── cta.yml # For component at /features/cta
│ └── styleguide.yml # Page-specific: /styleguide
├── zh-cn/
│ ├── __global.yml
│ ├── index.yml
│ ├── features.yml
│ ├── features/
│ │ ├── hero.yml
│ │ ├── agents.yml
│ │ └── cta.yml
│ └── styleguide.yml
├── zh-tw/
│ └── ...
└── ja/
└── ...
CRITICAL rules:
- Directory names MUST be lowercase:
zh-cn, en-us, ja (not zh-CN)
- Page locale file name = page route: route
/styleguide → styleguide.yml
__global.yml is merged into every page's locale at build time
- Sub-component translations MUST go in route-matching files (see next section)
- Do NOT put locale files inside component source directories (e.g.
features/hero/__locales/)
Sub-Component Locale Files (CRITICAL)
When a page includes sub-components via is="/features/hero", each sub-component gets its
own translation key prefix based on its route. The build process uses prefix filtering to
match keys to locale files:
- Parent page
/features → key prefix trans_features_ → reads __locales/<lang>/features.yml
- Sub-component
/features/hero → key prefix trans_features_hero_ → reads __locales/<lang>/features/hero.yml
- Sub-component
/features/agents → key prefix trans_features_agents_ → reads __locales/<lang>/features/agents.yml
The parent page's MergeTranslations only processes keys matching its own prefix.
Sub-component keys (e.g. trans_features_hero_*) do NOT match the parent prefix (trans_features_*),
so they are resolved from their own locale files via Merge(compLocale).
Wrong — putting all translations in the parent file only:
__locales/zh-cn/
└── features.yml # Has messages for hero, agents, etc.
# ✗ — sub-component keys won't be resolved!
Correct — one locale file per route (parent + each sub-component):
__locales/zh-cn/
├── features.yml # Translations for features.html itself (if any)
└── features/
├── hero.yml # Translations for features/hero component
├── agents.yml # Translations for features/agents component
├── technical.yml # Translations for features/technical component
└── cta.yml # Translations for features/cta component
Each sub-component file needs both keys: and messages::
keys:
trans__features_hero_1: AI 团队
trans__features_hero_2: 一应俱全
messages:
"Your AI Team": "AI 团队"
"Ready for Action": "一应俱全"
Managing keys: (CRITICAL — Learned the Hard Way)
NEVER clear keys: to {} and expect yao sui trans to regenerate them.
yao sui trans only updates existing keys — it does NOT create new keys from scratch.
The keys: are generated during yao sui build, which scans HTML s:trans tags, assigns
sequential numbers, and resolves translations from messages:. The result is written to
compiled output at public/.locales/<lang>/<route>.yml, NOT back to source files.
When you add new s:trans content to an existing component:
- Ensure the new text has corresponding
messages: entries in the locale file
- Run
yao sui build — it generates compiled locale with the new keys
- Check compiled output:
cat public/.locales/<lang>/<route>/component.yml
- Copy the new
keys: block from compiled output back into the source locale file
- Run
yao sui build again to finalize
If you accidentally cleared keys: {}:
- Run
yao sui build — the compiled output still generates correct keys from messages:
- Extract keys from compiled output:
cat public/.locales/zh-cn/features/hero.yml | grep "trans__features_hero_"
- Paste the keys block back into
__locales/zh-cn/features/hero.yml
- Rebuild
Why this matters: At render time, SUI runtime uses keys: from the source locale files
(merged during build). If source keys: is empty, the compiled output gets empty keys too
(for that locale), and translations silently fall back to English.
How keys: and messages: Work Together (Source Code Reference)
The build pipeline (sui/core/locale.go) resolves translations in this order:
MergeTranslations: For each extracted s:trans text, finds the matching messages: entry
and writes the translated value into keys: (e.g. trans_features_hero_1: 翻译值)
ParseKeys: If a keys: value points to another messages: key (indirect reference),
resolves it to the final translated text
Merge(compLocale): Merges sub-component locale data (their keys:) into the parent page locale
At render time (sui/core/parser.go), the transNode function checks:
- First:
keys[key] — if found and differs from source text, use it
- Then:
messages[sourceText] — fallback lookup
- Otherwise: return original text unchanged
Since compiled output strips messages:, runtime effectively relies on keys: only.
Locale File Format
__locales/zh-cn/__global.yml — shared across all pages:
direction: ltr
timezone: "+08:00"
messages:
Home: 首页
Features: 特性
"Get Started": 开始使用
__locales/zh-cn/styleguide.yml — page-specific:
messages:
"Toggle Theme": 切换主题
"Used for buttons, links, and interactive elements.": 用于按钮、链接等交互元素。
__locales/en-us/__global.yml — default locale (key = value):
messages:
Home: Home
Features: Features
"Get Started": Get Started
Format Details
| Field | Purpose | Required |
|---|
messages: | Map of original text → translated text. Key = exact text from s:trans element | Yes |
direction: | Text direction: ltr or rtl | No |
timezone: | Timezone offset string | No |
keys: | Auto-generated by yao sui trans — maps trans_<route>_N to translated text. Do NOT edit manually | No |
script_messages: | Translations for __m() / T() calls in .ts scripts | No |
messages: key matching: The key must be the exact trimmed text content from the HTML.
For <span s:trans>Hello World</span>, the key is Hello World.
Mixed HTML Content — Use Separate Elements
s:trans captures the full innerHTML of the element. If an element contains child HTML tags
(like <strong>, <a>, <code>), the entire innerHTML becomes the key, which is fragile and
often fails to match. Always split mixed content into separate translatable elements.
Wrong — s:trans on a parent that contains child tags:
<li s:trans><strong>Primary</strong> — Main page action (CTA)</li>
<p s:trans>Click <a href="#">here</a> to continue.</p>
Correct — put s:trans only on leaf elements containing pure text:
<li><strong>Primary</strong> — <span s:trans>Main page action (CTA)</span></li>
<p><span s:trans>Click</span> <a href="#" s:trans>here</a> <span s:trans>to continue.</span></p>
Rule of thumb: if an element contains any child HTML tags, do NOT put s:trans on it.
Instead, wrap each translatable text segment in its own <span s:trans> or put s:trans
on the child element directly. Non-translatable content (variable names, CSS class names,
code tokens) stays outside s:trans elements.
YAML Value Quoting
YAML values that contain colons (:) MUST be quoted, otherwise the YAML parser treats
the colon as a nested mapping delimiter and the entire file fails to parse.
Wrong:
messages:
"Header bar, floating panels.": 顶栏。配合 backdrop-filter: blur 使用。
Correct:
messages:
"Header bar, floating panels.": "顶栏。配合 backdrop-filter: blur 使用。"
Always quote values that contain: :, #, {, }, [, ], ,, &, *, ?, |, -, <, >, =, !, %, @, `.
Build Commands
yao sui trans <sui> <template>
yao sui trans <sui> <template> -l zh-cn,ja
yao sui build <sui> <template>
yao sui trans scans all pages, extracts s:trans / __m() text, merges with existing
messages: in locale files, generates keys: mappings, and then triggers a full build.
After adding new locale files, always run yao sui trans first (not just build)
to ensure keys: are properly generated from messages:.
Workflow: Adding i18n to a New Page
- Add
s:trans attributes to all translatable text in HTML
- Create locale files matching the page route structure:
__locales/<lang>/<page>.yml for the page itself
__locales/<lang>/<page>/<component>.yml for each sub-component
- Fill in
messages: with "English text": "翻译" pairs (set keys: {} initially)
- Run
yao sui build <sui> <template> to compile — this generates correct keys: in compiled output
- Extract keys from compiled output and write them back to source locale files:
cat public/.locales/zh-cn/<page>/<component>.yml | grep "trans__"
- Paste the
keys: block into __locales/zh-cn/<page>/<component>.yml (replacing keys: {})
- Run
yao sui build again to finalize
- Verify: switch language in browser, confirm all text is translated
Workflow: Adding New Translatable Content to an Existing Page
- Add new
s:trans elements in the component HTML
- Add corresponding
messages: entries to the locale files (all languages)
- Run
yao sui build — compiled output will contain new keys (e.g. trans__features_hero_21 onwards)
- Extract the full keys block from compiled output and replace source
keys: with it
- Rebuild — translations will now work correctly
Do NOT clear keys: to {} expecting yao sui trans to regenerate.
Do NOT rely on yao sui trans alone to add new keys — it only updates existing ones.
PITFALL — New s:trans elements not translated after build
When you add a new s:trans element to an existing component HTML, the build will
auto-assign the next sequential key (e.g. trans__features_cta_4). However, the new key
only appears in the compiled output (public/.locales/), not in the source locale
file (__locales/). If the source keys: block does not contain this new key with its
translated value, the compiled output will fall back to the English original.
Symptom: New s:trans text stays in English while other text on the page translates fine.
Fix: After adding new s:trans elements:
- Add the English → translated mapping to
messages: in all locale files
- Run
yao sui build once to generate the new key numbers
- Check the compiled output
public/.locales/<lang>/<route>.yml to find the new key
(e.g. trans__features_cta_4: Other platforms & architectures →)
- Copy the new key back to the source
keys: in __locales/<lang>/<route>.yml,
replacing the English value with the correct translation
- Rebuild
Example: Adding <a s:trans>Other platforms & architectures →</a> to cta.html
that already had 3 translated elements:
keys:
trans__features_cta_1: 你的 AI 团队待命中
trans__features_cta_2: 下载即用,几分钟上手。
trans__features_cta_3: 下载
keys:
trans__features_cta_1: 你的 AI 团队待命中
trans__features_cta_2: 下载即用,几分钟上手。
trans__features_cta_3: 下载
trans__features_cta_4: 其他平台与架构 →
Dynamic <html lang>
Use the built-in $locale variable to set the document language dynamically:
<html lang="{{ $locale }}">
Client-Side Locale Switching
Set the locale cookie (lowercase!) and reload:
document.cookie = "locale=zh-cn;path=/;max-age=31536000";
window.location.reload();
SUI reads the locale cookie on each request to determine which compiled locale file to use.
Agent SUI
Special configuration for AI Agent apps. Loads from /agent/template/ and /assistants/<name>/pages/.
Route Mapping
| File Path | Public URL |
|---|
/agent/template/pages/login/login.html | /agents/login |
/assistants/demo/pages/index/index.html | /agents/demo/index |
Build
yao sui build agent
yao sui watch agent
CUI Integration
window.addEventListener("message", (e) => {
if (e.data.type === "setup") {
const { theme, locale } = e.data.message;
document.documentElement.setAttribute("data-theme", theme);
}
});
const sendAction = (name: string, payload?: any) => {
window.parent.postMessage(
{ type: "action", message: { name, payload } },
window.location.origin
);
};
sendAction("notify.success", { message: "Done!" });
sendAction("navigate", { route: "/agents/demo/detail", title: "Details" });
Quick Reference
Common Patterns
Protected page with API (always set both guards):
{ "guard": "oauth:/login", "api": { "defaultGuard": "oauth" } }
Backend script with auth (ApiXxx uses Authorized(), NOT request):
declare function Authorized(): { user_id?: string; team_id?: string } | null;
function ApiDeleteItem(id: string) {
const auth = Authorized();
if (!auth?.user_id) throw new Error("unauthorized");
Process("models.item.Delete", id);
return { success: true };
}
List with delete:
<div s:for="{{ items }}" s:for-item="item">
{{ item.name }}
<button s:on-click="Delete" s:data-id="{{ item.id }}">×</button>
</div>
self.Delete = async (e: Event, data: EventData) => {
await $Backend().Call("DeleteItem", data.id);
(e.target as HTMLElement).closest("div")?.remove();
};
Form submission:
<form s:on-submit="Submit">
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
self.Submit = async (e: Event) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
await $Backend().Call("Save", Object.fromEntries(new FormData(form)));
};
File Reference
| File | Purpose |
|---|
<page>.html | Template |
<page>.css | Styles (auto-scoped) |
<page>.ts | Frontend script |
<page>.json | Data configuration |
<page>.config | Page settings |
<page>.backend.ts | Server-side logic |
__document.html | Global wrapper |
__data.json | Global data |
__locales/ | i18n translations |
__assets/ | Static files |