// Studija slučaja

Leskovački Kutak — roštilj sa dostavom, sajt i mobilna aplikacija

Restoran sa dve kuhinje na dva kraja Beograda kome je trebao sajt koji sam zna kuda porudžbina ide. Kupac otkuca adresu, sistem izabere bližu kuhinju, porudžbina padne na ekran u lokalu bez osvežavanja, a dostavljač je vidi na telefonu. Uz to jelovnik koji vlasnik menja sam i aplikacija koja se instalira direktno sa sajta.

Klijent
Leskovački Kutak
Delatnost
Leskovački roštilj, restoran i dostava hrane
Područje
Beograd — Veliki Mokri Lug i Leštane
Tip
Višestrani sajt sa online porudžbinom i aplikacijom
Tehnologije
PHP 8.3 · MariaDB · PWA · Android TWA
Status
Živ sajt, u aktivnom održavanju
Naslovna strana sajta Leskovački Kutak
Naslovna — leskovački roštilj sa dostavom u Beogradu

Čime se Leskovački Kutak bavi

Leskovački Kutak je roštilj sa dve lokacije u Beogradu — Veliki Mokri Lug na Zvezdari i Leštane u Grockoj. Po sopstvenoj priči rade od 2018. godine, sa istim receptima i istom kuhinjom na oba mesta.

Jelovnik je klasičan južnjački: pljeskavice od male do Kutak pljeskavice punjene kačkavaljem i slaninom, ćevapi, vešalice, uštipci, pileći program, roštilj na kilogram, prilozi, deserti i pića. Ukupno blizu devedeset stavki raspoređenih u sedam kategorija.

Posao ima dva toka koja se ne mešaju. Jedan je gost koji sedne u lokalu, drugi je dostava — besplatna iznad minimalnog iznosa porudžbine, svakog dana do pola jedanaest uveče. I tu je čvor celog projekta: dve lokacije nisu jedna firma sa dva ulaza, nego dve kuhinje sa svojim radnim vremenom, svojim telefonom, svojim poštanskim sandučetom i svojim delom grada. Porudžbina koja padne u pogrešnu kuhinju je hladna hrana i izgubljen kupac.

Šta je trebalo rešiti

Zahtev nije bio sajt sa jelovnikom u PDF-u. Trebao je pogon koji prima porudžbine i vodi ih do prave kuhinje, a klijent nije hteo da za to plaća proviziju po svakoj porudžbini.

  • Porudžbina mora da zna kuda ide — kupac ne bira lokal iz padajuće liste napamet, nego sistem bira po adresi na koju se dostavlja.
  • Adresa mora da bude tačna — pogrešno otkucana ulica je propala vožnja i hladna hrana.
  • Kuhinja mora da vidi porudžbinu odmah, bez osvežavanja stranice i bez čekanja da neko pozove telefonom.
  • Dostavljač radi sa telefona, ne sa papirića iz kuhinje.
  • Jelovnik menja vlasnik — cene, dostupnost, slike i prilozi, bez mene i bez dodirivanja koda.
  • Program lojalnosti koji se ne vodi u svesci — povrat se obračunava sam i troši se sam.

Uz to je išlo ono što se retko pominje u ponudi, a mora da postoji: uslovi korišćenja, politika privatnosti, saglasnost za kolačiće koja se stvarno poštuje i strana za prijave za posao, jer se u ugostiteljstvu ljudi traže neprekidno.

Struktura sajta i put do porudžbine

Javni deo ima naslovnu, jelovnik, stranu sa lokacijama, o nama, kredite, kontakt, galeriju, stranu o dostavi, česta pitanja, karijeru i dve pravne strane. Uz to ide sedam tematskih strana pisanih za pretragu — leskovački roštilj, roštilj Beograd, ćevapi, pljeskavica, dostava hrane, restoran i porodični obrok.

Jelovnik je podeljen na sedam kategorija sa ispisanim brojem stavki u svakoj, a filter se drži u adresi (?kategorija=), kao i izbor lokala (?restoran=). Svaka stavka ima sopstvenu adresu oblika /proizvod/kutak-pljeskavica, punu sliku, gramažu i opis — dakle svoju stranu koja može da se podeli i da se indeksira.

Prilozi nisu spisak zalepljen na sve. U bazi postoji podešavanje po kategoriji i posebno po pojedinačnom proizvodu, pa se uz pljeskavicu nudi jedno, uz pileći program drugo, a uz piće ništa. To je sitnica koju gotova rešenja obično rešavaju tako što ponude sve uz sve.

Korpa proverava dve stvari pre nego što pusti porudžbinu dalje: da li je dostignut minimalni iznos za dostavu (za lično preuzimanje ga nema) i da li je adresa u dometu izabranog lokala. Ako nije, kupac ne dobija tihu grešku nego jasnu poruku i broj telefona tog lokala.

Nalog nije uslov. Gost ostavi ulicu, kućni broj i telefon i porudžbina prolazi; nalog služi za kredite, sačuvane adrese i istoriju porudžbina.

Adresa, kućni broj i najbliža kuhinja

Ovo je deo koji se ne vidi, a najviše je vredeo. Polje za adresu ne veruje kupcu da će tačno otkucati ulicu, nego mu je nudi iz registra: u bazi sajta stoji 12.263 ulice i 297.101 kućni broj, pa se dok se kuca šapuće ime ulice, a zatim i broj koji za tu ulicu stvarno postoji.

Kada je adresa poznata, deo sistema zadužen za izbor lokala računa razdaljinu do obe kuhinje i predlaže bližu. Ako brauzer da koordinate, računa se direktno iz njih; ako ne da, traži se najsličnija ulica u registru pa se uzimaju njene koordinate; ako ni to ne uspe, ostaje podrazumevani lokal. Tri koraka, ali nikad prazan odgovor — porudžbina se ne prekida zato što neko ne dozvoljava pristup lokaciji.

Praktična posledica je jednostavna: kupac iz Leštana ne poruči iz Mokrog Luga i ne čeka duplo, a lokal ne vozi preko pola grada za jednu pljeskavicu. Ista računica služi i kao brana — ako je adresa van dometa, porudžbina se ne prima ćutke nego se kupac uputi na telefon lokala, gde se dogovor može napraviti kao i pre sajta.

Pet površina nad jednom bazom

Sajt je samo jedno lice sistema. Uz njega idu četiri odvojena poddomena, svaki sa svojim korenom i svojom prijavom: aplikacija za kupca, panel za lokal, panel za dostavljača, administracija — i REST sloj preko koga sve to razgovara sa istom bazom.

Uloge su četiri: kupac, lokal, dostavljač i administrator. Svaka vidi tačno svoj deo. Vlasnik jednog lokala ne vidi porudžbine drugog, dostavljač vidi samo ono što mu je dodeljeno, a administracija vidi sve.

Nova porudžbina ne čeka da neko osveži stranicu. Server je gura kroz trajnu vezu (Server-Sent Events), pa se na ekranu u kuhinji pojavi sama, dostavljač dobije svoju čim mu je dodele, a kupac prati kako mu se status menja. Za kuhinju postoji i štampa naloga, jer ekran je ekran, a papir ide uz kesu.

Sve to stoji u jednoj MariaDB bazi sa trideset sedam tabela — proizvodi, kategorije, prilozi i njihova podešavanja po kategoriji i po proizvodu, lokali, porudžbine i stavke, korisnici i adrese, krediti sa nivoima i transakcijama, promo kodovi, preporuke, obaveštenja i zone dostave. Jedna baza znači i da izveštaj o prometu ne mora da se sklapa iz tri izvora.

Krediti — program lojalnosti koji radi sam

Program lojalnosti je na strani Krediti i namerno je jednostavan za objasniti u jednoj rečenici: jedan kredit vredi jedan dinar, kreditima se plaća do polovine računa, i krediti ne ističu.

Procenat povrata zavisi od mesečne potrošnje i penje se kroz četiri nivoa — bronza, srebro, zlato i platina. Nivo se ne bira i ne traži; sistem sabira potrošnju u tekućem mesecu, sam dodeljuje nivo i svaku promenu upisuje u istoriju, da posle može da se proveri zašto je neko dobio koliko je dobio. Isto važi i za svaki dodeljeni i potrošeni kredit — knjiži se kao transakcija, ne kao ručno dopisivanje u tabeli.

Uz to ide pozivnica za prijatelja: svaki nalog ima svoj kod, novi kupac dobija povrat na prve porudžbine, a onaj ko ga je pozvao dobija svoj deo istog obračuna.

Razlog zašto povrat, a ne popust: popust obara cenu jednom i tu se završi, a kredit ostaje na nalogu i daje kupcu razlog da se vrati po drugu porudžbinu. Za restoran koji živi od ponavljanja to je smisleniji potez, a i lakše se objasni za pultom.

Aplikacija na telefonu

Na strani o aplikaciji stoji dugme koje je instalira direktno sa sajta — bez prodavnice i bez preuzimanja instalacije. To je progresivna veb aplikacija: manifest, servisni radnik i ikonica koja posle instalacije stoji na početnom ekranu kao svaka druga.

Kupac dobija ono što od aplikacije i očekuje: otvaranje jednim dodirom, zapamćenu prijavu, istoriju porudžbina, obaveštenja o statusu i rad iz keša kada veza pukne u liftu ili u podrumu. Aplikacija ima sopstveni poddomen i svoj ekran za prijavu, pa se ne meša sa sajtom.

Za Android je nad istom aplikacijom napravljen i pravi paket — TWA omotač koji u pozadini otvara istu adresu. Da bi radio bez adresne trake, na poddomenu stoji assetlinks.json u /.well-known/ i potvrđuje da su sajt i paket ista strana; bez tog fajla verifikacija pada i aplikacija izgleda kao brauzer sa logotipom. Paket je potpisan, a ključ za potpisivanje čuva se odvojeno i van radne mašine — ako se izgubi, kasnija ažuriranja nisu moguća ni pod kojim uslovom, mora nov paket sa novim imenom.

Dobit je u održavanju: menja se jedna aplikacija, a menja se i na sajtu i na telefonu.

Tehnička izvedba i mesta gde je pucalo

Sve je pisano u PHP-u 8.3 nad MariaDB bazom, bez WordPress-a i bez dodataka koje treba ažurirati. Adrese su čiste, a pravila servera prevode /proizvod/<slug> u pravu skriptu.

Pretraga je poseban slučaj, jer jelovnik piše „ćevapi", a kupac kuca „cevapi". Rešeno je u bazi: proizvodi, kategorije i lokali imaju generisane kolone koje čuvaju ime i opis bez dijakritike i u malim slovima (č i ć postaju c, š postaje s, ž postaje z, đ postaje dj), i te kolone su indeksirane. Ista funkcija postoji i u PHP-u i u JavaScript-u aplikacije, pa se pretraga ponaša isto na sajtu, u aplikaciji i u administraciji — jedan izvor pravila, tri mesta primene.

Dobar deo posla je bio popravljanje onoga što se zateklo. Sve strane proizvoda vraćale su 404 jer server nikad nije dobio pravilo za njihove adrese, a na njih je sa naslovne i iz menija vodilo trideset tri veze. Goli /dostava iz podnožja vodio je na prijavu za dostavljače umesto na stranu o dostavi. Dugme za poziv na naslovnoj nosilo je izmišljen broj telefona iz šablona. Nepostojeće adrese vraćale su 200 umesto 404, pa ih je pretraživač tretirao kao prazne strane. Tri PHP fajla bila su snimljena u UTF-16, pa ih PHP nije parsirao, a server ih je servirao kao običan tekst — izvorni kod na videlu.

Najgadnija je bila IPv6. Domen ima AAAA zapis, a blokovi servera slušali su samo IPv4, pa je posetilac koji ide preko IPv6 završavao na tuđem sajtu sa neispravnim sertifikatom. Ispravka je jedan red po bloku, ali se do nje dolazi tek kada se sajt proveri i sa jedne i sa druge strane, odvojeno. Iz iste serije je i pošta: verifikacione i kontakt poruke dvanaest dana nisu odlazile jer se sloj za slanje prijavljivao praznom lozinkom — konfiguracija u kojoj lozinka stoji nije bila učitana na tom putu, a nigde se nije videlo da nešto ne radi.

Registracije koje nisu bile ljudi

U avgustu 2026, tokom provere poštanskih sandučića na serveru, ispalo je da sanduče sa koga sajt šalje potvrde ima stotinak odbijenih poruka, i da odbijanja stižu i tog dana. Kad se pogledalo u bazu, slika je bila ovakva: od 2.256 naloga, 2.230 su napravili botovi. Trajalo je od decembra 2025, a poslednjih nedelja tempo je bio tridesetak registracija dnevno.

Potpis je bio jednoznačan — ime kao jedan niz malih slova bez razmaka, a mejl tuđa prava adresa. To znači da je svaka takva registracija poslala poruku „potvrdite nalog" nekom čoveku koji je nikad nije tražio, osam meseci u kontinuitetu, sa klijentovog domena. To nije samo smeće u bazi, to je reputacija domena sa koga stižu i potvrde porudžbina.

Brana je pravilo nad imenom koje se izvršava pre upisa u bazu i pre slanja pošte, a pre puštanja je provereno nad stvarnim podacima: pogađa svih 2.230 sumnjivih naloga, od kojih nijedan nikad ništa nije naručio, a ne dira nijednog pravog korisnika. Prag je namerno postavljen tako da ime od šest slova prođe.

Prva brana nije bila dovoljna. Botovi su nastavili da stižu, jer aplikacija ima svoj put registracije sa sopstvenom, slabijom proverom. Pouka koja vredi za svaki sajt sa nalozima: pre nego što formu proglasiš zaštićenom, nađi sva mesta koja prave korisnika, ne samo ono očigledno. Posle toga su lažni nalozi obrisani uz rezervnu kopiju cele baze, a provera je pokazala da nijedna povezana tabela nije ostala sa siročadima.

Vredi reći i šta ovaj broj ne znači: nalog nije uslov za porudžbinu. Većina ljudi poruči kao gost ili telefonom, pa broj naloga nikad nije ni bio broj mušterija.

SEO, analitika i saglasnost

Svaka strana ima svoj naslov, opis i kanonsku adresu, a naslovna nosi Restaurant i FoodEstablishment podatke po schema.org standardu — obe adrese, oba telefona, radno vreme, oblast dostave i vezu ka jelovniku. To je ono što pretraživač prikazuje kada neko sa telefona traži roštilj u Beogradu, i razlog zašto se strukturisani podaci pišu ozbiljno, a ne kao ukras.

Mapa sajta drži nešto manje od osamdeset adresa i vodi se pravilom da u njoj stoje samo strane koje vraćaju 200. Kada je Search Console prijavio da deo adresa iz mape ne može da se indeksira, uzrok su bile četiri tematske strane koje su u međuvremenu nestale, plus jedno preusmerenje i jedan poddomen. Mapa je očišćena, a te strane su kasnije vraćene kao pravi sadržaj — sa tekstom koji nekome nešto znači, a ne kao prazne adrese sa ključnim rečima.

Analitika je Google Analytics 4 sa Consent Mode v2 i podrazumevanim odbijanjem: dok posetilac ne pristane, ne šalje se ništa što ne mora. Tu je bila i jedna neprijatna istina. Brojač je stajao u zaglavlju koje se nikad nije prikazivalo, jer sve javne strane emituju sopstveni <head>. Mesecima je izgledalo kao da merenje radi, a nije merilo ništa. Rešeno je jednim zajedničkim fajlom uključenim u svih šesnaest mesta koja prave zaglavlje, pa je redosled sada isti svuda: saglasnost prvo, pa merenje.

Baner za kolačiće nudi i odbijanje, ne samo prihvatanje, izbor se pamti i poštuje se pri sledećoj poseti.

Šta klijent na kraju ima

Sopstveni kanal za porudžbine. Bez provizije po porudžbini i bez pretplate platformi — kupac, njegova adresa i njegova istorija ostaju u bazi koja je klijentova i koja se može prekopirati i preseliti.

Dan izgleda ovako: nova stavka se doda kroz administraciju i istog trenutka je na jelovniku; kupac otkuca adresu i sistem sam odredi iz koje kuhinje ide; porudžbina padne na ekran u lokalu i odmah može da se odštampa; dostavljač je vidi na telefonu; povrat u kreditima se obračuna bez ičije intervencije.

Sajt je i dalje u aktivnom održavanju, a od svega što sam radio ovaj projekat je najbolji podsetnik da posao ne prestaje na puštanju u rad. Nijedan od problema opisanih iznad nije se video na prvi pogled: sajt je izgledao ispravno i dok je slao poštu praznom lozinkom, i dok su sve strane proizvoda vraćale 404, i dok je preko IPv6 posetiocu prikazivao tuđi sajt. Video se tek kada se proverio deo po deo, sa obe strane mreže.

Ceo ekosistem, sa svim panelima i snimcima iz produkcije, prikazan je i na posebnoj strani o ovoj aplikaciji.

// Sajt uživo

Leskovački Kutak

Pogledajte jelovnik, izaberite lokaciju i poručite dostavu u Beogradu.

leskovackikutak.rs

Često postavljana pitanja

Zašto sopstvena porudžbina kad postoje platforme za dostavu?
Zato što na sopstvenom sajtu nema provizije po porudžbini, a kupac i njegova adresa ostaju u bazi restorana. Platforme donose doseg i imaju smisla kao dopuna, ali kanal koji je tvoj ne može sutra da ti podigne proviziju niti da te spusti u svojoj listi.
Može li vlasnik sam da menja jelovnik i cene?
Da. Proizvodi, kategorije, cene, slike, dostupnost i prilozi idu kroz administraciju, bez dodirivanja koda. Isto važi za lokale, radno vreme i podešavanja programa kredita.
Šta ako restoran ima više od dva lokala?
Sistem je od početka pisan za više lokala, a ne za jedan sa izuzetkom. Novi lokal se doda sa svojom adresom i koordinatama i odmah ulazi u računicu koja bira najbližu kuhinju za datu adresu dostave, sa svojim radnim vremenom i svojim panelom.
Mora li kupac da napravi nalog da bi poručio?
Ne. Gost ostavi ulicu, kućni broj i telefon i porudžbina prolazi. Nalog služi za kredite, sačuvane adrese i istoriju porudžbina — to je pogodnost, ne uslov.
Kako porudžbina stiže do kuhinje?
Server je gura na ekran lokala kroz trajnu vezu, pa se pojavi sama, bez osvežavanja stranice. Za kuhinju postoji i štampa naloga, a dostavljač svoju porudžbinu dobija na telefon čim mu je dodele.
Da li se aplikacija skida iz prodavnice?
Ne mora. Instalira se jednim dugmetom sa sajta i posle stoji na početnom ekranu kao svaka druga aplikacija. Za Android je nad istom aplikacijom napravljen i potpisan paket sa Digital Asset Links verifikacijom, pa objavljivanje u prodavnici ostaje otvorena opcija bez ponovnog pisanja aplikacije.

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