Saltar al contenido principal

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étodoDescripció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

EventoAcción
Publish de settingsInvalidate(theme) limpia el cache de templates
Code edit (Theme Editor)InvalidateForTenant(tenantID, theme) limpia cache del tenant
Request con ?preview=1No 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" }}