Bỏ qua

Case Study: Landing Page

Tổng quan

Xây dựng landing page marketing đơn giản với Light Stack.

Yêu cầu

  • Static content với CMS-like editing
  • Contact form với email notification
  • SEO optimized
  • Fast loading (< 1 second)
  • Analytics integration

Kết quả mong đợi

  • Landing page hoàn chỉnh
  • Form submission hoạt động
  • Deployed trên Cloudflare

Kiến trúc

┌─────────────────────────────────────────────────────────────┐
│                   LANDING PAGE ARCHITECTURE                  │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Visitors                                                    │
│    │                                                         │
│    ▼                                                         │
│  Cloudflare Edge (CDN)                                      │
│    │                                                         │
│    ├──▶ Static Assets (HTML, CSS, JS, Images)              │
│    │    └── Cached globally                                 │
│    │                                                         │
│    └──▶ Form Submission                                     │
│         │                                                    │
│         ▼                                                    │
│    Cloudflare Worker                                        │
│         │                                                    │
│         ├──▶ Validate input                                 │
│         ├──▶ Store in Supabase                              │
│         └──▶ Send email (SendGrid/Resend)                   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Stack được sử dụng

Component Technology Lý do
Frontend Next.js (SSG) SEO, performance
Hosting Cloudflare Workers Edge, fast
Form Backend Cloudflare Worker Simple API
Database Supabase Store submissions
Email SendGrid/Resend Notifications

Implementation Steps

Step 1: Project Setup

# Create Next.js project
npx create-next-app@latest landing-page --typescript --tailwind --app

cd landing-page

# Install dependencies
npm install @supabase/supabase-js

# Setup for Cloudflare
npm install @opennextjs/cloudflare

Step 2: Landing Page Structure

landing-page/
├── app/
│   ├── page.tsx          # Main landing page
│   ├── layout.tsx        # Root layout with meta
│   └── api/
│       └── contact/
│           └── route.ts  # Form handler (optional)
├── components/
│   ├── Hero.tsx
│   ├── Features.tsx
│   ├── Testimonials.tsx
│   ├── ContactForm.tsx
│   └── Footer.tsx
├── lib/
│   └── supabase.ts
└── workers/
    └── contact-form/     # Cloudflare Worker
        ├── src/
        │   └── index.ts
        └── wrangler.toml

Step 3: Landing Page Components

// app/page.tsx
import { Hero } from '@/components/Hero';
import { Features } from '@/components/Features';
import { Testimonials } from '@/components/Testimonials';
import { ContactForm } from '@/components/ContactForm';
import { Footer } from '@/components/Footer';

export const metadata = {
  title: 'Our Product - Best Solution for Your Needs',
  description: 'Discover how our product can help you achieve...',
  openGraph: {
    title: 'Our Product',
    description: 'Best Solution for Your Needs',
    images: ['/og-image.png'],
  },
};

export default function LandingPage() {
  return (
    <main>
      <Hero />
      <Features />
      <Testimonials />
      <ContactForm />
      <Footer />
    </main>
  );
}
// components/Hero.tsx
export function Hero() {
  return (
    <section className="py-20 bg-gradient-to-r from-blue-600 to-purple-600">
      <div className="max-w-6xl mx-auto px-4 text-center text-white">
        <h1 className="text-5xl font-bold mb-6">
          Build Faster with Light Stack
        </h1>
        <p className="text-xl mb-8 opacity-90">
          The modern stack for PoC and MVP development
        </p>
        <div className="flex gap-4 justify-center">
          <a
            href="#contact"
            className="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold"
          >
            Get Started
          </a>
          <a
            href="#features"
            className="border-2 border-white px-8 py-3 rounded-lg font-semibold"
          >
            Learn More
          </a>
        </div>
      </div>
    </section>
  );
}

Step 4: Contact Form

// components/ContactForm.tsx
'use client';

import { useState } from 'react';

export function ContactForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [message, setMessage] = useState('');

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setStatus('loading');

    const formData = new FormData(e.currentTarget);

    try {
      const response = await fetch('https://contact.your-domain.workers.dev/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: formData.get('name'),
          email: formData.get('email'),
          message: formData.get('message'),
        }),
      });

      if (!response.ok) throw new Error('Submit failed');

      setStatus('success');
      setMessage('Thank you! We will contact you soon.');
    } catch (error) {
      setStatus('error');
      setMessage('Something went wrong. Please try again.');
    }
  };

  return (
    <section id="contact" className="py-20 bg-gray-50">
      <div className="max-w-xl mx-auto px-4">
        <h2 className="text-3xl font-bold text-center mb-8">
          Get in Touch
        </h2>

        {status === 'success' ? (
          <div className="bg-green-100 text-green-700 p-4 rounded-lg text-center">
            {message}
          </div>
        ) : (
          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label className="block text-sm font-medium mb-1">Name</label>
              <input
                name="name"
                type="text"
                required
                className="w-full px-4 py-2 border rounded-lg"
              />
            </div>

            <div>
              <label className="block text-sm font-medium mb-1">Email</label>
              <input
                name="email"
                type="email"
                required
                className="w-full px-4 py-2 border rounded-lg"
              />
            </div>

            <div>
              <label className="block text-sm font-medium mb-1">Message</label>
              <textarea
                name="message"
                rows={4}
                required
                className="w-full px-4 py-2 border rounded-lg"
              />
            </div>

            <button
              type="submit"
              disabled={status === 'loading'}
              className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50"
            >
              {status === 'loading' ? 'Sending...' : 'Send Message'}
            </button>

            {status === 'error' && (
              <p className="text-red-600 text-center">{message}</p>
            )}
          </form>
        )}
      </div>
    </section>
  );
}

Step 5: Contact Form Worker

// workers/contact-form/src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createClient } from '@supabase/supabase-js';

interface Env {
  SUPABASE_URL: string;
  SUPABASE_SERVICE_KEY: string;
  SENDGRID_API_KEY: string;
  NOTIFICATION_EMAIL: string;
}

interface ContactSubmission {
  name: string;
  email: string;
  message: string;
}

const app = new Hono<{ Bindings: Env }>();

app.use('*', cors({
  origin: ['https://your-landing-page.com'],
  allowMethods: ['POST'],
}));

app.post('/submit', async (c) => {
  try {
    const body = await c.req.json<ContactSubmission>();

    // Validate input
    if (!body.name || !body.email || !body.message) {
      return c.json({ error: 'All fields required' }, 400);
    }

    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) {
      return c.json({ error: 'Invalid email' }, 400);
    }

    // Store in Supabase
    const supabase = createClient(
      c.env.SUPABASE_URL,
      c.env.SUPABASE_SERVICE_KEY
    );

    const { error: dbError } = await supabase
      .from('contact_submissions')
      .insert({
        name: body.name,
        email: body.email,
        message: body.message,
      });

    if (dbError) {
      console.error('DB Error:', dbError);
      throw new Error('Database error');
    }

    // Send notification email
    await sendEmail(c.env, body);

    return c.json({ success: true });
  } catch (error) {
    console.error('Error:', error);
    return c.json({ error: 'Server error' }, 500);
  }
});

async function sendEmail(env: Env, data: ContactSubmission) {
  await fetch('https://api.sendgrid.com/v3/mail/send', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.SENDGRID_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      personalizations: [
        { to: [{ email: env.NOTIFICATION_EMAIL }] }
      ],
      from: { email: 'noreply@your-domain.com' },
      subject: `New Contact: ${data.name}`,
      content: [
        {
          type: 'text/plain',
          value: `Name: ${data.name}\nEmail: ${data.email}\n\nMessage:\n${data.message}`
        }
      ],
    }),
  });
}

export default app;

Step 6: Database Schema

-- supabase/migrations/001_contact_submissions.sql
CREATE TABLE public.contact_submissions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  message TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  responded_at TIMESTAMPTZ,
  notes TEXT
);

-- RLS - only service role can access
ALTER TABLE public.contact_submissions ENABLE ROW LEVEL SECURITY;

-- No policies = service role only

Deployment

Deploy Landing Page

# Build for Cloudflare
npm run build

# Deploy
npx wrangler deploy

Deploy Contact Worker

cd workers/contact-form

# Set secrets
wrangler secret put SUPABASE_URL
wrangler secret put SUPABASE_SERVICE_KEY
wrangler secret put SENDGRID_API_KEY
wrangler secret put NOTIFICATION_EMAIL

# Deploy
wrangler deploy

Performance Tips

Image Optimization

// Use next/image for automatic optimization
import Image from 'next/image';

<Image
  src="/hero-image.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // Load immediately for LCP
/>

Static Generation

// app/page.tsx
// Default is SSG in Next.js App Router

// For dynamic data (testimonials from CMS):
export const revalidate = 3600; // Revalidate every hour

Kết quả

Metrics đạt được

  • First Contentful Paint: < 1s
  • Largest Contentful Paint: < 2.5s
  • Time to Interactive: < 3s
  • Lighthouse Score: 95+

Chi phí

  • Supabase: Free tier (minimal data)
  • Cloudflare: Free tier (static + 100k worker requests)
  • SendGrid: Free tier (100 emails/day)
  • Total: $0/month

Lessons Learned

What Worked

  • SSG cho performance tốt nhất
  • Worker cho form handler đơn giản, reliable
  • Supabase cho storage mà không cần setup backend

Challenges

  • CORS configuration cần chính xác
  • Cold start của Worker có thể delay response đầu tiên
  • Email deliverability cần proper domain setup

Improvements

  • Add honeypot for spam prevention
  • Implement rate limiting
  • Add reCAPTCHA for high-traffic sites

Tài liệu tham khảo