Grav es, sin dudas, una muy buena solución como flat CMS. Es sencilla, amigable y cumple su cometido con creces. El problema sería definir cuál es su cometido para no terminar decepcionado.

Hace un tiempo (unos 2 o 3 años creo) logré migrar este mismo blog. Un trabajo bastante agotador porque convertí todos los posts a markdown, revisé y corregí contenido, edité links rotos, etc.

Los beneficios fueron varios:

  • El bajo consumo de recursos y la no necesidad de algunos de ellos.
  • La facilidad para automatizar los deploys.
  • Una baja exposición a problemas de seguridad debido a plugins.
  • La limpieza del contenido al estar escrito en MarkDown.

Pero también surgieron problemas:

  • Las búsquedas que no logré resolver de forma cómoda.
  • No tenía comentarios (lo cual me llevó a migrar a Disqus y perder, por no haber migrado aún los viejos comentarios, algo de historia) (se que en algún backup los tengo).
  • No tengo soporte nativo para múltiples autores (si puedo asignarlos pero no hay soporte nativo para darles relevancia y a sus posts).
  • Al no haber base de datos, varias funcionalidades para mostrar y vincular posts no logré resolverla.
  • La estructura de posts con al menos un nivel de profundidad (usar categoría y no tener todos los posts en la raíz) no me funcionaba muy bien para las búsquedas y los tags.
  • Potencial problema de performance por volumen de posts en un único directorio/carpeta (por eso la idea de las carpetas en el punto anterior).

Por esos motivos, luego de varios meses de usarlo, decidí revertir y volver a WordPress.

A principio de este año se me ocurrió abandonar mi variación del theme TwentyTwelve y renovar un poquito el layout general, lo cual me llevó a revisar el tema de los diseños por defecto (porque me ayudan a reducir los problemas de seguridad) y, de paso, volver sobre la revisión de opciones de CMS para blogs.

Entre las varias pruebas TwentyNineteen fue la opción temporal por el «Mobile first», aunque en paralelo retomé la idea de ir a Grav, más aún sabiendo de la implementación de las Flex Pages para lidiar con los problemas de volumen que mencioné al comienzo.

Así fue que me propuse ir rehaciendo el blog (en términos funcionales) para ver si logro resolver los problemas que encontré en el pasado (producto de no comprender la herramienta más que por sus limitaciones).

El primer paso fue crear mi entorno con Docker, el cual usa PHP 7.4, y ahi mismo instalar Grav mediante Composer.

composer create-project getgrav/grav .

Una vez terminado el trabajo de Composer, fui al navegador y…

El siguiente paso fue poder acceder al backend para comenzar a configurar y crear contenido. Para esto, la url es: www.dominio.com/admin

Esto sucede porque en la instalación por defecto el plugin Admin no se instala. Para instalarlo, vamos a hacer uso de las herramientas para la consola que provee Grav.

Primero, ejecutamos bin/gpm index para ver todos los plugins posibles.

bin/gpm index

Y nos devolverá la lista completa de plugins y themes y al final veremos esto.

You can either get more informations about a package by typing:
    bin/gpm info <package>

Or you can install a package by typing:
    bin/gpm install <package>

Instalamos entonces el Admin, que es módulo oficial.

Ejecutamos este comando:

bin/gpm install admin

Veremos que tenemos que confirmar la instalación de otros módulos requeridos.

GPM Releases Configuration: Stable

The following dependencies need to be installed...
  |- Package form
  |- Package login
  |- Package email

Install these packages? [Y|n]

Una vez que se instalen los paquetes/dependencias se instalará el módulo.

Preparing to install Form [v4.0.3]
  |- Downloading package...   100%
  |- Checking destination...  ok
  |- Installing package...    ok                             
  '- Success!  

Preparing to install Login [v3.0.6]
  |- Downloading package...   100%
  |- Checking destination...  ok
  |- Installing package...    ok                             
  '- Success!  

Preparing to install Email [v3.0.6]
  |- Downloading package...   100%
  |- Checking destination...  ok
  |- Installing package...    ok                             
  '- Success!  

Dependencies are OK

Preparing to install Admin Panel [v1.9.12]
  |- Downloading package...   100%
  |- Checking destination...  ok
  |- Installing package...    ok                             
  '- Success!  

Clearing cache

Cleared:  /var/www/html/cache/twig/*
Cleared:  /var/www/html/cache/doctrine/*
Cleared:  /var/www/html/cache/compiled/*

Touched: /var/www/html/user/config/system.yaml

Ahora accedemos a /admin otra vez.

Creamos nuestro usuario y luego accedemos al administrador.

Grav funciona en base a páginas y cada página es en realidad un directorio con un archivo md.

Tenemos diferentes tipo de páginas por defecto.

Para avanzar con mi proyecto de blog, voy a crear una página de ese tipo.

Una vez creada, la dejaré como una página en blanco.

Si vamos al front veremos ahora que en la navegación apareció Blog en el menú y al clickear, aparece una página en blanco.

Turno de crear una página de tipo Item y como primer ejemplo voy a portar (más o menos) el post sobre Magento 2.3.4.

Vamos nuevamente al frontend y recargamos la página. Aparecerá ahora si nuestro post.

Y si miro el post ya tengo el contenido.

Ahora voy a crear un nuevo post, relacionado con la creación del blog usando Grav.

Tengo entonces dos páginas de tipo Item dentro del Blog. Una relacionada a Magento y otra a Grav.

Y en el frontend tengo mis dos posts presentados.

Comienza el momento de comparar funcionalidades con WordPress.

Lo primero que a mi TOC le molesta es que no tengo información en los posts relacionada con la taxonomía (tags, categoría y autor).

Voy a intentar completar esa información para acercarme a la estructura y funcionalidad de lo que uso en mi CMS actual.

Cuando editamos un post, en el tab Options podemos ver las opciones para categoría y tags.

Luego de editar los dos posts agregando los tags y la categoría, ya tengo los tags visibles pero no aparece referencia a la categoría o el autor.

En el caso del Autor tiene sentido que no aparezca, porque nunca lo ingresé. Voy a empezar a ajustar estos detalles.

Vayamos a editar las taxonomías.

Y vamos a agregar Author.

Cuando vuelva a editar el post veremos el campo Author que acabamos de agregar.

Ahora ya puedo asignarle un valor.

Cuando vuelvo al post en el frontend tampoco se ve el nombre del autor. Esto es porque las taxonomías no se muestran automáticamente en los themes.

Para que suceda vamos a necesitar editar el código de nuestro theme, y como no queremos que nuestros cambios bloqueen posibles upgrades, lo que tenemos que hacer es crear un child theme.

Para crear un child theme, por ejemplo, de Quark, bastará con estos archivos:

Básicamente, tienen que seguir éstas instrucciones: https://learn.getgrav.org/16/themes/customization#theme-inheritance

Cuando hayamos terminado veremos el theme en nuestro admin y podremos activarlo.

Tenemos el child theme base para poder pisar templates y comenzar a agregar tags, autor y categorías.

Como quiero usar Grav como blog únicamente (es decir que no tendré el sitio y el blog como una sección) voy a cambiar la configuración y a eliminar aquello que no me sirva.

En .gitignore agrego esta línea para que mi theme sea incluído en el repositorio.

!user/themes/miblog

Ahora vamos a incluir la categoría en los posts. Lo primero será copiar en nuestro theme los partials del blog, que sacamos del theme Quark. Es decir que vamos a copiar:

  • user/themes/quark/templates/partials/blog-item.html.twig a user/themes/miblog/templates/partials/blog-item.html.twig 
  • user/themes/quark/templates/partials/blog-list-item.html.twig a user/themes/miblog/templates/partials/blog-list-item.html.twig

Si miramos ambos templates veremos que el contenido de estos es:

<div class="content-item h-entry">

{% if not hero_image_name %}
   <div class="content-title text-center">
       {% include 'partials/blog/title.html.twig' with {title_level: 'h2'} %}
       {% if page.header.subtitle %}
       <h3 >{{ page.header.subtitle }}</h3>
       {% endif %}
       {% include 'partials/blog/date.html.twig' %}
       {% include 'partials/blog/taxonomy.html.twig' %}
   </div>
{% endif %}
   <div class="e-content">
       {{ page.content|raw }}
   </div>

   {% if page.header.continue_link is same as(true) and config.plugins.comments.enabled %}
       {% include 'partials/comments.html.twig' %}
   {% endif %}
</div>

<p class="prev-next text-center">
   {% if not page.isLast %}
           <a class="btn" href="{{ page.prevSibling.url }}"><i class="fa fa-angle-left"></i> {{ 'THEME_QUARK.BLOG.ITEM.PREV_POST'|t }}</a>
   {% endif %}

   {% if not page.isFirst %}
       <a class="btn" href="{{ page.nextSibling.url }}">{{ 'THEME_QUARK.BLOG.ITEM.NEXT_POST'|t }} <i class="fa fa-angle-right"></i></a>
   {% endif %}
</p>

Ese es el contenido de blog-item.html.twig mientras que para blog-list-item.html.twig tenemos:

<div class="card">
   {% set image = page.media.images|first %}
   {% if image %}
   <div class="card-image">
       <a href="{{ page.url }}">{{ image.cropZoom(800,400).html|raw }}</a>
   </div>
   {% endif %}
   <div class="card-header">
       <div class="card-subtitle text-gray">
           {% include 'partials/blog/date.html.twig' %}
   </div>
       <div class="card-title">
       {% include 'partials/blog/title.html.twig' with {title_level: 'h5'} %}
       </div>
   </div>
   <div class="card-body">
       {% if page.summary != page.content %}
           {{ page.summary|raw }}
       {% else %}
           {{ page.content|raw }}
       {% endif %}
   </div>
   <div class="card-footer">
       {% include 'partials/blog/taxonomy.html.twig' %}
   </div>
</div>

Nos interesa, en ambos casos, ver el include de:

{% include 'partials/blog/taxonomy.html.twig' %}

Es ese template el que se incluye para mostrar los tags.

Vamos a hacer lo mismo para las categorías. Creamos un nuevo template para esto: user/themes/miblog/templates/partials/blog/categories.html.twig con este contenido:

{% if page.taxonomy.category %}
<span class="categories">
   <p>Categories:
   {% for category in page.taxonomy.category %}
   <a class="label label-rounded {{ label_style ?: 'label-secondary' }} p-category" href="{{ blog.url|rtrim('/') }}/category{{ config.system.param_sep }}{{ category }}#body-wrapper">{{ category }}</a>
   {% endfor %}
   </p>
</span>
{% endif %}

Ahora, tanto para blog-item.html.twig como para blog-list-item.html.twig, agregamos esto luego de la línea de taxonomies.

{% include 'partials/blog/taxonomy.html.twig' %}
{% include 'partials/blog/category.html.twig' %}

El resultado será:

Ahora repetimos lo hecho para el autor. Creamos un nuevo template llamado user/themes/miblog/templates/partials/blog/author.html.twig que contendrá:

{% if page.taxonomy.author %}
<span class="author">
   <p>Por
   {% for author in page.taxonomy.author %}
   <a class="label label-rounded {{ label_style ?: 'label-secondary' }} p-category" href="{{ blog.url|rtrim('/') }}/author{{ config.system.param_sep }}{{ author }}#body-wrapper">{{ author }}</a>
   {% endfor %}
   </p>
</span>
{% endif %}

Y debajo del include que hicimos para las categorías, vamos a agregar el de autor (exactamente igual a lo hecho antes en los dos partials).

{% include 'partials/blog/taxonomy.html.twig' %}
{% include 'partials/blog/category.html.twig' %}
{% include 'partials/blog/author.html.twig' %}

Con esto ya logramos que la taxonomía de nuestro blog mejore bastante si lo comparamos con las opciones que se ofrecen por defecto.

Sigamos ajustando la configuración. Actualmente la home es la página definida por defecto al ingresar al sitio.

En lugar del blog.

Para cambiar este comportamiento, volvemos a la configuración y vamos a la sección Sistema para cambiar el valor de Home page.

Allí seleccionaremos la página principal del blog como página principal y además, configuraremos para que la URL de la home page no se muestre.

Una vez aplicados los cambios, deberíamos notar la diferencia.

Lo que sucede ahora es que el blog se mostrará al ingresar a nuestro dominio en lugar del la página de bienvenida original. Otro de los cambios se dará en las URLs de los posts.

Como hemos ocultado la página blog porque la usamos como homepage, ya no aparecerá ni será usada para las URLs de los posts.

De a poco el blog comienza a comportarse como tal, pero aún quedan configuraciones para hacer que se parezca a lo que podríamos estar acostumbrados al usar en WordPress.

Para ir terminando con esta primera etapa de customización, ahora me faltaría poder mostrar la lista de categorías con las que cuento. Dado que aquí no tenemos base de datos, haremos uso de las colecciones y vamos a agregar un bloque al layout.

Copiemos el template user/themes/quark/templates/partials/sidebar.html.twig a user/themes/miblog/templates/partials/sidebar.html.twig.

Y en la línea 9, agregamos esto:

<div class="sidebar-content">
   <h4>{{ 'Categorías' | capitalize }}</h4>
   <ul>
       {% set categories = taxonomy.taxonomy["category"]|keys|sort %}
       {% for category in categories %}
           <li>
               <a class="p-category" href="{{ blog.url|rtrim('/') }}/category{{ config.system.param_sep }}{{ category }}">{{ category | capitalize }}</a>
           </li>
       {% endfor %}
   </ul>
</div>

¿Qué hice?. Obtengo todos los valores de la taxonomía category y los ordeno. Luego una iteración en la cual imprimo cada valor y le agrego el link para que al clickear, se pueda filtrar por la taxonomía categoría (algo que ya habíamos visto cuando agregamos la categoría al post).

Cuando hayamos terminado, el resultado debería ser similar a esto:

La última customización que haré en ésta primera implementación, será la de agregar un buscador.

Vuelvo al backend (el cual, por comodidad uso en local pero no permito agregarlo al repositorio) y en la sección de plugins buscamos SimpleSearch (con “search” ya estamos).

Con un click ya tendremos SimpleSearch instalado.

Ahora tocará configurarlo.

Aquí cambiaré cómo busca en el contenido. En lugar de recorrer el código generado, hago las búsquedas sobre el contenido markdown. Si volvemos al blog, el buscador ya está habilitado y funcionando.

El resultado estético que obtenemos por defecto puede ser algo tosco, pero claramente, cumple su cometido.

El post ya es más extenso de lo que esperaba, pero no supe dónde cortarlo.

Entiendo que usando ésta configuración y estructura podríamos, en lo funcional, tener una opción más robusta que la que se ofrece por defecto.