SSR / Pages that work without JavaScript
Sixty seconds: one form that works both ways
A cart with one button. The Message carries the name, not the domain and its
price: a form posted without scripts can carry anything, so update decides
what the name is worth.
ts
import { Array, Schema } from 'effect'
import type { HtmlBuilder } from 'foldkit/html'
import { defineMessageUnion } from 'foldkit/message'
import { modifyFields } from 'foldkit/struct'
import { Resume, SSR } from 'foldkit-ssr'
import { Projection, Surface } from 'foldkit-surface'
const Model = Schema.Struct({ cart: Schema.Array(Schema.String) })
type Model = typeof Model.Type
const Message = defineMessageUnion({
Added: { name: Schema.String.check(Schema.isMaxLength(128)) },
})
type Message = typeof Message.Type
const initial: Model = { cart: [] }
const App = Surface.application({ Model, Message, initial, update: model => ({ model }) })
const update = (model: Model, message: Message) =>
Message.match(message, {
Added: ({ name }) => ({
model: Array.contains(model.cart, name)
? model
: modifyFields(model, { cart: cart => Array.append(cart, name) }),
}),
})
const view = (model: Model, h: HtmlBuilder<Message>) => {
const rh = Resume.builder(h)
return {
title: 'Cart',
body: rh.main(
[],
[
rh.p([], [`${model.cart.length} in the cart`]),
rh.form(
[rh.OnSubmit(Message.Added({ name: 'acme.com' }))],
[rh.button([rh.Type('submit')], ['Add acme.com'])],
),
],
),
}
}
/** The Messages a posted form may carry: what the cart's controls send. */
const CartControls = App.surface('CartControls', {
model: ({ model }) => ({ cart: model.cart }),
messages: [Message.Added],
})
const plan = SSR.plan(App, {
id: 'cart',
state: Projection.pick(App.model.cart),
surfaces: [Surface.at(CartControls, undefined)],
fallback: 'server',
})
export const { renderPage } = SSR.entry(
{ Model, init: () => ({ model: initial }), update, view, container: null },
plan,
{ buildId: 'build-1', origins: ['https://shop.example'] },
)What each part does, and does not do:
rh.formwithrh.OnSubmit(Message)is an ordinary Foldkit form while scripts run: the submit is the Message,updateanswers it, and the browser sends nothing. Because the plan hasfallback: 'server', the server also writes the formmethod="post"with the Message encoded in a hidden field.SSR.hydratetakes both out as the page boots, so the browser's first render matches the page it adopts.fallback: 'server'makesSSR.entrytake aPOST.SSR.handledecodes the posted Message through the plan's Message Schema (a name over 128 characters is a400), refuses one no active Surface lists, rebuilds the Model as aGETwould (init, then the plan'sboot), runsupdateand every Command after it, and renders the page.originsis required with a fallback: a form cannot carry a CSRF header, so a post whoseOriginis not listed is a403before its body is read.- It does not keep the cart. Each post starts from
init, so a second add finds an empty cart. Keeping state between posts is the application's (below).