Tengo un blog. Escribo en Sanity porque me gusta la experiencia, tengo control total del contenido y se integra perfectamente con mi portafolio. El problema es que también quiero publicar en Dev.to y Hashnode — donde vive buena parte de la comunidad técnica.
Durante un tiempo lo hacía a mano. Terminaba el post en Sanity, copiaba el markdown, abría Dev.to, pegaba, ajustaba los tags, publicaba. Repetía el proceso en Hashnode. Si después corregía algo en Sanity, volvía a hacer todo de nuevo.
No tardé mucho en decidir que eso no era sostenible.
La idea: Sanity como hub de publicación
Lo que quería era simple: escribir una sola vez en Sanity y que el resto pasara solo. Cuando publico un post, un webhook dispara una Lambda en AWS que toma el contenido y lo manda a todas las plataformas configuradas.
El flujo completo es este:
- Publico un post en Sanity CMS
- Sanity dispara un webhook POST /publish hacia API Gateway
- La Lambda valida la firma HMAC-SHA256 del webhook
- Consulta la GROQ API de Sanity para obtener el post completo
- Publica en cada plataforma configurada (Dev.to, Hashnode...)
- Devuelve 200 si todo salió bien, 207 si hubo éxitos parciales, 502 si todo falló
Sencillo en papel. Pero la parte interesante está en cómo está estructurado el código.
Por qué no usé un if-else gigante
Mi primer instinto al implementar esto fue escribir algo como:
if platform == "devto":
# publicar en dev.to
elif platform == "hashnode":
# publicar en hashnode
elif platform == "medium":
# publicar en medium
Y funciona. Hasta que quieres agregar una cuarta plataforma, o cambiar la lógica de una sin tocar las otras, o escribir tests aislados para cada integración. En ese momento el if-else se convierte en un problema.
Lo que apliqué fue el patrón Strategy: cada plataforma es un adapter independiente que implementa la misma interfaz. La Lambda no sabe qué plataforma está usando — solo llama a publish() y el adapter se encarga del resto.
El contrato es mínimo e intencional:
from abc import ABC, abstractmethod
class PublishingAdapter(ABC):
@abstractmethod
def render(self, post: dict, blog_base_url: str) -> dict:
"""Construye el payload listo para enviar a la API de la plataforma."""
@abstractmethod
def publish(self, post: dict, blog_base_url: str) -> dict:
"""Publica o actualiza el post. Devuelve: action, platform, platform_id, url, title."""
Y el facade BlogPublisher es el punto de entrada único. Se instancia con el nombre de la plataforma y delega todo al adapter correspondiente:
class BlogPublisher:
@classmethod
def _default_registry(cls):
from adapters.devto import DevToAdapter
from adapters.hashnode import HashnodeAdapter
return { "devto": DevToAdapter, "hashnode": HashnodeAdapter }
def __init__(self, platform: str, **adapter_kwargs) -> None:
self._adapter = self._registry[platform](**adapter_kwargs)
def publish(self, post: dict, blog_base_url: str) -> dict:
return self._adapter.publish(post, blog_base_url)
Ahora agregar Medium o cualquier otra plataforma es crear un archivo nuevo, implementar la interfaz y registrarlo. Nada más cambia.
El handler: limpio porque la complejidad está en otro lado
Con esa estructura, el handler de la Lambda queda legible. La lógica de orquestación es clara porque cada responsabilidad está donde debe:
results: list[dict] = []
for publisher in publishers:
try:
result = publisher.publish(post, blog_base_url)
results.append(result)
except (RuntimeError, NotImplementedError) as e:
results.append({"platform": publisher.platform, "error": str(e)})
succeeded = [r for r in results if "error" not in r]
status_code = 200 if not failed else 207 # 207 Multi-Status
return _response(status_code, {
"message": f"Published on {len(succeeded)}/{len(results)} platform(s)",
"results": results,
})
Si Dev.to falla pero Hashnode tiene éxito, la Lambda devuelve 207 con el detalle de cada resultado. El problema está localizado — no explota todo.
Otros detalles que sumaron a la solidez del sistema:
- Secretos desde AWS Secrets Manager, no como env vars de la Lambda. Se cachean en memoria para no pagar el costo de una llamada a Secrets Manager en cada warm start.
- Sin requests: todo el HTTP usa urllib de la stdlib. Mantiene el zip de la Lambda liviano y sin dependencias externas.
- Upsert en los adapters: Dev.to busca por título exacto antes de crear — si ya existe, hace PUT. Hashnode busca por slug. Sin artículos duplicados aunque el webhook se dispare dos veces.
- Conversión de Portable Text a Markdown implementada a mano en utils/portable_text.py, también sin dependencias externas.
Dónde entró el vibe coding — y dónde no
Seré honesto: usé vibe coding en este proyecto. Bastante. Le pedí a Claude que generara los adapters de Dev.to y Hashnode, que implementara la conversión de Portable Text, que escribiera los tests de integración.
Y funcionó bien. Rápido.
Pero hay algo que el vibe coding no pudo hacer por mí: decidir la arquitectura.
Cuando llegué con "necesito publicar un post en varias plataformas", lo que obtuve en una primera iteración fue exactamente el if-else gigante que describí arriba. Funcionaba. Pero yo sabía que no era lo que quería — porque conozco el patrón Strategy, porque he visto ese tipo de código crecer y volverse imposible de mantener, porque entiendo qué significa extensibilidad en este contexto.
La conversación que tuve para llegar al diseño final no fue "hazlo", fue "implementa un adapter para Dev.to que extienda esta clase abstracta, con estos métodos, siguiendo este contrato". Eso requería saber qué pedir.
El vibe coding no reemplaza el criterio de diseño. Lo amplifica. Si sabes lo que quieres construir, puedes construirlo mucho más rápido. Si no lo sabes, aceleras hacia el lugar equivocado.
Agregar una nueva plataforma son exactamente 4 pasos
Eso es lo que me gusta de cómo quedó. Si mañana quiero publicar también en Medium (el adapter está como TODO por ahora), el proceso es:
- Crear adapters/medium.py implementando PublishingAdapter
- Registrarlo en adapters/init.py
- Añadirlo al registry en adapters/base.py
- Agregar el manejo de credenciales en lambda_function.py
Nada más cambia. El handler no se toca. Los tests existentes siguen pasando.
Ese nivel de extensibilidad no llegó por accidente. Llegó porque antes de escribir la primera línea, pensé en cómo quería que se viera el código cuando tuviera que cambiarlo.
Conclusión
No estoy en contra del vibe coding — lo uso seguido y me parece una herramienta genuinamente poderosa para acelerar la ejecución. Pero hay un prerequisito que a veces se omite en la conversación: funciona mejor cuando quien lo usa tiene criterio técnico para guiarlo.
Patrones de diseño como Strategy, principios como separación de responsabilidades, decisiones como "la complejidad va en el adapter, no en el handler" — esas cosas no las aprende el modelo por ti. Las aprendes trabajando, equivocándote, refactorizando código que creciste.
Y cuando las tienes internalizadas, el vibe coding se convierte en lo que debería ser: un multiplicador, no un atajo.
El código completo está en GitHub si quieres revisarlo.
¿Usas vibe coding en tus proyectos? ¿Cómo lo combinas con las decisiones de arquitectura? Me interesa leer cómo otros están resolviendo esto.

Top comments (0)