SSR estático — render_to_html¶
Até aqui o tempestweb rodava a mesma árvore de widgets de dois jeitos: no browser (Modo A, WASM) e no servidor com cliente fino (Modo B, WebSocket/SSE). Ambos são interativos e dependem de JavaScript.
Agora existe um terceiro alvo: HTML estático. A mesma árvore tipada vira uma string de HTML no servidor — sem JavaScript, sem DOM, sem runtime. É a tese "uma árvore, N renderizadores" levada ao fim: o HTML é só mais um renderizador folha, ao lado do cliente DOM. 🚀
Quando usar SSR
Use render_to_html quando você precisa de first paint instantâneo,
SEO, e-mails em HTML, ou uma página que funciona sem JS. Para telas
ricas e interativas, continue nos Modos A/B. Os dois convivem: você pode
renderizar o casco no servidor e hidratar com o cliente depois.
Um exemplo mínimo e completo¶
Escreva uma árvore tipada e transforme em HTML:
from tempest_core import Column, Text, Button, Style
from tempest_core import Edge
from tempestweb.html import render_to_html
def view() -> Column:
"""Build a tiny typed page."""
return Column(
style=Style(gap=8.0, padding=Edge.all(16)),
children=[
Text(content="Olá, mundo!"),
Button(label="Clique"),
],
)
html: str = render_to_html(view())
print(html)
O resultado é uma string de HTML pronta para servir:
<div style="display: flex; flex-direction: column; gap: 8px; padding: 16px 16px 16px 16px"><span>Olá, mundo!</span><button>Clique</button></div>
Repare: um Column virou <div> com display: flex, o Text virou <span> e o
Button virou <button> — o mesmo mapeamento que o cliente DOM faz em
client/dom.js, e o mesmo CSS que o cliente geraria a partir do Style.
Uma página completa¶
render_to_html devolve um fragmento. Para uma página <!doctype html>
inteira, use render_document:
from tempest_core import Column, Text
from tempestweb.html import render_document
def view() -> Column:
"""Build the page body."""
return Column(children=[Text(content="Página renderizada no servidor")])
page: str = render_document(
view(),
title="Minha página",
lang="pt-BR",
htmx=True, # injeta o <script> do htmx
css_reset=True, # injeta um reset de CSS mínimo
)
Você recebe um documento auto-contido com <meta charset>, <title> escapado,
o reset opcional e — com htmx=True — a tag de script do htmx.
Entrega do htmx
Com htmx=True a página aponta para o htmx via CDN (unpkg.com). Um ciclo
futuro do SDK vai servir o htmx localmente; por isso a URL é apenas uma
string parametrizável na saída, nunca uma dependência rígida daqui.
HTML semântico com tag e attrs¶
Desde o tempest-core 0.9.0, todo Widget aceita dois campos opcionais:
tag: str | None— troca a tag HTML gerada (por exemplo<nav>no lugar de<div>).attrs: dict[str, str]— atributos HTML arbitrários:id,class,data-*,aria-*e, claro,hx-*do htmx.
Ambos fluem pelo build() e são honrados pelo renderizador de HTML:
from tempest_core import Container, Column, Text
from tempestweb.html import render_to_html
def nav() -> Container:
"""Build a semantic <nav> with an htmx attribute."""
return Container(
tag="nav",
attrs={"aria-label": "primary", "class": "topbar"},
child=Column(
tag="ul",
children=[
Container(tag="li", child=Text(tag="a", content="Início")),
Container(tag="li", child=Text(tag="a", content="Docs")),
],
),
)
print(render_to_html(nav()))
<nav aria-label="primary" class="topbar"><ul style="display: flex; flex-direction: column"><li><a>Início</a></li><li><a>Docs</a></li></ul></nav>
A tag muda o elemento, não o layout
O <ul> acima ainda é um Column, então mantém display: flex pela sua
natureza — a tag troca o elemento HTML, não a semântica de layout.
Segurança: tudo é escapado¶
Conteúdo de texto e valores de atributo passam sempre por escape:
from tempest_core import Text
from tempestweb.html import render_to_html
render_to_html(Text(content="<script>alert(1)</script>"))
# -> "<span><script>alert(1)</script></span>"
Guarda contra injeção de atributo
Além de escapar os valores, o renderizador valida as chaves de
attrs contra ^[a-zA-Z][a-zA-Z0-9:_-]*$. Uma chave inválida (por exemplo
"onload x" ou "a>b") levanta ValueError — assim ninguém contrabandeia
markup por uma chave maliciosa.
Componentes também funcionam¶
Um Component é expandido pelo build() antes do render, então SSR funciona com
suas abstrações de alto nível sem nenhuma mudança:
from tempest_core import Component, Container, Column, Text, Widget
from tempestweb.html import render_to_html
class NavBar(Component):
"""A composite that expands to a semantic nav tree."""
items: list[str]
def render(self) -> Widget:
"""Lower the nav into primitive widgets with semantic tags."""
return Container(
tag="nav",
child=Column(
tag="ul",
children=[
Container(tag="li", child=Text(tag="a", content=item))
for item in self.items
],
),
)
print(render_to_html(NavBar(items=["Início", "Docs"])))
Limitações conhecidas¶
Icon e Canvas no SSR estático
Icon precisa de JavaScript para injetar o glifo SVG e Canvas é uma
superfície de desenho imperativa — nenhum dos dois tem forma estática
útil. No SSR eles viram placeholders vazios (<span data-tw-type="Icon">
</span> e <canvas></canvas>) em vez de quebrar. Use-os nos modos
interativos (A/B).
Recapitulando¶
render_to_html(widget)transforma uma árvore tipada em um fragmento de HTML estático, reusandotempest_core.build().render_document(...)envolve a árvore em uma página<!doctype html>completa, comtitle,lang,headextra, reset de CSS e htmx opcionais.- O CSS é byte-idêntico ao do cliente DOM (
style_to_cssé um port fiel declient/style.js), então servidor e cliente concordam. tageattrs(novos notempest-core0.9.0) deixam você emitir HTML semântico e pronto para htmx a partir de uma árvore tipada.- Todo texto e atributo é escapado; chaves de
attrsinválidas levantamValueError. Icon/Canvassão placeholders no SSR estático — uma limitação documentada, não um crash.