Por qué: una migraña, las 7 de la tarde y dos farmacias sin el remedio
Mi pareja llegó a la casa con una migraña terrible. Eran las siete de la tarde y la mayoría de las farmacias ya estaban cerradas.
"No hay problema", pensé. "Busco una farmacia de turno y listo."
Iluso.
La página oficial del MINSAL simplemente no cargaba. Las alternativas que encontré tenían tanta publicidad que costaba distinguir dónde había que hacer clic. Cuando por fin saqué una dirección en limpio, fui a dos farmacias de turno de mi comuna y ninguna tenía el remedio. A volver a buscar, entonces, esta vez para ir a otra comuna, desde el celular y en la calle, mientras ella esperaba en la casa aguantando la migraña.
Esa noche, con mi pareja ya descansando, decidí que esto no podía seguir así.
Lo que más rabia me daba es que el dato existe: en Chile cada comuna tiene una farmacia de turno rotativa y el MINSAL publica la lista todos los días, pública y gratis. El problema nunca fue la información. Fue llegar a ella.
Y es una pena, porque la pregunta es una sola y no cambia nunca:
¿Qué farmacia está de turno cerca de mí, ahora, y cómo llego?
Nadie quiere explorar un mapa con alguien esperando en la casa. Nadie quiere filtrar por región, después por comuna, después aceptar cookies, después esquivar tres banners. Quiere un nombre, una dirección y un botón que le abra la ruta en Google Maps. Esa es la acción que importa: uno no se queda leyendo el sitio, uno se va manejando. Un dato público mal servido es, en la práctica, un dato que no existe — y acá el costo de no encontrarlo lo paga alguien que está mal a una hora mala.
Ese detalle de tener que buscar en otra comuna terminó siendo, meses después, una decisión de arquitectura del sitio. Ya vamos a llegar ahí.
En unos días tenía la primera versión arriba: una página simple con las farmacias de turno de tu comuna, sin pop-ups y sin 47 banners antes de ver la información. Los mismos datos oficiales del MINSAL, en una interfaz que sí funciona. El plan de negocio era igual de modesto: en algún momento, algo de publicidad que pagara el dominio sin llegar a ser invasiva.
Así nació mifarmaciadeturno.cl. Este post es el recorrido completo: cómo lo construí, qué se rompió, los números reales y en qué está hoy.
El stack, en una línea
Next.js 16 (App Router) + React 19 + Vercel KV + ISR, desplegado en Vercel con un cron diario. Sin base de datos, sin backend aparte, sin autenticación. El sitio no guarda nada de quien lo visita.
Paso 1: conseguir los datos (y pelear con Cloudflare)
El MINSAL expone un endpoint público con las farmacias de turno del día. En local funcionaba perfecto. En producción, desde Vercel, empezó a fallar de formas raras.
El diagnóstico tomó varios commits de puro logging (Add detailed logging to cron and MINSAL fetch, Add more debug details to MINSAL fetch errors, subir el timeout a 60 segundos): el endpoint está detrás de Cloudflare y las IPs de datacenter no le caían bien. Desde un notebook en Chile pasaba; desde la función en Vercel, no.
La solución terminó siendo pasar el request por ScraperAPI con country_code: cl, más mover la función a la región gru1 (São Paulo, la más cercana):
const params = new URLSearchParams({
api_key: SCRAPER_API_KEY,
url: MINSAL_API_URL,
country_code: 'cl',
device_type: 'desktop',
render: 'false',
});
Dato que me costó un día entero: mi primer intento fue con un proxy vía undici, y no funciona en el runtime de Vercel. El enfoque basado en URL sí, y en todos los entornos.
Paso 2: no depender del MINSAL en cada visita
Nadie debería esperar a que responda una API externa para saber dónde hay una farmacia abierta. Entonces el sitio nunca llama al MINSAL cuando alguien entra.
Un cron en Vercel corre todos los días a las 13:00 UTC (después de que el MINSAL actualiza sus datos), y ese es el único momento en que se toca la fuente:
{
"crons": [{ "path": "/api/cron/refresh-turnos", "schedule": "0 13 * * *" }]
}
El cron descarga, valida, normaliza y guarda todo en Vercel KV ya indexado por comuna y por región. Las páginas solo leen de ahí. Alrededor de eso hay tres cosas que resultaron ser las que más tranquilidad me dan:
- Un lock para que dos ejecuciones simultáneas no se pisen, con un intervalo mínimo de 5 minutos entre refrescos.
- Fallback al día anterior: si el MINSAL no responde, el sitio muestra los datos de ayer con su fecha visible, en vez de una página vacía. Un dato viejo y etiquetado es infinitamente mejor que un "no se encontraron resultados" a las 3 AM.
- Historial de los últimos 30 cron runs guardado en KV, para poder abrir el celular y ver si el de hoy pasó o falló, sin entrar a los logs de Vercel.
Paso 3: la app que la gente usa
Geolocalización con permiso del navegador, buscador por comuna con autocompletado, y mapa con Leaflet — diferido con next/dynamic({ ssr: false }) y un IntersectionObserver, porque el mapa es lo más pesado de la página y no tiene por qué bloquear lo único que importa: el nombre, la dirección y el botón de cómo llegar.
Ese botón es el corazón del sitio, y es deliberadamente aburrido: un <a> a google.com/maps/dir/?api=1&destination=lat,lng, que en el celular abre la app de mapas con la ruta ya cargada. Si la farmacia no trae coordenadas válidas, cae a maps/search/?api=1&query=dirección, comuna, Chile, que es peor pero igual deja a la persona parada frente a un mapa con la dirección buscada. El teléfono va al lado, como tel:, y es opcional: hay farmacias sin número publicado. La ruta, en cambio, siempre está.
Acá ya tenía un producto que funcionaba. Y durante meses, casi nadie lo usó.

Así se veía Analytics de enero a junio. Ojo con el eje: el techo del gráfico son 150 usuarios. El único peak es el lanzamiento, en febrero, cuando conté la historia en LinkedIn. Después, plano. Un sitio que funcionaba perfecto y que no encontraba nadie.
Paso 4: el muro — una sola URL indexable
Este fue el gran aprendizaje del proyecto.
El sitio era una SPA muy prolija: elegías tu comuna, el estado del cliente filtraba y la lista se actualizaba. Sin recargar. Precioso. Y con una sola URL indexable en todo el sitio.
Mientras tanto, la gente no busca "farmacia de turno". Busca "farmacia de turno puerto montt". Y mi competencia tenía una URL por comuna.
Estaba compitiendo con una página contra sitios con cientos.
Paso 5: 346 páginas que sí existen
Rehice la arquitectura en cinco hitos, todos en junio:
- Fundaciones: un seed estático de comunas generado por script (
scripts/build-comunas.ts), cruzando las 222 comunas que el MINSAL había reportado alguna vez con el listado INE/SUBDERE. Resultado: 346 comunas. - Una página ISR por comuna en
/farmacia-de-turno-[comuna], pre-generada congenerateStaticParams()y revalidada cada 24 horas, igual que la cadencia real de los datos. - Farmacias cercanas cuando la comuna no tiene turno publicado, para que la página igual sirva de algo. Acá es donde aterriza la noche de la migraña: si en tu comuna no hay turno —o hay pero no te sirve— el sitio te muestra las de al lado sin que tengas que empezar la búsqueda de nuevo.
- JSON-LD (
Pharmacy,FAQPage,BreadcrumbList) + 16 páginas de región + breadcrumbs visibles en el HTML, no solo en el schema. - Sitemap dinámico, enlazado interno y contenido por comuna.
La decisión técnica que más rindió: las páginas de comuna son Server Components puros. Sin 'use client', sin onClick. Googlebot recibe el listado completo, con <article>, <address> y los enlaces a Maps y a tel: ya resueltos en el HTML, sin ejecutar una línea de JavaScript. El mapa sigue siendo cliente, pero diferido y fuera del camino crítico.
Profundidad de rastreo: Home → Región → Comuna. Las 346 comunas a dos clics de la home, ninguna huérfana.
Paso 6: la auditoría que me dijo lo que no quería escuchar
Google empezó a indexar... a medias. Un montón de páginas atascadas en "Discovered — currently not indexed".
Me senté a auditar el sitio en frío, como si fuera de otra persona, y el hallazgo fue incómodo: 181 de las 346 comunas nunca habían tenido un turno reportado por el MINSAL. Sus páginas mostraban siempre el mismo bloque, con el mismo párrafo de plantilla y la misma FAQ, cambiando el nombre de la comuna.
O sea: yo había construido exactamente el patrón de contenido programático de bajo valor que Google castiga. Un 55% del sitio.
El arreglo fue chico y contraintuitivo — quitarme páginas del índice:
...(isThin && { robots: { index: false, follow: true } }),
follow: true es la parte importante: la URL sigue siendo rastreable y pasando enlaces, y vuelve sola al índice el día que esa comuna tenga datos reales. Más: filtrar esas URLs del sitemap para no quemar crawl budget, y reemplazar el copy de plantilla por contenido derivado de datos reales por comuna (población INE, historial de días con turno).
Paso 7: el bug que más me gustó encontrar
Preparando el sitio para la revisión de AdSense encontré uno bonito: las 124 comunas que venían del listado INE traían un código INE de 5 dígitos, mientras que el MINSAL indexa todo con su propio fk_comuna. Los dos espacios de IDs nunca se cruzan.
El resultado era una página que se contradecía sola: Arica decía "no hay turno publicado" y justo abajo listaba una farmacia de turno de Arica como "cercana".
El arreglo: una sola función matchesComuna() que une por ID o por nombre normalizado, en el único punto por donde pasan las tres rutas que hacían ese join (página de comuna, buscador de la home y la API). Un guard en la función compartida, no tres parches en los callers.
De yapa, build-comunas.ts ahora se niega a achicar el seed sin un flag explícito --allow-shrink: regenerarlo solo desde farmacias.json borraba 124 páginas ya indexadas, en silencio.
Los números
Google Analytics — 1 de enero al 21 de septiembre de 2026:
- Usuarios activos: 26.484 (26.317 nuevos)
- Eventos: 125.078
- Tiempo de interacción promedio: 19 segundos
- Google orgánico: 24.632 usuarios, el 93% del total
Search Console — todo el historial del sitio, que empieza el 18 de junio de 2026:
- Clics: 25.859
- Impresiones: 541.327
- CTR: 4,78%
- Posición media: 6,8
- Móvil: 97% de los clics
- Chile: 99% de los clics

El mismo gráfico de más arriba, tres meses después. Es literalmente el mismo reporte: lo único que cambió es que el eje ya no llega a 150, llega a 1.000.
Lo que más me dice de todo esto:
- El historial de Search Console parte el 18 de junio con 9 impresiones — justo el día que subí las páginas por comuna. Filtrando por los últimos 12 meses no hay un solo dato anterior a esa fecha. La curva mes a mes: junio (13 días) 652 clics → julio 4.082 → agosto 7.977 → septiembre, con 19 días corridos, 13.148.
- Fiestas Patrias fue el test de carga. El 17 de septiembre el sitio hizo 975 clics; el 18, con casi todo cerrado, 3.491 clics y 55.341 impresiones en un día — casi tantos clics en una jornada como en todo julio. El 19: 2.916. El cron y el KV aguantaron sin tocar nada.
- Las consultas que convierten son hiperlocales:
farmacia de turno tometiene 60,9% de CTR en posición 1,8.farmacia de turno concon, 38,8%.farmacia de turno illapel, 55,5%. La genéricafarmacia de turnotiene 12.608 impresiones y 1,74% de CTR. La long tail le gana a la keyword grande, por lejos. - Las páginas por comuna son el producto:
/farmacia-de-turno-concon(1.593 clics) y/farmacia-de-turno-tome(1.563) le ganan a la home (566) por tres a uno. Son exactamente las páginas que antes no existían. - 97% móvil. Obvio en retrospectiva: nadie busca una farmacia de turno desde el escritorio.
- Aparecieron asistentes de IA como fuente de tráfico: unos 130 usuarios entre ChatGPT, Perplexity, Copilot y Gemini. Poco en volumen, pero es una fuente que hace dos años no existía.
- 19 segundos de interacción promedio. Para este producto eso es éxito: entraste, viste la farmacia, tocaste "cómo llegar" y te fuiste manejando. El mejor resultado posible es que la persona se vaya rápido.
Estado actual
- En producción en mifarmaciadeturno.cl, con el cron corriendo todos los días.
- 372 páginas: 346 comunas + 16 regiones + guías, legales y el hub de regiones.
- AdSense aprobado. El sitio pasó la revisión y hoy monetiza con slots no invasivos, que era exactamente el plan del primer día: nada que tape el resultado, porque la publicidad tapando el dato es justo lo que me hizo construir esto. Para bajarle el humo a la palabra "monetiza": el reporte del 21 de septiembre dice 260 páginas vistas, 159 impresiones de anuncio, 1 clic y 34 pesos. Esto no paga el dominio todavía.
- Sección de guías con contenido propio (cómo se asignan los turnos, qué hacer si no hay farmacia cerca) y una página "sobre" con autoría real, ambas para reforzar E-E-A-T.
- Pendiente: activar el CMP de la UE en el panel de AdSense y enriquecer el contenido de las comunas que hoy están en
noindex.
Lo que me llevo
Un producto que funciona no es un producto que se encuentra. Tuve una app correcta durante meses con tráfico cercano a cero, y el cambio no fue de producto: fue darle una URL propia a cada intención de búsqueda.
La fuente de datos es el riesgo real. Casi todo el trabajo difícil no estuvo en React, estuvo en que una API pública respondiera de manera confiable desde un datacenter en Brasil.
A veces mejorar el SEO es publicar menos. El noindex en 181 páginas fue el cambio más valioso y también el más chico del proyecto.
Si te sirve la idea, el sitio está arriba y es gratis. Y si andas construyendo algo parecido y tienes dudas de alguna parte, escríbeme.