03 / 05Caso de estudio
Wikiverse
Enciclopedia web full-stack
Una enciclopedia construida desde cero, sin MediaWiki: 63 artículos con historial de revisiones, comparación palabra por palabra, discusiones, enlaces rojos y búsqueda de texto completo en PostgreSQL, en producción con dominio propio.
- Mi papel
- Producto, interfaz, desarrollo full-stack, contenido y despliegue
- Stack
- React 19 · TypeScript · Django REST Framework · PostgreSQL · Redis · Railway
- Estado
- En producción en Railway · dominio propio
Alcance
- 608pruebas en CI, del backend a Playwright
- 63artículos escritos, con 428 referencias
- 36rutas de API documentadas con OpenAPI
El reto
En una wiki cualquiera puede cambiar el texto, y el lector tiene que poder confiar en él y orientarse entre muchos artículos: la trazabilidad y la navegación son el producto; mostrar artículos es la parte fácil.
- Lo que construí
- Convertí mi proyecto de CS50W de 2024 en una enciclopedia completa —revisiones, comparación de versiones, discusiones, búsqueda en PostgreSQL, editor con vista previa y seguridad del contenido—, la llevé a producción con 608 pruebas en CI y escribí sus 63 artículos.
- Decisión técnica clave
- Cada guardado es una revisión inmutable y el índice de búsqueda lo mantiene un trigger de PostgreSQL: ni el historial ni la búsqueda dependen de que el código se acuerde de actualizarlos.
Decisiones de diseño

- Problema
- Un texto que cualquiera edita pierde confianza si no se ve quién cambió qué.
- Decisión
- Cada guardado es una revisión inmutable con autor, resumen y delta; dos cualesquiera se comparan palabra a palabra.

- Problema
- Comparar palabra por palabra un artículo largo de una sola pasada tardaba decenas de segundos.
- Decisión
- El diff compara primero líneas y sólo después palabras dentro de las líneas cambiadas.

- Problema
- Un índice de búsqueda que el código tiene que acordarse de actualizar acaba desfasado.
- Decisión
- Un trigger de PostgreSQL recalcula el vector ponderado de título, resumen y cuerpo en cada cambio.

- Problema
- Una búsqueda con una errata no encuentra nada y el lector se va.
- Decisión
- Sugerencias mientras se escribe y similitud trigram como segundo intento; «entrpy» encuentra Entropy.

- Problema
- Abrir cada enlace para saber de qué trata interrumpe la lectura.
- Decisión
- Al pasar el ratón, una tarjeta muestra la descripción corta y el extracto del artículo enlazado.

- Problema
- Un enlace a un artículo que aún no existe suele romperse o esconderse.
- Decisión
- Se muestra en rojo, abre el editor con el título listo y se vuelve azul solo cuando alguien lo escribe.

- Problema
- Una vista previa con otro render acaba mostrando algo distinto de lo que se publica.
- Decisión
- El editor previsualiza con el mismo pipeline de Markdown que el artículo.

- Problema
- Índice, artículo y herramientas en tres columnas no caben en un teléfono.
- Decisión
- Por debajo de 960 px el índice pasa a la cabecera y la infobox se apila; nada se desborda a 390 px.
Sistema
Elige un módulo: se enciende su ruta.Elige un módulo: debajo, su decisión.
Artículo destacado, curiosidades y efemérides que salen de bloques editables; la portada se guarda en caché por tiempo.
React 19 · TanStack Query 5
Por debajo de 960 px el índice pasa a la cabecera y la infobox se apila; ninguna pantalla se desborda a 390 px.
Tailwind CSS 4.3
El Markdown se renderiza en el cliente con un plugin propio para wikilinks, notas al pie y enlaces rojos, sin HTML crudo.
react-markdown 9 · remark-gfm 4 · Plugin remark propio
La consulta, el filtro y el orden viven en la URL: una búsqueda se puede enlazar y el botón atrás funciona.
React Router 7 · TanStack Query 5
Dos revisiones cualesquiera se comparan lado a lado o en una columna, con el delta de bytes de cada cambio.
React 19
Ediciones y mensajes de discusión en un solo feed, con cada filtro en la URL y un feed RSS.
React Router 7
Se carga sólo al usarlo, previsualiza con el mismo render que el artículo y guarda borradores sin restaurarlos solo.
React.lazy · localStorage
36 rutas documentadas con OpenAPI; JWT de 30 minutos con refresco rotado y revocado al cerrar sesión.
Django REST Framework 3.17 · SimpleJWT 5.5 · drf-spectacular 0.30
Una revisión inmutable por guardado, con padre, autor y delta; un guardado sin cambios no crea ninguna.
Django 5.2
websearch_to_tsquery con ranking, trigram cuando no hay resultados y fragmentos escapados antes de resaltar.
PostgreSQL full-text · pg_trgm
nginx envía sólo los bots de vista previa a Django, que responde con Open Graph y JSON-LD; las personas reciben la SPA.
Django 5.2 · Open Graph
Estadísticas, portada, sugerencias, diffs y límites de peticiones: perder Redis cuesta rendimiento, no datos.
Redis · django-redis 5.4
63 artículos en 21 módulos validados con 31 reglas; sembrar dos veces da exactamente la misma historia.
Comando seed propio
Un trigger mantiene el vector de búsqueda con pesos A, B y C; lo exclusivo de Postgres se decide al llamar, no al importar.
PostgreSQL 18 en Railway · tsvector + GIN
Frontend, backend, PostgreSQL y Redis como cuatro servicios; se despliega al hacer push a main.
Railway · Docker
Sirve la SPA, hace de proxy de /api y emite la misma CSP que Django; index.html sin caché y los assets con hash para siempre.
nginx 1.27
608 pruebas y además migraciones completas, esquema OpenAPI sin avisos y el corpus validado antes de fusionar.
GitHub Actions · pytest · Vitest · Playwright
Imágenes sólo de Wikimedia Commons, con autor y licencia; la lista blanca se valida también en el servidor.
Wikimedia Commons
Cliente
Portada
Artículo destacado, curiosidades y efemérides que salen de bloques editables; la portada se guarda en caché por tiempo.
- Tecnologías
- React 19 · TanStack Query 5
- Entrega a
- API REST
Tecnologías
48 herramientas en 10 áreas. En el sistema, cada módulo dice las suyas.
Lenguajes
- TypeScript 5.9
- Python 3.13
- SQL (PL/pgSQL)
- CSS
Frontend
- React 19.2
- React Router 7.18
- Vite 6.4
- react-markdown 9
- remark-gfm 4
- Plugin remark propio
Interfaz
- Tailwind CSS 4.3
- lucide-react
- Source Serif 4
- Inter
- IBM Plex Mono
- Temas claro y oscuro
Estado y datos del cliente
- TanStack Query 5
- Zustand 5
- Axios 1.18
Backend y API
- Django 5.2
- Django REST Framework 3.17
- SimpleJWT 5.5
- drf-spectacular 0.30
- django-filter 25
Datos y búsqueda
- PostgreSQL 16 y 18
- tsvector + GIN
- pg_trgm
- unaccent
- psycopg 3
- SQLite en desarrollo
Caché
- Redis 7
- django-redis 5.4
Servidor y despliegue
- nginx 1.27
- Gunicorn 23
- WhiteNoise 6.12
- Docker Compose
- Railway
Calidad y CI
- pytest 8.4
- Vitest 4.1
- Testing Library
- Playwright 1.61
- Ruff
- ESLint 9
- GitHub Actions
Herramientas
- Node.js 22
- Makefile
- Comandos de gestión propios
- Git
Resultados verificables
- En producción en wikiverse.jonasjavier.dev con nginx, Django, PostgreSQL y Redis en Railway
- Revisiones inmutables, historial y comparación palabra por palabra entre dos versiones cualesquiera
- Búsqueda de texto completo en PostgreSQL con ranking, autocompletado y tolerancia a erratas
- Enlaces rojos, notas al pie en ambos sentidos y vista previa de cada enlace al pasar el ratón
- Editor con vista previa en vivo y una infobox que se edita por filas
- 608 pruebas en CI, CSP estricta y ningún HTML crudo en el contenido
Recorrido por módulos
18 pantallas en 5 módulos.
Portada y artículos4 pantallas
Historial y cambios2 pantallas
Edición y API3 pantallas
En el teléfono4 pantallas
6 min de lectura
El caso completo
Contexto
Wikiverse empezó en abril de 2024 como el proyecto «Wiki» de CS50W, el curso de programación web de Harvard, que entregué y aprobé: Django con plantillas y cuatro entradas guardadas como archivos Markdown. En junio de 2026 lo reconstruí como una API en Django REST Framework con una aplicación en React, y en septiembre lo convertí en una enciclopedia completa y lo llevé a producción.
El diseño visual parte de Wikipedia como referencia, con tipografías, colores y maquetación propios. Los 63 artículos —unas 75.000 palabras sobre ciencia, geografía e informática— los escribí yo, y comprobé sus 428 referencias.
Problema
Una wiki tiene que resolver dos cosas a la vez: cualquiera puede cambiar el texto, y el lector tiene que poder confiar en él y orientarse entre muchos artículos. Por eso la trazabilidad —quién cambió qué y cuándo— y la navegación —enlaces, categorías y búsqueda— son el producto. Mostrar artículos es la parte fácil.
Mi función
El proyecto es mío de principio a fin: el producto, la interfaz, el modelo de datos, la API, la búsqueda, la seguridad, las pruebas, la integración continua, el despliegue, la documentación y el contenido.
De CS50W a una enciclopedia
El enunciado pedía una página por entrada, un índice, una búsqueda por subcadena, crear y editar en Markdown y una página aleatoria. Wikiverse conserva esas funciones y añade lo que hace que una wiki sea una wiki:
- revisiones inmutables y comparación palabra por palabra entre dos versiones;
- páginas de discusión con respuestas anidadas;
- cambios recientes con filtros en la URL y feed RSS;
- lista de seguimiento, cuentas con JWT y protección de páginas;
- enlaces rojos que se vuelven azules solos cuando alguien escribe el artículo;
- notas al pie en ambos sentidos y vista previa de cada enlace;
- búsqueda de texto completo con autocompletado y tolerancia a erratas;
- un modelo relacional en PostgreSQL en lugar de archivos sueltos.
Restricciones
- Dos bases de datos. Todo funciona en SQLite y en PostgreSQL; lo exclusivo de Postgres se elige al llamar, nunca al importar.
- Nada de HTML crudo. Ni
dangerouslySetInnerHTMLni HTML dentro del Markdown: una prueba falla si reaparece. - Un backend también público. En Railway el backend responde en su propio host, fuera de nginx, así que Django emite las mismas cabeceras de seguridad.
- Servicios que duermen. Tras un rato sin visitas, el primer acceso tarda unos segundos.
Decisiones de arquitectura
- Revisiones.
Articleguarda el estado actual y cada guardado crea unaRevisioninmutable con su padre, autor, resumen, tamaño y delta. Un guardado sin cambios no crea revisión, y borrar conserva el historial. - Grafo de enlaces. Se reconstruye en cada guardado. Un destino que no existe queda como enlace rojo con su título, y se vuelve azul el día que alguien lo escribe.
- Un solo render. Artículos, discusiones, infoboxes, referencias y la vista previa del editor pasan por el mismo pipeline de Markdown con un plugin propio para wikilinks y notas.
- Búsqueda. Un trigger de PostgreSQL mantiene el vector con pesos para el título, el resumen y el cuerpo. Si la consulta no encuentra nada, se repite con similitud trigram y se propone un título.
- Diffs en dos etapas. Primero por líneas y luego por palabras, sólo dentro de las líneas que cambiaron, con límites que truncan en lugar de agotar el tiempo.
- Caché en Redis. Estadísticas, portada, sugerencias y diffs históricos, que no expiran porque son inmutables.
Seguridad
- La misma CSP sale de nginx y de Django: scripts sólo del propio origen, el sitio no se puede incrustar y las imágenes sólo vienen de Wikimedia.
- Las coincidencias de búsqueda se marcan con delimitadores de control, se escapan y sólo después se convierten en resaltado.
- Nueve límites de peticiones configurables; el inicio de sesión y el registro también se limitan por usuario, así que cambiar de IP no los reinicia.
- JWT de 30 minutos con un refresco de dos días que rota y se revoca al cerrar sesión.
- Las tipografías se sirven desde el propio sitio.
Una SPA que se comparte bien
Una aplicación de una sola página no ofrece tarjetas de vista previa a las redes sociales. nginx reconoce a los bots de vista previa y sólo a ellos los envía a una vista de Django con Open Graph y JSON-LD; las personas y los buscadores reciben la aplicación. El sitemap y el robots.txt salen del mismo origen.
Datos reproducibles
El corpus vive en 21 módulos y se valida con 31 reglas antes de escribir nada en la base. La historia de cada artículo —de dos a nueve revisiones— se deriva del título, así que sembrar dos veces da exactamente lo mismo. El CI lo comprueba junto con las migraciones y el esquema de la API.
Desafíos
- Una base vacía con todo en verde. Producción salió sin artículos y los healthchecks no lo detectaban. Se sembró y el proceso de publicación ahora pide comprobar el contenido, no sólo la salud.
- Un diff cuadrático. Comparar por palabras un texto largo de una sola pasada tardaba decenas de segundos; las dos etapas lo resolvieron.
- Cinco especificaciones contradictorias. Se contradecían en 35 puntos, y un solo documento de decisiones pasó a ser la fuente de verdad.
- Un proxy atado a un nombre. El host del backend estaba fijo en la configuración de nginx; renombrar el servicio rompía el sitio sin ningún error. Pasó a variables de entorno.
- Una contraseña en el historial. Una contraseña de administrador de desarrollo quedó en git; se revocó con una migración y la cuenta se crea ahora con un comando propio.
Lo que falta
La interfaz sólo está en inglés. Quedan 57 artículos planificados que hoy son enlaces rojos, las imágenes principales se sirven a resolución completa en lugar de en miniatura, la protección máxima de una página nueva aún no se restringe al personal y el monitoreo de errores está conectado pero apagado. «On this day» muestra ocho efemérides fijas, no las del día.
Sobre el contenido y las capturas
El texto de los artículos es mío y se publica bajo CC BY 4.0; las imágenes vienen de Wikimedia Commons, cada una con su autor y su licencia. Los usuarios, las visitas, las discusiones y la historia de ediciones los genera el seed del proyecto: no son tráfico ni colaboradores reales. Las capturas se hicieron el 28 de septiembre de 2026 con el stack de producción en local, sobre el código más reciente, y la de la vista previa, en producción. Las pantallas con algún defecto visible se dejaron fuera.
Aprendizajes
Un healthcheck en verde no demuestra que el producto funcione: hay que comprobar el contenido. Fijar un contrato escrito antes de construir evitó reabrir decisiones. Y la seguridad que se prueba sola —una prueba que falla si reaparece HTML crudo— resiste mejor que la que se revisa a mano.
Estado actual
En producción en wikiverse.jonasjavier.dev: nginx y Django en Railway, con PostgreSQL y Redis. Si lleva un rato sin visitas, la primera carga puede tardar unos segundos. El repositorio es público bajo licencia MIT, y la integración continua pasa 608 pruebas: 333 del backend, 258 de Vitest y 17 recorridos de Playwright.










