12 min lectura
Tutoriales Frontend Craft CMS

Vite + Craft CMS 5 con DDEV: desarrollo frontend moderno desde cero

Avatar

Lucas Alonso

Desarrollador Web Full-Stack

Vite + Craft CMS 5 con DDEV: desarrollo frontend moderno desde cero

Por qué DDEV + Vite es el stack de desarrollo ideal para Craft CMS 5

DDEV

se ha convertido en el entorno de desarrollo local estándar para proyectos Craft CMS. Corre en Docker, es reproducible entre equipos, no instala nada en tu sistema operativo y tiene soporte oficial de Pixel & Tonic (el equipo detrás de Craft).

El problema es que DDEV añade una capa de complejidad a la hora de integrar Vite: el servidor de desarrollo de Vite corre dentro del contenedor Docker, pero el navegador lo accede desde fuera. Si no configuras correctamente el CORS y el origin, el HMR simplemente no funciona.

En este tutorial vas a configurar todo desde cero de forma correcta. Al final tendrás:

  • Entorno DDEV funcionando con Craft CMS 5
  • Vite.js

    con HMR instantáneo de JavaScript, CSS y Twig
  • TypeScript sin configuración extra
  • Tailwind CSS v4 integrado
  • Build de producción optimizado con assets hasheados
  • Todos los comandos corriendo dentro del contenedor DDEV con ddev npm

Requisitos previos

Antes de empezar necesitas tener instalado:

  • DDEV

    (v1.23 o superior)
  • Docker Desktop (macOS/Windows) o Docker Engine (Linux)
  • Composer instalado localmente o disponible vía DDEV

Si es tu primera vez con DDEV, la documentación oficial tiene guías de instalación para macOS, Windows (WSL2) y Linux.


Estructura del proyecto

mi-proyecto/
├── .ddev/
│   └── config.yaml             # Configuración de DDEV
├── config/
│   └── vite.php                # Configuración del plugin Vite para Craft
├── templates/
│   └── _layouts/
│       └── base.twig           # Layout base con tags de Vite
├── web/
│   └── dist/                   # Assets compilados (ignorar en Git)
├── src/
│   ├── js/
│   │   └── app.ts              # Entry point principal
│   └── css/
│       └── app.css             # Estilos con Tailwind v4
├── vite.config.ts
├── package.json
└── tsconfig.json

En un proyecto Craft estándar con DDEV, la carpeta web/ es el docroot y todo el código PHP de Craft vive en la raíz del proyecto. A diferencia del tutorial sin DDEV, aquí no hay subcarpeta cms/ — Craft y el frontend conviven en la misma raíz.


Paso 1: Crear el proyecto con DDEV

mkdir mi-proyecto && cd mi-proyecto

# Configurar DDEV para Craft CMS
ddev config --project-type=craftcms --docroot=web --project-name=mi-proyecto

# Iniciar los contenedores
ddev start

# Instalar Craft CMS vía Composer dentro del contenedor
ddev composer create -y craftcms/craft

Una vez completado, Craft estará disponible en https://mi-proyecto.ddev.site.


Paso 2: Instalar el plugin nystudio107/craft-vite

ddev composer require nystudio107/craft-vite

# Instalar el plugin desde la CLI de Craft
ddev craft install/plugin vite

Paso 3: Exponer el puerto de Vite en DDEV

Esta es la parte crítica que diferencia la configuración con DDEV de una configuración local estándar. El servidor de Vite necesita estar accesible desde el navegador, que está fuera del contenedor Docker. Para eso, DDEV debe exponer el puerto 3000.

Edita .ddev/config.yaml y añade la configuración de puerto:

# .ddev/config.yaml
name: mi-proyecto
type: craftcms
docroot: web
php_version: "8.3"
webserver_type: nginx-fpm
nodejs_version: "22"

# Exponer el puerto de Vite al host
web_extra_exposed_ports:
  - name: vite
    container_port: 3000
    http_port: 3000
    https_port: 3001

Después de editar el archivo, reinicia DDEV para aplicar los cambios:

ddev restart

Con esto, el servidor de Vite será accesible desde https://mi-proyecto.ddev.site:3001 en el navegador.


Paso 4: Configurar el plugin Vite en Craft

Crea el archivo config/vite.php:

<?php

use craft\helpers\App;

return [
    // Usa el dev server cuando el entorno sea 'dev'
    'useDevServer' => App::env('CRAFT_ENVIRONMENT') === 'dev',

    // Ruta al manifest generado por Vite 5+ (va en .vite/ por defecto)
    'manifestPath' => '@webroot/dist/.vite/manifest.json',

    // URL pública del dev server — usa el dominio DDEV con el puerto HTTPS expuesto
    'devServerPublic' => App::env('VITE_DEV_SERVER_PUBLIC') ?? 'https://mi-proyecto.ddev.site:3001/',

    // URL pública de los assets compilados para producción
    'serverPublic' => App::env('PRIMARY_SITE_URL') . '/dist/',

    // Entry point para inyectar en páginas de error (permite HMR incluso en errores)
    'errorEntry' => 'src/js/app.ts',

    // Verificar si el dev server está activo antes de usarlo
    'checkDevServer' => true,

    // URL interna del dev server (accesible desde PHP dentro del contenedor)
    'devServerInternal' => 'http://localhost:3000/',
];

Añade la variable de entorno en tu .env:

# .env
CRAFT_ENVIRONMENT=dev
PRIMARY_SITE_URL=https://mi-proyecto.ddev.site
VITE_DEV_SERVER_PUBLIC=https://mi-proyecto.ddev.site:3001/

Paso 5: Instalar dependencias de Node dentro de DDEV

Un punto importante: todos los comandos npm deben ejecutarse con el prefijo ddev para que corran dentro del contenedor, donde Node.js está disponible.

# Inicializar package.json
ddev npm init -y

# Instalar dependencias
ddev npm install -D vite typescript @tailwindcss/vite vite-plugin-restart

El package.json resultante:

{
  "name": "mi-proyecto",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4.0.0",
    "typescript": "^5.7.0",
    "vite": "^6.0.0",
    "vite-plugin-restart": "^0.4.0"
  }
}

Paso 6: Configurar Vite para DDEV

La clave de la configuración para DDEV es que server.host debe ser 0.0.0.0 para que Vite escuche en todas las interfaces de red del contenedor, y server.origin debe apuntar al dominio DDEV con el puerto expuesto para que los assets generados tengan la URL correcta.

// vite.config.ts
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import ViteRestart from "vite-plugin-restart";

// Leer variables de entorno de DDEV
const devServerPublic =
  process.env.VITE_DEV_SERVER_PUBLIC ?? "https://mi-proyecto.ddev.site:3001/";

export default defineConfig(({ command }) => ({
  // En dev, base vacío. En build, apunta a /dist/
  base: command === "serve" ? "" : "/dist/",

  build: {
    manifest: true,
    outDir: "./web/dist/",
    emptyOutDir: true,
    rollupOptions: {
      input: {
        app: "src/js/app.ts",
      },
    },
  },

  plugins: [
    tailwindcss(),

    // Recarga automática al cambiar templates Twig o config de Craft
    ViteRestart({
      reload: ["./templates/**/*", "./config/**/*"],
    }),
  ],

  server: {
    // 0.0.0.0 para escuchar en todas las interfaces del contenedor Docker
    host: "0.0.0.0",
    port: 3000,
    strictPort: true,

    // origin apunta al dominio DDEV externo con el puerto HTTPS expuesto
    // Esto garantiza que las URLs de los assets HMR sean correctas en el navegador
    origin: devServerPublic.replace(/\/$/, ""),

    // CORS: permitir solo dominios .ddev.site
    cors: {
      origin: /https?:\/\/([A-Za-z0-9\-\.]+)?\.ddev\.site(?::\d+)?$/,
    },

    // Necesario para servir archivos fuera de la raíz de Vite
    fs: {
      strict: false,
    },

    headers: {
      "Access-Control-Allow-Private-Network": "true",
    },
  },
}));

Paso 7: TypeScript y estilos

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "types": ["vite/client"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "web/dist"]
}

Entry point TypeScript

// src/js/app.ts
import "../css/app.css";

// HMR: necesario para hot-reload del entry point sin full page reload
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    console.log("HMR activo");
  });
}

console.log("Craft CMS 5 + Vite + DDEV ✓");

// Ejemplo de lazy loading — Vite genera un chunk separado automáticamente
document.addEventListener("DOMContentLoaded", async () => {
  if (document.querySelector(".slider")) {
    const { initSlider } = await import("./modules/slider");
    initSlider();
  }
});

Tailwind CSS v4

Con Tailwind v4 no hace falta tailwind.config.js. Todo se define en el CSS:

/* src/css/app.css */
@import "tailwindcss";

@theme {
  --font-sans: "Inter", system-ui, sans-serif;
  --color-brand-500: oklch(55% 0.2 250);
  --color-brand-900: oklch(25% 0.1 250);
}

@layer base {
  body {
    @apply font-sans text-gray-900 bg-white;
  }
}

@layer components {
  .btn-primary {
    @apply inline-flex items-center px-4 py-2 rounded-lg bg-brand-500 text-white hover:bg-brand-900 transition-colors font-medium;
  }
}

Paso 8: Integrar en Twig

{# templates/_layouts/base.twig #}
<!DOCTYPE html>
<html lang="{{ craft.app.language }}">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <title>{% block title %}{{ siteName }}{% endblock %}</title>

  {#
    En DEV (con dev server activo):
      → <script type="module" src="https://mi-proyecto.ddev.site:3001/src/js/app.ts">

    En PROD (con archivos compilados):
      → <link rel="stylesheet" href="/dist/assets/app-[hash].css">
      → <script type="module" src="/dist/assets/app-[hash].js">
  #}
  {{ craft.vite.asset('src/js/app.ts') }}

  {% block head %}{% endblock %}
</head>
<body>

  {% include '_includes/_nav' %}

  <main>
    {% block content %}{% endblock %}
  </main>

  {% include '_includes/_footer' %}

</body>
</html>

Flujo de trabajo diario con DDEV

Desarrollo

# Iniciar DDEV (si no está corriendo)
ddev start

# En una segunda terminal, arrancar Vite dentro del contenedor
ddev npm run dev

Con ambos procesos activos:

  • Tu sitio Craft corre en https://mi-proyecto.ddev.site
  • Vite escucha en el puerto 3000 dentro del contenedor
  • El navegador accede al HMR vía https://mi-proyecto.ddev.site:3001
  • Cambios en .ts/.js → HMR instantáneo
  • Cambios en .css → HMR de estilos
  • Cambios en .twig → recarga automática de página

Producción

# Build de producción (dentro del contenedor)
ddev npm run build

# Los assets quedan en web/dist/ con hashes en los nombres
# Craft lee el manifest.json y genera los tags correctos automáticamente

Makefile opcional para simplificar comandos

Si trabajas en equipo o quieres estandarizar los comandos, un Makefile en la raíz del proyecto simplifica mucho el día a día:

# Makefile
.PHONY: install up dev build

# Primera instalación completa
install:
	ddev start
	ddev composer install
	ddev craft install
	ddev npm install

# Arrancar el entorno
up:
	ddev start

# Modo desarrollo (Craft + Vite HMR)
dev:
	ddev npm run dev

# Build de producción
build:
	ddev npm run build

Uso:

make install   # Primera vez
make up        # Arrancar DDEV
make dev       # Iniciar Vite con HMR
make build     # Build para producción

Solución de problemas comunes

HMR no funciona o los assets no cargan

Verifica que el puerto esté correctamente expuesto en .ddev/config.yaml y que hayas reiniciado DDEV después de editar el archivo:

ddev restart

Error de CORS en la consola del navegador

Asegúrate de que el origin en vite.config.ts coincide exactamente con el dominio DDEV del proyecto. Puedes verificarlo con:

ddev describe | grep "https"

El plugin PHP no detecta el dev server

Revisa que devServerInternal en config/vite.php apunta a http://localhost:3000/ — esa es la URL que PHP usa dentro del contenedor para verificar si Vite está corriendo.

Node.js no disponible fuera de DDEV

Recuerda siempre usar el prefijo ddev para los comandos npm:

# ❌ Esto falla si no tienes Node instalado localmente
npm run dev

# ✅ Esto siempre funciona — usa Node del contenedor
ddev npm run dev

Resultado final

FeatureEstado
Entorno reproducible con DDEV
HMR de JavaScript y CSS✅ Instantáneo
HMR de templates Twig✅ Recarga automática
TypeScript✅ Sin config extra
Tailwind CSS v4✅ Plugin nativo
Code splitting automático✅ Por Rollup
Assets hasheados en producción✅ Con manifest
Comandos unificados con Makefile✅ Opcional

Conclusión

La combinación DDEV + Craft CMS 5 + Vite es hoy el stack más sólido para proyectos PHP con frontend moderno. La parte más delicada es la configuración de red entre el contenedor Docker y el navegador, pero con el config.yaml de DDEV y el origin correcto en Vite todo encaja.

Una vez configurado, el flujo de trabajo es tan fluido como cualquier proyecto Next.js o Astro — con la ventaja de tener todo el poder de Craft CMS como backend.


Fuentes y recursos

Tags:

  • craft-cms
  • vite
  • ddev
  • frontend
  • typescript
  • tailwind
  • hmr
Compartir: FacebookXLinkedInWhatsapp

¿Te fue útil este artículo? Puedes apoyar mi trabajo

Posts Relacionados

¿Te gustó este artículo?

Suscríbete y te aviso cuando publique nuevas guías y tutoriales.

Construyamos algo increíble juntos.