<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: oskar calvo</title>
    <description>The latest articles on DEV Community by oskar calvo (@oskar_calvo_1615a9b3b293f).</description>
    <link>https://dev.to/oskar_calvo_1615a9b3b293f</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1765681%2F71ebf4c9-9794-4626-a552-fe301f82d5bd.jpg</url>
      <title>DEV Community: oskar calvo</title>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oskar_calvo_1615a9b3b293f"/>
    <language>en</language>
    <item>
      <title>Configurar Graphify para usarlo con Claude cli</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:13:09 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/configurar-graphify-para-usarlo-con-claude-cli-k86</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/configurar-graphify-para-usarlo-con-claude-cli-k86</guid>
      <description>&lt;h2&gt;
  
  
  Instalación y uso de Graphify (Linux / WSL)
&lt;/h2&gt;

&lt;p&gt;(Contenido creado con IA)&lt;/p&gt;

&lt;p&gt;Guía paso a paso para instalar &lt;a href="https://github.com/safishamsi/graphify" rel="noopener noreferrer"&gt;Graphify&lt;/a&gt; una vez en el&lt;br&gt;
entorno (máquina/WSL) y luego activarlo en cada proyecto. Basada en la instalación real hecha en&lt;br&gt;
&lt;code&gt;drupal&lt;/code&gt; (ver sección 8 para las particularidades de ese proyecto).&lt;/p&gt;

&lt;p&gt;Graphify convierte una carpeta de código/config/docs en un grafo de conocimiento consultable&lt;br&gt;
(&lt;code&gt;graph.json&lt;/code&gt;, &lt;code&gt;graph.html&lt;/code&gt;, &lt;code&gt;GRAPH_REPORT.md&lt;/code&gt;) y puede integrarse con Claude Code para que&lt;br&gt;
consulte ese grafo antes de rastrear todo el código.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Qué es global y qué es por-proyecto
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Elemento&lt;/th&gt;
&lt;th&gt;Alcance&lt;/th&gt;
&lt;th&gt;Cuándo se instala&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;uv&lt;/code&gt; (gestor de Python)&lt;/td&gt;
&lt;td&gt;Global, una vez por máquina&lt;/td&gt;
&lt;td&gt;Sección 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;graphifyy&lt;/code&gt; (el CLI &lt;code&gt;graphify&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Global, una vez por máquina&lt;/td&gt;
&lt;td&gt;Sección 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key del backend LLM&lt;/td&gt;
&lt;td&gt;Global (variable de entorno / &lt;code&gt;.env&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Sección 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;graphify-out/&lt;/code&gt; (graph.json, graph.html, GRAPH_REPORT.md)&lt;/td&gt;
&lt;td&gt;Por proyecto — es el dato generado a partir de ESE código&lt;/td&gt;
&lt;td&gt;Sección 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; (hook), &lt;code&gt;.claude/skills/graphify/&lt;/code&gt;, sección en &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Por proyecto&lt;/td&gt;
&lt;td&gt;Sección 6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No hace falta reinstalar &lt;code&gt;uv&lt;/code&gt;/&lt;code&gt;graphify&lt;/code&gt; en cada repo. Lo único que se repite por proyecto es&lt;br&gt;
generar su grafo y activar la integración con Claude Code.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Instalar Graphify en el entorno (una vez)
&lt;/h2&gt;

&lt;p&gt;Requisitos: Python 3.10+ (ya viene en la mayoría de distros/WSL) y &lt;code&gt;curl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Instalar uv (gestiona su propio Python, no depende del venv del sistema)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.local/bin/env"&lt;/span&gt;   &lt;span class="c"&gt;# o reinicia la shell; añade ~/.local/bin al PATH&lt;/span&gt;

&lt;span class="c"&gt;# 2. Instalar graphify como herramienta global&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;graphifyy

&lt;span class="c"&gt;# 3. Comprobar&lt;/span&gt;
graphify &lt;span class="nt"&gt;--version&lt;/span&gt;
graphify &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nota WSL/Debian/Ubuntu&lt;/strong&gt;: si en algún momento usas &lt;code&gt;python3 -m venv&lt;/code&gt; directamente y falla con&lt;br&gt;
&lt;code&gt;No module named ensurepip&lt;/code&gt; pidiendo &lt;code&gt;apt install python3.X-venv&lt;/code&gt; (requiere sudo), &lt;strong&gt;no lo&lt;br&gt;
instales para esto&lt;/strong&gt;: usa &lt;code&gt;uv&lt;/code&gt;, que no depende del módulo &lt;code&gt;venv&lt;/code&gt; del sistema. Así es como lo&lt;br&gt;
resolvimos en &lt;code&gt;drupal&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.1 Instalar el soporte del backend LLM que vayas a usar
&lt;/h3&gt;

&lt;p&gt;El extractor semántico necesita un paquete de cliente extra según el backend. Instálalo con el&lt;br&gt;
extra correspondiente (si no sabes cuál usarás, &lt;code&gt;all&lt;/code&gt; los trae todos):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[gemini]"&lt;/span&gt;     &lt;span class="c"&gt;# Google Gemini&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[anthropic]"&lt;/span&gt;  &lt;span class="c"&gt;# Claude&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[openai]"&lt;/span&gt;     &lt;span class="c"&gt;# OpenAI&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[ollama]"&lt;/span&gt;     &lt;span class="c"&gt;# modelo local, sin API key&lt;/span&gt;
&lt;span class="c"&gt;# o directamente:&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[all]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si lo instalaste sin el extra y &lt;code&gt;graphify extract&lt;/code&gt; falla con &lt;code&gt;the 'openai'/'anthropic' package is&lt;br&gt;
required for this backend&lt;/code&gt;, reinstala con &lt;code&gt;--force&lt;/code&gt; y el extra correcto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[gemini]"&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Esto nos pasó la primera vez: el backend &lt;code&gt;gemini&lt;/code&gt; usa internamente el cliente &lt;code&gt;openai&lt;/code&gt;, y no se&lt;br&gt;
instala por defecto.)&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Configurar la API key
&lt;/h2&gt;

&lt;p&gt;Solo hace falta si vas a extraer contenido semántico (docs, config, PDFs, imágenes). El parsing de&lt;br&gt;
código puro (&lt;code&gt;--code-only&lt;/code&gt;) no requiere key.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Variable de entorno&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GEMINI_API_KEY&lt;/code&gt; o &lt;code&gt;GOOGLE_API_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MOONSHOT_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama (local)&lt;/td&gt;
&lt;td&gt;ninguna&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bedrock&lt;/td&gt;
&lt;td&gt;credenciales AWS/IAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tu-clave"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cuidado con las comillas&lt;/strong&gt;: si la clave está en un &lt;code&gt;.env&lt;/code&gt; como &lt;code&gt;GEMINI_API_KEY="AQ.xxx"&lt;/code&gt;, un&lt;br&gt;
&lt;code&gt;cut -d'=' -f2-&lt;/code&gt; se lleva las comillas literales dentro del valor y Gemini responde&lt;br&gt;
&lt;code&gt;Please pass a valid API key&lt;/code&gt;. Al extraer el valor de un &lt;code&gt;.env&lt;/code&gt;, quítalas explícitamente:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^GEMINI_API_KEY='&lt;/span&gt; .env | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="nt"&gt;-f2-&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="p"&gt;%\&lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;; val=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;val&lt;/span&gt;&lt;span class="p"&gt;#\&lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="p"&gt;
export GEMINI_API_KEY=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$val&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Límites del tier gratuito&lt;/strong&gt;: el tier gratuito de Gemini (&lt;code&gt;gemini-3-flash&lt;/code&gt;) tiene límites bajos&lt;br&gt;
(5 peticiones/min, 250k tokens/min). Si &lt;code&gt;graphify extract&lt;/code&gt; falla con errores &lt;code&gt;429&lt;br&gt;
RESOURCE_EXHAUSTED&lt;/code&gt;, repite con &lt;code&gt;--max-concurrency 1&lt;/code&gt; (más lento pero no satura la cuota). El&lt;br&gt;
caché interno evita reprocesar lo que ya se extrajo con éxito.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  4. Decidir el alcance del grafo en un proyecto
&lt;/h2&gt;

&lt;p&gt;Dos formas de usarlo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A) Graficar todo el repo&lt;/strong&gt; (proyectos pequeños, sin &lt;code&gt;vendor/&lt;/code&gt;, &lt;code&gt;node_modules/&lt;/code&gt;, dumps de BD, etc.&lt;br&gt;
mezclados con el código):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mi-proyecto
graphify extract &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;B) Graficar solo unas rutas concretas&lt;/strong&gt; (recomendado en proyectos con dependencias de terceros,&lt;br&gt;
builds, backups SQL, &lt;code&gt;sites/default/files&lt;/code&gt;, etc. que no aportan nada al grafo y disparan coste/&lt;br&gt;
ruido). Se crea una copia física de solo esas rutas y se extrae sobre la copia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mi-proyecto
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; graphify/source/&amp;lt;misma-estructura-de-carpetas&amp;gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ruta/a/incluir-1 graphify/source/ruta/a/incluir-1
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ruta/a/incluir-2 graphify/source/ruta/a/incluir-2
&lt;span class="c"&gt;# ... una copia por cada ruta a incluir, preservando la ruta relativa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Añade &lt;code&gt;graphify/&lt;/code&gt; a &lt;code&gt;.gitignore&lt;/code&gt; para que esta copia y las herramientas no se comiteen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/graphify/'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto es lo que se hizo en &lt;code&gt;drupal&lt;/code&gt;: ver sección 8 para el ejemplo completo con&lt;br&gt;
&lt;code&gt;config/sync&lt;/code&gt;, módulos y temas custom/contrib concretos.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Generar el grafo
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Extracción completa (AST + semántica vía LLM)&lt;/span&gt;
graphify extract &amp;lt;ruta&amp;gt; &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini

&lt;span class="c"&gt;# Si el backend gratuito da 429, repetir más despacio:&lt;/span&gt;
graphify extract &amp;lt;ruta&amp;gt; &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1

&lt;span class="c"&gt;# Generar/regenerar GRAPH_REPORT.md + graph.html con nombres de comunidad&lt;/span&gt;
graphify cluster-only &amp;lt;ruta&amp;gt; &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Flags útiles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--no-gitignore&lt;/code&gt;: procesa también archivos que estén en &lt;code&gt;.gitignore&lt;/code&gt; (útil si &lt;code&gt;&amp;lt;ruta&amp;gt;&lt;/code&gt; es una
copia aislada como en el método B, o si hay config versionada que normalmente se ignora).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--code-only&lt;/code&gt;: solo AST determinista, sin LLM, sin API key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--force&lt;/code&gt;: fuerza re-extracción completa ignorando caché.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resultado en &lt;code&gt;&amp;lt;ruta&amp;gt;/graphify-out/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;graph.json&lt;/code&gt; — datos completos del grafo (nodos, edges, comunidades)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;graph.html&lt;/code&gt; — visualización interactiva&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GRAPH_REPORT.md&lt;/code&gt; — resumen en lenguaje natural + preguntas sugeridas&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Es normal que algunos archivos de configuración muy simples (YAML triviales, toggles) no&lt;br&gt;
generen nodo semántico propio — el modelo decide que no aportan nada nuevo más allá del AST. No&lt;br&gt;
es un fallo; el resto del grafo (código, relaciones) sigue siendo válido.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  6. Integrar con Claude Code en el proyecto
&lt;/h2&gt;

&lt;p&gt;Si &lt;code&gt;&amp;lt;ruta&amp;gt;&lt;/code&gt; del paso anterior es la raíz del proyecto, el grafo ya queda en&lt;br&gt;
&lt;code&gt;./graphify-out/graph.json&lt;/code&gt;, la ubicación por defecto que espera Graphify (hook, skill y comandos&lt;br&gt;
sin &lt;code&gt;--graph&lt;/code&gt; explícito).&lt;/p&gt;

&lt;p&gt;Si usaste el método B (copia aislada, p. ej. &lt;code&gt;graphify/source/&lt;/code&gt;), el grafo se genera por defecto&lt;br&gt;
dentro de &lt;code&gt;graphify/source/graphify-out/&lt;/code&gt;, no en la raíz. Muévelo una vez a la raíz (ubicación&lt;br&gt;
que espera el resto de la integración):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;graphify/source/graphify-out ./graphify-out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partir de ahí, cualquier &lt;code&gt;extract&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt; que vuelvas a lanzar sobre &lt;code&gt;graphify/source&lt;/code&gt; debe&lt;br&gt;
llevar la variable &lt;code&gt;GRAPHIFY_OUT&lt;/code&gt; apuntando a esa ruta absoluta, o volverá a escribir dentro de&lt;br&gt;
&lt;code&gt;graphify/source/graphify-out&lt;/code&gt; (duplicando el grafo en vez de actualizar el de la raíz):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/graphify-out"&lt;/span&gt;
graphify update graphify/source        &lt;span class="c"&gt;# o extract, con --backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Una alternativa más simple si no te importa el symlink es dejar &lt;code&gt;graphify-out&lt;/code&gt; como&lt;br&gt;
&lt;code&gt;ln -s graphify/source/graphify-out graphify-out&lt;/code&gt;: las lecturas (&lt;code&gt;query&lt;/code&gt;, el hook, &lt;code&gt;graph.html&lt;/code&gt;)&lt;br&gt;
y las siguientes escrituras de &lt;code&gt;extract&lt;/code&gt;/&lt;code&gt;update graphify/source&lt;/code&gt; (que escriben en&lt;br&gt;
&lt;code&gt;graphify/source/graphify-out&lt;/code&gt;, el destino real del enlace) siguen funcionando igual sin tocar&lt;br&gt;
&lt;code&gt;GRAPHIFY_OUT&lt;/code&gt;. Mover el directorio de verdad a la raíz (como se hizo arriba) es preferible por&lt;br&gt;
tenerlo en la ubicación canónica sin depender de un enlace, pero ambas opciones son válidas.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instala la integración oficial (hook + skill + nota en &lt;code&gt;CLAUDE.md&lt;/code&gt;), sin &lt;code&gt;--strict&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphify claude &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto crea/actualiza:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; — hooks &lt;code&gt;PreToolUse&lt;/code&gt; (advisory, nunca bloqueante sin &lt;code&gt;--strict&lt;/code&gt;): avisan
antes de un &lt;code&gt;grep&lt;/code&gt;/&lt;code&gt;Bash&lt;/code&gt;/&lt;code&gt;Grep&lt;/code&gt; o de leer un archivo de código, sugiriendo &lt;code&gt;graphify query&lt;/code&gt;
primero.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/skills/graphify/&lt;/code&gt; — skill del comando &lt;code&gt;/graphify&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLAUDE.md&lt;/code&gt; — sección &lt;code&gt;## graphify&lt;/code&gt; con las reglas de cuándo consultar el grafo.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Revisa la sección añadida a &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt;: por defecto dice &lt;code&gt;graphify update .&lt;/code&gt; para&lt;br&gt;
refrescar el grafo tras cambios de código. Si usaste el método B (copia aislada), eso es&lt;br&gt;
&lt;strong&gt;incorrecto&lt;/strong&gt; — &lt;code&gt;.&lt;/code&gt; es la raíz real del proyecto, no la copia, y lanzaría una extracción sin&lt;br&gt;
acotar sobre todo el repo. Corrígelo a mano por &lt;code&gt;graphify update graphify/source&lt;/code&gt; (o la ruta de&lt;br&gt;
tu copia).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--strict&lt;/code&gt; es opcional y no se activó por defecto&lt;/strong&gt;: bloquearía la primera lectura de código&lt;br&gt;
por sesión hasta consultar el grafo. Con un grafo que no cubre todo el repo (método B), eso&lt;br&gt;
puede bloquear tareas legítimas fuera del alcance graficado — se dejó desactivado.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  7. Mantenimiento
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tras cambiar código dentro de las rutas graficadas&lt;/strong&gt;: &lt;code&gt;graphify update &amp;lt;ruta&amp;gt;&lt;/code&gt; (solo AST, sin
coste de API). Si usaste copia aislada, primero hay que reflejar el cambio dentro de
&lt;code&gt;graphify/source/&lt;/code&gt; (copiando el archivo modificado) y luego correr &lt;code&gt;update&lt;/code&gt; sobre esa copia con
&lt;code&gt;GRAPHIFY_OUT&lt;/code&gt; apuntando al &lt;code&gt;graphify-out/&lt;/code&gt; real (ver sección 6):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/graphify-out"&lt;/span&gt;
  graphify update graphify/source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Re-extracción semántica completa&lt;/strong&gt; (por ejemplo tras añadir muchas rutas nuevas):
&lt;code&gt;graphify extract &amp;lt;ruta&amp;gt; --backend &amp;lt;backend&amp;gt; --force&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desinstalar de un proyecto&lt;/strong&gt;: &lt;code&gt;graphify uninstall --project&lt;/code&gt; (quita hook, skill y sección de
&lt;code&gt;CLAUDE.md&lt;/code&gt;; con &lt;code&gt;--purge&lt;/code&gt; borra también &lt;code&gt;graphify-out/&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Limitaciones conocidas (comprobadas en la práctica)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cobertura parcial&lt;/strong&gt;: el grafo solo "sabe" de las rutas que se graficaron. Para cualquier otra
parte del proyecto, hay que seguir buscando en el código directamente — decídselo explícitamente
a Claude Code en &lt;code&gt;CLAUDE.md&lt;/code&gt; si usas el método B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El hook de lectura (&lt;code&gt;Read&lt;/code&gt;/&lt;code&gt;Glob&lt;/code&gt;) no se dispara para cualquier extensión&lt;/strong&gt;: solo para un
listado fijo de extensiones "genéricas" (&lt;code&gt;.php&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.md&lt;/code&gt;, etc. — ver
&lt;code&gt;_HOOK_SOURCE_EXTS&lt;/code&gt; en el código de Graphify). En un proyecto Drupal, por ejemplo, &lt;strong&gt;no&lt;/strong&gt; se
dispara para &lt;code&gt;.module&lt;/code&gt;, &lt;code&gt;.install&lt;/code&gt;, &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.theme&lt;/code&gt; ni &lt;code&gt;.twig&lt;/code&gt;, que es donde vive buena parte
del código/config. El aviso de búsqueda (&lt;code&gt;Bash|Grep&lt;/code&gt;) sí se dispara siempre que exista el grafo,
independientemente de la extensión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coste y cuotas&lt;/strong&gt;: la extracción semántica consume tokens del backend elegido. El tier
gratuito de Gemini es fácil de agotar en proyectos con muchos archivos de config; usar
&lt;code&gt;--max-concurrency 1&lt;/code&gt; lo evita a costa de tardar más.&lt;/li&gt;
&lt;li&gt;El grafo es un punto de partida para orientarse, no la fuente de verdad — conviene verificar
siempre en el código real antes de editar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Ejemplo real: &lt;code&gt;drupal&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;En este proyecto se usó el método B (copia aislada) porque el repo mezcla Drupal core, &lt;code&gt;vendor/&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;sites/default/files&lt;/code&gt;, dumps SQL, etc. junto al código propio. Solo interesaba graficar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="n"&gt;config/sync/**&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/modules/custom/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/modules/contrib/quiz&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/themes/custom/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;web/themes/contrib/bootstrap_barrio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pasos aplicados (resumen; ver &lt;code&gt;CLAUDE.md&lt;/code&gt; del proyecto para el resultado final):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 0. Instalación global (una vez en la máquina/WSL)&lt;/span&gt;
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"graphifyy[gemini]"&lt;/span&gt;     &lt;span class="c"&gt;# ~/.local/bin ya estaba en el PATH&lt;/span&gt;

&lt;span class="c"&gt;# 1. Copia aislada de solo las rutas a graficar&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; graphify/source/config graphify/source/web/modules/custom &lt;span class="se"&gt;\&lt;/span&gt;
         graphify/source/web/modules/contrib graphify/source/web/themes/custom &lt;span class="se"&gt;\&lt;/span&gt;
         graphify/source/web/themes/contrib
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; config/sync graphify/source/config/sync
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/modules/custom/. graphify/source/web/modules/custom/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/modules/contrib/quiz graphify/source/web/modules/contrib/quiz
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/themes/custom/. graphify/source/web/themes/custom/
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; web/themes/contrib/bootstrap_barrio graphify/source/web/themes/contrib/bootstrap_barrio

&lt;span class="c"&gt;# 2. Extracción (config/sync son YAML "contenido/conocimiento", no solo código -&amp;gt; --no-gitignore)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
graphify extract graphify/source &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--no-gitignore&lt;/span&gt; &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1
graphify cluster-only graphify/source &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--max-concurrency&lt;/span&gt; 1 &lt;span class="nt"&gt;--batch-size&lt;/span&gt; 100

&lt;span class="c"&gt;# 3. Mover el grafo a la raíz (ubicación canónica) e integrar con Claude Code&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;graphify/source/graphify-out ./graphify-out
graphify claude &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--project&lt;/span&gt;
&lt;span class="c"&gt;# (y corregir a mano el "graphify update ." -&amp;gt; ver sección 6/7, con GRAPHIFY_OUT, en CLAUDE.md)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resultado final: &lt;code&gt;./graphify-out/graph.json&lt;/code&gt; (directorio real en la raíz, ~1600 nodos / ~2850&lt;br&gt;
edges / ~296 comunidades), &lt;code&gt;graphify&lt;/code&gt; instalado en global (&lt;code&gt;~/.local/bin/graphify&lt;/code&gt;), copia&lt;br&gt;
aislada del código en &lt;code&gt;graphify/source/&lt;/code&gt; (usada solo como entrada para futuras&lt;br&gt;
&lt;code&gt;extract&lt;/code&gt;/&lt;code&gt;update&lt;/code&gt;), y el hook + la nota en &lt;code&gt;CLAUDE.md&lt;/code&gt; activos para ese subconjunto de rutas.&lt;/p&gt;

</description>
      <category>graphify</category>
      <category>claude</category>
      <category>drupal</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Borrar registros de hook_update_N()</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:39:33 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/borrar-registros-de-hookupdaten-3l5p</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/borrar-registros-de-hookupdaten-3l5p</guid>
      <description>&lt;p&gt;A veces ocurre que estas desarrollando y lanzas varias veces un hook_update_N para depurar el código, con este código podemos resetear el hook_updat_N de un módulo al "numero" que necesitemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ddev drush php:eval &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\D&lt;/span&gt;&lt;span class="s2"&gt;rupal::keyValue('system.schema')-&amp;gt;set('module_name', 10001);"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En este caso ponemos el valor del registro de hook_update_N para el $modulo_name en 10001, por lo que el siguiente a ejecutar sería 10002.&lt;/p&gt;

</description>
      <category>drupal</category>
    </item>
    <item>
      <title>Sincronizar bloques con Block placeholder</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Sun, 14 Jun 2026 11:46:53 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/sincronizar-bloques-con-block-placeholder-557</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/sincronizar-bloques-con-block-placeholder-557</guid>
      <description>&lt;p&gt;Uno de las cosas que tenemos que entender cuando trabajamos con Drupal es que tenemos algunas entidades que funcionan a la vez como contenido y como entidad de configuración.&lt;/p&gt;

&lt;p&gt;Drupal tiene un sistema de sincronización de configuraciones que es muy potente, y a su vez tiene módulos que extienden la sincronización de estas entidades con esa doble característica.&lt;/p&gt;

&lt;p&gt;Existen varios de estos módulos, algunos de ellos los he probado en el pasado y reconozco que no se como están en 2025; vaya esto por delante.&lt;/p&gt;

&lt;p&gt;Una de las cosas que vi en su día es que la sincronización funcionaba bien excepto cuando estas entidades llevaban asociadas files, en ese momento teníamos el problema de que la configuración generaba el yml pero el recurso multimedia que se había asociado no se sincronizaba por lo que o se subía a mano (no siempre se pueden subir a mano a según que servidores) o teníamos un problema.&lt;/p&gt;

&lt;p&gt;El módulo &lt;a href="https://www.drupal.org/project/block_placeholder" rel="noopener noreferrer"&gt;block placeholder&lt;/a&gt; nos ayuda a solucionar en cierta manera esto, &lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cómo lo hace Block Placeholder?
&lt;/h2&gt;

&lt;p&gt;Genera un tipo de bloque que es únicamente configuración, no se le puede añadir campos, únicamente un nombre y definir que ese placeholder se pueda usar por uno o más bloques a la vez; esto sería que el placeholder pinta un único bloque o que pinte uno, dos, tres... bloques, y también definimos que tipos de bloque vamos a usar con ese placeholder.&lt;/p&gt;

&lt;p&gt;Aquí un ejemplo del yml que genera el módulo de una entidad de configuración de block placeholder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;uuid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cf4113bd-c132-456a-aa59-b609009f81cd&lt;/span&gt;
&lt;span class="na"&gt;langcode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;  &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;imagen_de_prueba_placeholder&lt;/span&gt;
&lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Imagen&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;de&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prueba&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;placehodler'&lt;/span&gt;
&lt;span class="na"&gt;reference_limit_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unlimited&lt;/span&gt;
&lt;span class="na"&gt;reference_limited_value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;block_types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imagen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;imagen&lt;/span&gt;
&lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una vez tenemos creaco el placeholder, lo único que tenemos que hacer es crear el bloque en &lt;code&gt;admin/content/block&lt;/code&gt; o editarlo si lo tenemos creado y  en la sección de placeholder identificar el placeholder con el que queremos relacionarlo. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr2y1zfocpaxp3tm0g81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjr2y1zfocpaxp3tm0g81.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Los peros
&lt;/h2&gt;

&lt;p&gt;Al principio suele ser tedioso andar creando en un entorno los bloques de tipo contenido, y luego tener que asociarlos a los placesholder, pero una vez que un bloque de contenido está creado se puede asociar al placeholder de forma manual o se puede usar un hook_post_update o hook_deploy para sincronizar la relación entre bloques de contenido y placeholder si fuera necesario.&lt;/p&gt;

&lt;p&gt;En mi caso lo he empezado a usar en páginas que se construyen con la entidad views de drupal y el bloque va fuera de la vista, en una región superior, así que la configuración es añadir el placeholder y en el entorno de producción se configura la relación entre el placeholder y el bloque con el contenido.&lt;/p&gt;

&lt;p&gt;Se puede usar también en bloques que no aparecen en &lt;code&gt;admin/content/block&lt;/code&gt; pero en ese caso no veo tanta necesidad de hacerlo. &lt;/p&gt;

</description>
      <category>drupal</category>
      <category>blocks</category>
      <category>sync</category>
    </item>
    <item>
      <title>Usar h2non/imaginary con Ddev</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Sun, 14 Jun 2026 11:40:31 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/usar-h2nonimaginary-con-ddev-3e6h</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/usar-h2nonimaginary-con-ddev-3e6h</guid>
      <description>&lt;p&gt;Se tiene que crear el archivo docker-compose.imaginary.yaml dentro de .ddev y añadir el siguiente contenido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;imaginary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;h2non/imaginary:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ddev-${DDEV_PROJECT}-imaginary"&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;com.ddev.site-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DDEV_SITENAME}&lt;/span&gt;
      &lt;span class="na"&gt;com.ddev.approot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DDEV_APPROOT}&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000"&lt;/span&gt;
    &lt;span class="c1"&gt;# ports:&lt;/span&gt;
    &lt;span class="c1"&gt;#   - "9000:9000" # Cambia el puerto si es necesario&lt;/span&gt;
    &lt;span class="c1"&gt;# volumes:&lt;/span&gt;
    &lt;span class="c1"&gt;#   - "imaginary-data:/data" # Opcional, si necesitas almacenar datos persistentes&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-enable-url-source"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;VIRTUAL_HOST=$DDEV_HOSTNAME&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HTTP_EXPOSE=7000:9000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HTTPS_EXPOSE=7001:9000&lt;/span&gt;

&lt;span class="c1"&gt;# volumes:&lt;/span&gt;
&lt;span class="c1"&gt;#   imaginary-data:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y ya tenemos el proxy espuestos, lo siguiente es que cuando servimos una imagen lo tenemos que hacer apuntando al proxy y no a Drupal.&lt;/p&gt;

</description>
      <category>images</category>
      <category>proxy</category>
      <category>ddev</category>
    </item>
    <item>
      <title>Añadir un enlace a un campo booleano</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Fri, 12 Jun 2026 06:42:10 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-un-enlace-a-un-campo-booleano-1ad</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-un-enlace-a-un-campo-booleano-1ad</guid>
      <description>&lt;p&gt;Buenas, esto es algo que he tenido que sacar esta semana, algo que no esta resuelto en ningún módulo ni contribuido ni del core, no he hecho un módulo, únicamente un simple form_alter para resolverlo.&lt;/p&gt;

&lt;p&gt;Contexto: se ha creado un tipo de contenido (nodo) llamado incidencia para que los usuarios anónimos puedan dar de alta incidencias, se ha creado un nodo y no un webform porque esas incidencias tendrán estados, comentarios de las personas que tienen que resolverlas y supervisarlas, etc.&lt;/p&gt;

&lt;p&gt;Uno de los campos de este contenido es un cambo booleano tipo "Acepto condiciones y política de privacidad" y se quería añadir un enlace en la palabra "política de privacidad" para que llevase al nodo de política de privacidad.&lt;/p&gt;

&lt;p&gt;aquí el código:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;  &lt;span class="cd"&gt;/**
   * Privacy policy link.
   */&lt;/span&gt;
  &lt;span class="na"&gt;#[Hook('form_alter')]&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;privacy_policy_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;FormStateInterface&lt;/span&gt; &lt;span class="nv"&gt;$form_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'node_issue_form'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


     &lt;span class="c1"&gt;// NOTE: Para una segunda iteración hacer un formulario para elegir cual es el nodo de politica de privacidad y desde el hook_form_alter llamarlo para no tener a fuego la ruta.&lt;/span&gt;

    &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Url&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromUserInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/politica-privacidad'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'field_issue_privacy_policy'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'widget'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'#title_display'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'after'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'field_issue_privacy_policy'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'widget'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'#title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'Acepto la &amp;lt;a href=":url" target="_blank"&amp;gt;política de privacidad&amp;lt;/a&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;':url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>drupal</category>
      <category>drupal11</category>
      <category>bool</category>
      <category>link</category>
    </item>
    <item>
      <title>Reseña de la Commit Conf 2026</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Sun, 07 Jun 2026 10:00:54 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/resena-de-la-commit-conf-2026-4hpd</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/resena-de-la-commit-conf-2026-4hpd</guid>
      <description>&lt;p&gt;Este fin de semana ha sido la Commit Conf 2026.&lt;/p&gt;

&lt;p&gt;Tengo que reconocer que me ha gustado, lo primero agradecer a la organización porque solo aquellos que han organizado eventos saben el trabajo que supone preparar algo así y que salga todo a la perfección; desde aquí mi más sincero reconocimiento y aplauso.&lt;/p&gt;

&lt;p&gt;El segundo agradecimiento a los voluntarios, sin ellos esto no habría sido posible, que alguien pare durante dos días su día a día para ayudar en un evento como este no es algo que se vea todos los días en este mundo donde está todo cada vez más acelerado, loco y cambiante.&lt;/p&gt;

&lt;p&gt;Para mi este año ha sido diferente, y no se si seré capaz de explicar lo que quiero decir o va a quedar todo mal o peor, pero vamos con al disertación.&lt;/p&gt;

&lt;p&gt;Después de la pandemia y con la aparición de los modelos de IA débil las comunidades virtuales y también foros han ido desapareciendo, ya no necesitamos ir a un foro, chats, o portales para hacer preguntas; tenemos una caja de texto o una consola en la que planteamos lo que queremos hacer se resuelve (mejor o peor, pero se resuelve), es por eso que este año me fijé más en los Open Space que había y decidí a cuál podría ir. De las charlas, van a estar todas grabadas, incluso si no es posible encontrar al ponente para plantearle dudas después de ver la charla también se puede plantear la duda a un modelo de IA débil para leer su respuesta.&lt;/p&gt;

&lt;p&gt;No quiero decir con esto que las charlas (yo dí una charla) no sean necesarias, sean cosa del pasado y a otra cosa mariposa; lo que quiero hacer entender que en este mundo donde vivimos ahora mismo que nos suele separar del resto del mundo una interface el track de Espacio Abierto tiene un valor añadido por el contacto humano con otras personas que también asisten y no es tanto una relación "Maestro - Alumno" como podría ser una charla, es verdad que las sesiones de los Espacios Abiertos van guiadas por las personas que los proponen pero esas personas no están subidas a una tarima como si ocurre en las conferencias.&lt;/p&gt;

&lt;p&gt;No se si he conseguido expresar correctamente o me fan a "funar" por esto.&lt;/p&gt;

&lt;p&gt;El viernes el primer Espacio Abierto al que asistí fue el que organizaron: Maria Isabel Martin and Osmani Redondo con el título "Buscar trabajo en tiempos de crisis (y de IA)". Tengo que aplaudir porque fue una dinámica muy entretenida, se buscaba que todo el mundo participase y estaba muy bien organizado, con los tiempos controlados, se paraba a debatir los conceptos que se consideraban interesantes pero sin llegar a estancarnos.&lt;/p&gt;

&lt;p&gt;El siguiente Espacio Abierto al que asistí fue el organizado por Daniel Rey Rey "De meetups a movimiento: construir comunidades técnicas que crecen", y en esta dinámica se volvió a tocar el tema que se lleva años tocando sobre que está ocurriendo con las comunidades de antes de la pandemia, si es posible re-impulsar/recuperar comunidades o si las comunidades que había hasta 2019 ya es cosa del pasado y tenemos que asumir que "el mundo ha cambiado". &lt;/p&gt;

&lt;p&gt;El sábado la dinámica de Ana G Amor "Encajar, fingir o callar: cómo sobrevivimos (y qué nos cuesta). Masking y síndrome del impostor en entornos Tech" que quizás fue la que más me gustó; en esta sesión los asistentes nos pudimos sincerar con nuestros miedos, y pudimos descubrir que no somos los únicos que a veces pensamos cosas raras.&lt;/p&gt;

&lt;p&gt;Luego pude asistir a media dinámica de Jorge Castaño ¿Qué implica ser Sénior?, la parte en la que estuve de la dinámica también muy bien organizada, con preguntas que iban al debate interesante y enriquecedor.&lt;/p&gt;

&lt;p&gt;Y conseguí llegar a la segunda parte de de la dinámica Tech Communities, como siempre un placer debatir con otras comunidades de lo que nos duele y nos preocupa y quizás de esta Espacio Abierto es la frase que dijo una de las personas que participaban "si una comunidad tiene que morir que muera".&lt;/p&gt;

&lt;p&gt;Además de las dinámicas de los Espacios Abiertos estuve en algunas charlas y conversando con la gente de la comunidad de SysAdmin de Galicia que hicieron acto de presencia.&lt;/p&gt;

&lt;p&gt;De todo lo que he hablado con muchas personas en esta sesión de la Commit Conf es que todos estamos con dudas, con miedos, con incertidumbre en nuestras vidas, algunos he vivido más años otros menos pero las dudas, miedos, e incertidumbres pare que nos acompañan a todos en esta época tan líquida donde tenemos que vivir y creo que para mi esta Commit Conf 2026 es poder encontrar una pequeña isla en un mundo oceánico.&lt;/p&gt;

</description>
      <category>commitconf</category>
      <category>commitconf2026</category>
    </item>
    <item>
      <title>Añadir aljibe a un proyecto que ya tiene Ddev</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Tue, 14 Apr 2026 11:56:31 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-aljibe-a-un-proyecto-que-ya-tiene-ddev-c6m</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-aljibe-a-un-proyecto-que-ya-tiene-ddev-c6m</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/Metadrop/ddev-aljibe/" rel="noopener noreferrer"&gt;Ddev-aljibe&lt;/a&gt; es un plugin creado por la empresa &lt;a href="https://metadrop.net/es" rel="noopener noreferrer"&gt;Metadrop&lt;/a&gt; para añadir en una única instalación todas las dependencias necesarias para escribir y ejecutar test como la ejecutar herramientas de análisis de código estático de PHP que se pueden usar en proyectos de Drupal que se ejecutan mediante &lt;a href="https://ddev.com/" rel="noopener noreferrer"&gt;Ddev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Vamos con los pasos para instalar e implementar Aljibe y comentar que cosas me he encontrado.&lt;/p&gt;

&lt;p&gt;Tengo que avisar que previamente en mi proyecto en Ddev ya había instalado y configurado &lt;a href="https://github.com/phpro/grumphp" rel="noopener noreferrer"&gt;GrumPHP&lt;/a&gt; junto a &lt;a href="https://github.com/squizlabs/PHP_CodeSniffer" rel="noopener noreferrer"&gt;PHPCS&lt;/a&gt;, &lt;a href="https://phpstan.org/" rel="noopener noreferrer"&gt;PHPStan&lt;/a&gt; y &lt;a href="https://phpmd.org/" rel="noopener noreferrer"&gt;PHPMD&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;El primer paso para instalar ddev-aljibe es añadir el plugin &lt;code&gt;ddev add-on get metadrop/ddev-aljibe&lt;/code&gt;, una vez añadido el plugin reiniciamos &lt;code&gt;ddev restart&lt;/code&gt; y ejecutamos &lt;code&gt;ddev aljibe-assistant&lt;/code&gt;, el asistente pasará a realizarnos una serie de preguntas para realizar la instalación.&lt;/p&gt;

&lt;p&gt;Una cosa que tuve que hacer a mitad del proceso y que se puede hacer previamente es actualizar el proyecto &lt;code&gt;ddev composer update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Al ejecutar el asistente este irá haciendo una serie de preguntas, las respuesta que yo di para que no me sobrescribiera el proyecto son las siguientes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please enter the project name (default XXXX):&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En esta opción simplemente pulsar intro.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By default, Drupal 11 will be installed. Do you wnat to install Drupal 10 instead? [y/N]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En esta opción indicamos 10 u 11, en mi caso era un proyecto en Drupal 11 así que marqué N&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you want to initialize a git repository for your new project? [Y/n]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En mi caso como ya tenía un proyecto de git montado señalé n.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you want to install Drupal? [Y/n]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Como el proyecto ya tenía Drupal instalado le dije que no.&lt;/p&gt;

&lt;p&gt;A continuación pasó a instalar todo.&lt;/p&gt;

&lt;p&gt;Un problema que sufrí es que en la instalación me movió el paquete  &lt;code&gt;vlucas/phpdotenv&lt;/code&gt; de requirement a requirement-dev en el archivo composer.json, no tengo claro si esto es porque yo tenía alguna configuración rara o es así como lo hace, pero lo volví a mover a requirement y todo volvió a funcionar.&lt;/p&gt;

&lt;p&gt;Al ejecutar &lt;code&gt;ddev behat&lt;/code&gt; me daba los siguientes errores.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="no"&gt;PHP&lt;/span&gt; &lt;span class="nc"&gt;Fatal&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Declaration&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;NuvoleWeb\Drupal\DrupalExtension\ServiceContainer\DrupalExtension&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&lt;/span&gt; &lt;span class="nv"&gt;$builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;compatible&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Drupal\DrupalExtension\ServiceContainer\DrupalExtension&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&lt;/span&gt; &lt;span class="nv"&gt;$builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nuvoleweb&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;drupal&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;behat&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;DrupalExtension&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;ServiceContainer&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;DrupalExtension&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;

&lt;span class="nc"&gt;Fatal&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Declaration&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="nc"&gt;NuvoleWeb\Drupal\DrupalExtension\ServiceContainer\DrupalExtension&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&lt;/span&gt; &lt;span class="nv"&gt;$builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;compatible&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Drupal\DrupalExtension\ServiceContainer\DrupalExtension&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&lt;/span&gt; &lt;span class="nv"&gt;$builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nuvoleweb&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;drupal&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;behat&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;DrupalExtension&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;ServiceContainer&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;DrupalExtension&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;
&lt;span class="nc"&gt;Failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;behat&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El parche para solucionarlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- a/src/DrupalExtension/ServiceContainer/DrupalExtension.php
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/src/DrupalExtension/ServiceContainer/DrupalExtension.php
&lt;/span&gt;&lt;span class="p"&gt;@@ -16,7 +16,7 @@&lt;/span&gt; class DrupalExtension extends BaseDrupalExtension {
   /**
    * {@inheritdoc}
    */
&lt;span class="gd"&gt;-  public function load(ContainerBuilder $container, array $config) {
&lt;/span&gt;&lt;span class="gi"&gt;+  public function load(ContainerBuilder $container, array $config): void {
&lt;/span&gt;     parent::load($container, $config);
&lt;span class="err"&gt;
&lt;/span&gt;     // Load default service definitions.
&lt;span class="p"&gt;@@ -52,7 +52,7 @@&lt;/span&gt; class DrupalExtension extends BaseDrupalExtension {
   /**
    * {@inheritdoc}
    */
&lt;span class="gd"&gt;-  public function configure(ArrayNodeDefinition $builder) {
&lt;/span&gt;&lt;span class="gi"&gt;+  public function configure(ArrayNodeDefinition $builder): void {
&lt;/span&gt;     parent::configure($builder);
&lt;span class="err"&gt;
&lt;/span&gt;     $builder-&amp;gt;append(
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;También encontré que se modificaron los archivos .gitignore, settings.php, grumphp.yml; no tengo claro si la modificación de .gitignore y settings.php es por la actualización a la ultima versión del core de Drupal o por la instalación del plugin, en el caso del archivo grumphp.yml si que es cosa de Aljibe, mi solución fue revisar lo que había cambiado, lo que me gustaba mantenerlo, lo que no me gustaba quitarlo y recuperar lo que yo tenía.&lt;/p&gt;

&lt;p&gt;Por ejemplo yo sigo Conventional Commits mientras que Metadrop propone commits que empiezan por "Issue #$number: ....". También revisé las configuraciones de PHPStan, PHPmd para ver si lo que había añadido en las configuraciones es lo mismo que yo tenía añadido.&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>ddev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Añadir un capo de tipo hora y minutos en un formulario</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 01 Apr 2026 11:09:36 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-un-capo-de-tipo-hora-y-minutos-en-un-formulario-kp7</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-un-capo-de-tipo-hora-y-minutos-en-un-formulario-kp7</guid>
      <description>&lt;p&gt;Una de las cosas que más me gusta es el FORM API de Drupal.&lt;/p&gt;

&lt;p&gt;Hoy vamos a ver como añadir un campo para recoger únicamente horas y minutos, &lt;/p&gt;

&lt;p&gt;La forma de hacerlo es, a partir de la versión 11.30 de Drupal la siguiente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;    &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'datetime_time_only'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s1"&gt;'#type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'datetime'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'#date_date_element'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'#date_time_element'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'#date_time_format'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'H:i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si estamos trabajando en versiones anteriores a la 11.30 la forma que he encontrado de hacerlo es con el módulo duration_field el cual define un &lt;a href="https://git.drupalcode.org/project/duration_field/-/blob/8.x-2.2/src/Element/DurationElement.php" rel="noopener noreferrer"&gt;Element&lt;/a&gt; para que podamos añadir a nuestros formularios un campo de tipo time&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt; &lt;span class="c1"&gt;// Usage example:&lt;/span&gt;
 &lt;span class="c1"&gt;// @code&lt;/span&gt;
 &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'duration'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="s1"&gt;'#type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'duration'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'#title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Duration),
   // Only 15 minute increments.
   '&lt;/span&gt;&lt;span class="c1"&gt;#date_increment' =&amp;gt; 900,&lt;/span&gt;
   &lt;span class="c1"&gt;// Hide seconds.&lt;/span&gt;
   &lt;span class="s1"&gt;'#granularity'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'y:m:d:h:i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="c1"&gt;// Require hours and minutes.&lt;/span&gt;
   &lt;span class="s1"&gt;'#required_elements'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'h:i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comento esto porque algunos modelos LLM no saben las diferentes opciones que existen y sugieren paparruchas.&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>formapi</category>
      <category>time</category>
    </item>
    <item>
      <title>Drupal Canvas, creación de páginas I</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 24 Dec 2025 13:17:49 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/drupal-canvas-creacion-de-paginas-i-c3b</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/drupal-canvas-creacion-de-paginas-i-c3b</guid>
      <description>&lt;p&gt;Vamos con la creación de una página, para ello simplemente le damos a crear una página en el botón &lt;code&gt;+ Add Page&lt;/code&gt; que nos llevará a una página como esta:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj34fh1smxuu8f8tbzr03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj34fh1smxuu8f8tbzr03.png" alt="Crear una página con Canvas y template Byte" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Importante&lt;/strong&gt;, en mi caso he añadido dos idiomas a la web, con la ruta &lt;code&gt;https://drupalcms.ddev.site/en/canvas/editor/canvas_page/12&lt;/code&gt; y &lt;code&gt;https://drupalcms.ddev.site/es/canvas/editor/canvas_page/12&lt;/code&gt; no me funciona Canvas (&lt;em&gt;tengo que investigar esto&lt;/em&gt;), pero con la ruta &lt;code&gt;https://drupalcms.ddev.site/canvas/editor/canvas_page/12&lt;/code&gt; vemos la página lista para crear contenido.&lt;/p&gt;

&lt;p&gt;Esta página recuerda en su estructura a la de Gutenberg de Wordpress&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykwcld9oqeg9grtoxliw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykwcld9oqeg9grtoxliw.png" alt="Gutenberg editor" width="800" height="541"&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lateral izquierdo
&lt;/h2&gt;

&lt;p&gt;En el lateral izquierdo tenemos el menú principal, con los siguientes elementos en orden:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Library&lt;/li&gt;
&lt;li&gt;Layers&lt;/li&gt;
&lt;li&gt;Code&lt;/li&gt;
&lt;li&gt;Pages&lt;/li&gt;
&lt;li&gt;Templates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Library
&lt;/h3&gt;

&lt;p&gt;Esta opción del menú nos pinta todos los componentes que tenemos habilitados* en nuestro portal web y también nos permite elegir entre patrones si tenemos creado alguno.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6a7s07hpdcc7qxidgoze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6a7s07hpdcc7qxidgoze.png" alt=" " width="392" height="831"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Layers
&lt;/h3&gt;

&lt;p&gt;Esta opción nos permite definir con que capa de la página queremos trabajar, en e caso de Byte existen tres capas, Contenido, cabecera, y pie.&lt;/p&gt;

&lt;p&gt;También muestra los diferentes componentes que se están usando en cada capa en una estructura jerárquica, y en permite mover algunos componentes arriba y abajo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvizs7ojmvmvactlfjqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvizs7ojmvmvactlfjqa.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;

&lt;p&gt;Esta opción nos permite añadir componentes mediante código, a esto e dedicaremos una entrada en exclusiva de esta serie.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pages
&lt;/h3&gt;

&lt;p&gt;Esta opción nos muestra el listado de todas las páginas que existen, recordemos, páginas de Canvas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Templates
&lt;/h3&gt;

&lt;p&gt;Esta opción nos permite usar Canvas para personalizar los view mode que tenemos definidos de  tipos de nodos.&lt;/p&gt;

&lt;p&gt;En caso de que se quiera usar un view mode concreto tenemos que ver si está activado en el nodo para poder usarlo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTA&lt;/strong&gt;: Lo que he visto por ahora con los nodos es que si añadimos un componente a un tipo de nodo (por ejemplo de blog) en Canvas afecta a todos los nodos, no he visto una forma para que un componente se aplique a un único nodo, estoy preguntando y mirando esto, porque si esto no se puede realizar sería algo a tener en cuenta si queremos elegir entre Layout Builder y Canvas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lateral derecho
&lt;/h2&gt;

&lt;p&gt;En el lateral derecho vemos las opciones de configuración del componente que se está trabando. Cuando se entra a la página lo que se muestra son los valores básicos de la página: título, URL, SEO Settings, Media, Page Title (meta-atributos), Meta Description.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2t1ine7liqqhtz7l3g64.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2t1ine7liqqhtz7l3g64.png" alt="Page data" width="353" height="821"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Drupal Canvas, Adminstración de contenido</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 24 Dec 2025 12:08:50 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/canvas-parte-2-primeros-pasos-4gf4</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/canvas-parte-2-primeros-pasos-4gf4</guid>
      <description>&lt;p&gt;Este artículo los escribo con una instalación de DrupalCMS beta 2.x y template de instalación "Byte".&lt;/p&gt;

&lt;p&gt;Cuando hemos arrancado lo primero que he visto es que las páginas de canvas son independientes de la sección de contenido que solemos ver en Drupal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flsvbrnw82pdxh86ebq5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flsvbrnw82pdxh86ebq5w.png" alt=" " width="313" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pages nos pinta el listado de "paginas" (no confundir con "basic pages") de Canvas, y el enlace CMS nos pinta la administración clasica de &lt;code&gt;/admin/content&lt;/code&gt; de Drupal.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pages
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffew3z0wfmgbfceuiraj9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffew3z0wfmgbfceuiraj9.png" alt="Pages" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Drupal CMS
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnb3795xbm267av65t7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnb3795xbm267av65t7x.png" alt="Drupal CMS" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Como se puede ver en las imágenes la administración (publicar/despublicar/borrar) para las páginas de Canvas es parecida, las operaciones que podemos hacer son borrar/ver/editar, y crear una página nueva además de buscar por el título de la página y filtrar por publicadas/despublicadas.&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>drupalcms</category>
      <category>uidesign</category>
      <category>contentwriting</category>
    </item>
    <item>
      <title>Drupal Canvas, introducción</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Wed, 24 Dec 2025 11:57:09 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/canvas-parte-1-566b</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/canvas-parte-1-566b</guid>
      <description>&lt;p&gt;&lt;a href="https://www.drupal.org/project/canvas" rel="noopener noreferrer"&gt;Canvas &lt;/a&gt;es un módulo añadido a Drupal que permite la construcción de páginas webs simplemente agarrando y soltando elementos y luego personalizando dichos elementos.&lt;/p&gt;

&lt;p&gt;Uno de los puntos importantes respecto a otros módulos de Drupal que permitían construir páginas es que esta vez Canvas no viene de desarrolladores de backend dando funcionalidades a creadores de contenido/diseñadores, sino que ha sido un trabajo de análisis de que había en el mercado, ver como funcionaba, coger lo mejor de todo aquello que se ha analizado, hablar con diseñadores y creadores de contenido y luego sacar las funcionalidades que se quieren implementar, vamos que se ha pensando más como un producto con los diseñadores y creadores de contenido como  público objetivo. &lt;/p&gt;

&lt;p&gt;Que nadie me mal interprete, no digo que Layout Builder, Paragraphs, Panels, etc... no fueran buenos productos, pero mi opinión personal es que en la comunidad de Drupal durante muchos años hemos pensado en sacar módulos/productos pensados para desarrolladores backend/hardcoder y no tanto herramientas sencillas de usar para diseñadores o creadores de contenidos, y esto es precisamente lo que hizo que Wordpress triunfase donde no lo hizo Drupal, ellos sacaron un producto pensado para diseñadores o personas sin conocimientos y sin ganas de leerse un libro enorme.&lt;/p&gt;

&lt;p&gt;Con Canvas Drupal rompe esa dinámica y tenemos una herramienta pensada para ese tipo de usuarios.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi05u143qo880voyv6n93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi05u143qo880voyv6n93.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>uidesign</category>
      <category>ui</category>
      <category>contentwriting</category>
    </item>
    <item>
      <title>Añadir programáticamente un campo de tipo cshs a un formulario.</title>
      <dc:creator>oskar calvo</dc:creator>
      <pubDate>Tue, 11 Nov 2025 11:20:42 +0000</pubDate>
      <link>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-programaticamente-un-campo-de-tipo-cshs-a-un-formulario-45o3</link>
      <guid>https://dev.to/oskar_calvo_1615a9b3b293f/anadir-programaticamente-un-campo-de-tipo-cshs-a-un-formulario-45o3</guid>
      <description>&lt;p&gt;El módulo &lt;a href="https://www.drupal.org/project/cshs" rel="noopener noreferrer"&gt;Client-side Hierarchical Select&lt;/a&gt; de Drupal permite elegir un elemento de un select cuando tenemos una jerarquía en árbol con múltiples valores.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojixqxc4epdtu300xnek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojixqxc4epdtu300xnek.png" alt=" " width="640" height="150"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h6&gt;
  
  
  (Fuente de la foto: &lt;a href="https://www.drupal.org/project/cshs" rel="noopener noreferrer"&gt;https://www.drupal.org/project/cshs&lt;/a&gt;)
&lt;/h6&gt;
&lt;/blockquote&gt;

&lt;p&gt;Añadir este widget a las entidades de contenido es sencillo, simplemente en la pestaña de "Administrar la visualización del formulario" podemos elegir que widget queremos usar y si tenemos activado el módulo &lt;strong&gt;Cshs&lt;/strong&gt; lo veremos para los campos de taxonomías. &lt;/p&gt;

&lt;p&gt;El problema es que los formularios creados a medida para añadir funcionalidades, o los formularios de entidades de configuración no tienen la opción de "Administrar la visualización del formulario".&lt;/p&gt;

&lt;p&gt;En este caso muestro como usando el módulo Hux añado un hook_form_alter para añadir un campo adicional a la entidad que trae el módulo simplenews para poder categorizar los tipos de boletines que se crean usando un vocabulario ya definido en el proyecto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
  &lt;span class="cd"&gt;/**
   * Implement hook_form_alter().
   */&lt;/span&gt;
  &lt;span class="na"&gt;#[Alter('form')]&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;addThematicFieldToNewsletters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\Drupal\Core\Form\FormStateInterface&lt;/span&gt; &lt;span class="nv"&gt;$form_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'simplenews_newsletter_edit_form'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'simplenews_newsletter_add_form'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'container_thematics'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'#type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'fieldset'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'#title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Thematics'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;];&lt;/span&gt;

          &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'container_thematics'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'thematics'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'#type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cshs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'#title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Select the thematic'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'#options'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildCshsOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'thematics'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'#weight'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="cd"&gt;/** @var \Drupal\simplenews\Entity\Newsletter $newsletter */&lt;/span&gt;
            &lt;span class="s1"&gt;'#default_value'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$form_state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFormObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getEntity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getThirdPartySetting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'gobcan_newsletters'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thematics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

          &lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="c1"&gt;// Añadir entity builder para procesar los third party settings&lt;/span&gt;
            &lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'#entity_builders'&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'buildThematicsEntity'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para definir correctamente el widget de formulario de Cshs tenemos que definir el campo del formulario como ´'#type' =&amp;gt; 'cshs',´ , además el array de ´#Options´ no sirve un array de tipo clave valor, sino que tiene que tener una estructura concreta para que Cshs pueda procesarlo, por eso se ha creado el método ´buildCshsOptions()´.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;
    &lt;span class="cd"&gt;/**
     * Entity builder para las temáticas.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildThematicsEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$entity_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\Drupal\simplenews\Entity\Newsletter&lt;/span&gt; &lt;span class="nv"&gt;$newsletter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;\Drupal\Core\Form\FormStateInterface&lt;/span&gt; &lt;span class="nv"&gt;$form_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$thematics_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$form_state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'thematics'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nv"&gt;$newsletter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setThirdPartySetting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$module_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thematics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$thematics_value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta funciión es la función necesaria para guardar los valores en la entidad como configuraciones de terceros.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildCshsOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$vocabulary&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nv"&gt;$term_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;entityTypeManager&lt;/span&gt;
      &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'taxonomy_term'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;loadTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$vocabulary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kc"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$terms&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$termId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$term_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$termId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CshsOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;target_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$term_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$termId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CshsOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$term&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$term_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En este método construimos la estructura del array que necesita Cshs para poder construir el widget correcto, usamos la clase CshsOption que trae el módulo para hacerlo.&lt;/p&gt;

&lt;p&gt;El código es de &lt;a href="https://www.drupal.org/u/dmudie" rel="noopener noreferrer"&gt;David Mudie (dmudie)&lt;/a&gt; lo saqué de esta &lt;a href="https://www.drupal.org/project/cshs/issues/3361342" rel="noopener noreferrer"&gt;Issue&lt;/a&gt; en Drupal.org&lt;/p&gt;

</description>
      <category>drupal</category>
      <category>php</category>
      <category>formapi</category>
    </item>
  </channel>
</rss>
