Building a Sema Web App
This guide is the recommended path for building a real Sema Web application, not just embedding a small script in a page.
The key idea is simple:
- use
.semasource in development - ship a compiled
.vfsarchive in production - keep the same HTML shape in both modes
Recommended Project Shape
Start with a small, explicit structure:
my-app/
index.html
app.sema
public/index.htmlbootstrapsSemaWebapp.semacontains your Sema UI, state, and componentspublic/is where you emitapp.vfsfor production
For a larger app, split your Sema code into modules and keep app.sema as the entry point.
Development Flow
During development, point the page at source:
<script type="text/sema" src="/app.sema"></script>
<script type="module">
import { SemaWeb } from "@sema-lang/sema-web";
await SemaWeb.init();
</script>That keeps iteration fast and makes the app easy to inspect.
Recommended structure inside app.sema:
- Define top-level state first.
- Define derived state with
computed. - Define actions and event handlers.
- Define presentational helpers and components.
- Mount one root component at the end.
(def tasks (state '()))
(def draft (state ""))
(define (set-draft ev)
(put! draft (dom/event-value ev)))
(defcomponent app ()
[:div
[:input {:value @draft :on-input "set-draft"}]
[:p "Tasks: " (length @tasks)]])
(mount! "#app" "app")Production Flow
For production, compile the entry file to a .vfs archive:
sema build --target web app.sema -o public/app.vfsThen switch the script tag from source to the compiled artifact:
<script type="text/sema" src="/public/app.vfs"></script>
<script type="module">
import { SemaWeb } from "@sema-lang/sema-web";
await SemaWeb.init();
</script>The important part is that your JavaScript bootstrap does not need to change. SemaWeb.init() detects .vfs automatically.
Suggested App Architecture
For production apps, this shape scales well:
- persistent application state in top-level
statevalues - derived views and counters in
computed - browser effects in
on-mountandwatch - UI rendering in
defcomponent - routing in
router/* - visual styling in
css
Use watch for persistence and side effects:
(def todos (state (or (store/get "todos") '())))
(defcomponent app ()
(on-mount
(fn ()
(let ((watch-id
(watch todos
(fn (old new)
(store/set! "todos" new)))))
(fn ()
(unwatch! watch-id)))))
[:div ...])That gives you explicit cleanup and keeps side effects out of render logic.
init() vs create()
Use SemaWeb.init() when:
- your app is driven by
<script type="text/sema"> - you want automatic script discovery
- you are building a static or embedded page
Use SemaWeb.create({ autoLoad: false }) when:
- you want to evaluate code manually
- you need tighter JavaScript control over loading
- your app has a custom bootstrap flow
For most browser apps, init() is the right default.
Static Hosting
If your app does not use llm/*, deployment is just static hosting:
index.htmlapp.vfs- the built
@sema-lang/sema-webassets from your normal frontend pipeline
If your app uses LLM features, keep the browser app static and deploy the proxy separately. See LLM Proxy and Deployment.
Repository Example
This repository includes a complete compiled-app example at:
examples/sema-web-app/It demonstrates:
- compiled
.vfsloading - reactive state and
computed watchcleanup viaon-mount- routing with
router/* - persistence with
store/* - scoped styles with
css
From the repository root, build and run it with:
make sema-web-exampleThen open:
http://127.0.0.1:8788That target builds the archive, copies the browser runtime files into dist/vendor/, and serves the example folder with npx serve.