Skip to content
Watchara NoisriphanAI พารวย by Watchara | AI Builder

พิมพ์เพื่อเริ่มค้นหา

ต่อ analytics จดหมายข่าว และฟอร์มติดต่อ เข้าเว็บ static โดยไม่ต้องมี backend

เว็บ static ไม่มีเซิร์ฟเวอร์ให้รับข้อมูล แต่ยังเก็บสถิติ รับสมัคร subscriber และรับข้อความได้ครบ บันทึกบริการที่ผมใช้จริง วิธีสมัคร ค่าใช้จ่าย และกับดักที่ทำให้ฟอร์มพังเงียบ ๆ

อ่าน 15 นาที

เว็บนี้เป็น static site ล้วน ๆ — Astro build ออกมาเป็นไฟล์ HTML แล้วโยนขึ้น Vercel ไม่มีเซิร์ฟเวอร์ ไม่มีฐานข้อมูล ไม่มี API สักตัว

ปัญหาคือเว็บที่ไม่มี backend ก็ไม่มีที่รับข้อมูล พอจะทำ 3 อย่างนี้ก็ติดหมด:

  • อยากรู้ว่ามีคนเข้าเว็บไหม
  • อยากให้คนสมัครรับจดหมายข่าว
  • อยากให้คนติดต่อกลับได้

ทางออกที่ใช้คือ ไม่ต้องมี backend ของตัวเอง แต่ไปยืมของคนอื่น ฟอร์มบนเว็บยิงตรงไปที่บริการภายนอก แล้วบริการนั้นเป็นคนเก็บข้อมูลและส่งอีเมลแจ้งเรา

เส้นทางข้อมูลจากเว็บ static ไปยังบริการภายนอก สคริปต์ analytics ฟอร์มจดหมายข่าว และฟอร์มติดต่อบนเว็บ static ยิงตรงไปที่ Umami, Buttondown และ Formspree ตามลำดับ ผลลัพธ์ไปโผล่ที่หน้า dashboard รายชื่อ subscriber และอีเมลใน inbox โดยไม่มีเซิร์ฟเวอร์ของเราเองอยู่ในเส้นทาง เว็บ static ของเรา backend ของคนอื่น ที่ที่เราไปดูผล สคริปต์ analytics ฟอร์มจดหมายข่าว ฟอร์มติดต่อ Umami Cloud Buttondown Formspree หน้า dashboard รายชื่อ subscriber อีเมลเข้า inbox โหลดจากโดเมนภายนอก action = URL ภายนอก action = URL ภายนอก cloud.umami.is buttondown.com formspree.io จำนวนผู้เข้าชม ในระบบจดหมายข่าว ของเราโดยตรง
ในกรอบเส้นประคือทั้งหมดที่เราดูแลเอง — ไฟล์ HTML ล้วน ไม่มีเซิร์ฟเวอร์ ไม่มีฐานข้อมูล งานที่ต้องมีคนรับข้อมูลถูกส่งข้ามกรอบออกไปทั้งหมด

บทความนี้บันทึกว่าผมเลือกอะไร เพราะอะไร ต้องเอาอะไรมาแปะบ้าง และเจอกับดักอะไรระหว่างทาง

บริการที่ใช้จริง

งานบริการค่าใช้จ่ายตอนเริ่มต้องเอาอะไรมาแปะ
AnalyticsUmami CloudHobby plan ฟรีwebsite ID
จดหมายข่าวButtondownฟรี 100 subscribers แรกusername
ฟอร์มติดต่อFormspreeฟรี 50 submissions/เดือนform endpoint URL

ทั้งสามตัวเริ่มได้โดยไม่ต้องผูกบัตร และไม่ต้องเขียนโค้ดฝั่งเซิร์ฟเวอร์เลย

เลือกเพราะเป็น cookieless — ไม่ฝัง cookie ไม่เก็บข้อมูลส่วนบุคคล เลยไม่ต้องมีแถบขออนุญาต cookie ให้คนกดรำคาญ และตัวโปรเจกต์เป็น open source ถ้าวันหนึ่งไม่อยากใช้ cloud ก็ย้ายไป self-host ได้

วิธีขอ: สมัครที่ cloud.umami.is → Add website → ใส่ชื่อกับโดเมน จากนั้นหน้า Tracking code จะให้ script tag มาแบบนี้

<script defer src="https://cloud.umami.is/script.js" data-website-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></script>

ที่ต้องเก็บไว้คือค่าใน data-website-id

ตัวเลข event ต่อเดือนของ Hobby plan ผมยืนยันตัวเลขไม่ได้ (หน้า pricing เป็น JavaScript ล้วน ดึงมาอ่านตรง ๆ ไม่ได้) เอกสารระบุแค่ว่า Hobby plan ฟรีและเหมาะกับโปรเจกต์ส่วนตัวหรือเว็บทราฟฟิกไม่สูง ถ้าต้องการตัวเลขแน่นอนให้ดูที่ หน้า pricing โดยตรง

Buttondown — จดหมายข่าวสำหรับคนเขียนคนเดียว

เลือกเพราะฟรีช่วงเริ่มต้น เขียนด้วย Markdown ได้ และมีหน้า archive ให้อัตโนมัติ

วิธีขอ: สมัครที่ buttondown.com → ตั้ง username ของ newsletter → หน้า Embedding จะให้ฟอร์มตัวอย่างมา หน้าตาแบบนี้

<form action="https://buttondown.com/api/emails/embed-subscribe/USERNAME" method="post">
  <input type="email" name="email" />
  <input type="submit" value="Subscribe" />
</form>

ที่ต้องเก็บคือ URL ใน action ทั้งเส้น ไม่ใช่แค่ username

ราคา: ฟรีสำหรับ 100 subscribers แรก เกินจากนั้นคิดตามจำนวน ส่วนฟีเจอร์เสริมแยกขายเป็นรายตัว เริ่มที่ $9/เดือน (เช่น tagging, segmentation, analytics) ไปจนถึง $29 และ $79/เดือน สำหรับของอย่าง custom domain archive หรือ whitelabel (รายละเอียด)

ข้อดีคือช่วงเริ่มต้นที่ยังไม่มีคนอ่าน ไม่ต้องจ่ายอะไรเลย

Formspree — ฟอร์มติดต่อที่ส่งเข้าเมลตรง ๆ

เลือกเพราะติดตั้งง่ายที่สุดในสามตัว แค่เปลี่ยน action ของฟอร์มเป็น URL ที่เขาให้มา ก็จบ

วิธีขอ: สมัครที่ formspree.io → New Form → ตั้งชื่อและใส่อีเมลที่จะให้ส่งเข้า จะได้ endpoint มาหน้าตาแบบนี้

https://formspree.io/f/xxxxxxxx

ข้อจำกัดของ free plan: 50 submissions ต่อเดือน, สร้างฟอร์มได้ไม่จำกัด, เก็บประวัติย้อนหลัง 30 วัน, ผูกอีเมลรับได้ 2 address (รายละเอียด)

50 ครั้งต่อเดือนสำหรับเว็บ portfolio ถือว่าพอ และเขาจะส่งเมลเตือนเมื่อใช้ถึง 50%, 75% และ 90%

วิธีต่อเข้าเว็บ: อย่า hardcode

จุดที่อยากเน้นคือ อย่าเอาค่าพวกนี้ไปเขียนแปะไว้ในโค้ดตรง ๆ ให้อ่านจาก environment variable แทน

ในเว็บนี้ใช้ astro:env ประกาศ schema ไว้ที่ astro.config.ts ทุกตัวเป็น optional

PUBLIC_NEWSLETTER_PROVIDER: envField.enum({
  context: 'client',
  access: 'public',
  values: ['buttondown', 'beehiiv', 'convertkit', 'custom'],
  optional: true,
}),

ได้ประโยชน์ 3 อย่าง:

1. เปลี่ยนเจ้าได้โดยไม่แตะโค้ด — ถ้าวันหนึ่งย้ายจาก Buttondown ไป ConvertKit แก้แค่ค่าใน environment variable ตัว component ไม่ต้องแก้เลย เพราะชื่อ field ที่แต่ละเจ้าต้องการ ถูก map ไว้ในที่เดียว

2. เว็บ build ผ่านแม้ยังไม่ได้ต่ออะไรเลย — ตอนที่ยังไม่ใส่ค่า ฟอร์มจะไม่หายไปเฉย ๆ แต่จะแสดงข้อความบอกว่ายังไม่ได้เชื่อมต่อ ดีกว่าปล่อยให้ฟอร์มดูเหมือนใช้ได้แต่ข้อมูลหายจริง

3. ไม่มีความลับหลุด — ค่าพวกนี้ขึ้นต้นด้วย PUBLIC_ หมด เพราะมันถูก compile เข้าไปในไฟล์ที่ browser โหลด แปลว่าห้ามเอา API key หรือ secret มาใส่ตรงนี้เด็ดขาด ทั้งสามค่าที่ใช้อยู่เป็นข้อมูลที่เปิดเผยได้อยู่แล้ว (website ID, username, form URL)

ไฟล์ .env หน้าตาสุดท้ายเป็นแบบนี้

PUBLIC_ANALYTICS_PROVIDER=umami
PUBLIC_ANALYTICS_SITE_ID=<website-id-จาก-umami>
PUBLIC_NEWSLETTER_PROVIDER=buttondown
PUBLIC_NEWSLETTER_ENDPOINT=https://buttondown.com/api/emails/embed-subscribe/<username>
PUBLIC_CONTACT_ENDPOINT=https://formspree.io/f/<form-id>

กับดัก 3 ข้อที่เจอมาเอง

1. endpoint ต้องเป็น URL เต็ม ไม่งั้นข้อมูลหายเงียบ

อันนี้เจ็บที่สุด ตอนแรกผมเอา id ที่ copy มาใส่ตรง ๆ แบบนี้

PUBLIC_NEWSLETTER_ENDPOINT=2517b869-6aa5-4ff5-ba18-a45871477602

โค้ดเช็คแค่ว่า “มีค่าไหม” ไม่ได้เช็คว่า “เป็น URL ไหม” มันเลยผ่านเป็นสถานะเชื่อมต่อแล้ว แล้ว build ออกมาเป็น

<form action="2517b869-6aa5-4ff5-ba18-a45871477602" method="POST">

action ที่ไม่ขึ้นต้นด้วย https:// browser จะตีความเป็น path บนเว็บเราเอง คนกดสมัครก็จะถูกส่งไปที่หน้าที่ไม่มีอยู่จริง เจอ 404 และอีเมลหายไปเฉย ๆ

อาการนี้แย่กว่าตอนที่ยังไม่ได้ต่ออะไรเลย เพราะหน้าเว็บดูปกติทุกอย่าง ไม่มีอะไรฟ้อง

กฎง่าย ๆ: ทุก endpoint ต้องขึ้นต้นด้วย https:// ถ้าที่ copy มาเป็น id เปล่า ๆ แปลว่ายังไม่ครบ

2. ใส่ค่าในเครื่องอย่างเดียวไม่พอ

.env อยู่ใน .gitignore — ซึ่งถูกแล้ว ไม่ควร commit ขึ้น git แต่ผลคือ โฮสต์มองไม่เห็นค่าพวกนี้เลย ต้องไปใส่ในหน้า Environment Variables ของโฮสต์อีกรอบ

และเพราะค่า PUBLIC_* ถูกฝังเข้าไฟล์ตอน build ใส่เฉย ๆ แล้วไม่ deploy ใหม่จะไม่มีผล ผมเจอเองตอนไปเช็คเว็บจริงแล้วพบว่าไม่มี script ของ analytics โผล่มาสักตัว

3. CSP บล็อกสคริปต์ที่เพิ่งใส่

ถ้าเว็บมี Content Security Policy อยู่ การเพิ่ม analytics เจ้าใหม่ต้องเปิดโดเมนของเขาใน CSP ด้วย ทั้งใน script-src (สำหรับโหลดสคริปต์) และ connect-src (สำหรับตอนสคริปต์ส่งข้อมูลกลับ)

ที่ต้องระวังคือบางเจ้า โหลดสคริปต์จากโดเมนหนึ่ง แต่ส่งข้อมูลกลับไปอีกโดเมนหนึ่ง ถ้าเปิดแค่โดเมนแรกจะกลายเป็นสคริปต์โหลดได้แต่ส่งข้อมูลไม่ออก — เงียบสนิท ไม่มี error ให้เห็นชัด ๆ

กรณีของ Umami คือตัวอย่างของกับดักนี้เต็ม ๆ และตอนเขียนบทความนี้รอบแรกผมเขียนตรงนี้ผิด — ผมสรุปไปเองว่าโหลดสคริปต์กับส่งข้อมูลอยู่โดเมนเดียวกัน เลยไม่ได้แก้ CSP อะไรเพิ่ม

ความจริงคือสคริปต์โหลดจาก cloud.umami.is แต่ ส่ง event ไปที่ gateway.umami.is คนละโดเมนกัน พอ connect-src เปิดไว้แค่โดเมนแรก เบราว์เซอร์ก็บล็อกทุก request ที่ส่งออก

ผลคือหน้าเว็บปกติดีทุกอย่าง สคริปต์โหลดสำเร็จ ตรวจด้วย curl ก็เห็น data-website-id ครบ แต่ dashboard ไม่ขึ้นสักตัวเลข เพราะไม่มีข้อมูลออกไปถึงเลยสักครั้งเดียว

จับได้ตอนเปิดเว็บจริงด้วยเบราว์เซอร์แล้วดู console เจอบรรทัดนี้

Connecting to 'https://gateway.umami.is/api/send' violates the following
Content Security Policy directive: "connect-src 'self' ... https://cloud.umami.is"

วิธีดูว่าเจ้าที่ใช้อยู่ส่งข้อมูลไปโดเมนไหน คืออ่านจากตัวสคริปต์เขาตรง ๆ

curl -s https://cloud.umami.is/script.js | grep -o 'gateway\.umami\.is'

แล้วเติมโดเมนที่ได้เข้าไปใน connect-src ไม่ใช่แค่ script-src

ตรวจว่าติดจริงไหม อย่าเดา

บทเรียนที่ได้จากรอบนี้คือ ต้องไปดูเว็บจริง ไม่ใช่ดูแค่ในเครื่องแล้วเชื่อว่าน่าจะขึ้น

ตรวจว่า analytics ติดแล้วหรือยัง

curl -s https://<โดเมนคุณ>/ | grep -o 'data-website-id="[^"]*"'

ตรวจว่าฟอร์มชี้ไปถูกที่

curl -s https://<โดเมนคุณ>/contact | grep -o '<form[^>]*>'

สิ่งที่ควรเห็นคือ action ที่ขึ้นต้นด้วย https:// และเป็นโดเมนของบริการที่ต่อไว้ ถ้าเห็นเป็น path สั้น ๆ หรือ id เปล่า ๆ แปลว่าโดนกับดักข้อ 1 เข้าแล้ว

ส่วนที่ curl ตรวจแทนไม่ได้คือการกดส่งจริง — ต้องลองสมัครด้วยอีเมลตัวเองแล้วดูว่าชื่อขึ้นในระบบไหม และลองส่งฟอร์มติดต่อแล้วดูว่าเมลเข้า inbox จริงหรือเปล่า

อีกอย่างที่ curl ตรวจไม่ได้เลยคือ CSP เพราะ curl ดาวน์โหลด HTML มาเฉย ๆ ไม่ได้บังคับใช้ CSP เหมือนเบราว์เซอร์ กับดักข้อ 3 ข้างบนผมเลยหลุดรอดมาได้ทั้งที่ curl บอกว่าทุกอย่างเรียบร้อย ต้องเปิดเว็บจริงด้วยเบราว์เซอร์แล้วดู console ถึงจะเห็น

สรุป

เว็บ static ทำงานพวกนี้ได้ครบโดยไม่ต้องมีเซิร์ฟเวอร์ของตัวเอง แค่เปลี่ยนวิธีคิดจาก “ฉันต้องเขียน API มารับ” เป็น “ฉันส่งต่อให้คนที่ทำเรื่องนี้อยู่แล้ว”

สิ่งที่อยากให้จำกลับไป 3 ข้อ:

  1. ให้ค่าทุกอย่างมาจาก environment variable เปลี่ยนเจ้าได้ทีหลังโดยไม่ต้องรื้อโค้ด
  2. endpoint ต้องเป็น URL เต็ม ไม่งั้นฟอร์มจะพังแบบไม่มีอะไรฟ้อง
  3. เช็คบนเว็บจริงด้วยเบราว์เซอร์จริง curl บอกได้แค่ว่า HTML ถูกต้องไหม แต่บอกไม่ได้ว่าเบราว์เซอร์ยอมให้ส่งข้อมูลออกจริงหรือเปล่า

ค่าใช้จ่ายรวมตอนเริ่มต้นคือ ศูนย์บาท — และจะเริ่มจ่ายก็ต่อเมื่อมีคนอ่านมากพอจนเกิน free tier ซึ่งถึงตอนนั้นก็เป็นปัญหาที่น่ามีมากกว่า

แชร์:FacebookXLinkedIn

รับบทเรียน AI ที่ใช้ได้จริง ทางอีเมล

หนึ่งบทเรียน AI ที่ใช้ได้จริง การแกะโปรเจกต์ หรือข้อคิดจากการสร้างของ สัปดาห์ละครั้ง

ไม่มีสแปม ยกเลิกได้ทุกเมื่อ