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 Config:
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.
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 Config:
APP_FRONTEND = {
"type": "vite",
"framework": "react",
"typescript": False
}
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.
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.
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.