En este módulo aprenderás a transformar cualquier plantilla HTML estática en un theme funcional de Grav. El proceso requiere analizar el HTML, identificar componentes reutilizables y reorganizarlo siguiendo la estructura de Grav.
Antes de convertir una plantilla HTML a Grav, debes analizar su anatomía para entender:
Anatomía típica de una plantilla HTML:
<!DOCTYPE html>
<html>
<head>
<!-- Meta tags, título, CSS -->
</head>
<body>
<header>
<!-- Logo, navegación -->
</header>
<main>
<!-- Contenido principal (cambia por página) -->
</main>
<aside>
<!-- Sidebar (opcional) -->
</aside>
<footer>
<!-- Copyright, enlaces -->
</footer>
<!-- Scripts JS -->
</body>
</html>
Pregunta clave: ¿Qué partes son fijas (iguales en todas las páginas) y qué partes son dinámicas (cambian según la página)?
Supongamos que tienes esta plantilla HTML simple:
index.html (original):
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mi Sitio Web - Inicio</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- Header -->
<header class="site-header">
<div class="container">
<div class="logo">
<img src="images/logo.png" alt="Logo">
</div>
<nav class="main-nav">
<ul>
<li><a href="index.html">Inicio</a></li>
<li><a href="about.html">Nosotros</a></li>
<li><a href="services.html">Servicios</a></li>
<li><a href="contact.html">Contacto</a></li>
</ul>
</nav>
</div>
</header>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1>Bienvenido a Nuestro Sitio</h1>
<p>Ofrecemos las mejores soluciones para tu negocio</p>
<a href="#" class="btn btn-primary">Comenzar</a>
</div>
</section>
<!-- Content -->
<main class="content">
<div class="container">
<h2>Nuestros Servicios</h2>
<div class="row">
<div class="col-md-4">
<div class="service-card">
<h3>Diseño Web</h3>
<p>Creamos sitios web modernos y responsive</p>
</div>
</div>
<div class="col-md-4">
<div class="service-card">
<h3>SEO</h3>
<p>Optimizamos tu posicionamiento en buscadores</p>
</div>
</div>
<div class="col-md-4">
<div class="service-card">
<h3>Marketing</h3>
<p>Estrategias efectivas para tu marca</p>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="site-footer">
<div class="container">
<p>© 2024 Mi Sitio Web. Todos los derechos reservados.</p>
<div class="social-links">
<a href="#">Facebook</a>
<a href="#">Twitter</a>
<a href="#">Instagram</a>
</div>
</div>
</footer>
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>
Análisis de la estructura:
| Elemento | Tipo | Observación |
|---|---|---|
<head> |
Fijo | Mismo en todas las páginas, solo cambia <title> |
<header> |
Fijo | Logo y navegación idénticos en todo el sitio |
| Hero Section | Variable | Solo en homepage, no en otras páginas |
<main> |
Variable | Contenido diferente en cada página |
<footer> |
Fijo | Igual en todas las páginas |
| Scripts | Fijo | Mismas librerías JS en todo el sitio |
Conclusión del análisis:
base.html.twig)Un componente reutilizable es una porción de HTML que:
Tipos de componentes en Grav:
Partials: Fragmentos incluibles con {% include %}
Bloques: Secciones sobrescribibles con {% block %}
Macros: Funciones Twig reutilizables (avanzado)
Revisa tu plantilla y pregúntate:
✅ ¿Se repite este código en otras páginas? → Partial
✅ ¿Tiene una función específica y aislada? → Partial
✅ ¿Su contenido varía pero su estructura no? → Partial con parámetros
✅ ¿Aparece solo en ciertas páginas? → Incluir condicionalmente
Del HTML anterior, identificamos:
1. Header (componente fijo):
<header class="site-header">
<div class="container">
<div class="logo">
<img src="images/logo.png" alt="Logo">
</div>
<nav class="main-nav">
<ul>
<li><a href="index.html">Inicio</a></li>
<li><a href="about.html">Nosotros</a></li>
<li><a href="services.html">Servicios</a></li>
<li><a href="contact.html">Contacto</a></li>
</ul>
</nav>
</div>
</header>
→ Acción: Crear templates/partials/header.html.twig
2. Footer (componente fijo):
<footer class="site-footer">
<div class="container">
<p>© 2024 Mi Sitio Web. Todos los derechos reservados.</p>
<div class="social-links">
<a href="#">Facebook</a>
<a href="#">Twitter</a>
<a href="#">Instagram</a>
</div>
</div>
</footer>
→ Acción: Crear templates/partials/footer.html.twig
3. Service Card (componente repetido):
<div class="col-md-4">
<div class="service-card">
<h3>Diseño Web</h3>
<p>Creamos sitios web modernos y responsive</p>
</div>
</div>
→ Acción: Este patrón se repite 3 veces → Puede convertirse en un bucle con datos dinámicos
4. Hero Section (componente opcional):
<section class="hero">
<div class="container">
<h1>Bienvenido a Nuestro Sitio</h1>
<p>Ofrecemos las mejores soluciones para tu negocio</p>
<a href="#" class="btn btn-primary">Comenzar</a>
</div>
</section>
→ Acción: Crear templates/partials/hero.html.twig e incluir solo si existe
La separación estratégica sigue esta jerarquía:
base.html.twig ← Estructura HTML común (skeleton)
↓ extends
default.html.twig ← Template genérico (páginas simples)
↓ extends
blog.html.twig ← Template específico (listado blog)
item.html.twig ← Template específico (artículo individual)
Principio de separación:
Paso 1: 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.0">
<title>
{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}
</title>
{% block stylesheets %}
{{ assets.css()|raw }}
{% endblock %}
{% endblock head %}
</head>
<body>
{% block header %}
{% include 'partials/header.html.twig' %}
{% endblock %}
{% block hero %}
{# Hero opcional - se define en templates específicos #}
{% endblock %}
{% block content %}
{# Contenido principal - obligatorio en todos los templates #}
{% endblock %}
{% block footer %}
{% include 'partials/footer.html.twig' %}
{% endblock %}
{% block javascripts %}
{{ assets.js()|raw }}
{% endblock %}
</body>
</html>
Paso 2: Template Default (páginas simples)
templates/default.html.twig:
{% extends 'partials/base.html.twig' %}
{% block content %}
<main class="content">
<div class="container">
<article class="page">
<h1>{{ page.title }}</h1>
<div class="page-content">
{{ page.content|raw }}
</div>
</article>
</div>
</main>
{% endblock %}
Paso 3: Template Homepage (con hero)
templates/home.html.twig:
{% extends 'partials/base.html.twig' %}
{% block hero %}
{% include 'partials/hero.html.twig' %}
{% endblock %}
{% block content %}
<main class="content">
<div class="container">
<h2>Nuestros Servicios</h2>
<div class="row">
{% for service in page.header.services %}
<div class="col-md-4">
<div class="service-card">
<h3>{{ service.title }}</h3>
<p>{{ service.description }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
{% endblock %}
Paso 4: Partials
templates/partials/header.html.twig:
<header class="site-header">
<div class="container">
<div class="logo">
{% if theme_config.logo %}
<img src="{{ theme_config.logo }}" alt="{{ site.title }}">
{% else %}
<span class="site-title">{{ site.title }}</span>
{% endif %}
</div>
<nav class="main-nav">
<ul>
{% for item in pages.children.visible %}
<li class="{% if item.active %}active{% endif %}">
<a href="{{ item.url }}">{{ item.menu }}</a>
</li>
{% endfor %}
</ul>
</nav>
</div>
</header>
templates/partials/footer.html.twig:
<footer class="site-footer">
<div class="container">
<p>© {{ current_year }} {{ site.title }}. Todos los derechos reservados.</p>
{% if theme_config.social_links %}
<div class="social-links">
{% for social in theme_config.social_links %}
<a href="{{ social.url }}" target="_blank">{{ social.platform }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</footer>
templates/partials/hero.html.twig:
<section class="hero">
<div class="container">
<h1>{{ page.header.hero.title }}</h1>
<p>{{ page.header.hero.subtitle }}</p>
{% if page.header.hero.button %}
<a href="{{ page.header.hero.button.url }}" class="btn btn-primary">
{{ page.header.hero.button.text }}
</a>
{% endif %}
</div>
</section>
Antes de migrar, debes hacer un inventario completo de todos los recursos:
Tipos de assets:
Preguntas clave:
Del HTML original, identificamos:
CSS:
1. css/bootstrap.min.css → Framework CSS (puede usar CDN)
2. css/style.css → Estilos personalizados (migrar a theme)
JavaScript:
1. js/jquery.min.js → Librería externa (CDN recomendado)
2. js/bootstrap.min.js → Depende de jQuery
3. js/main.js → Script personalizado (migrar a theme)
Imágenes:
1. images/logo.png → Logo del sitio
2. images/* → Otras imágenes (migrar a theme/images/)
Organización en Grav:
mi-theme/
├── css/
│ └── style.css ← css/style.css (renombrado)
├── js/
│ └── main.js ← js/main.js
├── images/
│ └── logo.png ← images/logo.png
└── fonts/ ← Si hay fuentes personalizadas
mi-theme.php:
<?php
namespace Grav\Theme;
use Grav\Common\Theme;
class MiTheme extends Theme
{
public static function getSubscribedEvents()
{
return [
'onThemeInitialized' => ['onThemeInitialized', 0]
];
}
public function onThemeInitialized()
{
if (!$this->isAdmin()) {
$this->enable([
'onTwigSiteVariables' => ['onTwigSiteVariables', 0]
]);
}
}
public function onTwigSiteVariables()
{
// === CSS ===
// Bootstrap desde CDN
$this->grav['assets']->addCss(
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
['priority' => 100]
);
// CSS personalizado
$this->grav['assets']->addCss('theme://css/style.css', ['priority' => 90]);
// === JavaScript ===
// jQuery desde CDN
$this->grav['assets']->addJs(
'https://code.jquery.com/jquery-3.6.0.min.js',
['priority' => 100, 'group' => 'bottom']
);
// Bootstrap JS (depende de jQuery)
$this->grav['assets']->addJs(
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js',
['priority' => 90, 'group' => 'bottom']
);
// Script personalizado (se carga último)
$this->grav['assets']->addJs(
'theme://js/main.js',
['priority' => 80, 'group' => 'bottom']
);
}
}
Ventajas de usar CDN:
Cuándo usar archivos locales:
Una buena organización facilita:
Principios de organización:
mi-theme/
├── blueprints.yaml # Configuración Admin
├── mi-theme.yaml # Valores por defecto
├── mi-theme.php # Lógica PHP
├── README.md # Documentación
├── thumbnail.jpg # Preview del theme
│
├── templates/
│ ├── partials/
│ │ ├── base.html.twig # Template base
│ │ ├── header.html.twig # Cabecera
│ │ ├── footer.html.twig # Pie de página
│ │ ├── hero.html.twig # Hero section
│ │ └── navigation.html.twig # Menú (si es complejo)
│ │
│ ├── default.html.twig # Páginas generales
│ ├── home.html.twig # Homepage
│ ├── blog.html.twig # Listado de blog
│ └── item.html.twig # Artículo individual
│
├── css/
│ ├── style.css # Estilos principales
│ └── components/ # (opcional) CSS por componente
│ ├── header.css
│ ├── footer.css
│ └── cards.css
│
├── js/
│ ├── main.js # Script principal
│ └── components/ # (opcional) JS por componente
│ └── menu.js
│
├── images/
│ ├── logo.png
│ └── icons/
│ └── ...
│
└── fonts/ # (opcional) Fuentes custom
└── custom-font.woff2
Paso a paso para convertir HTML a Grav:
1. Preparación:
# Crear theme
cd user/themes/
mkdir mi-theme
cd mi-theme
# Crear estructura
mkdir -p templates/partials css js images
2. Migrar assets:
# Copiar archivos del HTML original
cp /ruta/plantilla-html/css/style.css css/
cp /ruta/plantilla-html/js/main.js js/
cp -r /ruta/plantilla-html/images/* images/
3. Crear configuración básica:
blueprints.yaml:
name: Mi Theme
version: 1.0.0
description: "Theme convertido desde HTML"
author:
name: Tu Nombre
mi-theme.yaml:
enabled: true
logo: ''
4. Convertir HTML a Twig:
base.html.twig con estructura común5. Registrar assets:
Crear mi-theme.php y registrar CSS/JS como vimos anteriormente.
6. Configurar página de inicio:
user/pages/01.home/home.md:
---
title: Inicio
hero:
title: Bienvenido a Nuestro Sitio
subtitle: Ofrecemos las mejores soluciones
button:
text: Comenzar
url: /servicios
services:
- title: Diseño Web
description: Creamos sitios modernos
- title: SEO
description: Optimizamos tu posicionamiento
- title: Marketing
description: Estrategias efectivas
---
# Contenido adicional de la página
Aquí puedes escribir más contenido que aparecerá después de los servicios.
7. Activar y probar:
# En Admin → Themes → Mi Theme → Activar
# O editar user/config/system.yaml:
pages:
theme: mi-theme
✅ Assets migrados:
✅ Templates creados:
✅ Configuración:
✅ Contenido dinámico:
✅ Testing:
Objetivo: Convertir una plantilla HTML Bootstrap en un theme de Grav.
Plantilla de partida: Landing page simple con:
Pasos:
Has aprendido a:
✅ Analizar estructura HTML: Identificar elementos fijos vs variables
✅ Extraer componentes: Crear partials reutilizables
✅ Separar en layouts: Sistema de herencia con base/templates específicos
✅ Inventariar assets: Catalogar CSS, JS, imágenes y dependencias
✅ Organizar eficientemente: Estructura óptima para desarrollo en Grav