Design técnico para adicionar um chat de IA de contato a um site Astro
Índice
- Estrutura geral
- Mantenha pequeno o contrato do endpoint
- Controle validação e modelo no servidor
- Torne o contexto do site explícito
- Escreva regras no prompt
- Separe as rotas de contato
- Preserve URLs por locale
- Adicione Origin check e rate limit
- Renderize links Markdown com lista permitida
- Teste local, preview e produção
- Métricas operacionais
- Escopo separado
- Resumo
Adicionar um chat de IA a um site é simples. O que precisa de desenho é a operação: até onde a IA pode responder, para onde deve guiar visitantes, quais URLs podem aparecer e como controlar custo de API.
No site da Acecore, adicionamos um chat de IA de contato a uma base estática Astro + Cloudflare Pages. A implementação principal está no PR que adicionou a IA de contato e o fluxo de tradução limitado ao CMS. Depois ajustamos a renderização segura de links Markdown em outro PR. Os detalhes desse renderizador estão em Renderizar links Markdown com segurança em respostas de chat com IA.
Este artigo organiza o design como um padrão reutilizável para outros sites estáticos. Mesmo fora de Astro, a ideia é a mesma: separar widget cliente, fronteira de API, prompt e renderer.
Estrutura geral
| Camada | Responsabilidade |
|---|---|
| Chat widget | UI, entrada, locale atual, histórico mínimo e renderização Markdown |
/api/ai-contact | Validação, Origin check, rate limit, prompt e chamada à OpenAI |
| OpenAI Responses API | Gerar resposta com contexto público e estado da conversa |
O navegador não deve chamar a OpenAI diretamente. Manter a chamada atrás de um endpoint evita exposição de chave, permite atualizar prompt e contexto no servidor e centraliza limites e erros.
Em Astro + Cloudflare Pages, a fronteira pode ser uma Pages Function em /api/ai-contact. Em Next.js seria um Route Handler; em Hono ou Express, uma rota API normal.
Mantenha pequeno o contrato do endpoint
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
}
Nome, e-mail, telefone, empresa e campos detalhados do formulário não precisam passar pelo chat. O papel do chat é ajudar a escolher serviço e rota de contato, não coletar dados pessoais.
Também limite o histórico a poucos turnos recentes e defina tamanho máximo por mensagem. Isso reduz prompt e custo.
Controle validação e modelo no servidor
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 })
}
O ponto é reduzir e validar entrada antes de chamar a IA. Mensagens longas, histórico ilimitado e chamadas de outros sites podem desestabilizar a operação.
OPENAI_MODEL deve vir de variável de ambiente, e OPENAI_API_KEY deve ficar apenas no servidor. Para distribuição e CSP, veja segurança com Cloudflare Pages.
Torne o contexto do site explícito
Para esse porte de site, não é necessário começar com banco vetorial. Um contexto estruturado com informações públicas já é útil.
Inclua resumo da empresa, serviços, exemplos de consulta, URLs, FAQ, regras de contato, áreas que a IA não deve afirmar e URLs internas por locale.
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',
},
}
}
O objetivo não é deixar o modelo responder com conhecimento geral, mas informar o que este site pode dizer. Se o site crescer, use Pagefind, CMS JSON, D1, Vectorize ou outra camada de recuperação.
Escreva regras no prompt
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
Um erro comum é a IA tentar ajudar demais e prometer demais. Preços, prazos e garantias devem receber orientação geral e encaminhamento ao formulário.
Separe as rotas de contato
| Rota | Papel |
|---|---|
| FAQ | Resolver dúvidas comuns na própria página |
| Chat de IA | Organizar serviços, rotas de contato e páginas relacionadas |
| LINE | Perguntas curtas, assuntos de escola e confirmações leves |
| Formulário | Orçamentos, produção, parcerias e recrutamento |
| Contato direto | Complemento após formulário ou confirmação urgente |
A IA conecta conteúdos gerais como introdução de serviços às rotas da página de contato. O padrão funciona para B2B, agências, escolas e suporte SaaS.
Preserve URLs por locale
Em um site multilíngue, não basta responder no idioma correto. A URL também precisa combinar com o locale.
function localizePath(path: string, locale: Locale) {
if (locale === 'ja') return path
return `/${locale}${path}`
}
Gerar isso no servidor é mais confiável do que apenas pedir ao modelo. A base da tradução está em Como operar um blog multilíngue com Sveltia CMS.
Adicione Origin check e 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 })
}
}
Rate limit por IP é um primeiro freio. Em Cloudflare, use CF-Connecting-IP, X-Forwarded-For ou CF-Ray. Para mais tráfego, considere Cloudflare WAF, Turnstile, KV, D1 ou Durable Objects. A operação CMS para atualização de conteúdo está no Guia de instalação do Sveltia CMS; proteção anti-bot de formulários e comentários deve ser tratada como outra camada.
Renderize links Markdown com lista permitida
Permita apenas parágrafos, listas, negrito, código inline e links Markdown. Depois restrinja destinos a caminhos internos, origin atual, https://acecore.net, LINE oficial e mailto: ou tel: necessários.
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
}
O trim() é essencial porque a IA pode gerar [Services]( /services/ ). Um renderer pequeno e rígido é mais fácil de manter.
Teste local, preview e produção
Astro dev ou preview não é igual ao ambiente de Cloudflare Pages Functions. Sem OPENAI_API_KEY, teste fallback e erros de UI.
Em preview ou produção, confirme POST em /api/ai-contact, variáveis OPENAI_API_KEY e OPENAI_MODEL, rejeição de outro Origin, limites de entrada, respostas no locale certo, URLs localizadas, ausência de afirmações sobre orçamento ou contrato, e links Markdown apenas quando permitidos.
Também teste entradas longas, perguntas inesperadas, páginas em inglês, pedidos de contato direto e perguntas de preço.
Métricas operacionais
Acompanhe taxa de erro da API, ocorrências de rate limit, média de mensagens por consulta, cliques para formulário e LINE, casos encaminhados ao formulário e uso por locale.
Se salvar conversas, defina privacidade antes. Um começo mais seguro é registrar eventos e erros sem texto das mensagens.
Escopo separado
Este artigo trata apenas do design técnico do chat de IA. A navegação que repassa o contexto da página de serviço para o formulário também está implementada, e está organizada em Design técnico para passar contexto do CTA de serviço para o formulário de contato.
- Chat de IA: organizar dúvidas em conversa e guiar com segurança
- CTA de serviço: passar ao formulário o contexto que o visitante estava lendo
Separar os temas melhora a leitura e facilita links internos.
Resumo
Ao adicionar um chat de IA a um site estático, desenhe primeiro a fronteira de API e o controle de respostas.
As decisões principais foram chamar OpenAI pela Cloudflare Pages Function, manter input pequeno, montar contexto e URLs no servidor, escrever limites no prompt, separar formulário, LINE e contato direto, adicionar Origin check e rate limit, e renderizar links Markdown com trim() e lista permitida.
Sites estáticos podem ter chats de IA úteis. O centro não é destacar a IA, mas ajudar o visitante a escolher o próximo passo com segurança.
Arquitetura de referência
Widget
A UI de chat em Astro envia apenas a pergunta, o locale atual e o histórico mínimo.
Function
A Cloudflare Pages Function valida entrada, verifica Origin, aplica rate limit e monta o prompt.
Model
A OpenAI Responses API recebe o contexto público do site e o estado da conversa.
Renderer
O cliente renderiza apenas Markdown permitido e leva a links internos ou canais aprovados.
Tudo misturado
- Chamar a API de IA diretamente do navegador
- Misturar contexto do site, API key, UI e renderização de links
- Permitir que a IA afirme preços, contratos ou prazos
- Renderizar Markdown e URLs diretamente como HTML
Responsabilidades separadas
- API keys e chamadas ao modelo ficam no servidor
- Informações públicas do site viram contexto explícito
- O prompt controla escopo de resposta e rotas de contato
- Markdown e URLs passam por listas permitidas
- Concluído: Definir o chat como orientação de rota, não como substituto total do formulário
- Concluído: Criar uma fronteira de API no servidor e não expor a API key ao navegador
- Concluído: Restringir respostas a informações públicas do site
- Concluído: Decidir o que a IA não deve afirmar, como preço, contrato, prazo e garantia
- Concluído: Definir quando usar formulário, LINE, e-mail e telefone
- Concluído: Gerar URLs por locale para preservar a navegação multilíngue
- Concluído: Adicionar Origin check, limites de tamanho, limites de histórico e rate limiting
- Concluído: Aplicar trim nas URLs Markdown antes de validar pela lista permitida
Preciso de RAG ou banco vetorial para criar esse chat?
A API key da OpenAI aparece no navegador?
A IA pode retornar qualquer link?
Comentários
Gui
CEO da Acecore. Lidera sistemas de negócio, web, bancos de dados e infraestrutura, qualidade e adoção de IA da definição de problemas de negócio ao design, implantação e melhoria após o lançamento. Parte de capacidade prática em C#/.NET e também cobre PHP/JavaScript, SQL Server/PostgreSQL/MySQL e Linux/Windows Server, desenhando requisitos, escolhas tecnológicas, padrões de qualidade e operações de desenvolvimento baseadas em GitHub como um fluxo único. Incorpora IA generativa a processos de desenvolvimento, verificação e organização de informações, como uma base prática para que equipes pequenas entreguem mais rápido e com maior confiabilidade.
Quer saber mais sobre nossos serviços?
Oferecemos suporte abrangente em desenvolvimento de sistemas, design web, design gráfico e educação em TI.
Artigos relacionados
Como projetar um site Astro + Cloudflare que cresce por funcionalidade 7 de junho de 2026 às 19:00
Como adicionar comentários a um blog Astro usando apenas Cloudflare 7 de junho de 2026 às 18:00