Puntos Clave / Resumen Ejecutivo:
- Implementación de un plugin personalizado en Strapi v4/v5 para automatizar la indexación en Typesense.
- Uso de los Lifecycle Hooks del Document Service para interceptar mutaciones de contenido en tiempo real.
- Estrategia resiliente con reintentos y manejo de errores optimizada para entornos de producción.
Integrar un motor de búsqueda open-source y ultra-rápido como Typesense en un CMS Headless es una de las mejores decisiones para mejorar la experiencia del usuario. Aunque existen soluciones genéricas, desarrollar un plugin personalizado en Strapi ofrece control total sobre la estructura de los datos indexados.
En este artículo técnico, aprenderás a construir un plugin en Strapi v4/v5 que escucha las mutaciones de contenido y sincroniza automáticamente los documentos en las colecciones de Typesense.
Arquitectura de la Sincronización: Strapi y Typesense
El flujo de trabajo se basa en eventos. Cada vez que un usuario crea, actualiza o elimina un documento en el panel de administración de Strapi, el ** Lifecycle Engine** emite un evento interno.
Nuestro plugin interceptará estos eventos mediante la API de suscriptores y enviará las cargas útiles formateadas a la API REST de Typesense de manera asíncrona para no bloquear el hilo principal de Node.js.
Ventajas frente a Webhooks convencionales
- Manejo de estado interno: Acceso directo a las configuraciones globales de la instancia.
- Transformación de datos: Formateo personalizado de campos complejos (relaciones, componentes, Markdown).
- Resiliencia: Reintentos automáticos y registro de auditoría local en caso de fallos de red.
Configuración e Inicialización del Plugin
Para iniciar la estructura modular de nuestro plugin dentro de un proyecto existente de Strapi, ejecutamos el comando de generación oficial de la CLI:
npx strapi generate plugin strapi-provider-typesenseUna vez creado el esqueleto en la carpeta src/plugins/strapi-provider-typesense, instalamos el SDK oficial de Typesense dentro del directorio de nuestro plugin:
cd src/plugins/strapi-provider-typesense
npm install typesenseImplementación del Cliente de Typesense
Definimos un servicio dentro del plugin que inicializa el cliente del motor de búsqueda utilizando las variables de entorno de la aplicación.
Crea el archivo server/services/typesense.ts:
import { Strapi } from '@strapi/strapi';
import Typesense from 'typesense';
export default ({ strapi }: { strapi: Strapi }) => ({
getClient() {
const apiKey = process.env.TYPESENSE_API_KEY;
const host = process.env.TYPESENSE_HOST || 'localhost';
const port = parseInt(process.env.TYPESENSE_PORT || '8108', 10);
const protocol = process.env.TYPESENSE_PROTOCOL || 'http';
if (!apiKey) {
throw new Error('TYPESENSE_API_KEY no está configurada en las variables de entorno.');
}
return new Typesense.Client({
nodes: [{
host,
port,
protocol,
}],
apiKey,
connectionTimeoutSeconds: 5,
});
},
async indexDocument(collectionName: string, document: Record<string, any>) {
const client = this.getClient();
try {
return await client.collections(collectionName).documents().upsert(document);
} catch (error) {
strapi.log.error(`[Typesense Error] Error al indexar en ${collectionName}:`, error);
throw error;
}
},
async deleteDocument(collectionName: string, documentId: string) {
const client = this.getClient();
try {
return await client.collections(collectionName).documents(documentId).delete();
} catch (error) {
strapi.log.error(`[Typesense Error] Error al eliminar de ${collectionName}:`, error);
}
}
});Interceptación de Eventos mediante Subscriber Hooks
En Strapi v4/v5, la forma más limpia de reaccionar a cambios de datos globales es registrando un suscriptor en el ciclo de vida de la aplicación. Configurarás esta lógica en el archivo server/bootstrap.ts del plugin.
import { Strapi } from '@strapi/strapi';
export default async ({ strapi }: { strapi: Strapi }) => {
// Suscripción global a eventos del Document Service
strapi.db.lifecycles.subscribe({
async afterCreate(event) {
await handleSync(event, 'create');
},
async afterUpdate(event) {
await handleSync(event, 'update');
},
async afterDelete(event) {
await handleDelete(event);
},
});
};
async function handleSync(event: any, action: 'create' | 'update') {
const { model, result } = event;
const collectionName = model.singularName;
// Solo sincronizamos modelos habilitados para búsqueda
const targetModels = ['article', 'product'];
if (!targetModels.includes(collectionName)) return;
const typesenseService = strapi.plugin('strapi-provider-typesense').service('typesense');
const formattedDocument = {
id: result.id.toString(),
title: result.title || '',
slug: result.slug || '',
createdAt: Math.floor(new Date(result.createdAt).getTime() / 1000),
};
await typesenseService.indexDocument(collectionName, formattedDocument);
}
async function handleDelete(event: any) {
const { model, result } = event;
const collectionName = model.singularName;
if (result?.id) {
const typesenseService = strapi.plugin('strapi-provider-typesense').service('typesense');
await typesenseService.deleteDocument(collectionName, result.id.toString());
}
}Buenas Prácticas y Consideraciones de Producción
- Procesamiento Asíncrono: Para proyectos con alto volumen de datos, evita la ejecución directa dentro del hilo HTTP. Delega los eventos a una cola de tareas como BullMQ o Redis.
- Manejo de Colecciones y Schemas: Asegúrate de crear el esquema de la colección en Typesense antes de lanzar operaciones
upsert. - Sanitización de HTML/Markdown: Si tus tipos de contenido incluyen editores WYSIWYG, utiliza bibliotecas como
striptagsantes de enviar el texto plano al motor de búsqueda.
Referencias y Documentación
- Strapi Documentation - Documentación oficial del CMS headless para la configuración de modelos, controladores y hooks de ciclo de vida.
- Typesense Documentation - Guía completa sobre la API del motor de búsqueda de código abierto, esquemas e indexación rápida.
- TypeScript Documentation - Especificación oficial del lenguaje para elado tipado y desarrollo escalable en Node.js.
