← Dnevnik
methodology·18 May 2026·6 min čitanja

MarketplacepolovnehokejaškeopremezaSrbijuodidejedoedge-ISR-azatrinedelje

Tri nedelje, brif jednog hokejaškog kluba, Nuxt 4 na Cloudflare Pages, edge ISR iznad Supabase-a, keš živi zauvek i pada samo na upis.

Danas smo pustili shop.propeleri.rs — marketplace polovne hokejaške opreme za Srbiju, napravljen za HC Propeleri Novi Sad na Nuxt 4 i Cloudflare Pages-u sa edge ISR-om iznad Supabase-a. Tri nedelje part-time rada uporedo sa glavnim klubskim sajtom. Ovaj tekst je inženjerski post-mortem: šta proizvod radi, kako smo ga keširali, i zašto se paterna ponavlja kroz svaki niški read-heavy marketplace.

Problem je mali ali stvaran. Hokej je niški sport na Balkanu, i ovde ga igraju gotovo isključivo odrasli — rekreativci, kasni starteri, povratnici. Ovo je namerno senior-only tržište, omladinske piramide nema. Pun komplet opreme košta preko €800, palice se lome, početnici prelaze sa iznajmljivane na svoju opremu, igrači se penzionišu ili sele i prazne podrum, a u regionu do sada nije postojalo mesto gde se polovna oprema lokalno kupuje ili prodaje. Prodavci su kačili oglase po Viber grupama i Facebook Marketplace-u, gde oglas umre za nedelju dana i ne možeš filtrirati po flex-u palice ili broju klizaljke. Klub je hteo jedno mesto koje će lokalni odrasli igrači stvarno otvarati.

Šta je sklopljeno: katalog sa hijerarhijskim kategorijama (palice, klizaljke, kacige, rukavice, štitnici, golmanska oprema, usluge), filteri po gradu, stanju, ceni i atributima po kategoriji (flex, savijenost, dužina šafta i tako dalje), foto galerije preko celog ekrana sa Product schema za SEO, i gejt "Otkrij kontakte" iza login-a — i štiti prodavce od skrejpera i daje poštenu metriku pregleda po oglasu. Iznad toga: jednokratne ponude na oglasima sa pregovaranjem, omiljeni oglasi, sačuvane pretrage koje šalju in-app notifikaciju i web push na novi pogodak, istorija pada cene, bundle oglasi (prodaja celog kompleta kao jedan oglas sa atributima po elementu), blokiranje prodavca i prijava oglasa sa admin moderation queue-om. Tri jezika: srpski kao default u rootu, ruski i engleski na /ru i /en. PWA koja se instalira, View Transitions API za meke prelaze, web push preko VAPID-a.

Najzanimljivija inženjerska odluka je bio keš. Prva verzija je bila običan SSR-na-edge Nuxt sajt. Oglasi su se renderovali kako treba, ali svaki direktan pogodak je palio svež render ka Supabase-u. Za nisko-prometni marketplace to je rasipanje, i još gore — limitira koliko regiona ima smisla da puštaš. Treba nam keš. Pitanje je bilo koji.

Dve očigledne opcije smo odbacili. SSG (nuxt generate) je otpao — svaki novi oglas znači rebuild, ćorsokak za marketplace. Stale-while-revalidate sa kratkim TTL-om je delovao primamljivo ali je bio pogrešan: na svakom isteku TTL-a pokreće pozadinski re-render bez obzira da li su se podaci pomerili. Na nisko-prometnom sajtu to znači da je većina renderovanja uzaludna. Mi smo hteli "keš živi dok se podaci ne pomere, ne duže".

Stigli smo do Nitro ISR-a bez TTL-a plus eksplicitne invalidacije na svakoj mutaciji. routeRules u nuxt.config.ts označava /listings/:slug, /u/:slug, /categories i /services kao isr: true. /help i /terms su prerender: true. Sve što traži login — /my/**, /favorites, /notifications, /sell/** — ostaje pun SSR. Ruta se renderuje jednom, sedi u Cloudflare edge kešu bez vremenskog limita i pada samo kad se oglas stvarno promeni.

Strana invalidacije živi u apps/shop/server/utils/cache.ts i izvozi tri helpera: invalidateListing(slug, sellerSlug?), invalidateSeller(slug), invalidateCatalog(). To su tanki omotači oko useStorage('cache').removeItem(...). Svaka server mutacija koja utiče na renderovanu HTML stranu, na kraju uspešne grane zove pravi helper — objavljivanje, promena statusa, pad cene, dodavanje/brisanje/preuređivanje fotografija, dodavanje/brisanje stavke bundla, patch profila. Jedina mutacija koja iz pretraživača ide pravo u Supabase (glavni SellForm.submitEdit, koji upisuje kroz RLS) posle uspeha lupne u mali owner-authed POST /api/listings/:id/invalidate-cache endpoint. Mentalni model u jednoj rečenici: keš živi dok se podaci ne pomere, i svaki put kroz kod koji pomera podatke dužan je da kažu kešu.

Paterna se pokazao tihim i dosadnim u najboljem smislu. Jedan edge region koji opslužuje hiljade pogodaka kataloga dnevno pipa Supabase tačno jednom po mutaciji, ne po posetiocu. Bez TTL-a prodavci nemaju "sačekaj minut pa osveži" — njihova izmena je živa u trenutku kad odgovor stigne. I zato što je invalidacija eksplicitna, režim otkaza je glasan: ako zaboraviš da pozoveš helper na novom putu mutacije, bag je očigledan na prvom testu i lako se popravlja. Ovu razmenu bismo uzeli na svakom marketplace-u gde su oglasi read-heavy a upisi retki.

Druga netrivijalna stvar je bila da user-aware UI ne procuri u keširani HTML. Strana detalja oglasa renderuje različite kontrole zavisno od toga ko gleda: vlasnik vidi Edit i Price Drop, ulogovan ne-vlasnik vidi Reveal Contacts i Make Offer, anoniman posetilac vidi Login CTA. Ako bilo šta od toga završi u SSR prolazu, prvi koji renderuje stranu zalepi svoj identitet u keš za sve sledeće. Lek je da se sva četiri bloka uvuku u <ClientOnly>. SSR prolaz je sad identičan za sve; per-user UI se rehidrira na klijentu. Očigledno unazad, lako se promaši u brzini.

Kratak osvrt na model podataka. Oko petnaest tabela: listings, listing_photos, listing_attributes, listing_items (deca bundla), listing_offers (jedan krug, opcionalno message polje, bez tredova), listing_favorites, listing_reports, categories (hijerarhijski), category_attribute_schemas (polja po kategoriji), saved_searches, contact_views, user_blocks, services, service_categories, push_subscriptions. Kategorije nose JSON attribute_schema, tako da dodavanje "Toe pattern: kvadratni/okrugli" u Palice je jedan red u bazi, ne izmena koda. Sesije su host-scoped — delimo Supabase projekat sa glavnim propeleri.rs sajtom, ali ne i kuki: raniji pokušaj sa kukijem na roditeljski domen je curio između dve aplikacije i lomio hidraciju na klubskom sajtu.

Šta je odsečeno za lansiranje: rejtinzi prodavaca i protok plaćanja. Nijedno ne blokira korist — kad kupac otkrije kontakte, razgovor pređe na telefon, Viber ili Telegram, što lokalnim kupcima i tako više odgovara — ali su oba na roadmap-u kad vidimo kako se prvi mesec ponaša. Šta sledi: štelovanje učestalosti digesta sačuvanih pretraga po stvarnom signalu, lagani badge verifikacije prodavca, i postavljanje iste arhitekture za prvo ne-hokejaško comunity koje je već pitalo (motokros, pa polovne skije). Marketplace paterna — read-heavy, write-rare, edge ISR sa eksplicitnom invalidacijom — pokazuje se kao iskoristiv kroz niška sportska zajednica, i to je verovatno pravi proizvod koji smo slučajno sklopili.

Spremni smo

Imateštadasegradi?

Recite nam na čemu radite. Čitamo svaku poruku i odgovaramo u roku od jednog radnog dana — sa konkretnim stavom i okvirnom procenom.

Popuni brief