AI Web Studio977-121 · PSU PHUKET
บทเรียน
ดาวน์โหลด Markdown

AI Web Studio · 977-121 · PSU Phuket

บทเรียน 05 — เว็บบริษัท: contact form ที่รันกับ demo นี้ได้จริง

ผลลัพธ์การเรียนรู้

ผู้เรียนจะเปิด /examples/company/, ส่ง contact form ไปยัง POST /api/contact, อ่านผลที่ Worker คืนและพิสูจน์ใน D1 ว่า demo เก็บเฉพาะค่า redacted. Lab นี้ใช้ contract ที่อยู่ใน src/worker.ts, migrations/0001_initial.sql และ examples/company/main.js ณ เวอร์ชันของหลักสูตร ไม่ใช่ API สมมติ.

ต้องมีก่อนเริ่ม

  • อยู่ที่ root ของ repository นี้และมี Node/npm ที่ติดตั้ง dependencies แล้ว
  • ใช้ชื่อ/อีเมล/ข้อความ สมมติ เท่านั้น เช่น Demo Learner, learner@example.test; ห้ามส่ง PII จริง
  • เข้าใจว่า wrangler.jsonc ตั้ง DEMO_MODE: "true": Worker ยัง validate input และเขียน row เพื่อสอน flow แต่แทน name, email, message ด้วย [demo-redacted] ก่อนเขียน D1; Turnstile ถูกข้ามเฉพาะ demo นี้

Prompt สำหรับสร้างเว็บบริษัท

ทำงานในสำเนา starter ของผู้เรียน อ่าน prompt นี้แล้วคัดลอกทั้งกล่องไปให้ coding agent ก่อนเริ่ม lab เปลี่ยนชื่อธุรกิจและเนื้อหาได้ แต่คง API contract ตามบทเรียน ตัวอย่างนี้เป็น prompt สำหรับสร้างงานตามข้อกำหนดปัจจุบัน ผลจาก AI แต่ละครั้งอาจจัดวางต่างจากภาพอ้างอิงได้

สร้างเว็บไซต์บริษัทสถาปัตยกรรมภาษาไทยชื่อ “เส้นตั้ง สตูดิโอ” ใน starter หลักสูตรนี้
เป้าหมาย: ผู้เข้าชมเข้าใจบริการ เห็นแนวทางออกแบบ และส่งข้อความสอบถามโครงการได้
เริ่มจากอ่าน package.json, DESIGN.md, examples/company/ และ contact route ใน src/worker.ts
ใช้ semantic HTML, CSS และ vanilla JavaScript ตาม stack เดิม ไม่เพิ่ม dependency
สรุปแผนและไฟล์ที่จะเปลี่ยน แล้วลงมือสร้างใน examples/company/{index.html,style.css,main.js}
รักษาเว็บตัวอย่างอื่นและ API เดิม หากขาด prerequisite ให้รายงานสิ่งที่ขาดตามจริง

แนวภาพ: สตูดิโอสถาปัตยกรรมร่วมสมัย สีกรมท่า ขาว และไม้ แสงธรรมชาติ พื้นที่ว่างอ่านสบาย
ใช้ฟอนต์ IBM Plex Sans Thai ที่มีใน public/fonts และภาพ /images/company-studio.webp
หน้าเว็บมีเมนู, hero พร้อม CTA “คุยเรื่องโครงการ”, บริการ, ผลงานสมมติ,
กระบวนการทำงาน, แบบฟอร์มติดต่อ และ footer ที่กลับไปยังหลักสูตรได้
ระบุว่าเป็นธุรกิจและผลงานสมมติ ห้ามแต่งคำรับรอง โลโก้ลูกค้า หรือสถิติเป็นข้อเท็จจริง
กำหนดขนาดภาพและ alt ที่เหมาะสม ทุก CTA ต้องนำไปยังเนื้อหาหรือการทำงานที่มีอยู่จริง

แบบฟอร์มมี label สำหรับ name, email, message และ consent พร้อม honeypot ชื่อ website
ส่ง JSON {name,email,message,consent,website} ไป POST /api/contact ที่ origin เดียวกัน
ส่ง Idempotency-Key และใช้ key เดิมเมื่อ retry ข้อมูลเดิมหลัง network error
อ่าน success {id,message} และ error {error:{code,message}} ตาม response จริง
ทำสถานะเริ่มต้น/ข้อมูลไม่ครบ/กำลังส่ง/สำเร็จ/ส่งไม่สำเร็จ พร้อมข้อความภาษาไทย
ปิดปุ่มเฉพาะระหว่างส่ง ไม่ล้างข้อความเมื่อเกิด error และไม่แสดงสำเร็จก่อน server ยืนยัน
ใช้ข้อมูลติดต่อสมมติและแจ้งว่า Worker ปกปิด name/email/message ก่อนบันทึก D1
ไม่มีการส่งอีเมลจริง ไม่มี secret ใน client และไม่เปลี่ยน DEMO_MODE ของ starter

รองรับ desktop และมือถือ 390px ไม่มีเนื้อหาล้นแนวนอน มี skip link,
heading ตามลำดับ, focus ที่เห็น, keyboard navigation, status ที่ screen reader อ่านได้,
ปุ่มใช้งานสะดวกอย่างน้อย 44px และ prefers-reduced-motion
ตรวจด้วย npm run build และ npm run lint จากนั้นทดลองกรอกผิด/ส่งสำเร็จ/จำลอง network error
ใช้ preview ตามพอร์ตที่โปรเจกต์จองไว้ ห้ามใช้พอร์ตของโครงการอื่น
ส่งสรุปไฟล์ที่สร้าง วิธีเปิดหน้า ภาพผลลัพธ์ และผลตรวจจริง; สิ่งที่ยังไม่ได้รันทดสอบให้ระบุไว้

ผลลัพธ์ที่ควรได้: หน้าเว็บบริษัทที่มีส่วนแนะนำบริการ/ผลงานและแบบฟอร์มทำงานได้ เปิดเทียบกับ เส้นตั้ง สตูดิโอ และ ภาพผลลัพธ์ แล้วตรวจ response ของ form ตาม lab ด้านล่าง ดู prompt ภาพประกอบที่ใช้ หากต้องเตรียมภาพใหม่

Prompt ติดตามผลหลังสร้าง

ตรวจเว็บบริษัทที่เพิ่งสร้างเทียบกับ brief ข้างต้น แล้วแก้เฉพาะจุดที่ไม่ผ่าน
ตรวจหน้า 390px, การใช้ Tab, label/focus, ข้อมูลผิด, contact success และ network error
ตรวจว่า retry ข้อมูลเดิมไม่สร้าง contact ซ้ำ และ success แสดงจาก response ของ API
สรุปเป็นตาราง ข้อกำหนด / ผลที่พบ / หลักฐาน / สิ่งที่แก้ พร้อมรันตรวจซ้ำเฉพาะส่วนที่แก้

Contract ที่มีอยู่จริง

ลำดับข้อความของ contact form จากหน้า examples/company และ main.js ส่ง POST api contact ไปยัง Worker ซึ่งตรวจคำขอและเขียนฟิลด์ที่ปกปิดแล้วลงตาราง contacts ก่อนคืน 201 พร้อม id และ messageลำดับข้อความของ contact form จากหน้า examples/company และ main.js ส่ง POST api contact ไปยัง Worker ซึ่งตรวจคำขอและเขียนฟิลด์ที่ปกปิดแล้วลงตาราง contacts ก่อนคืน 201 พร้อม id และ message
บนจอเล็ก เลื่อนแนวนอนเพื่ออ่านแผนภาพ หรือเปิดไฟล์ HTML จากลิงก์ใต้ภาพ

เปิดแผนภาพขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง

ความสัมพันธ์เดิมยังครบ: /examples/company/ กับ examples/company/main.js ส่ง POST /api/contact ไป src/worker.ts; Worker ใช้ D1 batch เขียนฟิลด์ที่ redacted ลง contacts แล้วคืน 201 {id, message} ให้หน้าเดิม ไม่มีลูกศรใดหมายความว่า browser เขียน D1 ได้โดยตรง

ฝึกอ่านภาพ: ชี้ข้อความแรกที่ข้าม trust boundary และหลักฐานสองจุดที่ยืนยัน success จริง โดยอย่างน้อยหนึ่งจุดต้องมาจาก response หรือ D1 ไม่ใช่ข้อความที่ frontend สร้างเอง

wrangler.jsonc กำหนด static assets และ run_worker_first: ["/api/*"]; /examples/company/ จึงเป็น asset และ /api/contact เป็น Worker route. Worker บังคับ Origin ให้เท่ากับ origin ของ request, Content-Type: application/json, object ที่มีเฉพาะ name, email, message, consent, website (และ turnstileToken สำหรับ non-demo). Idempotency-Key เป็น optional แต่หน้า demo ส่งให้ทุกครั้ง.

Lab: รันและตรวจ contact demo

  1. เตรียม local D1 และเปิด Worker. เปิด terminal หนึ่งหน้าต่างค้างไว้ที่ command ที่สอง:

    npm run db:local
    npm run dev
    

    ผลที่คาดหวัง: migration migrations/0001_initial.sql ถูก apply ลง local D1 และ Wrangler ฟังที่ http://localhost:3320. ถ้า port ถูกใช้แล้ว อย่าเปลี่ยน URL ใน curl โดยเดา; หยุด process เดิมหรือดู output ของ Wrangler ก่อน.

  2. เปิด http://localhost:3320/examples/company/. ส่ง form ด้วยชื่อและอีเมลตัวอย่าง, message ยาวอย่างน้อย 10 ตัวอักษร, tick consent. examples/company/main.js เรียก endpoint นี้จริง:

    fetch('/api/contact', { method: 'POST', headers: {
      'content-type': 'application/json', 'Idempotency-Key': submissionKey
    }})
    

    ผลที่คาดหวัง: หน้าจอแสดงข้อความ รับข้อความสาธิตแล้ว กรุณาอย่าส่งข้อมูลจริง และ reference id ที่เปลี่ยนทุก submission ใหม่.

  3. ทำซ้ำด้วย curl เพื่อเห็น contract และ Same-Origin rule ชัดเจน. Origin ต้องตรงกับ local Worker และ Idempotency-Key เป็น ASCII key ที่ยังไม่เคยใช้:

    curl -i -X POST http://localhost:3320/api/contact \
      -H 'Origin: http://localhost:3320' \
      -H 'Content-Type: application/json' \
      -H 'Idempotency-Key: company-lab-001' \
      --data '{"name":"Demo Learner","email":"learner@example.test","message":"ข้อความสำหรับทดสอบระบบสาธิต","consent":true,"website":""}'
    

    ผลที่คาดหวัง: HTTP 201 และ JSON รูปนี้ โดย id เป็น UUID ใหม่:

    {"id":"<uuid>","message":"รับข้อความสาธิตแล้ว กรุณาอย่าส่งข้อมูลจริง"}
    
  4. ส่ง payload ผิดเพื่อเรียนรู้ error ที่ Worker คืนจริง:

    curl -i -X POST http://localhost:3320/api/contact \
      -H 'Origin: http://localhost:3320' \
      -H 'Content-Type: application/json' \
      --data '{"name":"A","email":"not-email","message":"สั้น","consent":false,"website":""}'
    

    ผลที่คาดหวัง: HTTP 400 และ envelope เสมอเป็น { "error": { "code": "…", "message": "ข้อความภาษาไทย" } }. สำหรับ payload นี้ validation แรกของ Worker คือ name; ตัวอย่างผลคือ {"error":{"code":"INVALID_FIELD","message":"name ต้องยาว 2-100 ตัวอักษร"}}. ลองเอา Origin ออกเพื่อได้ 403 ORIGIN_REJECTED. Frontend ปัจจุบันแสดงข้อความ error จาก API ใน UI; เมื่อเขียน client ใหม่ให้ parse body.error.message ไม่ใช่สมมติว่า body.error เป็น string.

  5. ดู local database เพื่อพิสูจน์ redaction:

    npx wrangler d1 execute DB --local --command \
      "SELECT id, name, email, message, created_at FROM contacts ORDER BY created_at DESC LIMIT 3"
    

    ผลที่คาดหวัง: row จาก request ที่สำเร็จมี name, email, message เป็น [demo-redacted], ไม่ใช่ค่าตัวอย่างที่ส่ง. นี่เป็นหลักฐานว่า demo ไม่ต้องมี PII จริงเพื่อสอน endpoint/database flow.

  6. ทดลอง idempotency โดยส่ง command จากขั้น 3 ซ้ำ เหมือนเดิมทุก byte. ได้ 201 response เดิมและ header Idempotent-Replayed: true; ถ้าใช้ key เดิมแต่แก้ payload จะได้ 409 IDEMPOTENCY_CONFLICT. จากนั้นรัน:

    npm run typecheck
    npm run test
    npm run build
    

สิ่งที่ demo ทำ และสิ่งที่ยังไม่ใช่ production

หัวข้อ ใน DEMO_MODE=true ตอนนี้ เมื่อทำเป็นธุรกิจจริง (งานเพิ่ม)
contact data validate แล้ว redact ก่อน insert กำหนด lawful purpose, retention, access control, secure notification และ privacy copy
bot defense honeypot + daily limit; Turnstile skip ตั้ง TURNSTILE_SECRET, site key, hostname และทดสอบ verification
ส่งข้อความ เก็บ local D1 เท่านั้น เลือก provider/queue และป้องกัน replay/abuse ตาม policy
deploy local lab apply migration ไป environment ที่ตั้งใจ, preview smoke test, แล้วจึง deploy ตามบท 08

ส่วน “business implementation” ด้านขวาเป็นแนวทางออกแบบ ไม่ใช่เงื่อนไขที่ learner ต้องทำเพื่อผ่าน demo. OWASP แนะนำ syntactic และ semantic validation ตั้งแต่ต้นทาง และ parameterized statements สำหรับ SQL (Input Validation, SQL Injection Prevention); Worker ของ lab ใช้ D1 prepared statements/batch ตาม API ของ D1 (Cloudflare D1).

ข้อผิดพลาดที่พบบ่อย

อาการ สาเหตุ วิธีแก้
403 ORIGIN_REJECTED curl ไม่มี/ใช้ Origin คนละ port ใส่ Origin: http://localhost:3320 ตาม command
415 JSON_REQUIRED ไม่ได้ส่ง Content-Type ใช้ application/json
หน้า form บอก error แปลก ๆ client อ่าน body.error ทั้ง object อ่าน body.error.message ใน code ที่ปรับปรุง
D1 ไม่มี table ยังไม่รัน local migration รัน npm run db:local แล้ว restart dev หากจำเป็น
ค้นหา PII แล้วไม่พบ DEMO_MODE=true redact โดยเจตนา นี่คือ expected result; ห้ามปิด DEMO_MODE เพื่อทดสอบด้วย PII

แบบประเมิน

ผู้เรียนต้องสาธิต success 201, validation 400, origin 403 และ query ที่เห็น [demo-redacted]; อธิบายได้ว่าทำไม client-side validation และ browser success message ไม่ใช่ authority ของ data persistence. ส่งหลักฐานเป็น command/output ที่ปิดบัง id หากนำไปเผยแพร่.

อ่านต่อ: มาตรฐานคุณภาพและ commerce และ บท deploy.

บทถัดไป →

ความคืบหน้าบันทึกเฉพาะเบราว์เซอร์นี้ ไม่มีบัญชีผู้เรียนหรือการส่งข้อมูลการเรียนขึ้นเซิร์ฟเวอร์