Módulo 2: Anatomía y Estructura de un Theme en Grav

Introducción

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.


1. Estructura Completa de Carpetas de un Theme

1.1 Teoría: Organización del Theme

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

1.2 ¿Por qué esta estructura?

  • templates/: Grav busca automáticamente las plantillas aquí
  • partials/: Convención para componentes reutilizables (header, footer, etc.)
  • css/, js/, images/: Assets organizados por tipo
  • Archivos YAML en raíz: Detectados automáticamente por Grav
  • [theme].php: Opcional, solo si necesitas lógica personalizada

1.3 Ejemplo Práctico

Creamos un theme llamado "academy":

cd user/themes/
mkdir academy
cd academy
mkdir -p templates/partials css js images

Archivos mínimos a crear:

  • blueprints.yaml
  • academy.yaml
  • templates/partials/base.html.twig
  • templates/default.html.twig
  • css/custom.css

2. blueprints.yaml - Configuración del Admin Panel

2.1 Teoría: El "DNI" de tu Theme

blueprints.yaml cumple tres funciones críticas:

  1. Define metadatos: nombre, versión, autor, descripción
  2. Especifica dependencias: versión mínima de Grav, plugins requeridos
  3. Crea el formulario de configuración: opciones que aparecen en Admin → Themes

Es el único archivo que debe existir para que Grav reconozca tu theme.

Conceptos clave:

  • Usa la sintaxis YAML estándar
  • La sección form define campos que el usuario puede configurar
  • Grav valida automáticamente los datos según los tipos de campo
  • Los valores se guardan en user/config/themes/[theme].yaml

2.2 Ejemplo Práctico: blueprints.yaml

name: 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/off
  • select: Lista desplegable
  • text: Campo de texto
  • textarea: Área de texto multilínea
  • file: Subir archivos
  • list: Lista dinámica (añadir/eliminar items)

2.3 Resultado

Con este blueprint, en Admin → Themes → Academy verás un formulario visual donde el usuario puede:

  • Activar/desactivar el theme
  • Elegir color principal
  • Subir un logo
  • Configurar sidebar
  • Activar breadcrumbs

Todo sin tocar código.


3. [theme].yaml - Valores por Defecto

3.1 Teoría: Configuración Predeterminada

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):

  1. user/config/themes/[theme].yaml → Configuración del usuario (máxima prioridad)
  2. user/themes/[theme]/[theme].yaml → Valores por defecto del theme
  3. Valores default en blueprints.yaml → Fallback final

Regla de oro: Todo campo en blueprints debe tener su valor por defecto aquí.

3.2 Ejemplo Práctico: academy.yaml

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: []

3.3 Acceso desde Templates

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 %}

4. [theme].php - Clase PHP con Eventos

4.1 Teoría: Sistema de Eventos

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 theme
  • onTwigSiteVariables: Antes de renderizar las plantillas Twig
  • onAssetsInitialized: Para registrar CSS/JS

¿Cuándo necesitas este archivo?

  • Añadir CSS o JavaScript dinámicamente
  • Crear variables personalizadas para Twig
  • Procesar datos antes de mostrarlos
  • Registrar assets (CSS/JS)

Si tu theme solo usa plantillas estáticas, no necesitas este archivo.

4.2 Ejemplo Práctico: academy.php (Básico)

<?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');
    }
}

4.3 Uso Avanzado: CSS Dinámico

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);
}

5. Sistema de Templates Twig y Herencia

5.1 Teoría: Herencia de Plantillas

Twig es el motor de plantillas de Grav. Su característica principal es la herencia:

  1. Creas un template base con la estructura HTML común
  2. Defines bloques ({% block nombre %}) que pueden sobrescribirse
  3. Los templates específicos extienden el base y redefinen bloques

Ventajas:

  • No repites código HTML (header, footer, etc.)
  • Cambios en el base afectan a todas las páginas
  • Mantienes consistencia visual

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)

5.2 Ejemplo: Template Base

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>

5.3 Ejemplo: Template que Hereda

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 %}

5.4 Funciones Twig Esenciales

{# 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 %}

6. Manejo de Assets (CSS, JS, Imágenes)

6.1 Teoría: Sistema de Assets

Grav incluye un sistema avanzado para gestionar recursos estáticos:

Características principales:

  • Pipeline: Combina múltiples archivos CSS/JS en uno solo
  • Minificación: Reduce tamaño de archivos
  • Cache busting: Añade hash a URLs para evitar caché obsoleta
  • Prioridades: Controla orden de carga
  • Grupos: Organiza assets (head, bottom, etc.)
  • Async/Defer: Carga asíncrona de JavaScript

Tres formas de añadir assets:

  1. Desde PHP ([theme].php)
  2. Desde templates Twig
  3. Configuración global (system.yaml)

6.2 Registrar Assets desde PHP

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'
    );
}

6.3 Registrar Assets desde Twig

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 %}

6.4 Ejemplo: CSS Básico

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);
    }
}

6.5 Ejemplo: JavaScript Básico

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');
    });
}

6.6 Optimización: Activar Pipeline

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.


Resumen del Módulo

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