




Sophie Callander Consulting
Sitio multilingüe de una práctica de resolución de conflictos
- Rol
- Full-stack Engineer
- Tipo de trabajo
- Colaboración con equipo
- Industria
- Servicios profesionales • Mediación
Resumen
El desafío
Enlaces
Privado — proyecto de cliente
Arquitectura del sistema
Una aplicación Next.js 16 App Router renderizada en el servidor y prerenderizada de forma estática, construida en torno a un único árbol de rutas localizado que sirve siete idiomas. La decisión central es que casi nada corre en el cliente: diccionarios, armado de contenido, datos estructurados y metadata se resuelven en el servidor, y el bundle de cliente lleva sólo cinco componentes interactivos. El contenido se separa según cuánto cambia: los textos estables viven en diccionarios TypeScript tipados que se compilan en el build, mientras que los artículos del blog vienen de un CMS headless vía ISR. Así la clienta puede publicar sin un despliegue y los textos de marketing siguen bajo control de versiones y chequeo de tipos.
Arquitectura de la aplicación
- Un único árbol de rutas localizado con static params que genera una variante estática por idioma.
- El idioma por defecto se sirve sin prefijo mediante un rewrite interno, así las rutas en inglés y las localizadas comparten una misma implementación.
- Patrón de shell de servidor e isla de cliente: el header resuelve todos los datos en el servidor y le pasa las props ya resueltas a su hijo interactivo.
- Una capa de armado de contenido combina los textos traducidos con la estructura no traducida: los identificadores de slug estables que se usan como anclas de URL y claves de íconos explícitamente nunca se traducen.
- Todas las páginas se componen con primitivas de presentación, con SCSS Modules acotados por componente y sin framework de UI.
Internacionalización
- Siete idiomas, cada uno con su language tag, locale para redes sociales, locale de formato y dirección de escritura en un único registro de metadata.
- El persa activa right-to-left en la raíz del documento, sostenido por 62 declaraciones de propiedades lógicas de CSS.
- Los diccionarios se importan de forma lazy por request, así que al servidor sólo llega el idioma activo, y al cliente nunca.
- APIs nativas de internacionalización para plurales y fechas, con las fechas fijadas en UTC para que una publicación de madrugada no muestre el día anterior en zonas con offset negativo.
- El selector de idioma renderiza anclas reales con atributos de idioma y dirección, y funciona sin JavaScript.
Modelo de datos
- Sin base de datos, sin ORM y sin capa de persistencia: el sitio es de sólo lectura por diseño.
- Strapi 5 REST filtrado por el slug del proyecto como clave de tenant, para que un mismo CMS sirva a varios sitios.
- Una capa de mapeo convierte cada respuesta del CMS en tipos propios del sitio; la UI nunca toca la forma de la API.
- Datos derivados en el servidor: tiempo de lectura a partir de la cantidad de palabras del cuerpo, posts relacionados ordenados por categoría compartida y un parser que respeta el anidamiento y elimina el bloque de conclusiones clave que el CMS duplica.
- Las fallas del CMS se degradan a una lista vacía en lugar de propagarse: una caída se lleva puesto el blog, no el sitio.
Capa de SEO y answer engines
- hreflang × 7 + x-default
- Canonical por ruta
- JSON-LD @graph
- ProfessionalService
- FAQPage
- BreadcrumbList
- Article
- Sitemap generado
- robots generado
- Ruta llms.txt
- Allowlist de crawlers de IA
Infraestructura y deploy
Desplegado como un contenedor Docker self-hosted detrás de nginx en un VPS de Linode en Toronto, en lugar de una plataforma gestionada; la elección se explica porque el servidor ya aloja unos trece proyectos. Desarrollo y producción son dos clones independientes del mismo repositorio en la misma máquina, parametrizados por completo con variables de entorno para que ninguno choque con el nombre de contenedor ni el puerto del otro.
Contenedores y build
- Dockerfile de tres etapas sobre node:22-alpine, corriendo con un usuario no root.
- Output standalone, para que la imagen de runtime incluya sólo las dependencias que realmente se usan.
- Healthcheck de Compose que consulta la app cada 30 segundos, con un start period.
- La URL pública del sitio se pasa como build arg, no sólo como variable de entorno en runtime: queda fijada en tiempo de compilación porque firma los canonicals, el hreflang y el sitemap.
CI/CD
- Bitbucket Pipelines, con los pull requests corriendo el mismo paso de verificación que main.
- El orden de los controles es install, type-check, lint, build; el chequeo de tipos va antes del build para que los errores de claves de idioma aparezcan primero.
- Un push a main despliega a desarrollo y después a producción, mediante un script remoto que hace reset contra origin, recompila, reinicia y limpia las imágenes viejas.
- Una trampa del pipeline, documentada: la ruta del script de producción va entre comillas porque el CI enmascara cada aparición del valor de una variable protegida en el string del comando, y eso estaba corrompiendo la ruta.
Entornos y edge
- Desarrollo y producción conviven en un mismo host con directorios, nombres de contenedor y puertos distintos.
- Desarrollo sirve un header robots noindex desde la capa de nginx, para que nunca pueda competir con producción en los rankings.
- nginx termina TLS y fuerza HTTPS y la canonicalización del dominio raíz con redirecciones permanentes.
- Un snippet de real-IP de Cloudflare recupera las IPs de los visitantes desde el header de forwarding, confiando sólo en los rangos publicados; quedó entregado y documentado antes de activar el proxy.
Observabilidad
- Analytics se monta sólo cuando hay un measurement ID configurado: si no hay ninguno, el sitio no carga nada del proveedor ni setea cookies.
- Las fallas al consultar el CMS se loguean en el servidor con el status y la URL.
Construido con
Un sitio de marketing completo en siete idiomas, entregado en producción junto con su pipeline de despliegue, la configuración del servidor y una auditoría técnica por escrito.
Superficies de la aplicación
- Home, Services, About, FAQ, Contact, Reflections, índice del Blog y detalle del Blog: ocho tipos de ruta en siete idiomas.
- Header con panel mobile, link para saltar al contenido y selector de idioma; footer con la navegación completa.
- Detalle del blog con imagen de portada, tiempo de lectura, bloque de conclusiones clave, posts relacionados y metadata por post controlada desde el CMS.
- Página About narrativa con una línea de tiempo de su recorrido, credenciales que incluyen un grupo aparte, explícitamente separado, de formaciones en curso, y una tira animada de retratos.
- Página de contacto con email, teléfono y un link externo para reservar turnos.
Integraciones
- Strapi 5 como CMS headless para el contenido del blog, filtrado por tenant.
- Un widget de chat embebido en Shadow DOM, cargado desde el origen del CMS con un atributo de tenant.
- Agenda externa de reservas y analytics montado de forma condicional.
Infraestructura y documentación
- Dockerfile, archivo de Compose, script de despliegue remoto y configuración del pipeline de CI.
- Server blocks de nginx para ambos entornos, más el snippet de real-IP de Cloudflare.
- Una auditoría escrita de SEO y answer engines que documenta cinco defectos corregidos con evidencia del antes y el después, una tabla de verificación fase por fase, resultados de Lighthouse, afirmaciones marcadas explícitamente como no verificables y preguntas abiertas para la clienta.
- Un runbook para activar el proxy de Cloudflare, con la justificación basada en la latencia medida y el orden obligatorio de los pasos.