Introduction
Up to now everything we built lived on the server. WebFluid calls its frontend layer the surface, and it is what turns a backend into a fullstack runtime. Before we serve an actual frontend in the next chapter, let's get to know the tooling that sits underneath it — and finally cash in on all that "magic behind the scenes" we kept mentioning.
Batteries for the browser
The surface bundles two things you'd normally install by hand: a standalone
Node.js runtime and the Tailwind CLI. Neither ships
inside the package — they are downloaded on first use and cached, so the framework
can drive Vite and compile your CSS without you touching a global toolchain. If you
already have a system Node, WebFluid happily uses that instead.
You rarely call them directly, but when you need to, the CLI forwards straight through:
# Run any node/npm command against the bundled runtime
wf node node --version
wf node npm --version
# Run the Tailwind CLI directly
wf tailwind -- --help
Both commands forward the output of the underlying tool straight back to your terminal, so you see exactly what node, npm or Tailwind printed.
The three feature switches
Just like extensions, the surface features are optioned through switches — this time
in the [features] section of your app config:
[general]
SECRET_KEY = supersecret
[features]
WF_THEMES = 1
WF_TAILWIND = 1
WF_PROCESSING = 1
-
WF_PROCESSING— the heart of the surface. It injects a shared template context, registers the default error pages, a request logger and small framework endpoints (/wf-identityand/url-for). -
WF_TAILWIND— compiles atailwind_raw.cssinto a minifiedtailwind.cssfor the framework and every app static folder on startup. -
WF_THEMES— enables swappable stylesheets and a light/dark client helper.
Meet fluid_base.html
With processing enabled, the framework's own base template becomes useful. It ships inside
the package as fluid_base.html, already wired for themes, Tailwind, the
injected scripts and locale-aware text. Instead of hand-writing a full document like we did
in Getting Started, our page can now just extend it:
{% extends "fluid_base.html" %}
{% block title %}{{ _('Home') }}{% endblock %}
{% block content %}
<section>
<h1>{{ _('Hello %(name)s!', name=name) }}</h1>
<p>{{ _('Welcome to my liquified application.') }}</p>
</section>
{% endblock %}
Where the variables come from
Notice we never passed theme, src or url_for to the
template — yet fluid_base.html uses all of them. That is
WF_PROCESSING at work. It registers a context processor that adds, on every
render:
src— the framework's client scripts (base, and the i18n / events clients when those extensions are on).url_for— a request-aware reverse url helper that returns paths.theme— the active theme stylesheet link (when WF_THEMES is on).LANG,YEAR,id— small conveniences for your markup.- the underscore gettext helper — even without Babel, processing installs a no-op fallback so
_()always works.
This is why the base template just works. The default error pages (errors/403, errors/404, errors/500) come from the same place, so a 404 already renders a styled page out of the box. We'll see exactly how all of these templates are found in the Template resolution chapter.
Your scripts get a home
There is one more thing the base layout tidies up for us. All the way back, our hand-written
index.html carried its behaviour inline — the health check, and later the
model demo from the SQLAlchemy chapter, all lived
in a growing <script> block. Now that the framework owns the document, those
scripts move into fluid/static/js, and each page loads only the file it needs. The
app static folder is served at /static (reverse it with the "static"
route name), so the page just points at its script:
{% block content %}
<!-- ... your markup ... -->
<script src="{{ url_for('static', path='js/models.js') }}"></script>
{% endblock %}
And the behaviour lives on its own, out of the template:
function addModel() {
const value = document.getElementById('value')
const out = document.getElementById('newModel')
fetch('/my-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value: value.value })
})
.then(res => res.json())
.then(data => { out.innerText = JSON.stringify(data) })
}
That is the same fluid/static/js folder wf create project lays down
for you, and the same place the events subscription from the
Events chapter belongs. Keeping one small file per
page means the browser only ever downloads the behaviour that page actually uses.
Styling with Tailwind
With WF_TAILWIND on, the framework looks for a tailwind_raw.css
in any static/css folder and compiles a minified tailwind.css
next to it on startup. So all you maintain is the raw entry point:
@import "tailwindcss" source("../../");
@theme {
/* your design tokens go here */
}
The compiled stylesheet is then linked for you through the wf_tailwind variable
that fluid_base.html already includes.
Themes
When WF_THEMES is enabled the framework registers its own theme and exposes a
tiny API on your fluid: add_theme(name, link) to register more,
set_theme(request, name) to remember a choice in the session, and
get_theme() to resolve the active one. On the client, the injected
base.js already handles the light/dark preference via
window.wf.switchTheme().
That's the surface groundwork. Processing gives our templates their context, Tailwind gives them style, and the bundled Node runtime is standing by. Now let's actually serve a frontend.
Continue reading
From here you can continue straight with Integration.