Vercel Deployment

Deploy your frontend and serverless functions with Vercel

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

  1. ไปที่ Vercel dashboard
  2. เลือก project ของคุณ
  3. ไปที่ Settings → Git Integration
  4. Connect GitHub repository
  5. 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


ต้องการความช่วยเหลือ? ติดต่อเราได้ที่ ShantiLink.com 💬