Proyectos

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.

Artículo Entropy con índice lateral, infobox de termodinámica y un enlace rojoArtículo Coral reef en un teléfono con la imagen principal y la infobox
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

  1. Comparación lado a lado de dos revisiones de Marie Curie con la sección Legacy añadida
    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.
  2. Comparación de revisiones de Coral reef adaptada a la pantalla de un teléfono
    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.
  3. Resultados de búsqueda para energy con fragmentos resaltados, filtro de categoría y orden
    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.
  4. Buscador de Wikiverse con sugerencias para mer, Mercury planeta, elemento y desambiguación
    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.
  5. Tarjeta de vista previa del artículo Light sobre el artículo Black hole
    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.
  6. Artículo Entropy con índice lateral, infobox de termodinámica y un enlace rojo
    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.
  7. Editor del artículo Entropy con los campos a la izquierda y la vista previa a la derecha
    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.
  8. Artículo Coral reef en un teléfono con la imagen principal y la infobox
    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.

Arquitectura18 módulos · 17 conexiones

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

Inspector18 decisiones

Cliente

Portada

Artículo destacado, curiosidades y efemérides que salen de bloques editables; la portada se guarda en caché por tiempo.

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

  • Artículo Entropy con índice lateral, infobox de termodinámica y un enlace rojo

    Cada artículo combina índice, infobox, citas y enlaces rojos a lo que falta.

  • Artículo Black hole en tema oscuro con la imagen de M87 y su crédito de licencia

    Tema oscuro completo; cada imagen de Wikimedia muestra su autor y su licencia.

  • Portada de Wikiverse con el artículo destacado Entropy y los paneles Did you know y On this day

    La portada reúne un artículo destacado, curiosidades y efemérides.

  • Final del artículo Entropy con notas numeradas y la lista de referencias con DOI

    Las notas al pie enlazan en ambos sentidos con referencias completas.

Navegar y buscar5 pantallas

  • Tarjeta de vista previa del artículo Light sobre el artículo Black hole

    Al pasar sobre un enlace aparece un resumen, sin salir de la lectura.

  • Buscador de Wikiverse con sugerencias para mer, Mercury planeta, elemento y desambiguación

    El buscador sugiere títulos mientras se escribe y tolera erratas.

  • Resultados de búsqueda para energy con fragmentos resaltados, filtro de categoría y orden

    Búsqueda de texto completo en PostgreSQL, con ranking y coincidencias resaltadas.

  • Página All articles con filtros, índice alfabético y los primeros artículos

    Todo el catálogo se explora por letra, categoría, recientes o extensión.

  • Categoría Physics con índice alfabético y artículos agrupados por letra

    Las categorías reúnen artículos principales y secundarios con índice alfabético.

Historial y cambios2 pantallas

  • Comparación lado a lado de dos revisiones de Marie Curie con la sección Legacy añadida

    Cualquier par de revisiones se compara lado a lado, por líneas y palabras.

  • Cambios recientes de Wikiverse con filtros por tipo, periodo y colaborador

    Ediciones y discusiones en un solo feed, con cada filtro guardado en la URL.

Edición y API3 pantallas

  • Editor del artículo Entropy con los campos a la izquierda y la vista previa a la derecha

    El editor previsualiza con el mismo render que el artículo publicado.

  • Editor estructurado de la infobox de Entropy con filas reordenables

    La infobox se edita por filas tipadas, sin tocar código.

  • Documentación interactiva de la API de Wikiverse generada con OpenAPI

    36 rutas documentadas con OpenAPI y explorables desde el navegador.

En el teléfono4 pantallas

  • Artículo Coral reef en un teléfono con la imagen principal y la infobox

    Los artículos se leen cómodos en el teléfono, sin columnas laterales.

  • Portada de Wikiverse en un teléfono con el artículo destacado Entropy

    En el teléfono la portada abre con el artículo destacado y la búsqueda a mano.

  • Resultados de búsqueda para energy en un teléfono con fragmentos resaltados

    La búsqueda completa funciona igual en el teléfono.

  • Comparación de revisiones de Coral reef adaptada a la pantalla de un teléfono

    La comparación de versiones pasa a una columna en el teléfono.

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 dangerouslySetInnerHTML ni 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. Article guarda el estado actual y cada guardado crea una Revision inmutable 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.