Hasta ahora todo lo que había hecho con Grav se basaba en child themes (de themes existentes).

Como he adelantado en el Newsletter (link de suscripción), he comenzado a portar el blog (eso explicaría parcialmente de mi poca actividad por aquí), y dado que las opciones de diseños gratuitos que existen no me llamaron la atención (tampoco opciones pagas), decidí probar cómo sería arrancar desde 0 (Grav no posee un blank theme como, por ejemplo, en Magento).

Como extra (quizás hasta carente de creatividad), he decido usar TailwindCSS para armar mi nuevo theme custom.

Al momento de iniciar el proyecto, sólo tengo el theme Quark, que viene de serie con Grav.

Y mi única página de contenido se ve de esta forma en el frontend.

Nada raro hasta aquí.

Ahora voy a crear mi theme desde 0. Voy a llamar a mi theme dc2025 (super creativo).

Entonces debo ir a /user/themes y crear el directorio dc2025.

Y ahora creamos dos archivos:

  • dc2025.php: la clase del theme.
  • dc2025.yaml: el archivo de configuración del theme.

La definición de la case PHP será:

<?php

declare(strict_types=1);

namespace Grav\Theme;

use Grav\Common\Theme;

class Dc2025 extends Theme
{
    //
}

Realmente nada, sólo la base. Ahora, el archivo YAML debe contener estas definiciones mínimas:

enabled: true
streams:
  schemes:
    theme:
      type: ReadOnlyStream
      prefixes:
        '':
          - user/themes/dc2025

Ahora, en el mismo directorio, voy a agregar una imagen jpg de 550 px x 550 px y vamos a sumar un cuarto archivo: blueprints.yaml.

En este último archivo defino esto:

name: DC 2025
version: 0.1.0
description: "Blank theme using TailwindCSS"
icon: empire
author:
  name: Damián Culotta
  url: https://www.damianculotta.com.ar
homepage: https://www.damianculotta.com.ar/
docs: https://www.damianculotta.com.ar/
keywords: theme, responsive, tailwindcss
license: MIT

dependencies:
  - { name: grav, version: '>=1.7.00' }

¿Para qué sirve esto último?

¿Qué pasará si lo activo ahora?

Y si miro qué código llegó al navegador me encuentro con:

Este es un buen error.

¿Cómo quedó mi theme ahora?

Por este motivo no encuentra el template, porque no existe. Toca resolverlo.

Creamos el directorio templates y dentro de el mismo el archivo default.html.twig.

{% block content %}
    {{ page.content|raw }}
{% endblock %}

Si ahora miramos lo que pasa en el navegador nos vamos a encontrar con:

Y en el código generado:

De aquí en más tenemos que empezar a jugar con los partials ya que necesitamos que la página por defecto contenga toda la estructura html (que luego necesitaremos para incluir css)

Ahora vienen una serie de pasos que van a generar un primer cambio visual y sobre el cual, a futuro, serguiré construyendo.

Edito el template default.html.twig y agrego:

{% extends 'partials/base.html.twig' %}

{% block content %}
    {{ page.content|raw }}
{% endblock %}

Y creo el el directorio partials dentro de templates, y creo ese nuevo template.

<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: grav.config.site.default_lang }}">
<head>
    {% block head deferred %}
        <meta charset="utf-8" />
        <title>{% if page.title %}{{ page.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        {% include 'partials/metadata.html.twig' %}
        <link rel="icon" type="image/png" href="{{ url('theme://images/favicon.png') }}" />
        <link rel="canonical" href="{{ page.url(true, true) }}" />
        <link href="{{ url('theme://css/main.css') }}" rel="stylesheet">
        <script src="//unpkg.com/alpinejs" defer></script>
    {% endblock head %}
</head>
<body>
{% block header %}
        {% include 'partials/header.html.twig' %}
{% endblock %}
{% block body %}
    <section id="body" class="container px-6 py-12 mx-auto">
        {% block content %}{% endblock %}
    </section>
{% endblock %}
{% block footer %}
    {% include 'partials/footer.html.twig' %}
{% endblock %}
</body>
</html>

El ejemplo lo tomé del theme Quark (quitandole casi todo lo que necesitaba) y este ejemplo ya contiene el resultado final porque incluye clases de TailwindCSS (no nos detengamos en eso ahora).

Creo los partials header y footer.

El contenido que he definido apra header.html.twig.

<header class="container px-6 py-12 mx-auto">
    <nav x-data="{ isOpen: false }" class="relative bg-white dark:bg-gray-800 border-b-gray-500 border-b">
        <div class="container px-6 py-4 mx-auto md:flex md:justify-between md:items-center">
            <div class="flex items-center justify-between">
                <a href="{{ home_url }}">
                    <img class="w-auto h-72 sm:h-7" src="{{ url('theme://images/logotype-blue.svg') }}" alt="">
                </a>
                <div class="flex lg:hidden">
                    <button x-cloak @click="isOpen = !isOpen" type="button" class="text-gray-500 dark:text-gray-200 hover:text-gray-600 dark:hover:text-gray-400 focus:outline-none focus:text-gray-600 dark:focus:text-gray-400" aria-label="toggle menu">
                        <svg x-show="!isOpen" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16" />
                        </svg>
                        <svg x-show="isOpen" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>
            </div>
            <div x-cloak :class="[isOpen ? 'translate-x-0 opacity-100 ' : 'opacity-0 -translate-x-full']" class="absolute inset-x-0 z-20 w-full px-6 py-4 transition-all duration-300 ease-in-out bg-white dark:bg-gray-800 md:mt-0 md:p-0 md:top-0 md:relative md:bg-transparent md:w-auto md:opacity-100 md:translate-x-0 md:flex md:items-center">
                <div class="flex flex-col md:flex-row md:mx-6">
                    <a class="my-2 text-gray-700 transition-colors duration-300 transform dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 md:mx-4 md:my-0" href="#">Home</a>
                </div>
            </div>
        </div>
    </nav>
</header>

Y para el caso de footer.html.twig.

<footer class="bg-gray-100 dark:bg-gray-900">
    <div class="container px-6 py-12 mx-auto">
        <div class="flex flex-col items-center justify-between sm:flex-row">
            <a href="{{ home_url }}">
                <img class="w-auto h-7" src="{{ url('theme://images/logotype-black.svg') }}" alt="">
            </a>
            <p class="mt-4 text-sm text-gray-500 sm:mt-0 dark:text-gray-300">Escribiendo desde 2008.</p>
        </div>
    </div>
</footer>

Y claramente, he instalado TailwindCSS y tengo mis archivos de configuración en su lugar (dentro del directorio del theme).

De esa configuración, lo que nos va a importar es el archivo tailwind.config.js.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./templates/**/*.html.twig"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Así le indicamos que escuche por los cambios hechos en nuestros templates.

Una vez que todo haya quedado en su lugar, el resutlado debería ser:

Y cuando miramos el código html que obtuvimos:

Esto es sólo el comienzo. Más allá que debo avanzar con la parte estética y el trabajo específico con TailwindCSS, deberé tambien trabajar con los distintos templates y lógicas de Grav.

Con respecto a la estructura de archivos del theme, en este momento se ha transformado un poco.

Créditos: la foto de la portada es de Kelly Sikkema.

Unite a la lista de suscriptores

Una vez por mes vas a recibir un mail con contenido que se relaciona con lo que vemos en el blog, que extiende o anticipa lo que hacemos en Twitch, y que también suele incluir anécdotas del MundoReal® y algún que otro link.

Es gratis, no tiene publicidad y con el double opt-in de Mailchimp.