SiamCafe.net Blog
Technology

WordPress Headless Post-mortem Analysis

wordpress headless post mortem analysis
WordPress Headless Post-mortem Analysis | SiamCafe Blog
2025-06-05· อ. บอม — SiamCafe.net· 8,090 คำ

WordPress Headless CMS คืออะไร

WordPress Headless CMS คือการใช้ WordPress เป็น Backend สำหรับจัดการ Content เพียงอย่างเดียว โดยไม่ใช้ PHP Theme ของ WordPress ในการแสดงผล แต่ใช้ Frontend Framework อย่าง Next.js, Nuxt.js, Gatsby หรือ Astro เชื่อมต่อผ่าน REST API หรือ WPGraphQL เพื่อดึง Content มาแสดงผลแทน

สถาปัตยกรรมนี้แยก Backend (Content Management) ออกจาก Frontend (Presentation) อย่างชัดเจน ทำให้แต่ละส่วนสามารถ Scale, Deploy และ Maintain ได้อิสระจากกัน Frontend สามารถใช้ Static Site Generation (SSG) หรือ Incremental Static Regeneration (ISR) เพื่อ Performance ที่ดีเยี่ยม ขณะที่ Backend ยังใช้ WordPress Dashboard ที่ Content Team คุ้นเคย

การตั้งค่า WordPress สำหรับ Headless Mode

# wp-config.php — Configuration สำหรับ Headless Mode
// ปิด Frontend ของ WordPress (Redirect ไป Admin เท่านั้น)
define('WP_HOME', 'https://cms.example.com');
define('WP_SITEURL', 'https://cms.example.com');

// เพิ่ม Security Headers
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', false);

// Cache Configuration
define('WP_CACHE', true);
define('WP_REDIS_HOST', getenv('REDIS_HOST') ?: '127.0.0.1');
define('WP_REDIS_PORT', getenv('REDIS_PORT') ?: 6379);

// Memory Limit
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');

// REST API Configuration
define('REST_API_MAX_RESULTS', 100);

---
# functions.php — Custom REST API Endpoints และ CORS
// เปิด CORS สำหรับ Frontend Domain
add_action('rest_api_init', function() {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function($value) {
        $origin = get_http_origin();
        $allowed_origins = [
            'https://example.com',
            'https://www.example.com',
            'http://localhost:3000',  // Development
        ];
        if (in_array($origin, $allowed_origins)) {
            header("Access-Control-Allow-Origin: " . $origin);
            header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
            header("Access-Control-Allow-Headers: Authorization, Content-Type");
            header("Access-Control-Allow-Credentials: true");
        }
        return $value;
    });
});

// Custom REST API Endpoint สำหรับ Popular Posts
add_action('rest_api_init', function() {
    register_rest_route('custom/v1', '/popular-posts', [
        'methods'  => 'GET',
        'callback' => function($request) {
            $posts = get_posts([
                'meta_key'       => 'post_views_count',
                'orderby'        => 'meta_value_num',
                'order'          => 'DESC',
                'posts_per_page' => $request->get_param('per_page') ?: 10,
            ]);
            $data = array_map(function($post) {
                return [
                    'id'       => $post->ID,
                    'title'    => $post->post_title,
                    'slug'     => $post->post_name,
                    'excerpt'  => wp_trim_words($post->post_content, 30),
                    'date'     => $post->post_date,
                    'views'    => get_post_meta($post->ID, 'post_views_count', true),
                    'featured' => get_the_post_thumbnail_url($post->ID, 'large'),
                ];
            }, $posts);
            return rest_ensure_response($data);
        },
        'permission_callback' => '__return_true',
    ]);
});

// Webhook แจ้ง Frontend เมื่อ Content เปลี่ยน
add_action('save_post', function($post_id, $post) {
    if ($post->post_status !== 'publish') return;
    $webhook_url = getenv('FRONTEND_REVALIDATE_WEBHOOK');
    if (!$webhook_url) return;
    wp_remote_post($webhook_url, [
        'body' => json_encode([
            'type'  => 'post_updated',
            'id'    => $post_id,
            'slug'  => $post->post_name,
            'secret' => getenv('REVALIDATE_SECRET'),
        ]),
        'headers' => ['Content-Type' => 'application/json'],
        'timeout' => 10,
    ]);
}, 10, 2);

Next.js Frontend สำหรับ WordPress Headless

// lib/wordpress.ts — WordPress API Client
const WP_API_URL = process.env.WORDPRESS_API_URL || 'https://cms.example.com/wp-json';

interface WPPost {
  id: number;
  slug: string;
  title: { rendered: string };
  content: { rendered: string };
  excerpt: { rendered: string };
  date: string;
  featured_media: number;
  _embedded?: {
    'wp:featuredmedia'?: Array<{ source_url: string; alt_text: string }>;
  };
}

export async function getPosts(page = 1, perPage = 10): Promise<WPPost[]> {
  const res = await fetch(
    `/wp/v2/posts?page=&per_page=&_embed`,
    {
      next: { revalidate: 300 }, // ISR: Revalidate ทุก 5 นาที
      headers: { 'Accept': 'application/json' },
    }
  );
  if (!res.ok) throw new Error(`WP API Error: `);
  return res.json();
}

export async function getPostBySlug(slug: string): Promise<WPPost | null> {
  const res = await fetch(
    `/wp/v2/posts?slug=wordpress-headless-post-mortem-analysis&_embed`,
    {
      next: { revalidate: 60 },
      headers: { 'Accept': 'application/json' },
    }
  );
  if (!res.ok) return null;
  const posts = await res.json();
  return posts[0] || null;
}

// app/api/revalidate/route.ts — On-demand Revalidation
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();

  if (body.secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
  }

  if (body.type === 'post_updated' && body.slug) {
    revalidatePath(`/blog/`);
    revalidatePath('/blog');
    return NextResponse.json({ revalidated: true, slug: body.slug });
  }

  return NextResponse.json({ error: 'Unknown type' }, { status: 400 });
}

Post-mortem Analysis — ตัวอย่างจากเหตุการณ์จริง

ต่อไปนี้เป็นตัวอย่าง Post-mortem จากเหตุการณ์ที่ WordPress Headless ล่มเนื่องจาก REST API ตอบสนองช้าจนทำให้ Frontend Build ล้มเหลว

# Post-mortem: WordPress Headless API Timeout
# Date: 2026-02-20
# Duration: 45 นาที
# Severity: SEV-2
# Impact: Frontend แสดงข้อมูลเก่า (Stale Content) เป็นเวลา 45 นาที

## Timeline (UTC+7)
- 09:00 - Content Team Publish บทความ 15 บทความพร้อมกัน (Bulk Import)
- 09:01 - save_post Hook ส่ง Revalidation Webhook 15 ครั้งพร้อมกัน
- 09:02 - Next.js ISR Revalidation เริ่มดึงข้อมูลจาก WP REST API
- 09:03 - WP REST API Response Time เพิ่มจาก 200ms เป็น 8000ms
- 09:05 - MySQL Max Connections ถูกใช้จนเต็ม (151/151)
- 09:06 - Next.js Fetch Timeout — Serve Stale Content แทน
- 09:10 - Prometheus Alert: wp_api_response_time > 5000ms
- 09:12 - PagerDuty Incident Created → On-call Acknowledge
- 09:20 - Root Cause ระบุ: MySQL Connection Pool เต็ม
- 09:25 - เพิ่ม max_connections เป็น 300
- 09:30 - Restart PHP-FPM และ Redis
- 09:35 - WP REST API Response Time กลับสู่ปกติ
- 09:45 - ISR Revalidation สำเร็จ — Content ใหม่แสดงผล

## Root Cause
Content Team ทำ Bulk Import 15 บทความพร้อมกัน ทำให้ save_post Hook
ส่ง Revalidation Webhook 15 ครั้งพร้อมกัน Next.js ISR ดึงข้อมูลจาก
WP REST API 15 Request พร้อมกัน แต่ละ Request ทำ Complex Query
หลายตัว (Posts + Categories + Tags + Featured Media) ทำให้ MySQL
Connection Pool (default 151) ถูกใช้จนเต็ม

## Action Items
- [x] เพิ่ม MySQL max_connections เป็น 300
- [x] ใส่ Debounce ใน Revalidation Webhook (รอ 30 วินาทีก่อนส่ง)
- [x] เพิ่ม Redis Object Cache สำหรับ WP REST API
- [ ] ตั้ง Rate Limit สำหรับ Revalidation Endpoint
- [ ] เพิ่ม Circuit Breaker ใน Next.js API Client
- [ ] สร้าง Load Test สำหรับ Bulk Publish Scenario

การตั้งค่า Monitoring สำหรับ WordPress Headless

# docker-compose.monitoring.yml
version: "3.8"
services:
  prometheus:
    image: prom/prometheus:v2.50.0
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:10.3.0
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123

  wp-exporter:
    image: ghcr.io/aorfanos/wordpress-exporter:latest
    environment:
      - WORDPRESS_URL=https://cms.example.com
      - WORDPRESS_USER=monitoring
      - WORDPRESS_PASSWORD=
    ports:
      - "9850:9850"

  blackbox:
    image: prom/blackbox-exporter:v0.24.0
    ports:
      - "9115:9115"

---
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'wordpress'
    static_configs:
      - targets: ['wp-exporter:9850']

  - job_name: 'api-probe'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://cms.example.com/wp-json/wp/v2/posts?per_page=1
          - https://cms.example.com/wp-json/custom/v1/popular-posts
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox:9115

  - job_name: 'nextjs'
    static_configs:
      - targets: ['frontend:3000']
    metrics_path: /api/metrics

---
# Alert Rules
groups:
  - name: wordpress_headless
    rules:
      - alert: WPAPISlowResponse
        expr: probe_http_duration_seconds{job="api-probe"} > 3
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "WP REST API ตอบช้า {{ $value }}s"

      - alert: WPAPIDown
        expr: probe_success{job="api-probe"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "WP REST API ไม่ตอบสนองสำหรับ {{ $labels.instance }}"

      - alert: WPHighMemoryUsage
        expr: wordpress_memory_usage_bytes / wordpress_memory_limit_bytes > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "WordPress Memory Usage สูง {{ $value | humanizePercentage }}"

Best Practices สำหรับ WordPress Headless

WordPress Headless คืออะไรและต่างจาก WordPress ปกติอย่างไร

WordPress Headless ใช้ WordPress เป็น Backend จัดการ Content ผ่าน REST API หรือ GraphQL แล้วใช้ Frontend Framework อย่าง Next.js แสดงผล ต่างจาก WordPress ปกติที่ใช้ PHP Theme แสดงผลทั้งหมด ข้อดีคือ Performance ดีกว่า, Frontend ยืดหยุ่นกว่า และ Scale ง่ายกว่าเพราะแยก Backend กับ Frontend ออกจากกัน

Post-mortem Analysis คืออะไรและทำไมต้องทำ

Post-mortem Analysis คือการวิเคราะห์เหตุการณ์หลัง Incident เพื่อหา Root Cause และกำหนดมาตรการป้องกัน เป็น Blameless Process ที่ Focus ที่ระบบไม่ใช่ตัวบุคคล ช่วยให้ทีมเรียนรู้จากข้อผิดพลาดและทำให้ระบบ Reliable มากขึ้นในระยะยาว

ปัญหาที่พบบ่อยของ WordPress Headless มีอะไรบ้าง

ปัญหาที่พบบ่อยคือ REST API ตอบช้าเมื่อมี Content มาก, Cache Invalidation ไม่ทำงาน, CORS Error เมื่อ Frontend กับ Backend อยู่คนละ Domain, Authentication Token หมดอายุ, Plugin Update ทำให้ API Format เปลี่ยน และ MySQL Connection Pool เต็มเมื่อมี Request พร้อมกันมากๆ

ควรตั้ง Monitoring อะไรบ้างสำหรับ WordPress Headless

ควร Monitor API Response Time, Error Rate (4xx/5xx), Cache Hit Ratio ของ Redis, PHP Memory Usage, MySQL Query Time และ Connection Count, Frontend Build Time, CDN Cache Status และ ISR Revalidation Success Rate ใช้ Prometheus + Grafana สำหรับ Metrics และ Sentry สำหรับ Error Tracking

สรุปและแนวทางปฏิบัติ

WordPress Headless เป็นสถาปัตยกรรมที่ทรงพลังสำหรับ Content-driven Website แต่มีความซับซ้อนมากกว่า WordPress ปกติเพราะต้องดูแลทั้ง Backend API และ Frontend Application แยกกัน การมี Monitoring ที่ครอบคลุมทุก Layer, Redis Cache สำหรับ API Performance, Debounce สำหรับ Revalidation Webhook และ Post-mortem Process ที่เป็นระบบ จะช่วยให้ระบบมีเสถียรภาพสูงและตอบสนองต่อ Incident ได้รวดเร็ว

📖 บทความที่เกี่ยวข้อง

Mintlify Docs Post-mortem Analysisอ่านบทความ → WordPress Headless SSL TLS Certificateอ่านบทความ → AlmaLinux Setup Post-mortem Analysisอ่านบทความ → AWS Bedrock AI Post-mortem Analysisอ่านบทความ → Fivetran Connector Post-mortem Analysisอ่านบทความ →

📚 ดูบทความทั้งหมด →