Ir al contenido
Acecore
Índice
Diseño técnico para añadir un chat de IA de consultas a un sitio Astro

Añadir un chat de IA a un sitio web es sencillo. Lo que requiere diseño es hacerlo operable: qué puede responder la IA, a dónde debe dirigir al visitante, qué URLs se pueden mostrar y cómo se controla el coste de API.

Acecore añadió un chat de IA de consultas a un sitio estático con Astro + Cloudflare Pages. La implementación principal está en el PR que incorporó la IA de contacto y el flujo de traducción limitado al CMS. Más tarde ajustamos el renderizado seguro de enlaces Markdown en otro PR. El detalle de ese renderizado está separado en Renderizar con seguridad enlaces Markdown en respuestas de chat con IA.

Este artículo no es solo una bitácora del proyecto. Resume el diseño técnico como patrón reutilizable para otros sitios estáticos. Fuera de Astro, la idea sigue siendo la misma: separar widget cliente, límite de API, prompt y renderer.

Estructura general

La arquitectura tiene tres capas simples.

CapaResponsabilidad
Chat widgetUI, entrada, locale actual, historial mínimo y renderizado Markdown
/api/ai-contactValidación, Origin check, rate limit, prompt y llamada a OpenAI
OpenAI Responses APIGenerar respuesta desde contexto público y estado de conversación

El navegador no debe llamar directamente a OpenAI. Mantener la llamada detrás de un endpoint evita exponer claves, permite actualizar prompt y contexto desde servidor, y centraliza límites de entrada y errores.

En Astro + Cloudflare Pages, este límite puede ser una Pages Function en /api/ai-contact. En Next.js sería un Route Handler; en Hono o Express, una ruta API normal.

Mantener pequeño el contrato del endpoint

El payload debe ser estrecho.

type ContactAiRequest = {
  message: string
  locale: 'ja' | 'en' | 'zh-cn' | 'es' | 'pt' | 'fr' | 'ko' | 'de' | 'ru'
  history?: Array<{
    role: 'user' | 'assistant'
    content: string
  }>
}

type ContactAiResponse = {
  answer: string
}

Nombre, correo, teléfono, empresa y otros campos del formulario no necesitan pasar por el chat. Su rol es ayudar a decidir qué servicio ver y qué vía de contacto usar, no recoger datos personales.

El historial también debe limitarse a los últimos turnos y con tamaño máximo por mensaje. Así se evita que el prompt crezca y se controla el coste.

Controlar validación y modelo en el servidor

La Pages Function concentra el límite de seguridad y ejecución.

export async function onRequestPost({ request, env }: PagesFunction<Env>) {
  assertSameOrigin(request)
  assertRateLimit(request)

  const body = await request.json()
  const message = validateMessage(body.message)
  const locale = validateLocale(body.locale)
  const history = trimHistory(body.history)

  const prompt = buildContactPrompt({
    locale,
    message,
    history,
    siteContext: buildPublicSiteContext(locale),
  })

  const answer = await callOpenAIResponsesApi({
    apiKey: env.OPENAI_API_KEY,
    model: env.OPENAI_MODEL,
    prompt,
  })

  return Response.json({ answer })
}

El punto importante es validar y reducir la entrada antes de llamar a la IA. Mensajes largos, historial ilimitado o tráfico externo repetido vuelven inestable la operación antes de que la función aporte valor.

OPENAI_MODEL debe poder configurarse por variable de entorno. OPENAI_API_KEY permanece solo en servidor. Para el entorno de distribución y CSP, vea distribución segura con Cloudflare Pages.

Hacer explícito el contexto del sitio

Para sitios de este tamaño no hace falta empezar con una base vectorial. Un contexto estructurado con información pública suele ser suficiente.

Incluya:

  • Resumen de la empresa y servicios
  • Público objetivo, ejemplos de consulta y URLs de cada servicio
  • Preguntas frecuentes ya respondidas
  • Reglas para formulario, LINE, correo y teléfono
  • Áreas que la IA no debe afirmar, como precios, contratos o plazos
  • URLs internas por locale

La idea no es que el modelo conteste desde su conocimiento general, sino decirle qué está autorizado a decir este sitio.

function buildPublicSiteContext(locale: Locale) {
  return {
    services: [
      {
        name: 'Web production',
        summary: 'Corporate sites, recruiting sites, and landing pages',
        url: localizePath('/services/web-production/', locale),
      },
      {
        name: 'Business systems',
        summary: 'Reservation, inventory, and customer management systems',
        url: localizePath('/services/business-system/', locale),
      },
    ],
    contact: {
      form: localizePath('/contact/', locale),
      line: 'https://lin.ee/...',
      emailPolicy:
        'Show email only when the form cannot be used or follow-up is needed',
      phonePolicy: 'Show phone only for urgent confirmation',
    },
  }
}

Cuando aumenten páginas y frecuencia de actualización, esta capa puede evolucionar hacia Pagefind, CMS JSON, D1, Vectorize u otro mecanismo de recuperación.

Escribir reglas en el prompt

En un chat de consultas, el prompt debe definir límites y prohibiciones más que solo tono.

You are the contact guidance AI for this website.
Answer only from public site information.

Rules:
- Do not make firm statements about pricing, contracts, schedules, or guarantees
- Send formal consultations and estimates to the contact form
- Also suggest LINE for short questions and school-related inquiries
- Show email and phone only when the user asks for direct contact
- Use URLs that match the current locale
- If unsure, do not guess; guide the user to the form

El fallo típico es que la IA intenta ser demasiado útil y promete demasiado. Costes, fechas de entrega y garantías deben resolverse con una guía general y derivación al formulario.

Separar las rutas de contacto

El chat no reemplaza al formulario. Cada ruta tiene un papel.

RutaPapel
FAQResolver dudas comunes dentro de la página
Chat de IAOrdenar servicios, rutas de contacto y páginas relacionadas
LINEPreguntas breves, temas de escuela y verificaciones ligeras
FormularioPresupuestos, producción, alianzas y reclutamiento
Contacto directoComplementos tras el formulario o confirmación urgente

La IA conecta contenidos generales como la introducción de servicios con rutas concretas de la página de contacto. Es un patrón útil para B2B, agencias, escuelas y soporte SaaS.

No romper URLs por locale

En un sitio multilingüe no basta con responder en el idioma correcto. Las URLs también deben coincidir con el locale.

function localizePath(path: string, locale: Locale) {
  if (locale === 'ja') return path
  return `/${locale}${path}`
}

Es más estable generarlo en servidor que dejarlo solo como instrucción del prompt. La base de traducción está resumida en Cómo gestionar un blog multilingüe con Sveltia CMS.

Añadir Origin check y rate limit

/api/ai-contact es una API pública, por lo que necesita Origin check, límites de longitud, límite de historial y rate limit.

function assertSameOrigin(request: Request) {
  const origin = request.headers.get('Origin')
  if (!origin) return

  const requestUrl = new URL(request.url)
  const originUrl = new URL(origin)

  if (originUrl.host !== requestUrl.host) {
    throw new Response('Forbidden', { status: 403 })
  }
}

El rate limit por IP sirve como primer freno. En Cloudflare puede apoyarse en CF-Connecting-IP, X-Forwarded-For o CF-Ray. Para más tráfico, conviene mover el control a Cloudflare WAF, Turnstile, KV, D1 o Durable Objects. La operación CMS para actualizaciones de contenido se explica en la Guía de instalación de Sveltia CMS; la protección anti-bot de formularios y comentarios debe tratarse como otra capa.

Renderizar enlaces Markdown con lista permitida

Los enlaces son útiles, pero el Markdown no debe pasar directo a HTML. Permita solo el subconjunto necesario:

  • Párrafos
  • Listas
  • Negrita
  • Código en línea
  • Enlaces Markdown

Luego limite los destinos a rutas internas, origin actual, https://acecore.net, LINE oficial y los mailto: o tel: necesarios.

function sanitizeHref(rawHref: string, currentOrigin: string) {
  const href = rawHref.trim()

  if (href.startsWith('/')) return href
  if (href.startsWith(`${currentOrigin}/`)) return href
  if (href.startsWith('https://acecore.net/')) return href
  if (href.startsWith('https://lin.ee/')) return href
  if (href === 'mailto:[email protected]') return href
  if (href === 'tel:05088902788') return href

  return null
}

Hacer trim() es importante porque la IA puede devolver [Services]( /services/ ). Un renderer pequeño y estricto es más mantenible que implementar Markdown completo.

Probar local, preview y producción

Astro dev o preview no es idéntico al entorno de Cloudflare Pages Functions. Sin OPENAI_API_KEY, localmente conviene revisar fallback y errores de UI.

En Pages preview o producción, revise:

  • /api/ai-contact acepta POST
  • OPENAI_API_KEY y OPENAI_MODEL están configurados
  • Se rechazan solicitudes de otro Origin
  • Hay límites de longitud e historial
  • La respuesta coincide con el locale
  • Los enlaces internos usan URLs por locale
  • La IA no afirma presupuestos ni contratos
  • Correo y teléfono no se muestran por defecto
  • Markdown solo se convierte cuando la URL está permitida

No valide solo con una pregunta. Pruebe entradas largas, preguntas inesperadas, páginas en inglés, solicitudes de contacto directo y preguntas sobre precios.

Señales operativas

Después de publicar, mire:

  • Tasa de errores de API
  • Veces que se aplicó rate limit
  • Mensajes promedio por consulta
  • Transiciones a formulario y LINE
  • Casos en que la IA derivó al formulario
  • Uso por locale

Si guarda conversaciones, defina primero las reglas de privacidad. Un inicio más seguro es guardar solo eventos y errores, sin texto de mensajes.

Alcance separado

Este artículo trata solo el diseño técnico del chat de IA. La guía que pasa el contexto de la página de servicio al formulario también está implementada, y está organizada en Diseño técnico para pasar contexto del CTA de servicio al formulario de contacto.

  • Chat de IA: ordenar dudas en conversación y guiar con seguridad
  • CTA de servicio: pasar al formulario el contexto que el visitante estaba leyendo

Separarlos mejora la lectura y facilita enlaces internos posteriores.

Resumen

Para añadir un chat de IA a un sitio estático, diseñe primero el límite de API y el control de respuestas, antes de pulir la UI.

Las decisiones clave fueron:

  • Llamar a OpenAI desde Cloudflare Pages Function, no desde el navegador
  • Mantener pequeño el input y limitar historial y longitud
  • Construir contexto y URLs por locale en servidor
  • Escribir en el prompt lo que la IA no debe afirmar
  • Separar formulario, LINE y contacto directo
  • Añadir Origin check y rate limit
  • Renderizar enlaces Markdown tras trim() y con lista permitida

Un sitio estático puede tener un chat de consultas útil. Lo importante no es destacar la IA, sino ayudar al visitante a elegir el siguiente paso con seguridad.

Arquitectura de referencia

Widget

La UI de chat en Astro envía solo la pregunta, el locale actual y el historial mínimo necesario.

Function

Cloudflare Pages Function valida entradas, comprueba Origin, aplica rate limit y construye el prompt.

Model

OpenAI Responses API recibe el contexto público del sitio y el estado de la conversación.

Renderer

El cliente renderiza solo Markdown permitido y guía a enlaces internos o canales de contacto aprobados.

Responsabilidades que conviene separar

Cuando todo está mezclado

  • La API de IA se llama directamente desde el navegador
  • Contexto del sitio, API key, UI y renderizado de enlaces quedan acoplados
  • La IA puede afirmar precios, contratos o plazos con demasiada seguridad
  • Markdown y URLs pueden terminar renderizados como HTML sin control

Cuando las responsabilidades se separan

  • API keys y llamadas al modelo permanecen en el servidor
  • La información pública del sitio se gestiona como contexto explícito
  • El prompt controla el alcance de respuesta y las rutas de contacto
  • Markdown y URLs se renderizan con listas permitidas
Checklist de diseño para otros sitios
  • Completado: Definir el chat como guía de rutas, no como reemplazo total del formulario
  • Completado: Crear un límite de API en servidor y no exponer la API key al navegador
  • Completado: Limitar respuestas a información pública del sitio
  • Completado: Decidir qué no debe afirmar la IA, como precios, contratos, plazos y garantías
  • Completado: Definir cuándo usar formulario, LINE, correo y teléfono
  • Completado: Generar URLs por locale para no romper la navegación multilingüe
  • Completado: Añadir Origin check, límites de longitud, límites de historial y rate limiting
  • Completado: Hacer trim de URLs Markdown antes de validarlas con la lista permitida
Preguntas frecuentes
¿Hace falta RAG o una base vectorial para crear este chat?
Para un sitio corporativo pequeño, suele bastar con pasar al prompt un contexto estructurado a partir de páginas públicas. La búsqueda o la base vectorial pueden añadirse cuando crezcan las páginas o la frecuencia de actualización.
¿La API key de OpenAI queda expuesta en el navegador?
No. El navegador solo envía la pregunta a /api/ai-contact. Cloudflare Pages Function llama a OpenAI Responses API y gestiona la API key.
¿La IA puede devolver cualquier enlace?
No. Los enlaces se limitan a rutas internas, el origin actual, acecore.net, LINE oficial y, cuando haga falta, mailto o tel específicos. Las URLs Markdown se recortan antes de la comprobación de seguridad.

Comentarios

Cargando comentarios...

No se pueden publicar enlaces, correos ni textos promocionales.

G

Gui

CEO de Acecore. Lidera sistemas de negocio, web, bases de datos e infraestructura, calidad y adopción de IA desde la definición de problemas de negocio hasta el diseño, la puesta en marcha y la mejora posterior. Se apoya en capacidad práctica con C#/.NET y también cubre PHP/JavaScript, SQL Server/PostgreSQL/MySQL y Linux/Windows Server, diseñando requisitos, selección tecnológica, estándares de calidad y operaciones de desarrollo basadas en GitHub como un flujo coherente. Incorpora la IA generativa en procesos de desarrollo, verificación y organización de información, como una base práctica para que equipos pequeños entreguen más rápido y con mayor fiabilidad.

Definición de problemas de negocioSelección tecnológicaDiseño de sistemasC#/.NETDiseño de bases de datos/infraestructuraOperaciones de desarrollo en GitHubIA generativaDiseño de flujos de IADiseño de calidadIntegración en sitio

¿Quiere saber más sobre nuestros servicios?

Ofrecemos soporte integral en desarrollo de sistemas, diseño web, diseño gráfico y educación IT.

Artículos relacionados

Buscar artículos