Integration
A WebFluid app declares what kind of frontend it serves through a single config value:
APP_FRONTEND. There are three flavours — plain server-rendered,
htmx-enhanced, or a full Vite-driven SPA. We'll start with the small end and only serve the
main app, then get fancy with multiple frontends once we reach Additives.
The frontend config
APP_FRONTEND is just a dictionary in your config class. Its type
decides everything else:
none— pure SSR, no client tooling.htmx— SSR plus htmx (and optionally Alpine) injected for you.vite— a real Vite app served and hot-reloaded by the framework.
The easy win: htmx
The lightest frontend needs no Node at all. Declare the htmx type and the framework hands
your templates a frontend() helper that injects the right scripts (and the
compiled Tailwind link):
from webfluid.core.config import register_config
@register_config(10)
class MyConfig:
SESSION_COOKIE_SECURE = True
APP_FRONTEND = {
"type": "htmx",
"alpine": True
}
{% extends "fluid_base.html" %}
{% block head %}
{{ frontend() if frontend else "" }}
{% endblock %}
{% block content %}
<button hx-get="/health" hx-target="#out">{{ _('Ping') }}</button>
<pre id="out"></pre>
{% endblock %}
That is genuinely the whole story for an htmx app. The frontend() call resolves
to the htmx (and Alpine) script tags, and because we are extending
fluid_base.html, everything else is already in place.
The frontend() helper looks for a raw stylesheet in that surface's
static/css — tailwind_raw.css with themes enabled,
tailwind_no_themes.css without. If it finds neither while
WF_TAILWIND is on, it simply emits no stylesheet link at all; the htmx and
Alpine tags are unaffected. So a surface that does not style anything costs you nothing, and
a surface that does only needs the raw sheet put next to it.
Going SPA: the fluid/frontend workspace
For a richer client you switch to the vite type. The main app's frontend lives
in one place the framework knows to look for: a fluid/frontend directory, set
up as an npm workspace. Three pieces make that work:
- a root
package.jsonthat declares the workspace, - a root
vite.config.jsthat orchestrates it (the framework writes this one for you), - the actual Vite app inside
fluid/frontend.
For now we only have the main app, so the workspace list is tiny — just our one
frontend. (We'll add additives/*/frontend to it later.)
{
"name": "myapp",
"private": true,
"workspaces": [
"fluid/frontend"
],
"devDependencies": {
"vite": "^8.0.0"
}
}
You do not hand-write the root vite.config.js. The framework ships an orchestrator that discovers every workspace, merges their configs and adds the dev plugin that resolves your assets. We'll leave that machine in its box here and only touch the small per-app config.
Inside fluid/frontend you have an ordinary Vite project. The one thing the
framework needs from its config is the right base: the built assets are served
under /frontend/, while in development everything is proxied through
/vite-dev/:
import { defineConfig } from 'vite'
export default defineConfig(({ command }) => ({
base: command === 'build' ? '/frontend/' : '/vite-dev/',
}))
Tell the config which app this is and declare your APP_FRONTEND:
@register_config(10)
class MyConfig:
APP_FRONTEND = {
"type": "vite",
"framework": "react",
"typescript": False,
"register_index": True
}
How it gets served
When the type is vite, the framework takes over the / route and
serves your Vite app's index for you — you don't register a home handler at all. That is
the default (register_index: true); set register_index: false in your
APP_FRONTEND if you would rather own the / route yourself.
What happens behind that route depends on the mode:
-
Development (
wf run app -d): the bundled Node starts the Vite dev server, and the framework proxies it under/vite-dev/. You get hot module replacement for free, rewritten into your page on the fly. -
Production (no
-d): the workspaces are built once on startup and the resultingdistis mounted as static files under/frontend.
Deciding what happens on a production boot
That build step is where the two remaining feature switches earn their keep. On a production start the framework runs, in order:
-
WF_CHECK_FRONTEND—npm run check --workspaces. The scaffolder wires this script up for TypeScript templates, so a type error stops the boot instead of shipping. Off by default. -
WF_BUILD_FRONTEND—npm run build --workspaces, and a failing build aborts startup with the compiler output. On by default.
Both are worth turning off in an image that already built its assets at container build time
— there is no reason to compile the same bundle again on every restart, and a container
without a Node toolchain cannot anyway. The dist folders are mounted either way.
How a relative asset finds its workspace
One detail earns a paragraph, because it is the only place in the framework where a cookie
decides which file gets read. A Vite index can ask for /logo.svg without saying
which of your frontends it belongs to, so when the framework serves an index it sets a
vite_ns cookie naming that workspace, and a catch-all route resolves later
relative requests against it.
A cookie is client input, so it is treated as such: the namespace has to be one the app
actually registered, and the resolved file has to sit inside the project root. Anything else
is a 404 rather than a file. That check is new; without it, an empty cookie pointed the
lookup at the project root itself and /app_configs/<app>.ini was a public
URL.
What the check does not narrow is the directory: the lookup resolves under
<workspace>/<path> and <workspace>/public/<path>
— the workspace itself, not its dist. So in production any visitor holding
the cookie the framework handed them can fetch a file from inside a frontend workspace:
/package.json, /src/main.ts. Nothing you would not publish belongs
in one.
The register_index flag decides whether the framework takes the /
route for you. It defaults to true, and wf create project asks about it when you
pick the Vite type. Set it to false and the Vite index is still served — you just have to
wire the route yourself, which is what you want as soon as the SPA is not the front page.
Setting all of this up by hand is fiddly, and that is by design: it is exactly the kind of boilerplate the CLI exists to remove. When we reach wf create project you'll see this whole workspace generated in one command. Here it is enough to understand the moving parts.
Continue reading
From here you can continue straight with Template resolution.