Vibe Coding Guide

สร้าง AI Content Poster ตั้งแต่ศูนย์

10 ขั้นตอน · copy prompt (ภาษาไทย) · วาง Claude · ทำต่อ
prompt เป็นภาษาไทย — แก้บริบทในวงเล็บ [ ... ] ให้ตรงงานคุณก่อน copy
FastAPI SQLAlchemy HTMX Jinja2 Claude API OpenAI Images Facebook Graph API LINE Messaging API Docker Traefik
1
Project Skeleton
Setup
ตั้ง project structure, dependencies, และ FastAPI app พื้นฐาน ให้ server ขึ้นได้ก่อนทุกอย่าง
FastAPI lifespan requirements.txt Jinja2 templates Tailwind CDN
Prompt สำหรับ Claude
สร้างโปรเจกต์ Python FastAPI web app ชื่อ "[ai-content-poster]" โครงสร้างแบบนี้:

โครงสร้างไฟล์:
  app.py          — FastAPI entry point + lifespan handler
  db.py           — SQLAlchemy engine + session (SQLite)
  models.py       — ORM models (ว่างไว้ก่อน มีแค่ Base)
  requirements.txt — dependency ทั้งหมด
  ui/templates/
    base.html     — base Jinja2 template + Tailwind CDN + Inter font
    home.html     — extends base, แสดง "App is running ✓"

Dependencies ที่ต้องมี:
- fastapi, uvicorn[standard], sqlalchemy, jinja2, python-dotenv, httpx, pillow

app.py ต้อง:
- ใช้ @asynccontextmanager lifespan เรียก init_db() ตอน startup
- Mount /static สำหรับ static files
- มี route GET / ที่ render home.html
- อ่าน PORT จาก env (default 8000)

base.html ต้อง:
- มี Tailwind CSS ผ่าน CDN
- ใช้ Inter font จาก Google Fonts
- มี sidebar nav สีเข้ม (ลิงก์ placeholder ไปก่อน)
- content area อยู่ทางขวา

บริบทของฉัน: [ธุรกิจ/แบรนด์ = ..., โทนแอป = ...]

รันด้วย: uvicorn app:app --reload
แสดงไฟล์ทั้งหมดแบบเต็ม
2
Database Models
Database
ออกแบบ schema สำหรับเก็บ draft content, post history และ cost tracking
SQLAlchemy 2.0 JSON column auto-migration indexes
Prompt สำหรับ Claude
เพิ่ม SQLAlchemy models ใน models.py มี table ดังนี้:

1. Draft — เก็บ content ที่ AI สร้าง ก่อนโพสต์
   Columns: id (pk), source_url, title, body (Text), caption (Text),
   status (String: pending/summarizing/ready/image_gen/posting/posted/failed),
   pending_post (JSON — wizard state: version_idx, artwork_paths ฯลฯ),
   created_at, updated_at

2. Post — เก็บโพสต์ที่เผยแพร่สำเร็จ
   Columns: id (pk), draft_id (FK→Draft), fb_post_id (String, unique, indexed),
   permalink_url, published_at

3. CostEvent — track ค่า API ต่อ operation
   Columns: id (pk), model (String), input_tokens, output_tokens,
   cost_usd (Float), kind (String: summarize/classify/image),
   created_at

กฎ:
- timestamp เก็บเป็น UTC ผ่าน datetime.utcnow
- Draft.pending_post ใช้ JSON ที่ SQLAlchemy serialize อัตโนมัติ
- เพิ่ม index ที่ Draft.status และ Post.fb_post_id
- db.py: init_db() ใช้ metadata.create_all() — ไม่ต้องทำ migration แค่ auto-create

บริบทของฉัน: [ถ้าอยากเก็บ field เพิ่ม เช่น ..., ให้บอกตรงนี้]

แก้ db.py ให้ init_db() log จำนวน draft/post ที่มี ตอน startup
แสดง models.py และ db.py แบบเต็ม
3
RSS News Fetcher
Data
ดึงข่าว AI จาก RSS feeds หลายแหล่ง พร้อม dedup และ full-text scraping
feedparser httpx BeautifulSoup User-Agent spoofing
Prompt สำหรับ Claude
สร้าง core/sources.py — ตัวดึงข่าวจาก RSS:

RSS feeds ที่ต้องรองรับ (hardcode เป็น list) — [แก้ให้เป็นแหล่งข่าวที่ฉันอยากใช้]:
- https://feeds.feedburner.com/TechCrunch/
- https://venturebeat.com/feed/
- https://www.theverge.com/rss/index.xml
- https://techcrunch.com/tag/artificial-intelligence/feed/

Functions:

1. fetch_rss_feeds(max_per_feed=10) -> list[dict]
   - parse แต่ละ feed ด้วย feedparser
   - return list ของ {title, url, summary, published_at, source}
   - dedupe ด้วย URL
   - กรอง: เอาเฉพาะ 48 ชม.ล่าสุด

2. fetch_article_content(url: str) -> str
   - ใช้ httpx + Chrome User-Agent header
   - สำคัญ: Accept-Encoding ต้องเป็น "gzip, deflate" (ห้าม "br") — brotli ทำให้ได้ binary garbage
   - parse ด้วย BeautifulSoup ดึงเนื้อบทความหลัก
   - return 3000 ตัวอักษรแรก
   - error ให้ return "" (ห้าม raise)

3. route GET /api/news/browse ใน app.py
   - เรียก fetch_rss_feeds()
   - return JSON list
   - cache 10 นาที (dict cache + timestamp ง่ายๆ)

บริบทของฉัน: [หัวข้อข่าวที่สนใจ = ..., ภาษาแหล่งข่าว = ...]

เพิ่ม feedparser กับ beautifulsoup4 ใน requirements.txt
แสดง core/sources.py แบบเต็ม
4
AI Summarizer (Claude)
AI Core
ให้ Claude API อ่านข่าว classify ว่าเหมาะสำหรับ audience ไหม และสรุปเป็นภาษาไทยสไตล์ Facebook
Anthropic SDK claude-haiku (classify) claude-sonnet (summarize) prompt engineering token cost tracking
Prompt สำหรับ Claude
สร้าง core/summarizer.py ใช้ Anthropic Python SDK:

1. classify_article(title: str, summary: str) -> str ("USE" หรือ "SKIP")
   - ใช้ claude-haiku-4-5 (เร็ว ถูกสุด)
   - จัดว่าข่าวเหมาะกับ [กลุ่มเป้าหมายของฉัน = เจ้าของ SME ไทยที่อยากรู้เรื่อง AI] ไหม
   - USE: เครื่องมือ AI ใช้จริง, ประยุกต์กับธุรกิจ, AI ในชีวิตประจำวัน
   - SKIP: paper วิจัยลึก, เนื้อหาสำหรับ dev เท่านั้น, ข่าวภายในองค์กร
   - return "USE" หรือ "SKIP" เท่านั้น

2. summarize_article(title: str, body: str, url: str) -> dict
   - ใช้ claude-sonnet-4-6 (คุณภาพดีสุด)
   - output เป็น JSON: {
       "hook": "hook ภาษาไทย 1 ประโยค ดึงความสนใจสำหรับ Facebook",
       "body": "สรุปภาษาไทย 3-4 ย่อหน้า (คุยกันเป็นกันเอง ไม่วิชาการ)",
       "caption": "โพสต์ FB เต็ม = hook + body + hashtags + source URL",
       "tagline": "ข้อความบนรูป 5-10 คำไทย (ไม่มีจุดท้าย)"
     }
   - โทน: [อบอุ่น ใช้ได้จริง อ่านง่าย ไม่ทางการ — แก้ให้เป็นโทนแบรนด์ฉัน]
   - ใส่ hashtag ไทยที่เกี่ยว 3-5 อัน
   - ถ้า body สั้นกว่า 100 ตัวอักษร ให้ใช้ title + summary แทน (fallback ตอน scrape ไม่ได้)

3. track cost: หลังทุก API call ให้ log {model, input_tokens, output_tokens, cost_usd}
   (pricing Anthropic: haiku=$0.80/$4 ต่อ 1M, sonnet=$3/$15 ต่อ 1M input/output)

ANTHROPIC_API_KEY อ่านจาก env
แสดง core/summarizer.py แบบเต็ม
5
Image Generator
AI Media
สร้างภาพโพสต์ด้วย OpenAI gpt-image-2 และวาง text overlay ด้วย Pillow
OpenAI Images API Pillow / PIL Thai font rendering base64 decode
Prompt สำหรับ Claude
สร้าง core/image_maker.py สำหรับสร้างรูปโพสต์ Facebook:

1. generate_ai_image(tagline: str, style_prompt: str) -> str (file path)
   - ใช้ openai client, model="gpt-image-2"
   - Size: 1024x1024, quality="medium"
   - Prompt: f"Modern tech illustration for [Thai social media / แก้เป็นสไตล์ที่ฉันต้องการ]. {style_prompt}. Clean, vibrant."
   - บันทึกเป็น PNG ที่ /tmp/artwork_{uuid}.png
   - สำคัญ: re-encode เป็น JPEG q=92 ก่อน return (PNG อาจ 3-5MB, FB reject ถ้า >4MB)
   - return path ของ JPEG

2. render_color_bg_card(tagline: str, color_hex: str) -> str (file path)
   - ใช้ Pillow สร้างรูป 1080x1080
   - พื้นหลังสีทึบ color_hex
   - วาง tagline ภาษาไทยกลางรูป (ใช้ system font หรือ bundle NotoSansThai)
   - ใส่ [ชื่อแบรนด์ฉัน] มุมขวาล่าง ตัวเล็ก
   - บันทึกแล้ว return path

3. route serve_image: GET /artwork/{filename}
   - serve ไฟล์จาก /tmp/ เป็น image/jpeg หรือ image/png

บริบทของฉัน: [สไตล์ภาพ = ..., สีแบรนด์ = #...]

OPENAI_API_KEY อ่านจาก env
เพิ่ม openai ใน requirements.txt
แสดง core/image_maker.py แบบเต็ม
6
Facebook Graph API
Publishing
โพสต์ caption + รูปภาพขึ้น Facebook Page ทั้งแบบ publish ทันทีและ schedule ล่วงหน้า
FB Graph API v22 page access token photo upload scheduled post DRY_RUN flag
Prompt สำหรับ Claude
สร้าง core/facebook.py — wrapper ของ Facebook Graph API:

Environment variables:
  FACEBOOK_PAGE_ACCESS_TOKEN — long-lived page token
  FACEBOOK_PAGE_ID — page ID (ตัวเลข)
  FACEBOOK_DRY_RUN — "1" = simulate เท่านั้น (ไม่เรียก FB API), "0" = โพสต์จริง

Functions:

1. post_photo_to_page(caption: str, image_path: str, scheduled_unix: int | None) -> dict
   - ถ้า FACEBOOK_DRY_RUN=1: log แล้ว return ปลอม {"fb_post_id": "DRY_RUN_123", "permalink": ""}
   - upload รูปไป FB ผ่าน multipart POST ไป /v22.0/{page_id}/photos
   - สำคัญ: re-encode รูปเป็น JPEG bytes ใน memory ก่อน upload (เลี่ยง PNG ใหญ่)
   - ถ้ามี scheduled_unix: ใส่ published=false + scheduled_publish_time=unix_ts
   - ถ้าไม่มี: published=true
   - return {"fb_post_id": str, "permalink_url": str}
   - raise ถ้า FB error (parse key "error" จาก response JSON)

2. post_comment(post_id: str, message: str) -> str (comment_id)
   - POST ไป /v22.0/{post_id}/comments
   - เคารพ flag DRY_RUN
   - return comment_id หรือ "DRY_RUN" ถ้า dry

3. list_scheduled_posts() -> list[dict]
   - GET /v22.0/{page_id}/scheduled_posts
   - return list ของ {id, message, scheduled_publish_time}

หมายเหตุความปลอดภัย: [เริ่มด้วย DRY_RUN=1 เสมอ ทดสอบให้ชัวร์ก่อนค่อยเปิดโพสต์จริง]

Logging: log ทุก API call พร้อม method + endpoint + response status
แสดง core/facebook.py แบบเต็ม
7
LINE OA Push Notification
Notification
Push แจ้งเตือนไป LINE OA เมื่อโพสต์สำเร็จ พร้อม Flex Message สวยงาม
LINE Messaging API Flex Message channel access token push vs broadcast
Prompt สำหรับ Claude
สร้าง core/line_oa.py — โมดูลแจ้งเตือน LINE OA:

Environment variables:
  LINE_CHANNEL_ACCESS_TOKEN — channel access token
  LINE_GROUP_ID — group/room ID ที่จะ push (optional)
  LINE_DRY_RUN — "1" = log อย่างเดียว ไม่เรียก LINE API

Functions:

1. push_post_published(headline: str, caption_preview: str, fb_post_url: str) -> bool
   - เรียกหลังโพสต์ FB สำเร็จ
   - ส่ง Flex Message ไป LINE_GROUP_ID (หรือ broadcast ถ้าไม่มี group)
   - ดีไซน์ Flex Message:
     * Header: พื้นเขียว, "✅ โพสต์สำเร็จ!"
     * Body: headline (ตัวหนา), preview ของ caption (2 บรรทัด)
     * Footer: ปุ่ม "ดูโพสต์ใน Facebook" → เปิด fb_post_url
   - return True ถ้าสำเร็จ, False ถ้า error
   - ห้าม raise — นี่คือ notification ที่ไม่ critical

2. _build_flex_message(headline, preview, url) -> dict
   - return LINE Flex Message JSON ที่ valid
   - ใช้ container แบบ "bubble"

3. _call_line_api(endpoint: str, body: dict) -> bool
   - POST ไป https://api.line.me/v2/bot/{endpoint}
   - Auth: "Bearer {LINE_CHANNEL_ACCESS_TOKEN}"
   - log response status

บริบทของฉัน: [ข้อความแจ้งเตือน/ปุ่ม ปรับให้ตรงทีมฉัน]

Integration: ในฟังก์ชันโพสต์ FB (core/facebook.py) หลังโพสต์สำเร็จ
ให้เรียก line_oa.push_post_published() ใน try/except
เพื่อไม่ให้ LINE พังแล้วกระทบผล FB post

แสดง core/line_oa.py แบบเต็ม
8
HTMX Wizard UI
Frontend
สร้าง multi-step wizard ด้วย HTMX — browse → สรุป → เลือกรูป → preview → โพสต์ โดยไม่ต้องเขียน JavaScript
HTMX hx-post/get hx-swap / hx-target hx-trigger polling wizard state machine
Prompt สำหรับ Claude
สร้าง UI แบบ wizard หลายขั้นตอน ด้วย HTMX 2.0 (CDN) + Tailwind
ไม่ใช้ JS framework — ใช้แค่ HTMX + Jinja2

Route: GET /content → render หน้า wizard หลัก

ขั้นตอน wizard (เก็บ state ใน draft.status + draft.pending_post JSON):

Step 0 — Loading (status=summarizing)
  - auto-poll ทุก 3s: hx-get="/content/draft/{id}/step" hx-trigger="every 3s"
  - แสดง spinner + "กำลังสรุปข่าว..."

Step 1 — Browse ข่าว
  - แสดง list บทความจาก RSS (title, source, วันที่)
  - แต่ละแถว: ปุ่ม "เลือกข่าวนี้" → POST /content/drafts (สร้าง draft, เริ่ม job)
  - render เป็น HTMX partial, swap เข้า #wizard-frame

Step 2 — เลือกเวอร์ชัน body
  - แสดง body ภาษาไทย 3 เวอร์ชัน (radio button)
  - ปุ่ม "ถัดไป" → POST /content/draft/{id}/pick-body

Step 3 — สร้างรูป
  - แสดงช่อง tagline (เติมมาจาก AI แล้ว)
  - ปุ่ม "สร้างรูป" → POST /content/draft/{id}/gen-image (async job)
  - poll /content/draft/{id}/step ทุก 1.5s ระหว่าง status=image_gen

Step 4 — เลือกรูป
  - แสดงรูป 2 รูปที่ gen มา วางข้างกัน
  - คลิกเลือก → POST /content/draft/{id}/pick-image

Step 5 — Preview & โพสต์
  - แสดง caption สุดท้าย (textarea แก้ได้) + รูปที่เลือก
  - 2 ปุ่ม: "โพสต์เลย" กับ "ตั้งเวลา" (โผล่ date picker)
  - POST /content/draft/{id}/post

บริบทของฉัน: [ปุ่ม/ข้อความในแต่ละ step ปรับให้ตรงภาษาทีมฉัน]

ทุก step return HTML partial ให้ HTMX swap เข้า 
ใช้ Tailwind จัด style แสดงไฟล์ route ของ wizard + template ทุก step
9
Background Job Queue
Async
งาน AI ใช้เวลานาน — ต้องรันใน background thread แล้ว HTMX poll ดู status แทนการรอใน HTTP request
threading.Thread in-memory queue job progress dict startup cleanup
Prompt สำหรับ Claude
สร้าง workers/queue.py และ workers/jobs.py — ระบบ job แบบ in-process:

workers/queue.py:
  JOB_PROGRESS = {}  # job_id -> {status, message, result, error}

  def enqueue(job_fn, *args, job_id=None) -> str:
    - gen job_id (uuid) ถ้าไม่ส่งมา
    - set JOB_PROGRESS[job_id] = {status: "running", message: "starting..."}
    - เริ่ม threading.Thread(target=_run_job, args=(job_id, job_fn, args))
    - return job_id ทันที (non-blocking)

  def get_progress(job_id) -> dict:
    - return JOB_PROGRESS.get(job_id, {status: "not_found"})

  def _run_job(job_id, fn, args):
    - เรียก fn(*args, job_id=job_id)
    - สำเร็จ: set JOB_PROGRESS[job_id] = {status: "done", result: return_value}
    - exception: set {status: "error", error: str(e)}

workers/jobs.py:
  def summarize_article_job(draft_id: int, url: str, job_id=None):
    - update ข้อความ progress ทุก step
    - fetch เนื้อบทความ (core/sources)
    - classify ด้วย Haiku — ถ้า SKIP mark draft failed พร้อมเหตุผล
    - summarize ด้วย Sonnet (core/summarizer)
    - save ผลลง Draft
    - set draft.status = "ready"

  def generate_image_job(draft_id: int, tagline: str, job_id=None):
    - gen รูป 2 รูป (เรียก image_maker 2 ครั้ง)
    - save path ลง draft.pending_post["artwork_paths"]
    - set draft.status = "ready" (กลับจาก image_gen)

  def post_to_facebook_job(draft_id: int, publish_now: bool, scheduled_dt=None, job_id=None):
    - โพสต์ FB (core/facebook)
    - เพิ่ม comment ถ้ามี
    - push LINE notification (core/line_oa)
    - set draft.status = "posted", save fb_post_id

สำคัญมาก: ต้อง session.commit() ก่อน enqueue() — background thread เปิด session ของตัวเอง
ถ้า main thread ยังไม่ commit thread จะมองไม่เห็น draft

เพิ่ม route GET /api/jobs/{job_id} return JOB_PROGRESS[job_id] เป็น JSON
แสดง workers/queue.py และ workers/jobs.py แบบเต็ม
10
Docker + Deploy to VPS
Deploy
Package ทุกอย่างลง Docker container แล้ว deploy บน VPS ผ่าน Traefik สำหรับ HTTPS อัตโนมัติ
Dockerfile multi-stage docker compose Traefik labels volume persistence env secrets
Prompt สำหรับ Claude
สร้าง config deploy แอป FastAPI นี้บน Linux VPS ด้วย Docker + Traefik:

1. Dockerfile:
   FROM python:3.12-slim
   - ติดตั้ง system deps: libjpeg-dev libpng-dev libwebp-dev libfreetype6-dev ffmpeg
   - pip install จาก requirements.txt (แยก layer เพื่อ cache)
   - COPY โค้ดแอป
   - ENV APP_DB_PATH=/data/db.sqlite APP_STATE_DIR=/data/state
   - CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

2. docker-compose.yml:
   - service "web" build จาก .
   - Volume ./data:/data (SQLite ต้อง persist ข้าม redeploy — สำคัญมาก)
   - env_file: .env
   - เชื่อม external Docker network "[ชื่อ network ที่ Traefik อยู่]"
   - Traefik labels สำหรับ HTTPS อัตโนมัติ:
     traefik.enable=true
     traefik.http.routers.myapp.rule=Host(`[โดเมนของฉัน.com]`)
     traefik.http.routers.myapp.tls.certresolver=mytlschallenge
     traefik.http.services.myapp.loadbalancer.server.port=8000

3. .dockerignore:
   .venv, __pycache__, *.pyc, .git, .env, data/, *.sqlite

4. .env.example ใส่ key ที่ต้องมีทั้งหมด (ไม่ต้องใส่ค่าจริง):
   ANTHROPIC_API_KEY, OPENAI_API_KEY,
   FACEBOOK_PAGE_ACCESS_TOKEN, FACEBOOK_PAGE_ID, FACEBOOK_DRY_RUN=1,
   LINE_CHANNEL_ACCESS_TOKEN, LINE_GROUP_ID,
   APP_DEV_MOCK_USER=[you@email.com]  ← bypass auth ตอน dev

คำสั่ง deploy (รันบน VPS ผ่าน SSH):
   git clone  /opt/myapp && cd /opt/myapp
   cp .env.example .env && nano .env   # ใส่ค่าจริง
   mkdir -p data
   docker compose up -d --build
   docker logs -f myapp-web-1          # เช็ค startup

แสดงไฟล์ทั้ง 4 แบบเต็ม