// Studija slučaja

Prva Lekcija — sajt za pripremu mature sa nalozima i online naplatom

Priprema za maturu u Zagrebu kojoj je trebao sajt koji ume da naplati i isporuči. Digitalni materijal se plaća karticom, isporučuje se sam, a kupac ga posle nalazi na svom nalogu. Sve u sopstvenom PHP kodu, sa Stripe stranicom plaćanja i bez ijednog podatka o kartici na našem serveru.

Klijent
Prva Lekcija
Delatnost
Priprema za državnu maturu — instrukcije, eseji, online predavanja
Područje
Zagreb i cela Hrvatska, online
Tip
Sajt sa korisničkim nalozima i online naplatom
Jezik
Hrvatski (hr-HR)
Tehnologije
PHP 8.3 · MariaDB · Stripe Checkout · nginx
Status
Živ sajt, naplata aktivna
Naslovna strana sajta Prva Lekcija sa naslovom Uspjeh na maturi počinje ovdje
Naslovna — „Uspjeh na maturi počinje ovdje" i dva puta dalje: eseji ili predavanja

Čime se Prva Lekcija bavi

Prva Lekcija je zagrebačka priprema za državnu maturu. Nudi tri stvari koje se međusobno razlikuju više nego što se na prvi pogled čini: gotove eseje u PDF-u, online predavanja koja se snimaju i ostaju dostupna polaznicima, i instrukcije — individualne ili u manjoj grupi, uživo i online.

Sajt je na hrvatskom i pisan je za hrvatske maturante. To nije kozmetička stavka. Hrvatska državna matura ima svoje kriterije bodovanja eseja, svoju lektiru i svoj raspored, pa je ceo rečnik na sajtu vezan za taj sistem. Dokument je označen kao hr-HR, u strukturiranim podacima područje delovanja je Hrvatska, kontakt je zagrebački, a i traka za kolačiće govori hrvatski — što zvuči kao sitnica dok ne vidite koliko sajtova iz regiona posetiocu na lokalnoj strani servira englesku traku.

Eseji su glavni proizvod. Pisani su prema službenim kriterijima bodovanja, sa objašnjenjem kako se koji deo boduje. Na strani Eseji za maturu stoji i besplatan primer — analiza Gospode Glembajevih — koji se preuzima odmah, bez ostavljanja podataka, da učenik pre kupovine vidi šta tačno dobija.

Naslovna ima i blok sa brojkama o dosadašnjem radu — to su tvrdnje klijenta o sopstvenom poslu, ne naše merenje.

Šta je trebalo rešiti

Zahtev nije bio „napravi sajt za instrukcije". Imao je četiri tvrde tačke:

  • Prodaja digitalnog sadržaja — kupac plati i istog trenutka dobije materijal, bez čekanja da mu neko ručno pošalje PDF.
  • Nalog koji pamti šta je kupljeno — jednom plaćen sadržaj mora da bude dostupan i sledeći put, sa bilo kog uređaja.
  • Naplata za hrvatsko tržište — u evrima, na hrvatskom jeziku, sa iznosom koji je isti na sajtu i na stranici plaćanja.
  • Jedno mesto za sve upite — poruka sa kontakt-forme mora negde da sleti i mora da se vidi.

Uz to je išao deo koji se ne vidi. Sajt sa prijavom i naplatom mora da izdrži da mu neko zaviri u zahteve i pokuša da ih prepravi. Tri stvari se pri tome ne smeju odlučivati u brauzeru: cena, pravo pristupa i trenutak isporuke. Ako bilo koja od te tri putuje sa klijenta, prodavnica se otvara ključem koji se vidi u konzoli.

Prolaz kroz kod izvukao je i jedan tih problem. Kontakt-forma je izgledala kao da radi — dugme reaguje, poruka o uspehu se ispiše — ali zahtev nikada nije napuštao stranicu. Svaki upit se gubio, a niko to nije mogao da primeti sa sajta. O tome niže, jer je to jedna od onih grešaka koje koštaju najviše i vide se najmanje.

Tri strane, tri različita zadatka

Eseji, predavanja i instrukcije izgledaju kao tri stavke istog menija, a prodaju se na tri različita načina. Strana eseja prodaje fajl. Strana predavanja prodaje termin i snimak. Strana instrukcija prodaje dogovor. Zato nijedna od njih nije ista šablonska strana sa promenjenim tekstom.

Eseji počinju besplatnim primerom, pa tek onda idu na komplet. Redosled je namerno takav: učenik prvo vidi kvalitet, pa mu se onda nudi kupovina. Ispod dugmeta stoji jasna rečenica da se za kupovinu prvo treba prijaviti — jer bez naloga nema gde da se isporuči ono što se plati.

Online predavanja podeljena su po predmetima, sa ispisanim sadržajem svakog predavanja: na koje delove gradiva se fokusirati, šta donosi najviše bodova, kako izbeći česte greške, kako ubrzati rešavanje zadataka. Naglasak je na ispitnoj logici, a ne na ponavljanju gradiva, i to je rečeno otvoreno na strani. Predavanja se snimaju i ostaju dostupna posle termina, uz sažete materijale.

Instrukcije nude izbor između individualnog rada i rada u manjoj grupi, sa kratkim opisom šta se za koji dobija. Tu se ne kupuje online — tu se zakazuje termin, pa zato ta strana vodi na kontakt, a ne u korpu.

Svaka od tri strane ima svoj naslov, svoj opis i svoj kanonik. Komercijalne strane su ranije nosile naslove od dvadesetak znakova, iako je u rezultatu pretrage raspoloživo skoro tri puta toliko prostora — prepisani su tako da već u rezultatu kažu o čemu je reč i za koji ispit.

Nalog, prijava i lični račun

Prodaja digitalnog sadržaja bez naloga nema smisla. PDF poslat na mejl izgubljen je čim se izgubi taj mejl, a link sa tokenom je link koji se prosleđuje dalje. Zato sajt ima registraciju, prijavu i lični račun na kome kupac vidi šta je kupio i kada.

Prijavni deo je bio i mesto gde je nađen jedan skup problem. Forme su radile na serveru, ali ih frontend nije zvao: posle jednog ranijeg refaktora JavaScript je tražio klase i imena polja koja u HTML-u više nisu postojala, pa je slanje bilo utišano — bez greške u konzoli, bez ijednog zahteva ka serveru. Prijava, registracija i kontakt bile su tri različite forme koje su sve delovale živo, a nijedna nije radila. Nađeno je tek proverom u pravom pregledaču. Nijedan test sa komandne linije to ne bi uhvatio, jer je API sve vreme odgovarao tačno.

Oko prijave stoji i sloj koji korisnik ne vidi: ograničenje broja pokušaja po IP adresi, obnova identifikatora sesije posle uspešne prijave, i pravilo da svaka radnja koja menja stanje naloga nosi token koji dokazuje da zahtev dolazi sa sajta, a ne sa tuđe strane. Korisnik ne može da vidi tuđe kupovine ni tako što u adresu upiše tuđi broj naloga — provera se radi na serveru, nad prijavljenim korisnikom, ne nad onim što je stiglo u zahtevu.

Naplata — kako danas radi

Naplata ide preko Stripe stranice plaćanja. Kupac sa sajta odlazi na Stripe, tamo unosi podatke o kartici i vraća se nazad. Na našem serveru ne postoji nijedan broj kartice, nijedno polje koje ga prima, nijedan zapis o njemu — i to je namerno, jer je najjeftiniji način da se čuvaju tuđi podaci o kartici taj da se nikada ne dodirnu.

Redosled je važan i izveden je ovako. Sa sajta se šalje samo oznaka proizvoda. Cenu server uzima iz svoje baze; iznos poslat iz brauzera se ne gleda. Pre nego što se uopšte pozove Stripe, u bazu se upisuje red u stanju pending, da bi njegov broj mogao da uđe u podatke sesije plaćanja. Tek onda se otvara stranica plaćanja.

Isporuku radi jedno jedino mesto u kodu. Ono prevodi kupovinu iz pending u completed uslovnim upisom: ako je red već pomerio neko drugi, drugi poziv ne uradi ništa i ne pošalje drugi mejl. To je bitno zato što potvrdu o plaćanju sajt dobija sa dve strane — od povratne strane na koju se kupac vraća i od obaveštenja koje Stripe šalje direktno na server. Obe putanje zovu isti kod, a samo jedna od njih stvarno pomeri red.

Povratna strana ne veruje adresi na koju je kupac vraćen. Da bi zaključila da je plaćeno, ona ponovo pita Stripe za tu sesiju i gleda njegov odgovor, ne parametre iz linka. Obaveštenje koje stiže na server autentikuje se potpisom — bez ispravnog potpisa zahtev ne dodiruje bazu. Provereno je i suprotno: star potpis, izmenjen sadržaj i potpis sa pogrešnim ključem svi padaju.

Za kupca je posledica jednostavna. Dok plaćanje nije potvrđeno, sadržaj se ne isporučuje i ne pojavljuje se na nalogu. Kada jeste, pojavi se sam. Ako je isti komplet već kupljen, sajt ne otvara novu naplatu nego to kaže.

Cena koju kupac vidi na stranici plaćanja

Ovaj nalaz zaslužuje svoj deo, jer se ne može naći nijednim testom sa komandne linije.

Stripe nalog je podrazumevano imao uključenu opciju koja posetiocu izvan eurozone prikaže isti iznos preračunat u njegovu valutu. Zvuči kao usluga kupcu. Problem je kurs: preračun ide po Stripe kursu, koji je bio osetno lošiji od tržišnog, pa je posetilac iz Srbije na stranici plaćanja video iznos koji je u realnoj vrednosti bio nekoliko procenata veći od onog koji piše na sajtu.

Sajt kaže jednu cenu, stranica plaćanja naplaćuje drugu. Za sajt koji prodaje maturantima to nije tehnička sitnica nego pitanje poverenja, a kupac nema način da razliku primeti dok sam ne uporedi kurs.

Opcija je isključena pri kreiranju sesije plaćanja. Posle toga stranica plaćanja prikazuje tačan iznos u evrima, na hrvatskom, isti kao na sajtu, bez obzira odakle posetilac dolazi.

Ono što ovde vredi zapamtiti je metod, ne opcija. Zahtev je bio ispravan, odgovor servera ispravan, iznos u podacima sesije ispravan — greška je postojala samo na ekranu koji vidi kupac. Otud pravilo koje se drži i na ostalim projektima: naplata se ne prima kao gotova dok se kroz nju ne prođe u pravom pregledaču, kao pravi kupac.

Kontakt-forma koja se pravila da radi

Kontakt-forma na naslovnoj imala je dugme, polja i poruku o uspešnom slanju. Nije imala backend. Skripta iza nje odbrojala bi nešto vremena i ispisala potvrdu — bez ijednog zahteva ka serveru. Svaki upit poslat preko sajta bio je izgubljen u trenutku slanja, a posetilac je pri tome video da je poruka uspešno poslata.

Napisan je pravi prijemnik. Validacija se radi na serveru i vraća poruke na hrvatskom. Forma nosi skriveno polje kao zamku za automate — ako je popunjeno, poruka se odbacuje bez traga. Postoji ograničenje broja poruka po IP adresi u jedinici vremena, jer je kontakt-forma najlakša meta za masovno slanje. Svaka ispravna poruka se prvo upisuje u bazu, pa se tek onda pokušava mejl.

Taj redosled je ceo trik. Mejl je stvar koja može da otkaže iz deset razloga koji ne zavise od sajta — pun sandučić, greška u adresi, filter na drugoj strani. Ako se poruka upiše u bazu pre slanja, otkazan mejl znači samo da obaveštenje nije stiglo, a ne da je upit nestao. Sve poruke se vide u admin panelu, sa oznakom pročitano i mogućnošću brisanja.

Isti prolaz je zatvorio i mrtve linkove. Nekoliko dugmadi na sajtu vodilo je na sidro koje nikada nije postojalo, pa je klik ostavljao posetioca na istom mestu, bez ikakve reakcije — a to su bila upravo dugmad tipa Rezerviraj mjesto i Kupi eseje, dakle ona koja nose ceo posao. Sada vode na kontakt sekciju, odnosno na kupovinu.

Tehnička izvedba

Sajt je pisan u PHP-u 8.3 nad MariaDB bazom, bez WordPress-a i bez ijednog dodatka koji bi trebalo pratiti i ažurirati. Strane su podeljene po sekcijama — svaka sekcija ima svoj PHP, svoj CSS i svoj JS — pa se jedna sekcija naslovne može zameniti bez diranja ostatka strane.

Ni Stripe nije uvučen kao biblioteka. Ceo posao su tri poziva prema njihovom API-ju i provera potpisa, što ne opravdava ceo vendor/ direktorijum koji se posle održava i ažurira. Napisan je mali klijent na cURL-u, sa fiksiranom verzijom API-ja, tako da promena na njihovoj strani ne može tiho da promeni ponašanje naplate.

Pristupni podaci za bazu i ključevi za naplatu ne stoje u kodu nego u .env fajlu izvan webroota, čitljivom samo procesu koji pokreće sajt. U repozitorijumu nema nijedne tajne. Osetljivi fajlovi, rezervne kopije i .git direktorijum server odbija — provereno zahtevom, ne pretpostavkom.

Ispred PHP-a stoji nginx sa kešom gotovih strana. Anonimni posetilac dobija stranu iz keša, prijavljeni je nikada ne dobija iz keša. Da bi to radilo, sesija se na običnim stranama nastavlja samo ako kolačić već postoji — inače bi svaki slučajni posetilac dobio kolačić sesije i time oborio keš celom sajtu. Vreme do prvog bajta izmereno je na 30 do 50 milisekundi na svim stranama, i keširano i nekeširano.

Jedan nalaz sa ovog posla ne vidi se ni u kodu ni u brzini. Domen je imao IPv6 adresu, ali je server slušao samo na IPv4 — posetioci koji su dolazili preko IPv6 dobijali su tuđi podrazumevani sertifikat i upozorenje pregledača da veza nije bezbedna. Sajt je za njih izgledao kao da je provaljen. Popravljeno je dodavanjem IPv6 slušanja u konfiguraciju; obe adrese sada odgovaraju istim sertifikatom.

Brzina, pristupačnost i ono što je ostalo otvoreno

Pomeranje sadržaja pri učitavanju je nula na svim stranama — ništa ne skače pod prstom dok se strana slaže. Naslovna na telefonu meri 94 za performanse u Google merenju, sa najvećim elementom prikazanim za oko 2,6 sekunde.

Pristupačnost je dovedena do 100 na merenim stranama, i to ne kozmetički. Sajt donedavno nije imao main element ni na jednoj strani, a preskočni link na vrhu — onaj koji čitači ekrana koriste da preskoče navigaciju — ciljao je sidro koje postoji samo na naslovnoj, pa je na tri od četiri glavne strane vodio u prazno. Oboje je popravljeno. Uz to su polja kontakt-forme povezana sa svojim natpisima, a naslovi po stranama poređani u ispravnu hijerarhiju, tako da sve strane prolaze W3C proveru bez ijedne greške.

Ono što je ostalo otvoreno, ostalo je otvoreno svesno. Tailwind se učitava sa mreže i sklapa u brauzeru, što je jedina preostala stavka koja blokira prikaz i najviše košta na podstranama. Popravka je lokalni build, ali on nosi rizik po vizuelni sloj sajta koji je već objavljen i prodaje — pa je odložen za trenutak kada može da se odradi sa punom proverom na svim uređajima, umesto na brzinu. Kada u izveštaju stoji jedan broj koji nije najbolji mogući, iza njega obično stoji ovakva odluka, a ne previd.

Sertifikat je pod automatskom obnovom, sa dnevnom proverom roka. Sajt je na spisku za prinudni HTTPS na nivou pregledača, a zaglavlja bezbednosti su postavljena u punom setu.

SEO, schema i vidljivost

Svaka strana ima svoj naslov, svoj opis i tačan kanonik. Mapa sajta sadrži samo šest pravih adresa i svih šest vraća ispravan odgovor — prijava, registracija, nalog i panel su izbačeni, jer strana koja posetioca preusmerava nema šta da traži u mapi. Povratna strana posle plaćanja nosi zabranu indeksiranja i nije u mapi, iz očiglednog razloga.

Sajt nosi EducationalOrganization i WebSite strukturirane podatke, a strana eseja i Product sa ponudom. Ta ponuda se generiše iz istog reda baze iz koga se ispisuje i prikazana cena, tako da se prikaz i strukturirani podatak ne mogu razići — najčešća greška kod prodavnica je da se cena promeni na jednom mestu, a u strukturiranim podacima ostane stara. Ocene i recenzije nisu ubačene, jer ih nema; izmišljena ocena u strukturiranim podacima je najbrži način da strana ispadne iz rezultata.

Za pretragu preko jezičkih modela sajt je dobio pravila za veštačku inteligenciju u robots.txt i llms.txt na hrvatskom — kratak opis šta sajt nudi i gde se šta nalazi, pisan tako da ga model pročita i tačno prepriča. Isti fajl zatvara pristup API-ju, panelu i korisničkom nalogu.

Analitika radi sa pristankom po standardu Consent Mode v2: dok posetilac ne pristane, merenje se ne pali. Traka za kolačiće nudi i odbijanje, a izbor se pamti — uključujući i povratnu posetu, gde se pristanak obnavlja bez ponovnog pitanja.

Za hrvatsko tržište pripremljen je i poseban sloj prodajnih uslova. Pravo na odustajanje ne radi isto za PDF, koji je digitalni sadržaj, i za predavanje, koje je usluga — pa idu dva odvojena pristanka, a ne jedna kvačica za sve. Taj deo čeka podatke klijenta pre objave.

Admin panel i šta klijent na kraju ima

Panel je odvojen od sajta i radi ono što se stvarno radi svakog dana: korisnici, proizvodi sa cenom i sadržajem, kupovine sa statusima, poruke sa kontakt-forme i podešavanja. Kroz podešavanja se menja adresa na koju stižu obaveštenja o upitima, link za live predavanje i identifikator analitike — bez diranja koda i bez mene.

Panel je i mesto gde se dodaje sadržaj koji se prodaje. Proizvod bez sadržaja može da se napravi, ali onda kupac plati i nema šta da preuzme — pa je popunjavanje sadržaja prvi korak posle svake nove stavke u katalogu, a ne nešto što se ostavlja za kasnije.

Svaka radnja u panelu koja menja stanje upisuje se u zaseban dnevnik na serveru: ko, šta i kada. Administrator ne može sebe da obriše ni da sebi skine ovlašćenja, jer je to najlakši način da sajt ostane bez ijednog administratora.

Praktično, dan izgleda ovako. Učenik nađe sajt, preuzme besplatan primer eseja, napravi nalog i plati komplet karticom. Sadržaj mu se pojavi na nalogu čim Stripe potvrdi uplatu, bez ijednog koraka sa druge strane. Ako umesto toga hoće instrukcije, popuni kontakt-formu — poruka sleti u bazu i u panel, i vidi se i onda kada obaveštenje na mejl iz bilo kog razloga ne stigne.

Klijent na kraju ima sajt koji je njegov. Kod i baza stoje na serveru, mogu se prekopirati i preseliti; nema mesečne pretplate platformi ni provizije po prodaji osim onoga što uzima obrađivač kartica. Sajt je živ, naplata je aktivna, a izmene se rade na živom sajtu, sa rezervnom kopijom pre svake.

// Sajt uživo

Prva Lekcija

Eseji po službenim kriterijima bodovanja, online predavanja i instrukcije za hrvatsku državnu maturu.

prvalekcija.com

Često postavljana pitanja

Da li se podaci o kartici čuvaju na sajtu?
Ne. Unos podataka o kartici dešava se na Stripe stranici plaćanja, van sajta. Na serveru ne postoji nijedno polje koje prima broj kartice, pa nema ni šta da procuri. Sajt zna samo da je kupovina plaćena i koji je proizvod u pitanju.
Šta ako kupac plati pa zatvori prozor pre povratka na sajt?
Isporuku ne radi povratna strana sama. Potvrda o plaćanju stiže i direktno na server, a oba puta vode u isti kod koji sadržaj isporučuje tačno jednom. Kupovina se pojavljuje na nalogu kada je uplata potvrđena, bez obzira gde je kupac u tom trenutku.
Može li neko da izmeni cenu pre plaćanja?
Ne može. Sa sajta se šalje samo oznaka proizvoda; cenu server uzima iz svoje baze i iznos poslat iz brauzera uopšte ne gleda. Isto važi i za pravo pristupa — sadržaj se izdaje samo za kupovinu koja je potvrđeno plaćena.
Da li vlasnik sam može da menja cene i sadržaj?
Može, kroz admin panel. Proizvodi, cene, sadržaj koji se isporučuje, podešavanja i adresa za obaveštenja menjaju se bez diranja koda. Cena prikazana na sajtu i cena koja se naplaćuje dolaze iz istog zapisa, pa ne mogu da se raziđu.
Zašto sajt traži prijavu pre kupovine?
Zato što se prodaje digitalni sadržaj. Bez naloga nema gde da se isporuči ono što je plaćeno, a materijal poslat samo mejlom nestaje sa tim mejlom. Sa nalogom kupac ima svoje kupovine na jednom mestu i dolazi do njih sa bilo kog uređaja.
Koliko traje izrada ovakvog sajta sa naplatom?
Sama naplata je manji deo posla nego što se očekuje. Najviše vremena odnesu katalog, sadržaj koji se isporučuje i provera graničnih slučajeva — od duplog klika na dugme do zakasnele uplate. Realno se meri nedeljama, ne danima.

Treba vam ovakav sajt?

Recite mi čime se bavite i šta sajt treba da uradi za vas — dobijate fiksnu ponudu i rok, u pisanoj formi.

Pozovi