yumemi

Have a sweet Gleam. 設計を書けば、コードが生まれる。

A framework for Gleam on Cloudflare Workers. You describe an application in five words — Entity, Property, Type, Service, Authorization — and write each Service’s logic; yumemi derives the Gleam code around that logic (types, codecs, reads, authorization checks, writes, the route table, SQL) and the HTTP entrance to it. MCP and CLI entrances are designed to come from the same description but are not generated yet.

Open source, coming soon. https://gleam.canon-ical.com/

Status

Under development. The API may change between 0.x releases.

Extracted from the framework/ directory of a production application on 2026-09-16, history included. Since 0.12 the module namespace is yumemi/* (it was framework/* up to 0.11) and the generator is the module yumemi/gen of the same package, so the Hex package yumemi carries both: the generator that runs is the yumemi version your manifest.toml locks. The database schema the runtime reads is still named framework (db/queries/framework/*.sql, keys such as framework/outbox_claim).

Layout

ModuleWhat
yumemi/spec yumemi/erEntity / Property / Type ── the model and its derivation
yumemi/entry yumemi/verb yumemi/party yumemi/requireEntrances and Authorization
yumemi/step yumemi/effect yumemi/query yumemi/ioService logic: read / guard / apply / call / done
yumemi/connector yumemi/page yumemi/blob yumemi/vectorConnectors, SSR pages, R2, Vectorize
yumemi/secret yumemi/sealed yumemi/timeSecrets, sealed values, time
yumemi/front yumemi/front/*Faces: Layout, Page, Frame, Style, the client runtime (navigate.mjs)
yumemi/server yumemi/server/*.mjs yumemi/gateThe back-end runtime the generated code imports, and a face’s gate
yumemi/genThe generator (reads the app’s Gleam sources, writes the derived Gleam, SQL and JS)

The generator

Run it from any package that depends on yumemi (the app’s api/, or a face):

gleam run -m yumemi/gen -- <app dir> <out dir>

For example, in a repository with api/ and faces beside it, cd api && gleam run -m yumemi/gen -- . .. writes the back under api/ and each face’s files under the face’s package. It exits non-zero when the output is incomplete; the exit code says who fixes it (yumemi/gen/stop).

One version (0.12, article 41). The generator reads the manifest.toml of the app package and of every face it writes, and stops (exit 4) naming the package whose locked yumemi differs from the version that is running (<dir>/manifest.toml: yumemi の lock <locked> が走っている生成器の版 <running> と違う); run gleam update yumemi there. A package without a manifest.toml is not checked, and packages the generator does not read or write (another app’s auth service, say) are outside the check.

A face’s priv/static/_yumemi/client.mjs is bundled with the esbuild the face’s lockfile installed (<face>/node_modules/.bin/esbuild). When it is missing the generator stops with exit 3 and names the path — add esbuild to the face’s devDependencies and run npm ci in the face. It does not fall back to npx.

Developing yumemi

npm ci        # the esbuild the generator tests bundle with
gleam test    # the runtime checks and the generator tests (fixtures in gen/fixtures)

gen/fixtures (sample apps) and gen/scripts (one-off verification scripts from earlier releases; the ones that ran the old yumemi_gen package were removed in 0.12) are not part of the Hex package.

To generate a fixture in place, run the generator from the root (gleam run -m yumemi/gen -- gen/fixtures/article gen/fixtures/article). The fixture faces have no package.json, so first link the root’s esbuild into each face that has a client (public in article: mkdir -p gen/fixtures/article/public/node_modules/.bin && ln -s "$PWD/node_modules/.bin/esbuild" gen/fixtures/article/public/node_modules/.bin/esbuild); without it the generator stops with exit 3. When yumemi’s [dependencies] change, run gleam update yumemi in each fixture face, or its client build cannot see the new packages.

yumemi/front ── Elements and attributes (0.12)

A Block, a Layout’s Block and the docs page are written with yumemi’s own element tree; a ★ module imports yumemi/front/el, yumemi/front/html, yumemi/front/attr and yumemi/front/css, and never lustre or sketch/lustre (the generator stops on that import, exit 4, article 1; a face’s gleam.toml listing lustre / sketch_lustre stops too, article 2). There is no exception: interactive Components are written with the same words (below). A .gleam file placed by hand under a face’s src/gen/ (one without the //// GENERATED first line) is read as ★ and checked the same way. A ★ module may not use @external (exit 4, article 4); JavaScript goes in a named escape (below).

import yumemi/front/attr
import yumemi/front/css
import yumemi/front/el.{type Element}
import yumemi/front/html

const card = [css.Color(css.Token("ma-color-ink")), css.Space(css.Padding, css.Px(16.0))]

pub fn view(it: In) -> Element {
  html.section(css.class(card), [attr.data("slug", it.slug)], [
    html.h1_([], [el.text(it.title)]),
    html.a_([attr.href(it.link)], [el.text("more")]),
  ])
}

The raw-CSS escape (0.12)

shell.stylesheets (below) and el.stylesheet(el.static_sheet("fonts/mincho.css")) in a Block load a .css file from the face’s priv/static/. Both are an escape hatch that the generator checks and counts:

yumemi/front ── Interactive Components (0.12)

An interactive Component is a src/components/<name>.gleam module with a few constants and a view that takes Args and returns elements. There is no update, no State, no Event and no lustre: the state lives in the form’s controls and in the element’s custom state (:state()), and the generated client runtime (yumemi/front/interactive.mjs) moves it.

import gen/live/article_publish as publish
import gen/live/article_create as create
import gen/route
import gen/service
import gleam/option.{None, Some}
import yumemi/front/css
import yumemi/front/el.{type Element}
import yumemi/front/html
import yumemi/front/live

pub const calls: List(service.Service) = [service.ArticlePublish, service.ArticleCreate]

pub const after_send: List(#(service.Service, live.After(route.Route))) = [
  #(service.ArticlePublish,
    live.After(ok: live.GoTo(route.ArticleArgSlug, notice: Some("Published")), error: live.Stay)),
  #(service.ArticleCreate, live.After(ok: live.ReloadPage(notice: None), error: live.Stay)),
]

pub fn view(p: publish.Args, c: create.Args) -> Element {
  html.div_([], [
    html.input_([publish.bind(publish.Slug, p.slug)]),
    publish.send(css.class([]), [el.text("Publish")]),
    el.dialog_opener("copy", css.class([]), [el.text("Copy")]),
    el.dialog("copy", css.class([]), [], [
      html.input_([create.bind(create.Slug, c.slug)]),
      create.send(css.class([]), [el.text("Create")]),
    ]),
    publish.when(publish.Refused(publish.AlreadyPublished), [el.text("Already published")]),
    publish.when(publish.Succeeded, [el.text("Version "), publish.out(publish.OutVersion)]),
  ])
}

yumemi/front ── Named escapes (0.12)

JavaScript a page needs and yumemi has no word for goes in src/escapes/<name>.mjs ([a-z][a-z0-9_]*, article 6) and nowhere else under src/ (exit 4, article 5). The file exports mount(element) (exit 3 when it does not), which may return a function to call when the element leaves.

// src/escapes/chat_socket.mjs
export function mount(element) {
  const socket = new WebSocket(element.dataset.url);
  return () => socket.close();
}

The generator writes src/gen/escapes.gleam with one function per file, escapes.chat_socket(attributes, fallback), which renders <yumemi-escape name="chat_socket" …>fallback</yumemi-escape>; the fallback shows without JavaScript. The client bundles the files and mounts each element when it connects (also after client navigation) and unmounts it when it leaves. An escape may sit inside an interactive Component and write its controls.

The escape line. Per face the generator prints one warning (exit 0, also in _diagnostics.txt) counting every escape hatch: [exit 0 警告] <face>: 逃げ道 ── escape <kinds> 種 <calls> か所(うち interactive Component の中 <n>)、生の CSS <files> file <rules> 規則、数える口の送り先 ToService <n>.

yumemi/front ── Shell: the home screen, the service worker and the stylesheets (0.11.12, 0.11.15)

A face’s src/shell.gleam holds the constants the generator puts in every page’s <head>. lang, title and theme are required (as before; from 0.12 theme is pub const theme: theme.Defaults = theme.Defaults(background: "#FAF7F0", text: "#3D2419", accent: "#A93632", background_image: None) — the three colours are #rrggbb literals, exit 4 otherwise, and background_image is None or Some("<path under priv/static>"), exit 3 when the file is missing; article 26). Five more are optional String constants; each one that is present adds one element after the <title>, in this order, and a shell without them generates exactly what 0.11.11 did:

ConstantGoes out as
manifest<link rel="manifest" href="..">
theme_color<meta name="theme-color" content="..">
icon<link rel="icon" href=".."> (the favicon)
apple_touch_icon<link rel="apple-touch-icon" href="..">
service_workerthe generated priv/static/_yumemi/client.mjs calls navigator.serviceWorker.register("..") once when it loads

stylesheets (0.11.15) is an optional List(String) constant: one <link rel="stylesheet" href=".."> per item, in list order, after the elements above. From 0.12 each file is checked and counted as the raw-CSS escape (above). Put the face’s site-wide stylesheets here rather than in a Block: a <link> in <head> holds the first paint until it has loaded, and client navigation keeps it (see Client navigation below), where a <link> in <body> is rebuilt — and revalidated — on every navigation. An empty list adds nothing.

pub const manifest: String = "/manifest.webmanifest"
pub const theme_color: String = "#A93632"
pub const icon: String = "/favicon.svg"
pub const apple_touch_icon: String = "/apple-touch-icon.png"
pub const service_worker: String = "/sw.js"
pub const stylesheets: List(String) = ["/brand/tokens.css", "/brand/site.css"]

yumemi/front ── Client navigation (0.11.4, 0.11.15, 0.12)

The generated client calls yumemi/front/navigate.mjs’s start({routes, csp}). routes lists every Page route of the face; csp maps each route of a gate frame_src Page to the content-security-policy value the gate sends with it (frame-src https://<host> .. from the entry’s frame_src hosts). A face without such Pages gets no csp key, exactly as in 0.11.14.

Since 0.12 the browser’s Navigation API does the browser’s part: it reports the navigation (navigation’s navigate event), keeps history, restores scroll, resets focus, aborts a superseded navigation and handles fragments. navigate.mjs takes a navigation with event.intercept() and does the rest: the fetch, the swap check, the head diff, the stylesheet wait, the body swap, and the pageview mark. Interactive Components and escapes in the new body start by themselves: their custom elements are defined once, and each connects when the body is inserted (0.12 has no boot).

yumemi/front ── Page theme: where the colours come from (0.11.13, 0.12)

Page(.., theme: Some(name), ..) paints the page with a yumemi/front/theme.PageTheme read on the server; theme: None paints the shell’s theme defaults only.

// yumemi/front/theme
pub type PageTheme {
  PageTheme(background: Option(color.Color), background_image: Option(blob.Blob),
            text: Option(color.Color), accent: Option(color.Color))
}

yumemi/front ── Style (0.11.10)

yumemi/front/css’s Style is the typed vocabulary a Page / Area / component carries; yumemi/front/sketch_css maps it to CSS (sketch classes; from 0.12 a class name is a hash of the rendered CSS, so the same Style gets the same name in node, the Worker and a minified bundle, and an interactive Component’s Style lands in the page’s <style> like a Block’s). css.class(styles) makes the Class a tag takes. Every word also works inside State(Hover / Focus / FocusVisible / HoverCapable / Disabled / Current / Waiting / Succeeded / Failed / ThemeImage, ..) and Responsive(SP / PC / Tablet, ..).

Colours are a closed type, css.Paint (0.12; before, a string passed straight through):

PaintCSS
Hex("#a1b2c3")the hex, lower case — a string literal of # and six hex digits (exit 4 otherwise, article 23)
Token("ma-color-ink")var(--ma-color-ink) — the name is [a-z0-9-]+ (exit 4 otherwise, article 24)
Theme(ThemeBackground / ThemeText / ThemeAccent)var(--yumemi-theme-bg) / -text / -accent (Page theme, above)
Mix(a, b, percent)color-mix(in oklab, a percent%, b) — percent 0–100 (exit 4 otherwise, article 25)
Transparent / CurrentColortransparent / currentcolor
StyleCSS
Color(value)color
Background(value)background-color
Border(edge: AllEdges / TopEdge / BottomEdge, width, style: Solid / Dashed / Dotted, color)border / border-top (0.12) / border-bottom (e.g. the 2px tab underline)
TextAlign(align: TextStart / TextCenter / TextEnd)(0.12) text-align: start / center / end
Outline(width, offset, color)outline-style: solid / outline-width / outline-offset / outline-color — the focus ring, inside State(FocusVisible, ..) (0.11.14; State(Focus, ..) also keeps it after a tap)
Space(property:, value:)margin / padding / gap / width / height / border-radius, and (0.11.7) min-width / min-height / max-width
Text(family:, size:, weight:, line_height:)font-family (System / SansSerif / Serif / Monospace, or Named("var(--ma-font-ui)") / Named("\"Noto Sans JP\", sans-serif"); each comma-separated Named item must be var(--<name>), a quoted family name or bare words of [A-Za-z0-9_-], else the value is unset), font-size, font-weight (Normal 400 / Medium 500 / SemiBold 600 / Bold 700), line-height
Crop(fit: Cover / Contain / Fill / ScaleDown / FitNone, ratio: Ratio(w, h))object-fit and aspect-ratio
Sizing(box: BorderBox / ContentBox)(0.11.8) box-sizing — BorderBox counts padding and border inside width / min-height, so an input or an <a> button with min-height: 48px and padding stays 48px
Marker(marker: NoMarker)(0.11.8) list-style: none — drops the ul / li bullet (clear the indent with Space(Padding, Px(0.0)))
Decoration(line: NoDecoration / Underline)(0.11.8) text-decoration: none / underline — e.g. a row link without the underline, underlined again inside State(Hover / Focus, ..)
Wrap(wrap: Anywhere / BreakWord / WrapNormal)(0.11.10) overflow-wrap: anywhere / break-word / normal — Anywhere breaks a long URL or an unbroken word at the line’s width, so the row’s scrollWidth stays its clientWidth (it also lowers the min-content width, so the word does not widen a flex / grid item); BreakWord breaks it too but keeps the min-content width
State(FocusVisible, styles)(0.11.14) :focus-visible — the focus ring for keyboard focus only. Focus (:focus) also matches after a tap or a click, so a ring put there stays on a tapped button; put the ring here instead
State(HoverCapable, styles)(0.11.14) @media (hover: hover) { .class:hover { .. } } — hover on devices that can hover (a mouse). On a touch screen :hover stays true after a tap, so a Hover background stays on the tapped button; this one is not applied there. Same specificity as Hover (the class plus :hover), so it beats the class’s own background. Inside Responsive(at, ..) it becomes one media, @media (<at>) and (hover: hover)
State(Current, styles)(0.11.9) [aria-current]:not([aria-current="false"]) — the item that is where the user is (aria-current="page", also step / location / true). Mark the nav item with aria-current (a Layout block learns the page from CurrentRoute, below) and give it e.g. the 2px underline with Border(BottomEdge, ..)
State(Waiting / Succeeded / Failed, styles)(0.12) inside an interactive Component whose custom state is waiting / ok / error. Written twice: @supports selector(:state(x)) { .class:is(:state(x) *) {..} } and @supports not selector(:state(x)) { .class:is([data-yumemi-state~="x"] *) {..} } for browsers without :state(). Inside Responsive(..) the two selectors are written without @supports (sketch drops an at-rule inside a media)
State(ThemeImage, styles)(0.12) .class:is([data-yumemi-theme-image] *) — on a page with a background image
Flow(..) State(..) Responsive(..)layout, interaction states, breakpoints (unchanged). Animation was removed in 0.12 (no @keyframes was ever emitted)

Lengths (css.Length) are Px(n) / Rem(n) and, from 0.11.9:

LengthCSS
Var(name)var(--name) — e.g. Var("ma-space-2"). The name passes only [a-z0-9-]; any other name (;, }, ), a space, upper case, empty) is written as unset by sketch_css, and the generator stops on it in an Area’s gap
Env(SafeTop / SafeRight / SafeBottom / SafeLeft)env(safe-area-inset-top, 0px) … (0px where the device has no safe area)
Dvh(n)n dvh — e.g. Space(MinHeight, Dvh(100.0)) for a short page that still fills the screen

They work everywhere a Length does: Space, Text, Border, Outline, Flow gaps, and an Area’s flow gap in the generated grid CSS (literal or a style constant).

yumemi/front ── Grid style and the bottom bar (0.11.10)

yumemi/front ── Layout vars and Overlay (0.11.9)

yumemi/front ── Area flow (0.11.8)

An Area’s flow in a Layout or a Page’s Frame now reaches the generated grid CSS (the <style> that htmlWithGridCss puts in SSR pages and _yumemi/style.css, which carry the same text): the Area’s rule gets the flow’s declarations after grid-area, so the blocks placed in the Area are laid out by it. Before 0.11.8 the Area stayed display: block and the flow was ignored.

flowdeclarations in the Area’s rule
Stack(gap:)display: flex; flex-direction: column; align-items: stretch; gap — blocks keep the Area’s full width
Row(gap:, wrap:)display: flex; flex-direction: row; flex-wrap: wrap / nowrap; gap
Grid(cols:, gap:)display: grid; grid-template-columns: repeat(cols, minmax(0, 1fr)); gap
GridTracks(cols:, gap:)unchanged (already written since 0.11.x: display: grid, the tracks, gap)
Scrollerdisplay: flex; flex-direction: row; overflow-x: auto

yumemi/server ── what the app must provide (0.11.1)

The back-end runtime (yumemi/server/*.mjs) is JavaScript that the generated src/gen/*.mjs imports. It knows no application names: route names, cookie names, key bindings, declaration and consent kinds and the party a queue consumer reads its borrowed root as come from the app’s src/server.gleam (attached_roles, browser, consents, hooks, roots’ QueueParty). The generated face gate (<face>/src/gen/gate.mjs, declared in <face>/src/gate.gleam with yumemi/gate) reads the session through the ReadSession attached route. Gleam packages cannot declare npm dependencies, so the app supplies the following itself.

Generated live modules (0.11.2) send Args by their declared type: Bool as a JSON boolean (the live field’s "true" / "false"), Int / Float as numbers. On a GET route the Args that are not path holes go on the query string (an empty Option is left out), and the runtime reads bool (true / false) and float spellings from the query of GET / HEAD requests. POST / PUT / DELETE bodies are read as JSON as before.

A live field for a List(X) Arg (X a scalar, value type, id or enum) holds a JSON array of strings (["a","b"], an empty field is []); each item is sent by X’s rule. A field for a record, tuple, Dict or a List of those holds the JSON body itself (for example {"background":"#112233"}), which is sent as is and read by the back-end decoder. A field that does not parse is sent as a string, and the back end answers invalid_argument. Sum types with several constructors are sent as strings.

A Service whose logic runs step.commit and continues after it (and is not a queue consumer that only uses the commit as a boundary) ends in Accepted over HTTP: the runtime answers 202 with a one-field body ({"<root>": id}, a respond hook may rename the field). The generated live for such a Service (0.11.3) holds Reply instead of the Service’s Out: Accepted(id) for the 202 body and Replied(out) for a 200 that ends before the commit. Both arrive as Done(Ok(_)) and go on to after_send (for example ReloadPage). Lives of other Services are unchanged.

0.11.4 adds, without changing the existing public types:

0.11.5 adds, without changing or removing the existing public types:

0.11.6 changes when the outbox is swept, without changing the public types:

0.11.7 adds, without changing the existing variants’ meaning or output:

0.11.8 adds, without changing the existing variants’ meaning or output:

0.11.9 adds, without changing the existing variants’ meaning or output:

0.11.10 adds, without changing the existing variants’ meaning or output:

0.11.11 changes documentation only (one README sentence and one source comment). No code, type or generated output changes.

0.11.12 adds, without changing the existing output:

0.11.13 fixes Page(theme: Some(name)), which always gave None at runtime (the shell looked up name at the top of the last source’s Out):

0.11.14 adds, without changing the existing variants’ meaning or output:

0.11.15 changes client navigation and adds a Shell constant:

Imports outside the package

ImportImported byProvided by
@neondatabase/serverlessyumemi/server/driver.mjs (Neon HTTP transport, database(env, observe))the app’s package.json
cloudflare:workersyumemi/server/worker.mjs (DurableObject / WorkerEntrypoint)the Workers runtime (wrangler / workerd); not an npm package, so modules that import worker.mjs (the generated shell.mjs) do not load under plain node

SQL — the runtime runs these keys through the app’s SQL bundle (src/gen/sql.mjs, built from db/queries/**). The app writes them as db/queries/framework/<name>.sql against its own framework schema (DDL is the app’s). Holes are positional, in the order below.

KeyHolesUsed for
framework/session_resolve_staffsession id, at, first tryresolve a session cookie (a row with retry asks for a second pass)
framework/api_key_resolvekey digest, atresolve an API key entrance
framework/session_issueparty, session id, credential version, expires atissue a session (auth binding)
framework/credential_floorparty, floorraise the credential version floor
framework/session_revoke_partyparty, versionrevoke a party’s sessions below a version
framework/session_revokesession idrevoke one session (auth binding)
framework/session_onboardsession id, party, subject idbind a created subject to the session
framework/session_subject_staffsession id, party, kind, subject idthe SwitchSubject attached route
framework/browserbrowser id, at, kind (0.12)the Declare(attached, kind) attached route, once per declared kind
framework/auditseed, stage, service, party, outcome, atone row per entrance stage
framework/outbox_parentid, dedupe key, payload (json), event id, at, foldedoutbox parent row of a write
framework/outbox_childid, kind, payload (json), parent id, atoutbox child row (one queue message)
framework/outbox_doneevent idmark a consumed event done
framework/outbox_getidload a queued message
framework/outbox_sentidmark a swept message sent (not called since 0.11.6)
framework/outbox_sweepkindslist unsent messages to resend
framework/outbox_claimidclaim one swept row before sending it (0.11.6; generated by default, see above)
framework/pageview_recordevent id, mark, face, at, kind, path, prev, referrer path, referrer host, client at, party, extra (json), dims (json), excludedone counted event into the Store (0.12; generated by default, calls framework.pageview_record from db/framework/pageview.sql)
framework/pageview_rollupatthe daily rollup of the Store (0.12; generated by default)

Cron — a face whose gate counts into the framework Store makes the generated shell.mjs run the rollup on the cron expression 0 1 * * * (01:00 UTC, fixed by the framework). Add that expression to the Worker’s triggers.crons.

Worker env — DATABASE_URL, COOKIE_DOMAIN, OUTBOX (queue binding), <ENTRY>_HOST per entrance, the key_binding of browser (a Secret Store binding), the KEK bindings of Sealed properties, and optionally ISOLATE_MARKER=1 (test header).

The gate and the CMS features (0.12)

0.12 moves the features every business CMS needs into the framework: declarations, consents, counting pageviews (with uniques and visits), the CSP and image variants. The app only names kinds and values in closed vocabularies; nothing application-specific is spelled in the framework, the runtime or the generator. This is a breaking change.

The gate (<face>/src/gate.gleam, pub const gate: Gate):

Gate(
  sign_in: SignIn(path:, fallback_origin:),
  rules: List(Rule),            // Rule(pages, except, checks)
  redirects: List(Redirect),    // Redirect(pages, when, to)
  csp: List(Csp),               // was frame_src (the entry's frame_src is gone too)
  pageview: Pageview,           // NoPageview | Pageview(pages, only, fields, sink)
)
Check = SignedIn(fail) | Declared(kind, fail) | SubjectKind(kinds, fail) | Consented(kind, fail)
When  = WhenDeclared(kind) | WhenSignedIn

Declarations (Declared(kind)) are browser-level statements such as an age declaration. The app names the kinds in src/server.gleam:

pub const browser: BrowserCookie = BrowserCookie(
  cookie: "_b", key_binding: "BROWSER_KEY", max_age_days: 400,
  declarations: [Declaration(kind: "<kind>", claim: "<kind>_declared_at", max_age_days: 395)],
)

Consents (Consented(kind), ConsentedFor(kind, subject: fn(args) -> String)) are party-level. The app lists the kinds in src/server.gleam (pub const consents: List(String)); a kind outside it in a requirement or a gate check stops with exit 4 (rule 40). ConsentedFor matches the consent’s subject against subject(args). A missing consent answers 403 {"code": "consent_missing", "consent": "<kind>"}. ReadSession returns every consent the party holds as consents: {<kind>: true}.

The CSP is csp: [Csp(pages: List(Match), directives: [FrameSrc(hosts) | ImgSrc(hosts) | ConnectSrc(hosts) | MediaSrc(hosts)])]. The gate sets content-security-policy on HTML responses of the matched Pages (directives of every matching Csp merged in order, hosts as https://<host>). Every declared directive starts with 'self', added by the generator, because the pageview receiver, the client navigation fetch and /media/... live on the same origin: ConnectSrc(["api.example"]) becomes connect-src 'self' https://api.example. There is no way to drop 'self' in 0.12. Hosts are [a-z0-9.-]+ with one optional leading *.; a page outside the face’s routes or a bad host stops with exit 4 (rule 38). There is no script-src. Client navigation still moves only between Pages whose CSP string equals the current document’s.

Counting pageviews (Pageview(pages, only, fields, sink)):

Colours in Args. A Service’s Args field typed color.Color (or Option(color.Color)) is decoded with color.parse; a broken colour answers 400 invalid_argument with its field and never reaches the Service.

Image variants. pub const media: Media = Media(path:, variants: [ImageVariant(name:, width:, height: Option(Int), fit: ScaleDown | Cover | Contain)], theme_background:) in src/server.gleam makes the generator write <face>/src/gen/media.gleam per face: a closed Variant type, name(variant), theme_background and url(blob, variant) (<path>/<key, each "/"-separated segment percent-encoded>?v=<name>). Duplicate or badly spelled names, or a theme_background outside the table, stop with exit 4 (rule 39). Serving, caching and purging stay in the app.

✨ Search Document