Programări online pe site cu Google Calendar, fără SaaS
Programări online pe site cu Google Calendar, fără abonament SaaS: sloturi libere din free/busy, protecție la programări duble, reamintiri și linkuri Meet.

Programări online pe site cu Google Calendar nu cer niciun abonament la un SaaS de programări. Site-ul citește intervalele ocupate din calendar prin API-ul free/busy al Google, transformă golurile în sloturi și creează un eveniment când un vizitator face o programare. Danube Labs rulează exact așa pe danubelabs.net, fără bază de date. Calendarul este locul unde stau datele. Articolul arată cum funcționează și când Calendly sau paginile de programări din Google Calendar sunt alegerea mai bună.
Cum funcționează programările online pe site cu Google Calendar?
Iată partea vizitatorului, pas cu pas:
- Vizitatorul deschide
/book. Nu are nevoie de cont. - Alege o zi și un slot, afișate în fusul lui orar.
- Introduce un nume, un email și, opțional, notițe.
- Serverul verifică din nou că slotul e liber, creează evenimentul în Google Calendar cu un link Meet și o cameră Jitsi de rezervă și trimite un email de confirmare. Google trimite și el propria invitație din calendar.
- Reamintirile ajung cu 24 de ore înainte de apel și în momentul în care începe.
Partea proprietarului este mai liniștită. O programare este un eveniment obișnuit în calendarul tău. Adminul are un ecran Appointments care listează următoarele 60 de zile grupate pe zile, cu un buton Start call care deschide linkul Meet. Anularea de acolo șterge evenimentul și anunță participantul. Programările făcute prin pagina de programări din Google Calendar (appointment schedule) apar în aceeași listă, pentru că sunt evenimente obișnuite în același calendar.
Cum obții sloturi libere când Google nu are un API pentru ele?
freebusy.query din Google returnează intervalele ocupate ale calendarelor pe care le ceri, între un timeMin și un timeMax. Nu returnează timp liber. Site-ul inversează răspunsul. Pornește de la programul tău de lucru, scade intervalele ocupate, iar ce rămâne sunt sloturile care se pot rezerva.
POST https://www.googleapis.com/calendar/v3/freeBusy
{
"timeMin": "2026-10-05T00:00:00Z",
"timeMax": "2026-10-26T00:00:00Z",
"items": [{ "id": "primary" }]
}Setările noastre sunt de luni până vineri, 10:00–18:00 ora Bucureștiului, apeluri de 30 de minute, o pauză de 15 minute, 12 ore de preaviz și 21 de zile înainte. O versiune simplificată a logicii:
// Simplificat. Codul real se află în src/lib/booking/slots.ts
const busy = await freeBusy(timeMin, timeMax, calendarId);
const slots = [];
for (const day of nextDays(21)) {
for (const start of workingStarts(day, "Europe/Bucharest", 30)) {
if (start < now + 12 * HOUR) continue; // preaviz minim
if (touchesBusy(busy, start, start + 30 * MIN, 15 * MIN)) continue; // pauză
slots.push(start);
}
}Programul de lucru este exprimat în ore de pe ceas în Europe/Bucharest și convertit în UTC câte o zi pe rând. O oră de start 10:00 înseamnă 08:00 UTC iarna și 07:00 UTC vara. Testele acoperă ambele părți ale limitei de trecere la ora de vară, iar noi le rulăm cu npm run test:slots după orice modificare a codului de sloturi.
Cum previi programările duble și le stochezi fără bază de date?
Protecție împotriva programărilor duble
Sloturile se recalculează la fiecare încărcare a paginii. Când vizitatorul trimite formularul, serverul cere din nou free/busy de la Google înainte să scrie ceva. Dacă slotul a fost luat între timp, site-ul răspunde cu HTTP 409, iar vizitatorul vede un mesaj că slotul tocmai a fost ocupat. Acel 409 este răspunsul nostru. API-ul Google Calendar folosește 409 pentru identificatori duplicați.
O reverificare nu este un lacăt de bază de date. Verificarea și scrierea sunt două apeluri separate, deci fereastra pentru o suprapunere este mică, dar reală. Dacă în calendarul tău intră multe programări pe minut, folosește o unealtă construită pe tranzacții.
Starea programării în proprietăți extinse private
În loc de bază de date, fiecare programare își păstrează starea pe evenimentul din calendar, în proprietăți extinse private:
| Cheie | Ce conține |
|---|---|
dlBooking | 1, marchează evenimentul ca fiind creat de site |
dlName, dlEmail | Cine a făcut programarea |
dlJitsi | Camera video fără cont |
dlLocale | Limba emailurilor |
dlTz | Fusul orar al vizitatorului |
dlRemind24, dlRemindStart | Marcaje de timp care opresc trimiterea unei reamintiri de două ori |
Documentația Google spune că proprietățile private sunt setate pe o singură copie a evenimentului, deci invitatul nu le vede. Cheile sunt limitate la 44 de caractere, valorile la 1.024, iar un eveniment la 300 de proprietăți și 32 kB în total, limite în care chei scurte ca acestea încap cu spațiu de rezervă. Apelul events.list poate filtra și după o proprietate, de exemplu privateExtendedProperty=dlBooking=1.
Cum funcționează emailurile de confirmare, reamintirile și linkurile de anulare?
Reamintiri dintr-un cron extern
Emailul de confirmare pleacă prin SMTP când se creează programarea, iar Google trimite separat invitația din calendar. Reamintirea de 24 de ore și emailul de la începutul apelului vin de la un singur endpoint, GET /api/cron/reminders. Un job gratuit pe cron-job.org îl apelează la fiecare 5 minute cu un bearer token:
curl -H "Authorization: Bearer $CRON_SECRET" https://danubelabs.net/api/cron/remindersPlanificatoarele de joburi ale hostului depind de host și de plan. Planul Hobby Vercel permite o singură rulare de cron pe zi și face să eșueze deploy-ul dacă o expresie ar rula mai des. Funcțiile programate Netlify rulează în UTC, au o limită de 30 de secunde, rulează doar pe deploy-uri publicate și sunt un tip de funcție separat de o rută Next.js. Un planificator extern care apelează o rută obișnuită ține programul independent de host.
Endpoint-ul este idempotent. Fiecare trimitere este înregistrată chiar pe eveniment, în dlRemind24 sau dlRemindStart, deci o dublă declanșare nu trimite nimic de două ori. Fereastra pentru emailul de la începutul apelului merge de la 5 minute înainte până la 10 minute după start, pe care un cron la 5 minute nu o poate rata. În configurația noastră, cron-job.org dezactivează un job după aproximativ 25 de rulări eșuate, așa că reactivează-l dacă endpoint-ul a întors erori câteva ore.
Linkuri de anulare semnate
Emailul de confirmare conține un link de forma /book/cancel?id=…&t=…. Valoarea t este un HMAC al id-ului evenimentului, cu o cheie secretă care nu părăsește niciodată serverul, comparat cu timingSafeEqual. Să cunoști id-ul unui eveniment nu ajunge ca să anulezi apelul altcuiva. O versiune minimă a ideii în Node:
import { createHmac, timingSafeEqual } from "node:crypto";
function sign(id) {
return createHmac("sha256", process.env.BOOKING_SECRET).update(id).digest("hex");
}
function isValid(id, token) {
const a = Buffer.from(sign(id));
const b = Buffer.from(token);
return a.length === b.length && timingSafeEqual(a, b);
}Algoritmul de hash și codificarea rămân la alegerea ta. Verificarea lungimii contează pentru că timingSafeEqual aruncă o eroare când buffer-ele au dimensiuni diferite.
De ce Google Meet plus o cameră Jitsi de rezervă?
Crearea unui eveniment cu link Meet cere conferenceDataVersion=1 în cerere și un requestId unic în conferenceData.createRequest. Fără parametru, Google elimină tăcut conferința. Reutilizarea unui request id între evenimente scurge accesul la întâlnire, așa că fiecare eveniment primește unul propriu.
POST https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=all
{
"summary": "Intro call",
"start": { "dateTime": "2026-10-12T07:00:00Z" },
"end": { "dateTime": "2026-10-12T07:30:00Z" },
"attendees": [{ "email": "visitor@example.com" }],
"conferenceData": {
"createRequest": {
"requestId": "unique-per-event",
"conferenceSolutionKey": { "type": "hangoutsMeet" }
}
},
"extendedProperties": { "private": { "dlBooking": "1", "dlTz": "Europe/London" } }
}Meet are o problemă pentru vizitatorii din afară. Un invitat fără cont Google trebuie să bată la ușă și să aștepte să fie primit, dacă accesul la întâlnire nu este setat pe Open, așa cum descriu setările de acces la întâlnire din Google. De aceea fiecare programare primește și o cameră Jitsi cu un nume aleatoriu, imposibil de ghicit, de felul https://meet.jit.si/danubelabs-…. Jitsi Meet este gratuit și open source și nu cere cont și nici sală de așteptare. Ambele linkuri intră în emailul de confirmare și în evenimentul din calendar, iar noi urmărim ambele linkuri când se apropie un apel.
Cum funcționează fusurile orare pentru vizitator și pentru proprietar?
O programare se stochează ca un singur moment absolut. Fusurile orare schimbă doar felul în care este afișat, deci vizitatorul și proprietarul văd mereu același moment.
- Vizitatorul vede sloturile în fusul orar al dispozitivului, citit cu
Intl.DateTimeFormat().resolvedOptions().timeZone, cu o listă derulantă prin care îl poate schimba. - Emailurile folosesc fusul din care a făcut programarea vizitatorul, stocat ca
dlTzpe eveniment. - Proprietarul vede ora Bucureștiului în admin, indiferent ce arată ceasul laptopului, plus ora locală a vizitatorului pe fiecare card.
- Google Calendar afișează orele în propriul fus de afișare, așa că setează-l pe București în setările Google.
Citim fusul orar al dispozitivului în loc să ghicim după adresa IP, pentru că ceasul dispozitivului rămâne corect în spatele unui VPN și în călătorii. Serverul grupează sloturile după data din calendarul nostru, iar widget-ul le regrupează după data vizitatorului. Fără acest pas, un vizitator din Auckland ar vedea un slot de marți la 03:30 listat sub luni.
Ce cere configurarea OAuth cu Google?
Parola de aplicație Gmail nu ajută aici. Ea autentifică doar SMTP, în timp ce API-ul Calendar vorbește OAuth 2.0 și o respinge, indiferent de planul Workspace. Configurarea OAuth se face o singură dată:
- Creează un proiect Google Cloud și activează Google Calendar API.
- Setează tipul de utilizator al ecranului de consimțământ OAuth pe Internal.
- Adaugă scope-ul
https://www.googleapis.com/auth/calendar. - Creează un OAuth client ID de tip Web application, cu un redirect URI local pentru consimțământul unic.
- Rulează o dată un script mic, aprobă accesul și copiază refresh token-ul pe care îl afișează.
Setarea Internal contează. Documentația OAuth Google spune că un proiect cu ecran de consimțământ External în starea Testing primește refresh token-uri care expiră în 7 zile. Internal este disponibil când deții un domeniu Google Workspace, sare peste verificarea Google, iar în configurația noastră nu este supus regulii celor 7 zile. Nu îl comuta pe External.
Șapte variabile de mediu poartă configurația: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN, GOOGLE_CALENDAR_ID, GOOGLE_CALENDAR_TIMEZONE, BOOKING_SECRET și CRON_SECRET. Pe Netlify ele ajung la funcțiile care rulează abia după următorul build, așa că fă un redeploy după ce le setezi.
Când sunt Calendly sau paginile de programări din Google alegerea mai bună?
O pagină de programări făcută la comandă costă timp de dezvoltare și îți lasă de întreținut OAuth, un planificator și livrarea emailurilor. O unealtă gata făcută este alegerea mai bună când:
- Ai nevoie de plăți, programare round-robin pentru echipe sau integrări CRM, pe care Calendly le vinde în planurile plătite.
- Oferi multe tipuri de evenimente. Planul gratuit Calendly are un singur tip de eveniment și o singură conexiune de calendar, iar automatizările și reamintirile sunt în planurile plătite. Verifică pagina curentă de prețuri.
- Primești multe programări pe minut și ai nevoie de garanții tranzacționale.
- Nimeni din echipa ta nu poate întreține codul.
Paginile de programări din Google Calendar nu cer cod. Oferă o pagină de programare, Google Meet, emailuri de confirmare și până la 5 reamintiri, iar vizitatorii nu trebuie să fie autentificați într-un cont Google. Un cont personal poate crea o singură pagină de programări de bază, iar pagina de comparație Google pune paginile multiple și plățile pe planurile plătite.
| Soluție proprie | Pagină de programări Google | Calendly | |
|---|---|---|---|
| Cost | Timp de dezvoltare, unelte gratuite | Inclus, funcții premium în planurile plătite | Plan gratuit cu limite, planuri plătite per utilizator |
| Aspect și domeniu | Complet ale tale | Pagină găzduită de Google | Pagină găzduită, personalizabilă |
| Unde stau programările | În calendarul tău | În calendarul tău | La Calendly, plus un calendar conectat |
| Întreținere | Tu | Furnizorul |
Construiește-ți propria soluție când ai deja un site și un programator, vrei programările în designul și pe domeniul tău, ai nevoie de unul sau două tipuri de evenimente și vrei ca datele să rămână în propriul calendar.
Încearcă fluxul de programare pe danubelabs.net
Apelul introductiv gratuit de pe pagina noastră de programări rulează exact pe acest sistem. Danube Labs adaugă programări în site-uri ca parte din dezvoltarea web, de la paginile de programări din Google până la un flux la comandă ca acesta. Site-ul din spate funcționează fără bază de date, ceea ce explicăm în articolul despre CMS bazat pe Git, iar rezultatul îl poți vedea în studiul de caz danubelabs.net.
Întrebări frecvente
- Cum adaug programări pe site cu Google Calendar?
- Conectează site-ul la Google Calendar API prin OAuth, citește orele ocupate cu freebusy.query și transformă golurile din programul tău de lucru în sloturi. Când un vizitator face o programare, creezi un eveniment în calendar cu link Meet. Ai nevoie de un client OAuth, de un loc unde să păstrezi un refresh token și de un expeditor de email. Paginile de programări din Google fac același lucru fără cod.
- Are Google Calendar un API pentru sloturi libere?
- Nu. Metoda freebusy.query returnează doar intervalele ocupate ale calendarelor cerute, între o oră de început și una de sfârșit. Ca să oferi sloturi, pornești de la programul tău de lucru, scazi intervalele ocupate, aplici o pauză și un preaviz minim și listezi ce rămâne. Inversarea înseamnă câteva zeci de linii de cod, iar schimbările de oră de vară cer teste.
- Cum previi programările duble fără bază de date?
- Recalculezi sloturile libere la fiecare încărcare a paginii și verifici din nou free/busy în momentul trimiterii, înainte să creezi evenimentul. Dacă slotul a dispărut, returnezi HTTP 409 și ceri vizitatorului să aleagă altul. Este o reverificare, nu un lacăt, deci o cursă foarte strânsă este posibilă în teorie. Calendarele foarte aglomerate ar trebui să folosească o unealtă construită pe tranzacții.
- Au vizitatorii nevoie de cont Google ca să facă o programare?
- Nu pe pagina de programări de pe danubelabs.net. Vizitatorii aleg un slot și scriu un nume și un email. Nici paginile de programări din Google Calendar nu cer autentificare pentru a rezerva. Pentru apelul în sine, un invitat Meet fără cont Google trebuie să bată la ușă ca să fie primit, așa că fiecare programare primește și o cameră Jitsi care nu cere cont.
- De ce expiră refresh token-urile Google OAuth după 7 zile?
- Documentația Google spune că un proiect al cărui ecran de consimțământ este External, cu starea de publicare Testing, primește refresh token-uri care expiră în 7 zile. Un ecran de consimțământ Internal, disponibil pentru domeniile Google Workspace, nu intră în acest caz în configurația noastră, motiv pentru care Danube Labs folosește Internal la integrarea de programări.
- Când să folosesc Calendly în loc să construiesc propriul sistem de programări?
- Folosește Calendly sau o unealtă similară când ai nevoie de plăți, programare round-robin pentru echipe, integrări CRM sau multe tipuri de evenimente, ori când nimeni nu poate întreține codul. Construiește-ți propria soluție când vrei programările în designul și pe domeniul site-ului tău, ai un programator, ai nevoie de unul sau două tipuri de evenimente și vrei datele în propriul calendar.


