Migration Supabase → Firebase : Brancher un Formulaire de Contact sur Firestore Sans Backend
เผยแพร่เมื่อ 29 April 2026
- ฉาก: แบบฟอร์มที่ไม่จัดเก็บสิ่งใดเลย
- ทำไมต้อง Firebase แทน Supabase?
- ระยะที่ 1: สร้างโปรเจกต์ Firebase
- ระยะที่ 2: เขียน JavaScript สำหรับการส่งใหม่
- ระยะที่ 3: ทำความสะอาดโค้ดที่ไม่ได้ใช้งานของ Supabase
- ระยะที่ 4 : กำหนดค่าส่วนท้าย
- ระยะที่ 5 : สถาปัตยกรรมสุดท้าย
- สิ่งที่การย้ายระบบครั้งนี้บอกเกี่ยวกับ dogfooding
- ขั้นตอนถัดไป (backlog)
- เอกสารอ้างอิง
เป็นเวลาหลายเดือน ฟอร์มติดต่อของเว็บไซต์นี้ทำงานบน JavaScript จำลอง — Promise ที่มีโอกาสสำเร็จ 85% Firestore ปลอม และไม่มีข้อมูลถูกจัดเก็บเลย แผนเริ่มแรกวางไว้ให้ใช้ backend Supabase พร้อม Google Apps Script สำหรับการแจ้งเตือนทางอีเมล ถูกยกเลิกไปแล้ว วันนี้ฉันเล่าเกี่ยวกับการย้ายไปยัง Firebase Firestore: การสร้างโปรเจกต์ กฎความปลอดภัย การเขียน JS ใหม่ การล้างโค้ดที่ไม่ได้ใช้งานของ Supabase และทำไมทางเลือกนี้จึงสะท้อนอะไรที่กว้างกว่าด้านปรัชญาการพัฒนา
ฉาก: แบบฟอร์มที่ไม่จัดเก็บสิ่งใดเลย
เว็บไซต์นี้ถูกสร้างขึ้นโดย JBake ซึ่งเป็นปลั๊กอิน Gradle ของฉัน`bakery`. มันเป็นแบบสแตติก 100 % — ไม่มีแบ็กเอนด์ ไม่มีฐานข้อมูล ยกเว้นว่าฉันมีแบบฟอร์มติดต่อ หน้า`contact.html`มีอยู่, HTML พร้อมแล้ว (ช่องชื่อ, อีเมล, โทรศัพท์, หัวข้อ, ข้อความ, การตรวจสอบความถูกต้อง HTML5, honeypot ป้องกันสแปม), สไตล์ Bootstrap ถูกจัดวางเรียบร้อยแล้ว เมื่อมองดูแล้วทุกอย่างสมบูรณ์แบบ
ยกเว้นว่าเมื่อส่งแล้วไม่มีอะไรเกิดขึ้น
const firebaseMock = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.85) {
resolve({ status: 201, message: 'Message stored in Firestore.' });
} else {
reject({ status: 500, message: 'Firestore write failed.' });
}
}, 1500);
});
ม็อกหนึ่งอัน คำสัญญาที่แสร้งทำเป็น ผู้ใช้เห็นสปินเนอร์ แล้วตามด้วยข้อความ «ส่งข้อความสำเร็จ!» แต่ข้อมูลกลับหายไปในความว่างเปล่า ไม่มีข้อความใดถูกจัดเก็บไว้ที่ไหนเลย
สถานการณ์นี้แย่กว่าแบบฟอร์มที่พัง — มันเป็นแบบฟอร์มที่โกหก
มรดก Supabase
แผนเริ่มต้น ซึ่งมีเอกสารบันทึกไว้ใน`content/draft/integration_formulaire_contact_supabase.adoc`, คาดการณ์ว่า:
-
ฐานข้อมูล Supabase พร้อมตาราง`contacts`และ Row Level Security
-
RPC หนึ่งรายการ`handle_contact_form`ฝั่งเซิร์ฟเวอร์
-
ทริกเกอร์ SQL ที่เรียกใช้เว็บฮุก Google Apps Script
-
Google Apps Script ที่ส่งอีเมลแจ้งเตือนผ่าน Gmail
โค้ด JavaScript ที่สอดคล้องกันยังคงมีอยู่ใน`script.js`. มีชั้นเรียนหนึ่ง`SupabaseManager`ซึ่งเริ่มต้นไคลเอนต์ Supabase ด้วยตัวแปรโกลบอล`SUPABASE_URL` et SUPABASE_KEY, และคลาสหนึ่ง`ContactFormHandler`ซึ่งฟังเหตุการณ์ submit ของฟอร์มและเรียก`SupabaseManager.submitContactForm()`.
ปัญหา: ตัวแปรโกลบอลเหล่านี้ไม่ถูกฉีดเข้าไปในฟุตเตอร์อีกต่อไป`<script src="supabase-js">`ถูกลบออกแล้ว โค้ดเรียก`supabase.createClient()`บนตัวแปร`supabase`ซึ่งไม่มีอยู่อีกแล้ว ดังนั้น:
console.error : 'Supabase client library (supabase-js) is not loaded.'
ไม่เพียงแต่ข้อมูลจะไม่ถูกจัดเก็บเท่านั้น แต่โค้ดสำหรับการส่งก็ใช้การไม่ได้แล้ว
การส่งซ้ำแบบผี
เพื่อทำให้เรื่องแย่ลงไปอีก มีการแข่งขันอันเงียบงันระหว่าง handler สองตัวบนฟอร์มเดียวกัน:
-
`contact.js`ฟัง submit แล้วเรียกใช้ mock Firebase
-
script.js— ผ่าน`ContactFormHandler`— ฟังเหตุการณ์ submit ด้วย เรียก`SupabaseManager`
ทั้งสองทำ`event.preventDefault() + event.stopPropagation(). เช่น`contact.js`ถูกโหลดก่อนใน`footer.thyme, ตัวจัดการของมันถูกผูกไว้ก่อน มันบล็อกการแพร่กระจาย`ContactFormHandler`จะไม่ถูกทริกเกอร์เลย
มันไม่ใช่แม้แต่บั๊กที่ยังทำงานอยู่ — มันคือซอมบี้ โค้ดที่ไม่เคยมีโอกาสได้ทำงาน
@startuml skinparam backgroundColor #FEFEFE title État Initial — Formulaire de Contact rectangle "contact.thyme\n(HTML Bootstrap, honeypot)" as FORM rectangle "footer.thyme\n(Firebase SDK avec config placeholder)" as FOOT rectangle "contact.js\n(mock Firebase, 85% succès)" as CONT rectangle "script.js\n(SupabaseManager + ContactFormHandler, morts)" as SCRIPT rectangle "Utilisateur" as USER USER -> FORM : Soumission du formulaire FORM -> CONT : submit event (attaché en premier) CONT -> CONT : preventDefault + stopPropagation CONT -> CONT : mock Promise (1.5s, aléatoire) note right of CONT ⚠️ Aucune donnée stockée end note FORM ..> SCRIPT : submit event (bloqué par stopPropagation) note right of SCRIPT : ❌ Jamais déclenché note right of SCRIPT : ❌ supabase-js non chargé @enduml
ทำไมต้อง Firebase แทน Supabase?
การตัดสินใจเรื่องการย้ายระบบได้รับการบันทึกไว้ใน`AGENT.adoc`No source text was provided to translate. Please send the French text you’d like translated into Thai.
ตอนนี้ Firebase ได้รับเลือกให้ใช้ด้วยเหตุผลดังต่อไปนี้: แพ็กเกจฟรีที่ดีกว่า, Firestore แบบเนทีฟ, Cloud Functions ที่ผสานรวมในตัว, ระบบนิเวศ Google ที่เหมาะสมกว่า ส่วนการใช้งาน Supabase ที่มีอยู่เดิมถูกทำเครื่องหมายว่า « ⚠️ ถูกยกเลิก
นอกเหนือจากแผนฟรีแล้ว ยังมีเหตุผลเชิงสถาปัตยกรรม เว็บไซต์นี้อยู่ในระบบนิเวศของ Google: รีโพซิทอรีเป้าหมายคือ`cheroliv.github.io`, CNAME ชี้ไปที่ GitHub Pages, การ build ด้วย Gradle จะ push ไปยัง GitHub ผ่าน JGit การเพิ่มบริการของ Google (Firebase) แทนบริการของบุคคลที่สาม (Supabase) ช่วยลดพื้นที่การกระจาย
Firestore ในโหมดเนทีฟ (ไม่ใช่โหมด Datastore) ก็ใกล้เคียงกับโมเดลความคิดแบบ NoSQL document ที่ฉันมีในหัวมากกว่าเช่นกัน: คอลเลกชัน เอกสาร ฟิลด์ที่มีชนิดข้อมูล ทิมสแตมป์จากเซิร์ฟเวอร์ และกฎความปลอดภัยในตัว
ระยะที่ 1: สร้างโปรเจกต์ Firebase
การเริ่มต้น
เนื่องจากไม่ได้ติดตั้ง Firebase CLI บนเครื่องของฉัน ฉันจึงใช้คอนโซลเว็บ:
-
ไปที่https://console.firebase.google.com/[คอนโซล Firebase]
-
สร้างโปรเจกต์`cheroliv-contact`(หรือนำโปรเจกต์ที่มีอยู่แล้วกลับมาใช้)
-
เปิดใช้งาน Firestore ในโหมดเนทีฟ (ไม่ใช่ Datastore)
-
สร้างฐานข้อมูลในภูมิภาค`eur3`(ยุโรป)
สำหรับการใช้งานแบบมินิมอลเหมือนของเรา (คอลเลกชันเดียว เขียนแบบสาธารณะ) โหมดเนทีฟเป็นตัวเลือกที่เหมาะสม ไม่จำเป็นต้องมีกฎ Datastore ที่ซับซ้อน
กฎความปลอดภัยของ Firestore
แบบฟอร์มนี้เป็นแบบสาธารณะ — ใครก็สามารถส่งข้อความได้ แต่ฉันต้องการจำกัดการละเมิด:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /contact_messages/{messageId} {
// Lecture : admin uniquement (authentifié)
allow read: if request.auth != null;
// Écriture : publique, mais limitée
allow create: if request.auth == null
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.name.size() <= 100
&& request.resource.data.email is string
&& request.resource.data.email.matches('.*@.*\\..*')
&& request.resource.data.email.size() <= 254
&& request.resource.data.subject is string
&& request.resource.data.subject.size() >= 3
&& request.resource.data.subject.size() <= 200
&& request.resource.data.message is string
&& request.resource.data.message.size() >= 10
&& request.resource.data.message.size() <= 5000
&& request.resource.data.created_at == request.time
&& request.resource.data.user_agent is string
&& request.resource.data.user_agent.size() <= 500;
}
}
}
ประเด็นสำคัญ:
-
allow read— เฉพาะผู้ใช้ที่ได้รับการยืนยันตัวตนเท่านั้นจึงจะสามารถอ่านข้อความได้ (ฉัน ผ่านคอนโซล Firebase) -
allow create— ใครก็สามารถสร้างเอกสารได้ แต่ต้องมีการตรวจสอบความถูกต้องของฟิลด์ -
การตรวจสอบฝั่งเซิร์ฟเวอร์: ขนาดต่ำสุด/สูงสุด, รูปแบบอีเมล,
created_at`ต้องสอดคล้องกับ`request.time(การป้องกันการปลอมแปลง) -
`user_agent`ถูกส่งเพื่อการตรวจสอบย้อนกลับ (ไม่สำคัญแต่มีประโยชน์)
กฎเหล่านี้เข้มงวดกว่าสิ่งที่เรียบง่าย`allow write: if true;`. สิ่งเหล่านี้ป้องกันไม่ให้ผู้โจมตีแทรกเพย์โหลดขนาดมหึมาหรือฟิลด์ที่ผิดรูปแบบ
ระยะที่ 2: เขียน JavaScript สำหรับการส่งใหม่
สัญญานั้นง่าย:
-
อ่านข้อมูลแบบฟอร์ม
-
ตรวจสอบฮันนี่พอท (ฟิลด์`hp_name`— ถ้าถูกกรอก แสดงว่าเป็นบอท เราจำลองความสำเร็จโดยไม่ส่งอะไรเลย)
-
โทร`addDoc(window.FIREBASE.collection(db, "contact_messages"), {…})`
-
แสดงความสำเร็จหรือข้อผิดพลาด
การพึ่งพา `window.FIREBASE
ใน`footer.thyme`, สคริปต์มอดูลเริ่มต้น Firebase SDK และเปิดเผยอ็อบเจ็กต์โกลบอล:
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-app.js";
import { getFirestore, collection, addDoc, serverTimestamp }
from "https://www.gstatic.com/firebasejs/11.6.0/firebase-firestore.js";
const firebaseConfig = { /* valeurs réelles */ };
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
window.__FIREBASE__ = { db, collection, addDoc, serverTimestamp };
</script>
สคริปต์โมดูลจะทำงานก่อน`DOMContentLoaded`, ดังนั้น`window.FIREBASE`รับประกันได้ว่าพร้อมใช้งานเมื่อแฮนด์เลอร์`contact.js`ถูกทริกเกอร์ โดยเป็นมาตรการป้องกัน ฉันยังเพิ่มการ polling ทุก 5 วินาที เผื่อว่า CDN จะช้า
contact.js` ใหม่
document.addEventListener('DOMContentLoaded', function () {
'use strict';
const form = document.getElementById('contact-form');
if (!form) return;
const submitButton = form.querySelector('button[type="submit"]');
const successMessage = document.getElementById('contact-success-message');
const errorMessage = document.getElementById('contact-error-message');
// Éléments de validation
const nameInput = form.querySelector('input[name="name"]');
const emailInput = form.querySelector('input[name="email"]');
const phoneInput = form.querySelector('input[name="phone"]');
const subjectInput = form.querySelector('input[name="subject"]');
const messageInput = form.querySelector('textarea[name="message"]');
const honeypotInput = form.querySelector('input[name="hp_name"]');
/**
* Attend que window.__FIREBASE__ soit disponible.
* Timeout de 5 secondes — si le CDN Firebase est lent, on abandonne.
*/
function waitForFirebase(timeoutMs = 5000) {
return new Promise((resolve, reject) => {
if (window.__FIREBASE__) {
resolve(window.__FIREBASE__);
return;
}
const start = Date.now();
const interval = setInterval(() => {
if (window.__FIREBASE__) {
clearInterval(interval);
resolve(window.__FIREBASE__);
} else if (Date.now() - start > timeoutMs) {
clearInterval(interval);
reject(new Error('Firebase SDK non disponible après timeout'));
}
}, 100);
});
}
// --- Validation (identique à l'existant) ---
function validateForm() {
nameInput.setCustomValidity('');
emailInput.setCustomValidity('');
if (phoneInput) phoneInput.setCustomValidity('');
subjectInput.setCustomValidity('');
messageInput.setCustomValidity('');
if (nameInput.value.trim().length < 1) {
nameInput.setCustomValidity('Veuillez saisir votre nom.');
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailInput.value.trim())) {
emailInput.setCustomValidity('Veuillez saisir une adresse email valide.');
}
if (phoneInput && phoneInput.value.trim() !== '') {
const phonePattern = /^\d{10,15}$/;
if (!phonePattern.test(phoneInput.value.trim())) {
phoneInput.setCustomValidity('Veuillez saisir un numéro valide (10 à 15 chiffres).');
}
}
if (subjectInput.value.trim().length < 3) {
subjectInput.setCustomValidity('Veuillez saisir un sujet (3 caractères minimum).');
}
if (messageInput.value.trim().length < 10) {
messageInput.setCustomValidity('Veuillez saisir un message (10 caractères minimum).');
}
form.classList.add('was-validated');
return form.checkValidity();
}
// --- Handler de soumission ---
form.addEventListener('submit', async function (event) {
event.preventDefault();
event.stopPropagation();
if (!validateForm()) return;
// Honeypot : si rempli, simuler un succès sans rien envoyer
if (honeypotInput && honeypotInput.value.trim() !== '') {
successMessage.style.display = 'block';
form.reset();
form.classList.remove('was-validated');
return;
}
// UI : état d'envoi
submitButton.disabled = true;
submitButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Envoi en cours...
`;
successMessage.style.display = 'none';
errorMessage.style.display = 'none';
try {
const fb = await waitForFirebase();
const messagesCollection = fb.collection(fb.db, 'contact_messages');
await fb.addDoc(messagesCollection, {
name: nameInput.value.trim(),
email: emailInput.value.trim(),
phone: phoneInput ? phoneInput.value.trim() : '',
subject: subjectInput.value.trim(),
message: messageInput.value.trim(),
created_at: fb.serverTimestamp(),
user_agent: navigator.userAgent.substring(0, 500)
});
successMessage.style.display = 'block';
form.reset();
form.classList.remove('was-validated');
} catch (error) {
console.error('Erreur Firestore:', error);
errorMessage.style.display = 'block';
} finally {
submitButton.disabled = false;
submitButton.innerHTML = `
<i class="bi bi-send me-2"></i>
Envoyer le Message
`;
}
}, false);
});
การเปลี่ยนแปลงเมื่อเทียบกับ mock:
-
waitForFirebase()— การ polling พร้อม timeout ซึ่งทนทานแม้ CDN จะช้า -
honeypot— หากฟิลด์ที่ซ่อนอยู่`hp_name`ถูกเติมแล้ว จำลองความสำเร็จโดยไม่มีการเรียก Firestore บอทเชื่อว่าสำเร็จแล้วแต่ไม่มีสิ่งใดถูกจัดเก็บ -
addDoc(collection, {…})— การเรียก Firestore จริงด้วย`serverTimestamp()` etuser_agent -
การจัดการข้อผิดพลาดด้วย`try/catch`อะซิงโครนัส
-
การทำความสะอาดของ`finally`(การคืนค่าปุ่ม)
|
ทำไม`user_agent`? เป็นตัวเลือก แต่มีประโยชน์สำหรับการวินิจฉัย หากมีข้อความแปลก ๆ เข้ามา การรู้ว่ามันมาจากเบราว์เซอร์เดสก์ท็อป มือถือ หรือสคริปต์ curl ช่วยในการคัดแยก |
ระยะที่ 3: ทำความสะอาดโค้ดที่ไม่ได้ใช้งานของ Supabase
`script.js`มี 250 บรรทัดของโค้ดที่ไม่ได้ใช้งาน:
-
SupabaseManager(บรรทัด 417-481) — 65 บรรทัด -
ContactFormHandler(บรรทัด 490-551) — 62 บรรทัด -
บล็อกการเริ่มต้น (บรรทัด 645-654) — 10 บรรทัด
รวม : ~140 บรรทัดที่ต้องลบ
บล็อก`DOMContentLoaded`สร้าง`SupabaseManager`แล้วหนึ่ง`ContactFormHandler`แนบกับแบบฟอร์ม ตามที่อธิบายไว้ข้างต้น โค้ดนี้จะไม่ถูกดำเนินการเลย (ถูกบล็อกโดย`contact.js`), และแม้ว่ามันจะทำงาน มันก็จะล้มเหลว (ไม่ได้โหลด SDK Supabase)
ฉันลบ:
-
ชั้นเรียน`SupabaseManager`
-
ชั้นเรียน`ContactFormHandler`
-
บล็อกการเริ่มต้นใน`DOMContentLoaded`(บรรทัดที่ 645-654)
Le reste de script.js`ยังคงสภาพสมบูรณ์ :`ThemeManager, ScrollToTopButton, MobileMenuManager, SmoothScrollWithOffset, NavbarHeightUpdater, DynamicNavbarBreakpoint, CodeBlockManager, TooltipManager, `PhoneInputManager`Please provide the French text you’d like me to translate into Thai.
ระยะที่ 4 : กำหนดค่าส่วนท้าย
`footer.thyme`มี boilerplate ของ Firebase อยู่แล้วแต่เป็นค่า placeholder ฉันกำลังแทนที่:
const firebaseConfig = {
apiKey: "REMPLACER_PAR_VOTRE_API_KEY",
authDomain: "REMPLACER_PAR_VOTRE_AUTH_DOMAIN",
projectId: "REMPLACER_PAR_VOTRE_PROJECT_ID",
storageBucket: "REMPLACER_PAR_VOTRE_STORAGE_BUCKET",
messagingSenderId: "REMPLACER_PAR_VOTRE_SENDER_ID",
appId: "REMPLACER_PAR_VOTRE_APP_ID"
};
ด้วยค่าจริงที่ดึงมาจากการตั้งค่าโปรเจกต์ > ทั่วไป > แอปของคุณ > เว็บแอปในคอนโซล Firebase
ค่าต่างๆ มีความอ่อนไหว (apiKey`เป็นสาธารณะโดยการออกแบบที่ Firebase แต่ฉันไม่ต้องการคอมมิตมันแบบเปิดเผย) ฉันเก็บมันไว้ใน`site.yml(มีอยู่แล้วใน`.gitignore`) และปลั๊กอิน`bakery`ฉีดสิ่งเหล่านั้นเข้าไปในเทมเพลตผ่านตรรกะที่ต้องเพิ่มฝั่ง build
ตอนนี้ฉันใส่มันโดยตรงใน`footer.thyme`— บิลด์`./gradlew serve`จะโหลดในเครื่อง เมื่อดีพลอย ฉันจะย้ายการฉีดไปยัง`site.yml`หรือไปยังตัวแปร Gradle
|
L'`apiKey`Firebase ไม่ใช่ไม่ความลับ มันเป็นสาธารณะโดยการออกแบบ สิ่งที่ปกป้องข้อมูลของคุณคือกฎความปลอดภัย Firestore, ไม่ใช่คีย์ API อย่าใส่ไว้ใน`.env`โหลดฝั่งเซิร์ฟเวอร์ — มีไว้เพื่อเปิดเผยต่อเบราว์เซอร์ |
ระยะที่ 5 : สถาปัตยกรรมสุดท้าย
@startuml
skinparam backgroundColor #FEFEFE
title Architecture Finale — Formulaire de Contact Firebase
actor Utilisateur as USER
package "Navigateur" #E8F5E9 {
rectangle "contact.thyme\n(HTML, honeypot)" as FORM
rectangle "contact.js\n(validation + Firestore)" as CONT
rectangle "footer.thyme\n(Firebase SDK init)" as FOOT
}
cloud "Firebase" #BBDEFB {
rectangle "Firestore\n(contact_messages)" as FS
}
FORM --> CONT : submit event
CONT --> FOOT : window.__FIREBASE__.addDoc()
FOOT --> FS : Insert document\n(règles de sécurité validées)
note right of FS
Firestore Rules :
- create: public, validé
- read: auth uniquement
end note
@enduml
สิ่งที่การย้ายระบบครั้งนี้บอกเกี่ยวกับ dogfooding
เว็บไซต์นี้ถูกสร้างขึ้นโดยปลั๊กอิน Gradle ของฉันเอง`bakery`. แบบฟอร์มติดต่ออยู่ภายในเว็บไซต์ การย้ายจาก Supabase → Firebase มีเอกสารประกอบอยู่ใน`AGENT.adoc`, มันถูกพูดคุยในแบ็กล็อก, มันถูกทดสอบผ่าน`./gradlew serve`, และมันสร้างบทความบล็อก (บทความที่คุณกำลังอ่านนี้)
นี่คือด็อกฟู้ดดิ้งแท้ ๆ เว็บไซต์เป็นผลิตภัณฑ์ของปลั๊กอิน ปลั๊กอินเป็นผลิตภัณฑ์ของนักพัฒนา และนักพัฒนาจัดทำเอกสารกระบวนการไว้ในเว็บไซต์เอง
วงจรได้ปิดลงแล้ว
การที่ต้องลากม็อกตัวหนึ่งไว้เป็นเวลาหลายเดือน (ทั้งเซสชันที่ฟอร์มโกหกอย่างเงียบ ๆ) ทำให้ฉันตระหนักถึงบางอย่าง: แบ็กล็อกของเว็บไซต์ส่วนตัวแบบสแตติกไม่มีทาง “เสร็จสมบูรณ์” ได้เลย มันมียูสสตอรี่ที่สำคัญกว่าอยู่เสมอ มีบทความที่ยังเป็นฉบับร่างอยู่เสมอ มีส่วนที่ถูกคอมเมนต์ไว้ในเทมเพลตอยู่เสมอ วินัยไม่ใช่การทำทุกอย่างให้เสร็จ — แต่คือการทำสิ่งที่ผู้ใช้มองเห็นให้เสร็จ
แบบฟอร์มติดต่อที่ใช้การไม่ได้ แย่กว่าการไม่มีแบบฟอร์มเลยด้วยซ้ำ นั่นคือคำสัญญาที่ไม่ได้รักษา
สรุปการแก้ไข
| ไฟล์ | การแก้ไข | ผลกระทบ |
|---|---|---|
|
การสร้างบทความ |
เอกสารประกอบ |
|
การเขียนใหม่ (mock → Firestore จริง) |
ใช้งานได้ |
|
การลบ SupabaseManager + ContactFormHandler + บล็อก init |
การทำความสะอาด |
|
การแทนที่ placeholder ของ config → ค่าจริง |
การกำหนดค่า |
ขั้นตอนถัดไป (backlog)
-
อีเมลแจ้งเตือน: Cloud Function หนึ่งรายการ`onCreate`บน`contact_messages`ซึ่งส่งอีเมลผ่าน SendGrid แบบฟอร์มบันทึกข้อมูล แต่ฉันไม่ได้รับการแจ้งเตือน ความสำคัญปานกลาง — ข้อความต่าง ๆ มองเห็นได้ในคอนโซล Firebase
-
การจำกัดอัตราการเรียกใช้ฝั่งไคลเอนต์: เพิ่ม timestamp ใน localStorage เพื่อป้องกันการส่งหลายครั้งแบบรัวๆ ฮันนี่พอตบล็อกบอทแบบง่ายๆ ส่วน rate limiter จะบล็อกบอทที่ฉลาดขึ้นเล็กน้อย
-
การทดสอบ: การทดสอบด้วย Playwright ที่ส่งแบบฟอร์มและตรวจสอบว่าเอกสารปรากฏใน Firestore สำหรับตอนนี้ ฉันกำลังทดสอบด้วยตนเองผ่าน`./gradlew serve`.