Vercel คือ platform สำหรับ deploy frontend และ serverless functions ที่ทำงานร่วมกับ GitHub ได้อย่างราบรื่น
Why Vercel?
Core Features
Vercel ให้บริการ:
├─ Static Site Hosting
├─ Serverless Functions
├─ Edge Network (CDN)
├─ Automatic Deployments
├─ Performance Analytics
└─ Custom Domains
ข้อดีสำหรับ Half-Stack:
✅ Deploy จาก GitHub อัตโนมัติ
✅ Zero-config deployment
✅ Global CDN ทั่วโลก
✅ Serverless functions ฟรี
✅ เหมาะกับ Next.js
✅ Preview deployments
Free Tier Benefits
✅ ฟรี:
├─ 100GB Bandwidth/month
├─ 100 Serverless Function invocations
├─ Unlimited static sites
├─ Unlimited projects
├─ Custom domains
├─ SSL certificates
└─ GitHub integration
❌ ข้อจำกัด:
├─ Function execution timeout: 10 seconds
├─ Function memory: 1GB
├─ Build timeout: 10 minutes
└─ No team collaboration
Getting Started
1. Setup Project
# Create Next.js project
npx create-next-app@latest shantilink-dashboard
cd shantilink-dashboard
# Install dependencies
npm install @supabase/supabase-js
npm install @supabase/auth-helpers-nextjs
npm install tailwindcss
npm install lucide-react
2. Configure for Supabase
// lib/supabase.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
// Client-side Supabase client
export const createClientComponentClient = () =>
createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: true,
},
})
3. Create Serverless API
// pages/api/orders.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
try {
const { customerId, items, totalAmount } = req.body
// Validate input
if (!customerId || !items || items.length === 0) {
return res.status(400).json({ error: 'Invalid order data' })
}
// Create order
const orderId = `ORD${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`
const { data: order, error } = await supabase
.from('orders')
.insert({
order_id: orderId,
customer_id: customerId,
items: items,
total_amount: totalAmount,
status: 'pending'
})
.select()
.single()
if (error) throw error
// Trigger n8n workflow
await fetch('https://your-n8n-instance.com/webhook/new-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order, items })
})
res.status(200).json({ success: true, order })
} catch (error) {
console.error('Order API error:', error)
res.status(500).json({ error: 'Internal server error' })
}
}
4. Create Frontend Dashboard
// pages/dashboard.js
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import { Package, Users, TrendingUp, Clock } from 'lucide-react'
export default function Dashboard() {
const [stats, setStats] = useState({
totalOrders: 0,
totalCustomers: 0,
todayRevenue: 0,
pendingOrders: 0
})
const [orders, setOrders] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchStats()
fetchOrders()
// Real-time subscription
const subscription = supabase
.channel('orders')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'orders' },
() => {
fetchStats()
fetchOrders()
}
)
.subscribe()
return () => subscription.unsubscribe()
}, [])
async function fetchStats() {
try {
const today = new Date().toISOString().split('T')[0]
const [ordersCount, customersCount, todayOrders, pendingOrders] = await Promise.all([
supabase.from('orders').select('id', { count: 'exact' }),
supabase.from('customers').select('id', { count: 'exact' }),
supabase.from('orders').select('total_amount').eq('created_at', `gte.${today}`),
supabase.from('orders').select('id').eq('status', 'pending', { count: 'exact' })
])
const todayRevenue = todayOrders.data?.reduce((sum, order) => sum + order.total_amount, 0) || 0
setStats({
totalOrders: ordersCount.count || 0,
totalCustomers: customersCount.count || 0,
todayRevenue,
pendingOrders: pendingOrders.count || 0
})
} catch (error) {
console.error('Stats error:', error)
} finally {
setLoading(false)
}
}
async function fetchOrders() {
try {
const { data } = await supabase
.from('orders')
.select(`
*,
customers (display_name, phone)
`)
.order('created_at', { ascending: false })
.limit(10)
setOrders(data || [])
} catch (error) {
console.error('Orders error:', error)
}
}
if (loading) {
return <div className="flex justify-center items-center h-screen">Loading...</div>
}
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-8">ShantiLink Dashboard</h1>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
icon={Package}
title="Total Orders"
value={stats.totalOrders}
change="+12%"
/>
<StatCard
icon={Users}
title="Customers"
value={stats.totalCustomers}
change="+5%"
/>
<StatCard
icon={TrendingUp}
title="Today Revenue"
value={`฿${stats.todayRevenue.toLocaleString()}`}
change="+18%"
/>
<StatCard
icon={Clock}
title="Pending Orders"
value={stats.pendingOrders}
change="-2%"
/>
</div>
{/* Recent Orders */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Recent Orders</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Order ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Customer</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{orders.map((order) => (
<tr key={order.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{order.order_id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{order.customers?.display_name || 'Unknown'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
฿{order.total_amount.toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
order.status === 'confirmed' ? 'bg-green-100 text-green-800' :
order.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{order.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)
}
function StatCard({ icon: Icon, title, value, change }) {
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="flex-shrink-0">
<Icon className="h-8 w-8 text-blue-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">{title}</p>
<p className="text-2xl font-semibold text-gray-900">{value}</p>
<p className="text-sm text-green-600">{change}</p>
</div>
</div>
</div>
)
}
Deployment Process
1. Connect to Vercel
# Install Vercel CLI
npm install -g vercel
# Login to Vercel
vercel login
# Deploy project
vercel
# Follow prompts:
# ? Set up and deploy "~/project"? [Y/n] y
# ? Which scope do you want to deploy to? Your Name
# ? Link to existing project? [y/N] n
# ? What's your project's name? shantilink-dashboard
# ? In which directory is your code located? ./
# ? Want to override the settings? [y/N] n
2. Configure Environment Variables
# Add environment variables
vercel env add NEXT_PUBLIC_SUPABASE_URL
vercel env add NEXT_PUBLIC_SUPABASE_ANON_KEY
vercel env add SUPABASE_URL
vercel env add SUPABASE_SERVICE_KEY
# Or add in Vercel dashboard:
# Project Settings → Environment Variables
3. GitHub Integration
- ไปที่ Vercel dashboard
- เลือก project ของคุณ
- ไปที่ Settings → Git Integration
- Connect GitHub repository
- Enable auto-deployment
4. Custom Domain
# Add custom domain
vercel domains add shantilink.com
# Or in dashboard:
# Project Settings → Domains
# Add: shantilink.com
# Add: www.shantilink.com
Advanced Features
Edge Functions
// api/line-webhook.ts
import { NextRequest, NextResponse } from 'next/server'
export const config = {
runtime: 'edge',
}
export default async function handler(req: NextRequest) {
if (req.method !== 'POST') {
return new NextResponse('Method not allowed', { status: 405 })
}
try {
const body = await req.json()
const events = body.events || []
for (const event of events) {
if (event.type === 'message' && event.message.type === 'text') {
// Process message
await processLineMessage(event)
}
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Webhook error:', error)
return new NextResponse('Internal server error', { status: 500 })
}
}
async function processLineMessage(event: any) {
// Process LINE message logic
const response = {
replyToken: event.replyToken,
messages: [{
type: 'text',
text: 'ได้รับข้อความของคุณแล้วครับ'
}]
}
await fetch('https://api.line.me/v2/bot/message/reply', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LINE_CHANNEL_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(response)
})
}
Image Optimization
// components/OptimizedImage.js
import Image from 'next/image'
export default function OptimizedImage({ src, alt, width, height }) {
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={width < 400}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
/>
)
}
API Routes with Middleware
// pages/api/protected.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
)
// Middleware for authentication
async function authenticate(req) {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null
}
const token = authHeader.split(' ')[1]
const { data: user, error } = await supabase.auth.getUser(token)
if (error || !user) {
return null
}
return user
}
export default async function handler(req, res) {
// Authenticate request
const user = await authenticate(req)
if (!user) {
return res.status(401).json({ error: 'Unauthorized' })
}
// Your protected API logic here
res.status(200).json({ message: 'Authenticated!', user: user.user })
}
Performance Optimization
1. Static Generation
// pages/products/[id].js
export async function getStaticPaths() {
const { data: products } = await supabase
.from('products')
.select('id')
const paths = products.map(product => ({
params: { id: product.id.toString() }
}))
return {
paths,
fallback: 'blocking'
}
}
export async function getStaticProps({ params }) {
const { data: product } = await supabase
.from('products')
.select('*')
.eq('id', params.id)
.single()
return {
props: { product },
revalidate: 60 // Revalidate every 60 seconds
}
}
2. Caching Strategy
// pages/api/stats.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
)
// Simple in-memory cache
const cache = new Map()
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
export default async function handler(req, res) {
const cacheKey = 'dashboard_stats'
const cached = cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return res.status(200).json(cached.data)
}
// Fetch fresh data
const [orders, customers] = await Promise.all([
supabase.from('orders').select('id, total_amount', { count: 'exact' }),
supabase.from('customers').select('id', { count: 'exact' })
])
const data = {
totalOrders: orders.count,
totalCustomers: customers.count,
totalRevenue: orders.data?.reduce((sum, order) => sum + order.total_amount, 0) || 0
}
// Cache the result
cache.set(cacheKey, {
data,
timestamp: Date.now()
})
res.status(200).json(data)
}
Monitoring & Analytics
1. Vercel Analytics
# Install analytics
npm install @vercel/analytics
# Add to _app.js
import { Analytics } from '@vercel/analytics/react'
function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<Analytics />
</>
)
}
2. Speed Insights
# Install speed insights
npm install @vercel/speed-insights
# Add to _app.js
import { SpeedInsights } from '@vercel/speed-insights/react'
function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<SpeedInsights />
</>
)
}
3. Custom Monitoring
// lib/monitoring.js
export function trackEvent(eventName, properties) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, properties)
}
}
export function trackPageView(path) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
page_path: path,
})
}
}
// Use in components
import { trackEvent } from '../lib/monitoring'
function OrderButton({ onClick }) {
const handleClick = () => {
trackEvent('order_button_click', {
button_location: 'dashboard'
})
onClick()
}
return <button onClick={handleClick}>Place Order</button>
}
Best Practices
1. Environment Variables
# .env.local (development)
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-local-key
SUPABASE_URL=http://localhost:54321
SUPABASE_SERVICE_KEY=your-local-service-key
# Production (set in Vercel dashboard)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-prod-key
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your-prod-service-key
2. Error Handling
// components/ErrorBoundary.js
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo)
// Send to error reporting service
}
render() {
if (this.state.hasError) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold text-gray-900">Something went wrong</h2>
<p className="mt-2 text-gray-600">Please try refreshing the page</p>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary
3. Security
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// Add security headers
const response = NextResponse.next()
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
return response
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
Common Issues
Build Failures
- ✅ ตรวจสอบ environment variables
- ✅ ตรวจสอบ Node.js version compatibility
- ✅ ตรวจสอบ import paths
Function Timeouts
- ✅ ใช้ Edge Functions สำหรับ operations เร็วๆ
- ✅ Optimize database queries
- ✅ ใช้ caching
Performance Issues
- ✅ ใช้ Next.js Image optimization
- ✅ Enable static generation
- ✅ ใช้ Vercel CDN
Next Steps
- APIs & Webhooks - การเชื่อมต่อระบบ
- Client Examples - ดูตัวอย่างจริง
- Real-time Dashboard - ตัวอย่าง dashboard
ต้องการความช่วยเหลือ? ติดต่อเราได้ที่ ShantiLink.com 💬