Frontend

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:

terminal bash
# 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:

app_configs/app.ini ini
[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-identity and /url-for).
  • WF_TAILWIND — compiles a tailwind_raw.css into a minified tailwind.css for 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:

fluid/templates/index.html html
{% 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:

fluid/templates/index.html html
{% 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:

fluid/static/js/models.js javascript
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:

fluid/static/css/tailwind_raw.css css
@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.