KiraWebs

Next.jsNotion APIRedis

Kirawebs.com is a marketing site for a web-development agency, built around an interactive multi-step form that lets a prospective client simulate their website's cost before ever talking to a salesperson — turning "how much would this cost?" into a self-serve answer instead of a quote request.

Architecture and Tech Stack

Core Architecture

  • Framework: Next.js (App Router) + React
  • UI: Tailwind CSS + shadcn/ui components
  • Validation: Zod on every contact-form submission
  • Rate limiting: Redis, fixed-window limiter (3 messages / minute per IP)
  • Data storage: Notion API — contact submissions are written straight into a Notion database, no separate backend needed
  • Deployment: Appwrite

Contact Form Flow

The whole submission path runs inside one Server Action: validate first, rate-limit second, persist third — each step can reject early with a specific, user-facing message instead of a generic error.

Key Features

Features at a Glance

Interactive Website Cost Simulator

A guided multi-step form — templates, sections, services, then a summary — walks a visitor through configuring their site and returns a live cost estimate, replacing a back-and-forth quote request with something the visitor can explore on their own.

Image

Image

Contact Form with Validation and Rate Limiting

Email + message are Zod-validated client- and server-side before anything is written; a Redis fixed-window limiter caps submissions at 3 per minute per IP, so the same abuse pattern that would spam a mailbox instead gets a clear "please wait" response.

Image

Image

Technical Highlights

Notion as the contact-form database

Instead of standing up a database for a handful of contact submissions, sendEmail writes each one straight into a Notion database via the Notion API — zero infra to run for what's genuinely a low-volume form.

const response = await notion.pages.create({
  parent: { database_id: DATABASE_IDS.contact! },
  properties: {
    email: { title: [{ text: { content: email } }] },
    description: { rich_text: [{ text: { content: description } }] },
  },
})

Rate limiting checked before validation

The Redis fixed-window limiter runs first in the Server Action, before Zod even looks at the payload — an abusive client is turned away before the app spends any work parsing or validating its input.

const ratelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.fixedWindow(3, "60 s"),
})

export async function sendEmail(prevState: unknown, formData: FormData) {
  const clientIp = formData.get("clientIp") as string
  const result = await ratelimit.limit(clientIp)

  if (!result.success) {
    return {
      success: false,
      title: "Submission limit reached",
      details: "Please wait a moment before trying again.",
    }
  }
  // ...Zod validation, then notion.pages.create
}

Every rejection has a specific, user-facing reason

Rate-limited, invalid input, and a failed Notion write each return their own title + details pair instead of a single generic error — the visitor always knows exactly what to fix or wait for.

Project Structure

src/
├── app/
│   ├── page.tsx                 # Single-page marketing site
│   ├── layout.tsx
│   └── actions/
│       └── contact.ts           # Server Action: rate limit → Zod → Notion write
├── components/
│   ├── home/
│   │   ├── hero-section.tsx
│   │   ├── services-section.tsx
│   │   ├── compare-section.tsx
│   │   ├── price-simulator-section.tsx
│   │   ├── price-simulator/      # Multi-step cost simulator (templates → sections → services → summary)
│   │   ├── steps-section.tsx
│   │   ├── faq-section.tsx
│   │   └── contact-section.tsx
│   │       └── contact/
│   │           └── contact-form.tsx
│   ├── layout/                   # Header, footer
│   └── ui/                       # shadcn/ui primitives

Impact and Scalability

  • Self-serve cost estimation: replaces manual quote requests with an instant, explorable simulator
  • Zero-infra data capture: contact submissions land directly in Notion, no database to provision or maintain
  • Abuse-resistant by default: Redis rate limiting protects the form without needing a separate service
  • Deployed and live: running on Appwrite at kirawebs.appwrite.network

Notes

Built with Next.js, Tailwind CSS, Redis, and the Notion API, deployed on Appwrite. Code is public on GitHub.

🖼️ IMAGE PLACEHOLDER — Notion database view showing stored contact submissions


© 2026 Felipe Giraldo