··
Autoalojar un blog significa que la seguridad, el SEO y el rendimiento son tu responsabilidad. Cabeceras CSP, rate limiting, imágenes OG generadas, ISR y las lecciones que aprendimos configurándolo todo.

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.
Hace unos meses metí el dominio entero detrás de Cloudflare. Las ventajas obvias son edge cache en cientos de ubicaciones, WAF gestionado, mitigación de DDoS y certificados TLS renovados automáticamente. Lo que no era obvio es que poner un proxy delante rompe silenciosamente el rate-limit por IP si no actualizas la forma de leer la IP del cliente.
El problema es el siguiente. Cuando Cloudflare forwardea una petición a mi origen, la IP TCP que ve Next.js es la de Cloudflare, no la del visitante. Si el rate-limit hashea esa IP como clave, todas las peticiones del mundo acaban bajo un puñado de claves (una por cada PoP de Cloudflare). Durante semanas el login de admin tuvo un rate-limit que no limitaba nada, porque 10 intentos por IP de Cloudflare significa decenas de miles de intentos por segundo repartidos entre todos los visitantes.
La solución es usar la cabecera que Cloudflare inyecta, cf-connecting-ip, que contiene la IP real del cliente. El helper getClientIp la lee como prioridad, cae a x-real-ip si no está (para entornos detrás de Traefik sin CF) y finalmente a x-forwarded-for tomando el primer elemento. Ninguna de esas cabeceras se acepta a ciegas, el origen solo confía en ellas porque tiene Authenticated Origin Pulls activo, que restringe las conexiones TLS entrantes a las que presentan un certificado cliente firmado por Cloudflare. Si un atacante intentara hablar directamente con el VPS saltándose a Cloudflare, el handshake TLS le rebotaría antes de tocar nada.
Un efecto colateral agradable del cache edge es que ahora los endpoints públicos (manifest, post detail, search) se sirven desde el PoP más cercano al visitante sin tocar mi origen. Pero eso trajo su propia trampa, en Cloudflare Free la regla de cache ignora los Set-Cookie y ciertas cabeceras Vary, y después de unos cuantos intentos de afinarlo acabé simplificándolo todo y dejando el cache edge solo donde era seguro. La lección es que un proxy delante no es gratis, hay que entender qué cambia en el camino para no sorprenderse luego con rate-limits que no limitan o caches que no cachean.
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.
Meter un proxy delante rompe cosas en silencio. Cualquier pieza que dependiera de la IP del cliente hay que revisarla, porque lo que ve el origen ahora es la IP del proxy. Rate-limits, logs, geo-lookups, todo.
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
Otra entrega de la serie Construyendo este blog. Vienes de Next.js, SQLite y Docker, el stack técnico y sigues con Cómo verificamos la integridad de los posts.

Jose, autor del blog
QA Engineer. Escribo en voz alta sobre automatización, IA y arquitectura de software. Si algo te ha servido, escríbeme y cuéntamelo.
¿Qué te ha parecido? ¿Qué añadirías? Cada comentario afina la siguiente entrada.
Si esto te ha gustado

Cómo está montado por dentro el chat con agente de IA embebido en mi portfolio. Streaming SSE con eventos tipados, tool calling contra la API pública del blog, prompt como código y sesiones con cookie HttpOnly.

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.