
Cuando usas una plataforma gestionada como WordPress.com o Vercel, muchas cosas vienen resueltas de serie. Los certificados SSL se renuevan solos, las cabeceras de seguridad están preconfiguradas y el CDN se encarga de la caché. Cuando autoalojas, todo eso es tu responsabilidad.
No es tan difícil como parece, pero hay que hacerlo bien. Un blog mal configurado no solo es vulnerable, también se posiciona peor en buscadores y ofrece una experiencia lenta que espanta a los lectores.
Cada respuesta HTTP de este blog incluye un conjunto de cabeceras de seguridad que los navegadores usan para restringir comportamientos peligrosos. Desde la perspectiva de seguridad, en la seguridad en la cadena de suministro de Docker profundizamos en este aspecto.
La Content Security Policy (CSP) es la más importante y la más difícil de configurar bien. Define exactamente qué scripts, estilos, imágenes e iframes puede cargar la página. Nuestra política permite scripts propios e inline (necesarios para Next.js), estilos propios, imágenes desde nuestro dominio y HTTPS genérico, y solo iframes de YouTube y Vimeo. Todo lo demás está bloqueado. Si alguien inyectara HTML malicioso en un post, el navegador rechazaría cualquier script o recurso externo que no esté en la whitelist.
HSTS (HTTP Strict Transport Security) fuerza conexiones HTTPS durante dos años con preload habilitado, lo que significa que los navegadores ni siquiera intentarán conectar por HTTP plano.
X-Frame-Options DENY impide que el blog se cargue dentro de un iframe en otro sitio, previniendo ataques de clickjacking. Referrer-Policy controla cuánta información se envía a sitios externos cuando un lector hace clic en un enlace. Permissions-Policy bloquea el acceso a cámara, micrófono y geolocalización, que un blog no necesita bajo ninguna circunstancia.
Sin rate limiting, un atacante podría bombardear el formulario de login con miles de contraseñas, saturar la búsqueda con queries masivas o abusar del endpoint de generación de IA.
Implementamos limitadores específicos para cada tipo de endpoint. Login tiene un límite de 10 intentos cada 15 minutos por IP. La búsqueda pública permite 30 queries por minuto. El generador de IA, 20 peticiones por hora por sesión. La API administrativa, 60 peticiones por minuto por clave. Cada limitador devuelve un HTTP 429 con cabecera Retry-After cuando se excede el límite.
Los limitadores usan almacenamiento en memoria con limpieza automática para evitar fugas. Las claves de rate limit hashean las IPs con SHA-256 y un salt aleatorio, de modo que las direcciones IP originales nunca se almacenan en memoria.
El panel de administración usa Better Auth con autenticación por email y contraseña. El registro está deshabilitado porque el blog tiene un único administrador. Las sesiones duran 7 días y se validan en el middleware de Next.js, que redirige a /login cualquier petición a /admin/* sin sesión válida.
La API REST usa una clave API con comparación timing-safe (para evitar ataques de temporización) y está separada de la autenticación por sesión. Esto permite que herramientas externas publiquen posts sin necesidad de un navegador.
El SEO de este blog no depende de ningún plugin. Está integrado directamente en el código.
Cada página genera sus propias etiquetas Open Graph y Twitter Card con título, descripción, tipo de contenido y fecha de publicación. Cuando un post tiene imagen de portada, esa imagen se usa como og:image. Cuando no la tiene, Next.js genera automáticamente una imagen de 1200x630 píxeles con el título del post, las categorías y el branding del blog. Todo server-side, sin servicios externos.
El sitemap XML se genera dinámicamente e incluye todos los posts publicados (respetando el flag noindex), categorías activas, tags con posts, series y páginas estáticas. Cada entrada incluye la fecha de última modificación y la prioridad relativa.
Los datos estructurados JSON-LD incluyen schema.org BlogPosting para cada artículo (con autor, fecha, wordcount), BreadcrumbList para la navegación y CollectionPage para las páginas de archivo. Google Search Console los valida sin errores.
El feed RSS incluye el contenido completo de los últimos 50 posts (no solo el extracto), con categorías como elementos XML separados y la imagen de portada como enclosure. Cualquier lector de feeds puede consumir el blog sin visitar la web.
Next.js soporta Incremental Static Regeneration (ISR), que combina lo mejor de los sitios estáticos y dinámicos. Las páginas se renderizan en el servidor la primera vez que se visitan y se cachean durante 5 minutos. Las visitas posteriores reciben la versión cacheada instantáneamente mientras el servidor regenera la página en segundo plano si ha expirado.
Además de ISR, configuramos cabeceras Cache-Control diferenciadas. Los assets estáticos de Next.js (JavaScript, CSS) llevan max-age=31536000, immutable (un año, inmutable) porque el nombre del archivo incluye un hash del contenido. Si el contenido cambia, cambia el hash y por tanto la URL, invalidando la caché automáticamente.
Las imágenes subidas tienen una caché de un día con stale-while-revalidate de una semana, equilibrando frescura y rendimiento. El feed RSS tiene una caché de una hora en el proxy.
SQLite contribuye al rendimiento de una forma que PostgreSQL no puede igualar en este contexto. Las queries no pasan por red, no hay serialización de protocolo, no hay pool de conexiones. Una consulta de lectura tarda microsegundos, no milisegundos.
Una feature que no encontrarás en WordPress es la verificación de integridad. Tenemos un sistema externo que calcula hashes SHA-256 de cada post publicado y los almacena como baselines firmadas con Ed25519. Periódicamente, un agente compara el contenido actual con las baselines y alerta si algo ha cambiado sin una actualización explícita. Desde la perspectiva de seguridad, en el sistema de verificación de integridad del blog profundizamos en este aspecto.
Esto nos da la confianza de que si alguien comprometiera la base de datos y modificara un post, lo detectaríamos automáticamente.
La CSP es la cabecera más valiosa y la más difícil. Empieza restrictiva y abre solo lo que necesites. Cada excepción debe tener un motivo documentado.
El rate limiting no es opcional. Sin él, cualquier endpoint público es un vector de abuso. Implementarlo es simple comparado con lidiar con las consecuencias de no tenerlo.
El SEO es código, no magia. Metadatos correctos, datos estructurados, sitemap y URLs canónicas. No necesitas un plugin que te diga que tu título es demasiado largo.
La caché debe ser deliberada. Assets inmutables con caché larga, contenido dinámico con stale-while-revalidate, y siempre una estrategia clara de invalidación.
SQLite en producción no es una herejía. Para el patrón de acceso correcto, es más rápido, más simple y más fiable que una base de datos cliente-servidor.
La seguridad no es una feature que añades al final. Es una decisión que tomas al principio y que moldea todo lo demás

Cada decisión técnica tiene un porqué. Elegimos Next.js sobre Astro, SQLite sobre PostgreSQL y Docker con Dokploy sobre Vercel. Aquí explicamos las razones y lo que aprendimos por el camino.

Teníamos WordPress, Ghost y decenas de plataformas a un clic de distancia. Pero ninguna nos daba lo que necesitábamos sin compromisos. Esta es la historia de por qué acabamos escribiendo nuestro propio motor de blog.