Pipeline de renderizado
El pipeline de renderizado transforma un request HTTP en HTML servido al visitante. Tiene dos caminos: Go templates (default) y Liquid (feature-flagged).
Flujo general
Request HTTP
→ ThemeLoader.Load() Resuelve el FS del tema (overlay 4 capas)
→ Renderer.RenderForTenant() Orquesta la carga y renderizado
├─ Detectar formato ¿Go template o Liquid?
├─ Go template path template.ExecuteTemplate()
└─ Liquid path LiquidRenderer.Compile() → Render()
→ inyectarGlobalHeadHTML() Agregar GlobalHeadHTML al <head>
→ HTTP Response
ThemeLoader — Resolución del filesystem
El ThemeLoader intenta 3 fuentes en orden:
1. TenantThemeSource Overrides del tenant en GCS + drafts
2. ReleaseThemeSource Snapshot publicado (current_release_id)
3. LocalThemeSource Overlay de 4 capas en el filesystem
Cada fuente fallbacka a la siguiente. El resultado es un fs.FS unificado.
TenantThemeSource
// Agrega overrides del tenant encima del tema base
// Permite preview con draft overrides
func (s *TenantThemeSource) OpenPreview(ctx, tenantID, themeName string) (fs.FS, error)
LocalThemeSource
// Construye el overlay de 4 capas
func (s *LocalThemeSource) floorFS(themeName string) fs.FS {
// _platform → _base → dawn (si tema ≠ dawn)
}
func (s *LocalThemeSource) Open(ctx, tenantID, themeName string) (fs.FS, error) {
floor := s.floorFS(themeName)
return overlayThemeOnBase(floor, themeFS) // tema gana
}
Renderer — Carga y cache de templates
type Renderer struct {
loader *ThemeLoader
cache map[cacheKey]*template.Template // cache compilado
liquidRenderer *LiquidRenderer
}
El renderer cachea templates compilados por (theme, templateName). Cuando un tema cambia (publish o code edit), se llama Invalidate(theme) para limpiar el cache.
Métodos principales
| Método | Descripción |
|---|---|
Render(w, theme, name, data) | Renderiza un template (sin tenant override) |
RenderForTenant(ctx, w, tenantID, theme, name, data) | Renderiza con overrides del tenant |
RenderSection(w, theme, sectionID, data) | Renderiza una sección individual |
RenderSectionForTenant(ctx, w, tenantID, theme, sectionID, data) | Sección con overrides |
TemplateExists(ctx, tenantID, theme, name) | Verifica si existe un template |
LoadJSONTemplate(ctx, tenantID, theme, name) | Lee template JSON (sections-everywhere) |
Invalidate(theme) | Limpia cache de un tema |
Detección de motor
El sistema detecta automáticamente el motor de renderizado:
// liquid/detect.go
func IsLiquidSource(source string) bool {
// true si usa delimitadores {% %}
// false si usa delimitadores Go {{define}}, {{block}}
}
- Go template: archivos con
{{define "sections/name"}}→html/template - Liquid: archivos con
{% %}→ motor Liquid custom
Go template path (default)
1. compileFS(themeFS) Parsea todos los .html del FS en un template.Tree
2. InjectGlobalHeadHTML() Agrega el snippet global head
3. tmpl.ExecuteTemplate(w, name) Renderiza el template con el StoreContext
Inyección de assets embebidos
Las secciones pueden incluir <style> y <script> inline. InjectEmbeddedAssets() extrae estos bloques del HTML renderizado y los inyecta en el <head>:
<!-- En la sección -->
{{style}}
.hero { padding: {{.section.padding}}px; }
{{endscript}}
<!-- El engine los extrae y agrega al <head> -->
Liquid path
1. LiquidRenderer.Compile() Parsea el fuente Liquid → AST
2. LiquidRenderer.Render() Evalúa el AST con el StoreContext
3. SectionRenderer callback Si hay {% section "name" %}, renderiza la sección Go
Puente entre Liquid y Go
Cuando un template Liquid contiene {% section "hero" %}, el evaluator llama al SectionRenderer callback, que ejecuta la sección Go correspondiente:
// renderer.go
func buildSectionRenderer(tmpl *template.Template, data any) func(string, io.Writer) error {
return func(sectionID string, w io.Writer) error {
base := sectionBase(sectionID)
return tmpl.ExecuteTemplate(w, "sections/"+base, data)
}
}
Esto permite templates Liquid que embeben secciones Go.
JSON Template (Sections-Everywhere)
Los templates JSON definen un layout de secciones con settings individuales:
{
"name": "homepage",
"entity_type": "page",
"sections": {
"hero-abc123": {
"type": "hero",
"settings": { "variant_id": "full-bleed", "fb_title": "Bienvenido" }
},
"featured-def456": {
"type": "featured_products",
"settings": { "source": "collection", "collection_id": "abc" }
}
},
"order": ["hero-abc123", "featured-def456"]
}
Cada sección tiene un ID único (type + sufijo), su tipo, y settings individuales. El engine renderiza cada sección en orden, pasando sus settings como contexto.
Caché y invalidación
| Evento | Acción |
|---|---|
| Publish de settings | Invalidate(theme) limpia el cache de templates |
| Code edit (Theme Editor) | InvalidateForTenant(tenantID, theme) limpia cache del tenant |
Request con ?preview=1 | No usa cache, siempre renderiza fresh con draft |
GlobalHeadHTML
Un snippet especial head.html en _base/snippets/ o el tema puede inyectar HTML en el <head> de todas las páginas. Se carga una vez y se cachea:
<!-- _base/snippets/head.html -->
<meta name="theme-color" content="{{ store.accent_color }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
{{ googleFontsURL "Inter:wght@400;500;600;700" }}