Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/santiagodc8/tu_perfil.net/llms.txt

Use this file to discover all available pages before exploring further.

Readers can reach the editorial team through the contact page at /contacto.

URL

/contacto

Form fields

The contact form collects three required fields:
FieldTypeConstraints
nametextRequired
emailemailRequired, must be a valid email address
messagetextareaRequired, maximum 5,000 characters
The form is a React client component ("use client") that manages its own state and submits via fetch:
src/app/(public)/contacto/page.tsx
async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  const res = await fetch("/api/contact", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: name.trim(),
      email: email.trim(),
      message: message.trim(),
    }),
  });
  // ...
}
While the form is submitting, the submit button shows “Enviando…” and is disabled. On success, the form is replaced with a confirmation screen:
Mensaje enviado — Gracias por contactarnos. Te responderemos lo antes posible.
On error, a red error banner appears above the form with the API’s error message.

API endpoint

POST /api/contact
Content-Type: application/json
Request body:
{
  "name": "María García",
  "email": "[email protected]",
  "message": "Quisiera enviar una denuncia sobre..."
}
Success response:
{ "ok": true }
Error responses:
StatusCondition
400Missing fields, invalid email format, or message exceeds 5,000 characters
429Rate limit exceeded
500Database error

Rate limiting

The /api/contact endpoint enforces a rate limit of 5 messages per IP address per hour:
src/app/api/contact/route.ts
const { allowed } = checkRateLimit(getClientIp(request), "contact", {
  limit: 5,
  windowSeconds: 3600,
});
if (!allowed) {
  return NextResponse.json(
    { error: "Demasiados mensajes. Intenta de nuevo más tarde." },
    { status: 429 }
  );
}
If the limit is exceeded, the API returns HTTP 429 and the form displays the rate-limit error to the reader.

How a message is processed

1

Server-side validation

The API validates that all three fields are present, the email matches a basic regex, and the message does not exceed 5,000 characters. Invalid requests are rejected immediately with HTTP 400.
2

Saved to the database

Valid messages are inserted into the contacts table using the Supabase admin client. This step is critical — if it fails, the API returns HTTP 500 and no email is sent.
src/app/api/contact/route.ts
const supabase = createAdminClient();
const { error: dbError } = await supabase
  .from("contacts")
  .insert({ name, email, message });
3

Email notification sent (non-blocking)

After saving to the database, the API sends an HTML email notification to the admin address via Resend. If the email fails (e.g., missing API key, Resend error), the failure is logged but does not affect the HTTP response — the reader still receives a success confirmation.
src/app/api/contact/route.ts
const { error: emailError } = await resend.emails.send({
  from: fromEmail,
  to: adminEmail,
  subject: "Nuevo mensaje de contacto — TuPerfil.net",
  html: buildEmailHtml(name, email, message, receivedAt),
});
The email includes the sender’s name, email (as a mailto: link), message body, and a direct “Responder” button.
Email notifications require both RESEND_API_KEY and ADMIN_EMAIL environment variables to be set. If either is missing, messages are still saved to the database but no email is sent.

The Contact interface

The Contact TypeScript type is defined in src/types/index.ts:
src/types/index.ts
export interface Contact {
  id: string;
  name: string;
  email: string;
  message: string;
  read: boolean;        // Marked as read by admin
  created_at: string;
}

Admin inbox

All submitted messages are visible in the admin panel at /admin/mensajes. From there, an admin can:
  • View each message’s name, email, and full message text.
  • Mark messages as read (read = true).
  • Reply directly by clicking the sender’s email address.
Unread messages are highlighted in the admin inbox. The notification email sent via Resend also includes a link to the admin panel: /admin/mensajes.
Because messages are always saved to the database first, you will never lose a submission even if Resend is temporarily unavailable.

Build docs developers (and LLMs) love