Un theme en Grav es un conjunto organizado de archivos que define la apariencia y comportamiento de tu sitio. En este módulo aprenderás los componentes esenciales y cómo trabajan juntos.
Todo theme vive en /user/themes/[nombre-theme]/. Grav busca archivos en ubicaciones específicas siguiendo una convención establecida.
Estructura básica (mínima funcional):
mi-theme/
├── blueprints.yaml # Metadatos y configuración Admin
├── mi-theme.yaml # Valores por defecto
├── mi-theme.php # Lógica PHP (opcional)
├── templates/ # Plantillas Twig
│ └── default.html.twig
├── css/
│ └── custom.css
└── js/
└── custom.js
Estructura completa (recomendada):
mi-theme/
├── blueprints.yaml
├── mi-theme.yaml
├── mi-theme.php
├── templates/
│ ├── partials/ # Componentes reutilizables
│ │ ├── base.html.twig # Plantilla base
│ │ ├── header.html.twig
│ │ └── footer.html.twig
│ ├── default.html.twig # Plantilla por defecto
│ ├── blog.html.twig # Plantilla para listados
│ └── item.html.twig # Plantilla para artículos
├── css/
│ └── custom.css
├── js/
│ └── custom.js
├── images/
│ └── logo.png
├── languages/ # Traducciones (opcional)
│ ├── en.yaml
│ └── es.yaml
├── README.md
└── thumbnail.jpg # Preview 400x300px
templates/: Grav busca automáticamente las plantillas aquípartials/: Convención para componentes reutilizables (header, footer, etc.)css/, js/, images/: Assets organizados por tipo[theme].php: Opcional, solo si necesitas lógica personalizadaCreamos un theme llamado "academy":
cd user/themes/
mkdir academy
cd academy
mkdir -p templates/partials css js images
Archivos mínimos a crear:
blueprints.yamlacademy.yamltemplates/partials/base.html.twigtemplates/default.html.twigcss/custom.cssblueprints.yaml cumple tres funciones críticas:
Es el único archivo que debe existir para que Grav reconozca tu theme.
Conceptos clave:
form define campos que el usuario puede configuraruser/config/themes/[theme].yamlname: Academy
version: 1.0.0
description: "Theme educativo moderno y responsive"
icon: graduation-cap
author:
name: Tu Nombre
email: tu@email.com
homepage: https://github.com/tu-usuario/grav-theme-academy
keywords: education, documentation, responsive
license: MIT
# Dependencias
dependencies:
- { name: grav, version: '>=1.7.0' }
# Formulario de configuración
form:
validation: loose
fields:
enabled:
type: toggle
label: Activar Theme
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
# Sección: Colores
color_scheme:
type: select
label: Esquema de Color
default: blue
options:
blue: Azul
green: Verde
purple: Púrpura
red: Rojo
# Sección: Logo
logo:
type: file
label: Logo del Sitio
destination: 'theme://images'
accept:
- image/*
# Sección: Layout
sidebar_enabled:
type: toggle
label: Mostrar Sidebar
default: 1
options:
1: Sí
0: No
sidebar_position:
type: select
label: Posición del Sidebar
default: right
options:
left: Izquierda
right: Derecha
# Sección: Características
show_breadcrumbs:
type: toggle
label: Mostrar Breadcrumbs
default: 1
options:
1: Sí
0: No
Tipos de campo más comunes:
toggle: Interruptor on/offselect: Lista desplegabletext: Campo de textotextarea: Área de texto multilíneafile: Subir archivoslist: Lista dinámica (añadir/eliminar items)Con este blueprint, en Admin → Themes → Academy verás un formulario visual donde el usuario puede:
Todo sin tocar código.
Este archivo establece los valores iniciales para todas las opciones definidas en blueprints. Si el usuario no cambia nada en el Admin, estos valores se aplican.
Jerarquía de configuración (orden de prioridad):
user/config/themes/[theme].yaml → Configuración del usuario (máxima prioridad)user/themes/[theme]/[theme].yaml → Valores por defecto del themedefault en blueprints.yaml → Fallback finalRegla de oro: Todo campo en blueprints debe tener su valor por defecto aquí.
enabled: true
color_scheme: blue
logo: ''
# Layout
sidebar_enabled: true
sidebar_position: right
# Características
show_breadcrumbs: true
show_search: false
# Tipografía
font_headings: 'Poppins'
font_body: 'Open Sans'
# Footer
footer_text: '© 2024 Academia. Todos los derechos reservados.'
# Redes sociales (lista vacía por defecto)
social_links: []
En cualquier template Twig, accedes a esta configuración con theme_config:
{# Verificar si breadcrumbs está activado #}
{% if theme_config.show_breadcrumbs %}
<nav class="breadcrumbs">...</nav>
{% endif %}
{# Usar el esquema de color como clase CSS #}
<body class="color-{{ theme_config.color_scheme }}">
{# Mostrar logo si existe #}
{% if theme_config.logo %}
<img src="{{ theme_config.logo }}" alt="Logo">
{% endif %}
Grav usa un sistema de eventos (hooks) que permite ejecutar código en momentos específicos del ciclo de vida de una página.
Eventos más importantes:
onThemeInitialized: Se ejecuta cuando Grav carga el themeonTwigSiteVariables: Antes de renderizar las plantillas TwigonAssetsInitialized: Para registrar CSS/JS¿Cuándo necesitas este archivo?
Si tu theme solo usa plantillas estáticas, no necesitas este archivo.
<?php
namespace Grav\Theme;
use Grav\Common\Theme;
class Academy extends Theme
{
// Registrar eventos
public static function getSubscribedEvents()
{
return [
'onThemeInitialized' => ['onThemeInitialized', 0]
];
}
// Ejecutar cuando el theme se inicializa
public function onThemeInitialized()
{
// Habilitar el evento para añadir assets
if (!$this->isAdmin()) {
$this->enable([
'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
]);
}
}
// Añadir variables y assets antes de renderizar
public function onTwigSiteVariables()
{
// Registrar CSS
$this->grav['assets']->addCss('theme://css/custom.css');
// Registrar JavaScript
$this->grav['assets']->addJs('theme://js/custom.js');
// Crear variable personalizada para Twig
$twig = $this->grav['twig'];
$twig->twig_vars['current_year'] = date('Y');
}
}
public function onTwigSiteVariables()
{
$config = $this->config->get('themes.academy');
// CSS básico
$this->grav['assets']->addCss('theme://css/custom.css');
// CSS condicional según configuración
if ($config['sidebar_enabled']) {
$this->grav['assets']->addCss('theme://css/sidebar.css');
}
// Generar CSS con variables dinámicas
$colorScheme = $config['color_scheme'];
$colors = [
'blue' => '#007bff',
'green' => '#28a745',
'purple' => '#6f42c1',
'red' => '#dc3545'
];
$primaryColor = $colors[$colorScheme];
$customCss = ":root { --color-primary: {$primaryColor}; }";
$this->grav['assets']->addInlineCss($customCss);
}
Twig es el motor de plantillas de Grav. Su característica principal es la herencia:
{% block nombre %}) que pueden sobrescribirseVentajas:
Jerarquía de búsqueda:
1. user/themes/[theme]/templates/[template].html.twig
2. user/themes/[theme]/templates/default.html.twig (fallback)
3. system/pages/... (fallback de Grav)
templates/partials/base.html.twig:
<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: 'es' }}">
<head>
{% block head %}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
{% if page.header.description %}
<meta name="description" content="{{ page.header.description }}">
{% endif %}
{% block stylesheets %}
{{ assets.css()|raw }}
{% endblock %}
{% endblock head %}
</head>
<body class="color-{{ theme_config.color_scheme }}">
{% block header %}
{% include 'partials/header.html.twig' %}
{% endblock %}
{% if theme_config.show_breadcrumbs %}
{% block breadcrumbs %}
<nav class="breadcrumbs">
{% for crumb in page.breadcrumbs %}
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
{% endfor %}
</nav>
{% endblock %}
{% endif %}
<main class="content">
{% block content %}
{# Aquí va el contenido específico #}
{% endblock %}
</main>
{% block footer %}
{% include 'partials/footer.html.twig' %}
{% endblock %}
{% block javascripts %}
{{ assets.js()|raw }}
{% endblock %}
</body>
</html>
templates/default.html.twig:
{% extends 'partials/base.html.twig' %}
{% block content %}
<article class="page">
<header>
<h1>{{ page.title }}</h1>
{% if page.date %}
<time>{{ page.date|date('d/m/Y') }}</time>
{% endif %}
</header>
<div class="page-content">
{{ page.content|raw }}
</div>
</article>
{% endblock %}
templates/blog.html.twig:
{% extends 'partials/base.html.twig' %}
{% block content %}
<div class="blog-list">
<h1>{{ page.title }}</h1>
{% for post in page.collection().order('date', 'desc') %}
<article class="post-card">
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<time>{{ post.date|date('d M Y') }}</time>
<p>{{ post.summary|striptags|truncate(150) }}</p>
</article>
{% endfor %}
</div>
{% endblock %}
{# Información de la página #}
{{ page.title }} {# Título #}
{{ page.content|raw }} {# Contenido HTML #}
{{ page.url }} {# URL completa #}
{{ page.date }} {# Fecha de publicación #}
{# Configuración del sitio #}
{{ site.title }} {# Nombre del sitio #}
{{ base_url }} {# URL base #}
{# Configuración del theme #}
{{ theme_config.color_scheme }}
{{ theme_config.sidebar_enabled }}
{# Filtros comunes #}
{{ texto|striptags }} {# Quitar HTML #}
{{ texto|truncate(100) }} {# Truncar a 100 caracteres #}
{{ fecha|date('d/m/Y') }} {# Formatear fecha #}
{{ url|e }} {# Escapar HTML (seguridad) #}
{# Condicionales #}
{% if condicion %}
...
{% endif %}
{# Bucles #}
{% for item in lista %}
{{ item }}
{% endfor %}
Grav incluye un sistema avanzado para gestionar recursos estáticos:
Características principales:
Tres formas de añadir assets:
[theme].php)system.yaml)En academy.php:
public function onTwigSiteVariables()
{
// CSS principal
$this->grav['assets']->addCss('theme://css/custom.css');
// JavaScript (al final del body)
$this->grav['assets']->addJs('theme://js/custom.js', [
'group' => 'bottom'
]);
// CSS desde CDN (Google Fonts)
$this->grav['assets']->addCss(
'https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap'
);
}
En cualquier template:
{% extends 'partials/base.html.twig' %}
{# Añadir CSS específico de esta página #}
{% block stylesheets %}
{{ parent() }}
{% do assets.addCss('theme://css/blog.css') %}
{% endblock %}
{# Añadir JS específico de esta página #}
{% block javascripts %}
{{ parent() }}
{% do assets.addJs('theme://js/blog.js', {'group': 'bottom'}) %}
{% endblock %}
css/custom.css:
/* Variables CSS */
:root {
--color-primary: #007bff;
--font-body: 'Open Sans', sans-serif;
--spacing: 1rem;
}
/* Reset básico */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
line-height: 1.6;
color: #333;
}
/* Esquemas de color */
body.color-blue { --color-primary: #007bff; }
body.color-green { --color-primary: #28a745; }
body.color-purple { --color-primary: #6f42c1; }
/* Container */
.content {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing);
}
/* Responsive */
@media (max-width: 768px) {
.content {
padding: calc(var(--spacing) * 0.5);
}
}
js/custom.js:
// Smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Mobile menu toggle
const menuToggle = document.querySelector('.menu-toggle');
const mainNav = document.querySelector('.main-nav');
if (menuToggle) {
menuToggle.addEventListener('click', () => {
mainNav.classList.toggle('active');
});
}
En user/config/system.yaml:
assets:
css_pipeline: true # Combinar CSS
css_minify: true # Minificar CSS
js_pipeline: true # Combinar JS
js_minify: true # Minificar JS
enable_asset_timestamp: true # Cache busting
Resultado: Grav combina todos tus CSS en un solo archivo optimizado.
Has aprendido los 6 pilares de un theme en Grav:
✅ Estructura de carpetas: Organización clara y convenciones
✅ blueprints.yaml: Metadatos y formulario de configuración en Admin
✅ [theme].yaml: Valores por defecto de configuración
✅ [theme].php: Eventos PHP para lógica personalizada y assets
✅ Templates Twig: Sistema de herencia y bloques reutilizables
✅ Assets: Gestión de CSS, JS e imágenes