Frontend

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):

fluid/config.py python
from webfluid.core.config import register_config


@register_config(10)
class Config:
    SESSION_COOKIE_SECURE = True

    APP_FRONTEND = {
        "type": "htmx",
        "alpine": True
    }
fluid/templates/index.html html
{% 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:

  1. a root package.json that declares the workspace,
  2. a root vite.config.js that orchestrates it (the framework writes this one for you),
  3. 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.)

package.json json
{
  "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/:

fluid/frontend/vite.config.js js
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:

fluid/config.py python
@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 resulting dist is 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.