// DIRECTORIO · REFORMAS · PRODUCTO EN PRODUCCIÓN

Protop

El tablón de los profesionales de reformas, en digital. Un directorio de toda España donde cada profesional tiene su ficha, los particulares piden presupuesto sin llamar uno a uno y las valoraciones solo cuentan si hay una factura detrás.

Protop en escritorio y móvil: el tablón de corcho de la portada y los electricistas de Santander
1.219Profesionales
357Municipios
22Oficios con página propia
1.608Páginas prerenderizadas
0 €Comisión por trabajo
1Persona detrás

EL PRODUCTO

En Obramat, donde trabajo, hay un tablón de corcho en el que los profesionales de reformas clavan su tarjeta. Funciona, pero solo lo ve quien pasa por la tienda, y nadie sabe si ese profesional trabaja bien. Protop lleva ese tablón a internet: cada profesional tiene su ficha con oficio, zona y trabajos; el particular busca por oficio y localidad o cuenta su obra una sola vez y recibe propuestas; y la reputación se construye con valoraciones que solo se publican si el cliente enseña la factura. Lo he concebido, diseñado y programado yo solo, de la marca al modelo de datos.

INFRAESTRUCTURA TÉCNICA

  • FRONT-END: React 19 y TypeScript con Vite, TanStack Query y Tailwind CSS 4 con los tokens de la marca.
  • BACKEND: Supabase: Postgres con RLS, Auth, Storage privado para facturas y funciones SQL para la lógica sensible.
  • SEO: renderizado en el servidor durante el build: cada página pública sale como HTML con su título, Open Graph, canónica y JSON-LD.
  • ENTREGA: GitHub Actions compila, prerenderiza, genera el sitemap y publica en Hostinger por FTPS.

01 / DIRECTORIO NACIONAL

1.219 profesionales desde el primer día

Un directorio vacío no le sirve a nadie. Protop arranca con fichas creadas a partir de datos públicos de OpenStreetMap, revisadas desde un panel de administración, y cada negocio puede reclamar la suya o pedir que se borre.

  • Fichas sin reclamar: se crean con una función SQL que exige indicar la fuente, y se muestran con el aviso «Ficha creada con datos públicos del negocio. ¿Es el tuyo?».
  • Reclamar: si el email confirmado de la cuenta coincide con el de la ficha, se asigna sola; si no, pasa a revisión.
  • Baja sin cuenta: cualquier negocio puede pedir que se elimine su ficha sin registrarse.
protop.es/electricistas/santander
Protop - electricistas en Santander

// Pantalla: página de oficio y localidad, con su contenido propio.

02 / CONFIANZA VERIFICABLE

Valoraciones que no se pueden inventar

Las reseñas de internet se compran. En Protop, para valorar a un profesional hay que subir la factura del trabajo, y la valoración solo se publica cuando un administrador la ha revisado.

  • Facturas privadas: se guardan en un bucket privado al que solo accede administración, con enlaces firmados que caducan a los 5 minutos.
  • Reputación automática: un trigger en Postgres recalcula la puntuación del profesional cuando se aprueba una valoración.
  • Campos blindados: permisos por columna para que el dueño de una ficha edite sus datos, pero nunca su puntuación, sus distintivos ni su posición destacada.
protop.es/profesionales/herveluz
Protop - ficha de un profesional con contacto directo

// Pantalla: ficha de profesional con contacto directo y sin intermediarios.

03 / FLUJO PRINCIPAL

Cuéntalo una vez y que te escriban ellos

El particular describe su obra una sola vez. La solicitud llega a los profesionales que trabajan ese oficio en esa zona, que responden con su propuesta, y el particular las compara en un solo sitio.

  • Reglas en la base de datos: quién ve cada solicitud y cada propuesta lo decide RLS en Postgres, no la interfaz.
  • Gratis para el particular: sin comisiones por trabajo. El modelo de negocio es un plan destacado por oficio para profesionales, preparado y apagado hasta que haya tráfico.
  • Legal: el aviso a un negocio que aún no está en Protop lo envía el propio particular desde su WhatsApp o email, nunca Protop, para cumplir la LSSI.
protop.es/solicitar-presupuesto
Protop - pedir presupuestos

// Pantalla: la solicitud de presupuesto, clavada en el tablón.

04 / MOBILE FIRST

Para quien tiene la obra delante

Quien busca un fontanero suele hacerlo desde el móvil y con prisa. Las fichas llevan el botón de llamar fijo abajo, el buscador entiende «cerca de mí» con la ubicación del teléfono y cada página carga ya pintada desde el HTML prerenderizado.

Protop - portada en móvil

// Portada

Protop - página de oficio en móvil

// Oficio y localidad

Protop - ficha de profesional en móvil

// Ficha

05 / SEO E INGENIERÍA DE ENTREGA

Una SPA que Google lee como una web estática

Protop es una aplicación de React, pero Google necesita HTML. En el build, el mismo código se renderiza en el servidor para cada oficio, cada localidad y cada ficha pública: 1.608 páginas con su contenido, su título y sus datos estructurados. En el navegador, React toma el control y todo sigue funcionando como una app.

01 · PUSHGitHub

Push a main lanza el flujo de GitHub Actions.

02 · BUILDVite + SSR

Compila la app y la versión de servidor.

03 · PRERENDER1.608 páginas

HTML por oficio, localidad y ficha, más el sitemap.

04 · DEPLOYHostinger · FTPS

El .htaccess sirve cada página antes que la app.

06 / ARQUITECTURA

Cómo encajan las piezas

  • 26migraciones SQL
  • 20tablas
  • 70políticas RLS
  • 1.608páginas prerenderizadas
  • TypeScripten todo el front
Navegadorlo que recibe el usuario
HTML prerenderizadocontenido, título, Open Graph y JSON-LD
React 19 + TypeScripttoma el control como SPA
TanStack Querycaché de datos
Tailwind CSS 4tokens de la marca
Builddonde nace el SEO
Índice de oficios por lugarqué combinaciones merecen página
Vite + build SSRel mismo código en el servidor
prerender.mjsescribe un HTML por ruta
Sitemapgenerado con las rutas reales
Supabasedatos y reglas
Postgres con RLSquién ve cada solicitud
Funciones SQLreputación, reclamar fichas
Storage privadofacturas con enlaces de 5 min
Authcuentas de particulares y profesionales
Entregadel commit a producción
GitHub Actionsbuild completo en cada push
FTPSa Hostinger
.htaccesssirve el HTML de cada ruta antes que la app

// Decisiones técnicas y por qué

ProblemaUna SPA de React es invisible para Google.
DecisiónUn segundo build en modo SSR con el mismo código y un script que renderiza cada ruta pública a HTML.
En vez deMigrar todo a Next.js o mantener páginas estáticas aparte.
ProblemaMiles de combinaciones de oficio y localidad, muchas vacías.
DecisiónUna página de oficio y lugar solo se indexa si tiene al menos 2 fichas (MINIMO_FICHAS_PARA_INDEXAR).
En vez dePublicarlas todas y que Google las trate como contenido pobre.
ProblemaEl dueño de una ficha no debe tocar su puntuación.
DecisiónPermisos de Postgres por columna y reputación recalculada por un trigger security definer.
En vez deEsconder los campos en la interfaz, que se salta con una petición a mano.
ProblemaSi Supabase falla durante el build, no puede caer la web.
DecisiónEl prerender captura el error: esas rutas se quedan como SPA y el despliegue sigue.
En vez deUn build que falla entero por una consulta.
ProblemaLas facturas son datos sensibles.
DecisiónBucket privado, solo legible por administración, con enlaces firmados que caducan a los 5 minutos.
En vez deUn bucket público con URLs difíciles de adivinar.

07 / EN EL CÓDIGO

Fragmentos reales del repositorio

Prerender: SEO sin cambiar de framework

Después del build, el script importa la versión de servidor de la app y la renderiza para cada ruta: páginas fijas, una por oficio, cada oficio en cada lugar que merece página y cada ficha real. El resultado se inyecta en la plantilla con su <head> completo.

scripts/prerender.mjsJavaScript
const { render, RUTAS_ESTATICAS, OFICIOS_SEO, lugarIndexable } = await import(
  pathToFileURL(ruta('dist-servidor/entry-servidor.js')).href
)

// Páginas con datos de Supabase: el directorio, una por oficio y cada ficha real.
// Si Supabase falla en una, esa ruta se queda como SPA: nunca hace fallar el build.
const conDatos = ['/profesionales', ...OFICIOS_SEO.map((oficio) => `/${oficio.url}`)]
for (const oficio of OFICIOS_SEO) {
  for (const lugar of Object.keys(indice?.oficios[oficio.categoria]?.lugares ?? {})) {
    if (lugarIndexable(indice, oficio.categoria, lugar)) conDatos.push(`/${oficio.url}/${lugar}`)
  }
}
try {
  const fichas = await consultarTodo(leerEnv(), 'profesionales', 'select=slug&es_demo=eq.false')
  conDatos.push(...fichas.map((ficha) => `/profesionales/${ficha.slug}`))
} catch (error) {
  console.warn(`prerender: sin la lista de fichas (${error.message}); se quedan como SPA.`)
}
Campos blindados y reputación automática

El dueño de una ficha puede editar sus datos, pero Postgres solo le concede permiso sobre esas columnas: la puntuación, los distintivos y la posición destacada quedan fuera. La reputación la recalcula un trigger cada vez que se aprueba, cambia o borra una valoración.

supabase/migrations/…_fichas_sin_reclamar.sqlSQL
-- El dueño de una ficha solo puede escribir los campos de su ficha.
revoke insert, update on table profesionales from anon, authenticated;
grant update (
  nombre_comercial, categoria_id, especialidades, localidad,
  descripcion_breve, descripcion, foto_perfil_url, fotos_trabajos, contacto
) on table profesionales to authenticated;

-- …_valoraciones_y_facturas.sql
create trigger valoraciones_recalcular
  after insert or update or delete on valoraciones
  for each row execute function public.trg_valoraciones_recalcular();

RESUMEN DEL IMPACTO

Protop nace de algo que veo cada día en la tienda donde trabajo y lo convierte en un producto completo: directorio nacional con datos abiertos, un flujo de presupuestos con las reglas de acceso en la base de datos, valoraciones verificadas por factura y SEO técnico a escala de miles de páginas. Muestra cómo trabajo: detectar un problema real, entender a las dos partes (quien busca y quien ofrece) y llevarlo hasta algo que funciona.