Referencia de Templates
Un theme de Chinaski es un directorio plano de templates de Template Toolkit 2 (.tt). El proceso de build renderiza tus templates a HTML estático para cada página y post, una vez por idioma configurado. Esta página cubre los archivos del theme, las variables que reciben, y los helpers y reglas que vienen con ellos.
Dónde viven los themes
- El theme default integrado viene con la aplicación y sirve como fallback.
- Los themes custom viven en
builder/themes/<your-theme-slug>/. Cuando un theme con el slug del theme activo existe allí, el builder lo usa; si no, cae a los templates integrados. - Cuál theme está activo se elige en Settings.
Archivos de template requeridos y opcionales
| Archivo | Propósito | ¿Requerido? |
|---|---|---|
page.tt | Template por defecto para cada página publicada | Yes |
post.tt | Template para cada post de blog publicado | Yes |
blog_index.tt | Listado de blog paginado y páginas de categoría | Yes |
404.tt | Página 404 por idioma | Yes |
_head.tt | Partial de <head>, CSS, meta tags, imports de fuentes | Convention |
_header.tt | Header del site, logo, nav, tagline | Convention |
_footer.tt | Footer del site, copyright, nav secundaria | Convention |
_cookie_notice.tt | Banner GDPR opcional | No |
_sidebar.tt | Partial de sidebar opcional | No |
search.tt | Página de resultados de búsqueda opcional | No |
_password_gate.tt | UI de password-gate del lado cliente para páginas protegidas | Requerido si page.tt/post.tt soporta password_hash |
<custom>.tt | Cualquier página que lo seleccione vía la setting template | No |
Los archivos que empiezan con _ son partials y nunca se renderizan como páginas independientes.
Variables de template
Los templates reciben un conjunto plano de variables, no hay objetos page. ni site.. Usa site_name, title, lang, etc. directamente.
page.tt y 404.tt
| Variable | Tipo | Notas |
|---|---|---|
title | string | Título de la página |
body | HTML string | Pre-renderizado, no lo escapes con | html |
slug | string | URL slug |
lang / language | string | Código de 2 letras, p. ej. en |
meta_title | string? | Sobrescribe <title> |
meta_description | string? | Descripción SEO |
og_title | string? | Título OpenGraph |
og_description | string? | Descripción OG |
og_image_url | string? | Imagen OG |
canonical_url | string | Auto-generada |
featured_image_url | string? | Imagen hero |
featured_image_alt | string? | Texto alt |
updated_at | string | datetime de SQLite |
site_name | string | De las settings |
site_tagline | string | De las settings |
footer_copyright | string | De las settings |
year | integer | Año del build |
url_base | string | Prefijo de URL, puede estar vacío; antepón a TODOS los links internos |
cookie_notice_active | 0/1 | Si renderizar _cookie_notice.tt |
noindex | 0/1 | Emitir <meta name="robots" content="noindex"> |
nav_items | array | {label, url, slot, sort_order}, slot es header o footer |
hreflang_links | array | {hreflang, href} para traducciones |
breadcrumbs | array | {name, url, is_current} |
json_ld | HTML string | JSON-LD pre-serializado, emítelo raw |
ga4_id | string? | ID de Google Analytics |
gtm_id | string? | ID de contenedor GTM |
twitter_site | string? | Handle de Twitter con @ |
password_hash | string? | Si está establecido, DEBES envolver el contenido en _password_gate.tt (ver abajo) |
post.tt, todo lo de page.tt, más
| Variable | Tipo | Notas |
|---|---|---|
excerpt | string | Excerpt suministrado por el author; puede estar vacío |
category | string? | Nombre de una sola categoría |
tags | string | Separadas por comas; dividir con tags.split(',') |
author | string | Nombre para mostrar |
is_sticky | 0/1 | Fijado a la parte superior del índice del blog |
published_at | string? | datetime de SQLite; momento del primer publish |
date | string | Porción YYYY-MM-DD de published_at |
feed_url | string | URL absoluta del feed RSS |
og_type | string | Siempre 'article' |
blog_index.tt
| Variable | Tipo | Notas |
|---|---|---|
posts | array | Hashes de post ordenados con keys p.title, p.slug, p.date, p.body, p.category |
pagination | hash? | {current_page, total_pages, prev_url, next_url, abs_prev_url, abs_next_url}, undef cuando solo hay una página |
title | string | "Blog" localizado o nombre de la categoría |
category_name | string? | Establecido al filtrar por categoría |
search.tt
| Variable | Tipo | Notas |
|---|---|---|
query | string | Término de búsqueda |
results | array | {title, url, excerpt} |
result_count | integer | Total de coincidencias |
Blocks
Los authors colocan blocks escribiendo {{block:my-cta}} en un body de página o post. Al momento del build, el builder encuentra el shortcode, busca el block publicado con ese nombre e idioma, lo renderiza con su template y sustituye el HTML renderizado en el body.
Los block templates se comparten entre themes y viven fuera de tu directorio de theme; tu theme es responsable del CSS que estiliza su output.
Clases CSS que tu theme debe estilar
Cada tipo de block emite una clase wrapper predecible y clases internas. Tu _head.tt debe estilizar estas:
| Tipo | Wrapper | Clases internas |
|---|---|---|
cta | .cta-block | .cta-inner .cta-heading .cta-subheading .cta-button |
image | .image-block | .image-caption |
quote | .quote-block | .quote-text .quote-attribution |
hero | .hero-block | .hero-media .hero-inner .hero-heading .hero-subheading .hero-button |
testimonial | .testimonial-block | .testimonial-label .testimonial-body .testimonial-avatar .testimonial-text .testimonial-attribution |
stat | .stat-block | .stat-number .stat-label |
promo | .promo-block | .promo-media .promo-content .promo-heading .promo-body .promo-button |
embed | .embed-block | .embed-caption .embed-content |
video | .video-block | .video-heading .video-wrapper .video-caption |
form | .form-block | .form-heading .form-subheading .block-form .block-form__field |
newsletter | .newsletter-block | .newsletter-inner .newsletter-heading .newsletter-subheading .newsletter-form .newsletter-form__row |
accordion | .accordion-block | .accordion-heading .accordion .accordion__item .accordion__summary .accordion__body |
table | .table-block | .table-heading .table-wrapper .block-table |
columns | .columns-block | .columns-heading .columns .columns--2 .columns--3 .columns--4 .column .column__image .column__heading .column__body |
gallery | .gallery-block | .gallery-heading .gallery .gallery__item |
Estilización mínima requerida para las clases de utilidad:
.video-wrapper:position: relative; padding-bottom: 56.25%;con el iframeposition: absolute; inset: 0; width: 100%; height: 100%;(video responsivo 16:9).columns--2/.columns--3/.columns--4:display: gridcon elgrid-template-columnsapropiado.table-wrapper:overflow-x: autopara que las tablas hagan scroll en celular.gallery: grid responsivo con gap.accordion__item/.accordion__summary: reset básico dedetails/summary
Páginas multi-idioma
- Cada página se renderiza una vez por idioma configurado.
- Los links internos deben incluir el prefijo de idioma: usa
[% url_base %]/[% lang %]/contact/o similar. La única excepción esnav_items[].url, que ya contiene la URL completa correcta, úsala tal cual. - El elemento
<html>debe cargar el idioma:<html lang="[% lang | html %]">.
Reglas de seguridad
Las siguientes directivas están prohibidas en el contenido de themes y blocks y son rechazadas:
[% PERL %]/[% RAWPERL %][% INSERT '/absolute/path' %][% USE ... %]de módulos como File/IO/Socket/Net/LWP/WWW/HTTP/System/Cmd/Process/Pipe/Expect
Páginas protegidas por contraseña
La protección por contraseña es una puerta orientada al lector: el contenido de la página sigue presente en el HTML publicado, y _password_gate.tt lo esconde detrás de una verificación de JavaScript hasta que el visitante introduce la contraseña correcta. page.tt y post.tt deben implementarlo así:
[% IF password_hash %]
[% WRAPPER '_password_gate.tt' %][% PROCESS page_content %][% END %]
[% ELSE %]
[% PROCESS page_content %]
[% END %] (Envuelve el block que contenga tu contenido principal. El nombre exacto del block no importa, solo que esté definido y se procese dentro del wrapper.) Si tu template omite por completo el branch de password_hash, un administrador puede aún establecer una contraseña en una página usando tu theme, y la página renderizará públicamente sin ninguna advertencia de que la puerta falta.
Hoja de referencia rápida
[% variable %]: output raw[% variable | html %]: escapado en HTML (usa para TODO el texto controlado por el usuario)[% variable | uri %]: codificación URI (para segmentos de path de URL)[% IF / ELSE / END %],[% UNLESS ... %]: condicionales[% FOREACH item IN items %]: loops;loop.index/loop.countdisponibles[% INCLUDE '_partial.tt' %]: incluye un partial, compartiendo las variables actuales; añadex = 42para pasar o ensombrecer valores[% PROCESS '_macros.tt' %]: como INCLUDE pero sin introducir un nuevo scope[%- ... -%]: recorta el whitespace circundante
Gotchas clave
- Stash plano: sin objetos anidados.
site.title,page.title,post.titleestán todos mal; usasite_nameytitle. - Los campos de nav item son
label,url,slot: noname,href,active. - Fechas: nunca llames a formateo de fechas sobre
published_at; usa la variable pre-calculadadate, o toma los primeros 10 caracteres depublished_at. - El body es HTML pre-renderizado: emite
[% body %]sin escapar. - Los partials tienen prefijo
_. - Todos los links internos necesitan
url_baseylang.