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 |
| 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¶
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