Digital Academy × RU4M

Vodič za integraciju događaja

Ovaj vodič objašnjava kako da Digital Academy prikaže RU4M događaje, a da prijava i plaćanje ostanu u RU4M aplikaciji. Partner-feed pristup se uključuje pojedinačno za RU4M nalog.

Sadržaj vodiča
  1. Pregled
  2. Dobijanje API ključa
  3. Adrese feedova
  4. Autentifikacija
  5. Polja i formati
  6. Pravila za linkove
  7. Načini integracije
  8. Prikaz događaja
  9. Keširanje i osvežavanje
  10. QR kodovi
  11. Analitika i UTM
  12. Testiranje
  13. Česte greške i pitanja

1. Pregled

RU4M partnerski feed objavljuje javne, objavljene događaje kojima upravlja vaš RU4M nalog ili kompanija čiji ste administrator/moderator. Vaš sajt preuzima podatke i prikazuje karticu. Posetilac bira „Prijavi se” i nastavlja preko RU4M linka: aplikacija se otvara ako je instalirana; u suprotnom se otvara prodavnica aplikacija, a Branch prosleđuje korisnika do događaja nakon instalacije. Prijava i plaćanje preko Monri-ja ostaju u RU4M aplikaciji — ne pravite sopstveni tok prijave ili naplate.

Kako dobijate pristup

  1. RU4M platform administrator (nalog sa ulogom ADMIN ili SUPER_ADMIN) uključi „API pristup” za vaš nalog.
  2. Prijavite se u RU4M kontrolnu tablu i napravite svoj API ključ.
  3. Sačuvajte ključ na svom serveru i pozivajte feed server-to-server.

2. Dobijanje API ključa — korak po korak

Ključ počinje sa ru4m_ i pripada vašem RU4M nalogu. RU4M vam ne šalje produkcijski ključ: pošto je pristup uključen, sami ga napravite u kontrolnoj tabli.

2.1 Preduslovi

Koristite RU4M nalog sa potvrđenom email adresom kojim pravite događaje. Ako događaj pripada kompaniji, vaš nalog mora biti administrator ili moderator te kompanije. Feed proverava vlasništvo i članstvo; događaji drugog naloga/kompanije se neće pojaviti.

2.2 Zatražite API pristup

Pošaljite RU4M timu email adresu svog RU4M naloga i zamolite da uključe API pristup. Kontakt: [kontakt RU4M tima]. API pristup je podrazumevano isključen; dok ga tim ne uključi, stavka „API ključevi” neće biti dostupna u meniju kontrolne table. Uključenje prikazuje poruku „API pristup je uključen”. Ako pristup nije uključen, kontrolna tabla prikazuje naslov „API pristup nije uključen za vaš nalog” i tekst „API pristup uključuje RU4M tim. Javite nam se ako želite da prikazujete RU4M događaje na svom sajtu ili u aplikaciji.”

2.3 Prijavite se u RU4M kontrolnu tablu

Otvorite https://app.ru4m.com i prijavite se uobičajenim RU4M nalogom. Kada je pristup uključen, u navigaciji izaberite API ključevi.

2.4 Napravite ključ

Izaberite Novi API ključ i popunite polja tačno kako su prikazana:

PoljeŠta unositePravila iz aplikacije
NazivNaziv integracije, na primer „Digital Academy sajt – produkcija”.Obavezno, 1–60 znakova. Validacione poruke: „Unesite naziv.” i „Naziv može imati najviše 60 znakova.”
Kanal (opciono)Oznaka izvora poseta za Branch, na primer digital_academy.2–40 malih slova, cifara ili donjih crta: ^[a-z0-9_]{2,40}$. Poruka validacije je „Koristite 2–40 malih slova, cifara ili donjih crta.” Ako ga ostavite praznim, kanal je partner_feed. Izabrani kanal ulazi u Branch ~channel i web utm_source linka, pa se klikovi mogu pripisati tom izvoru.
IstičeOdaberite koliko dugo ključ važi.Bez isteka, Za 30 dana, Za 90 dana, Za 180 dana ili Za 365 dana. Za stabilnu produkcionu integraciju preporuka je Bez isteka ili Za 365 dana; za test izaberite Za 30 dana.

Pritisnite Napravi ključ. Možete imati najviše pet aktivnih ključeva. Kada je limit dostignut, aplikacija prikazuje: „Možete imati najviše 5 aktivnih ključeva. Opozovite jedan da biste napravili novi.”

2.5 Sačuvajte ključ

U prozoru Vaš novi API ključ pritisnite Kopiraj (potvrda: „Kopirano u privremenu memoriju”) i sačuvajte vrednost u password manager-u ili tajnom server-side podešavanju. Poruka aplikacije glasi „Ovaj ključ se prikazuje samo jednom”: po zatvaranju prozora ključ više nije moguće prikazati. Ako ga izgubite, opozovite ga i napravite novi.

2.6 Pregledajte ključeve

U odeljku Vaši ključevi videćete sledeće kolone: Naziv, Ključ (prikazuje sačuvani prefix i elipsu, nikada punu vrednost), Kanal, Napravljen, Poslednje korišćenje, Ističe, Status i Akcije. Statusi su Aktivan, Opozvan i Istekao. Početna lista prikazuje aktivne ključeve; uključite Prikaži opozvane i istekle ({count}) da vidite ostale. Poslednje korišćenje prikazuje Nikad dok ključ nije uspešno korišćen; vreme se osvežava najviše jednom u minutu. Za ranije unete, neizmenjene naloge Administracija prikazuje Nikad menjano.

2.7 Opozovite stari ključ pri rotaciji

U aplikaciji nema automatskog „rotiranja” ključa. Napravite novi ključ, zamenite staru vrednost na svom serveru, proverite da se kod novog ključa promenilo Poslednje korišćenje, pa pritisnite Opozovi pored starog ključa. Potvrdite u prozoru Opozvati API ključ? izborom Opozovi. Aplikacija upozorava: „„{name}” odmah prestaje da radi i integracije koje ga koriste gube pristup feedu. Ovo ne može da se poništi.” Za planiranu rotaciju ostavite stari ključ aktivan najviše sedam dana dok prelazite, pa ga opozovite sami.

2.8 Ako se API pristup isključi

RU4M može isključiti pristup u okviru Administracija → API pristup. Isključivanje odmah opoziva sve aktivne ključeve tog naloga; sledeći feed zahtev vraća 401. Administrator potvrđuje izbor u prozoru „Isključiti API pristup?” dugmetom „Isključi pristup”; uspešna promena prikazuje „API pristup je isključen”. Obratite se RU4M timu da proveri pristup. U tom panelu statusi su Uključen i Isključen, a administrator koristi Samo uključeni za filtriranje. Potvrda isključenja prikazuje naslov „Isključiti API pristup?”, dugme „Otkaži” i potvrdu „Isključi pristup”. Poruke su: „Korisnik nema aktivnih ključeva. Više neće moći da pravi nove.”; „Aktivni ključ korisnika biće odmah opozvan i integracije koje ga koriste prestaće da rade.”; „Svih {count} aktivna ključa biće odmah opozvana i integracije koje ih koriste prestaće da rade.”; „Svih {count} aktivnih ključeva biće odmah opozvano i integracije koje ih koriste prestaće da rade.” U tabeli su statusi „Uključen” i „Isključen”; kontrola je označena „API pristup za {user}”, a filter se zove „Samo uključeni”.

Ilustracija; izgled se može neznatno razlikovati.
Administracija → API pristup

API pristup

Samo uključeni

KorisnikStatusPromenjenoAktivni ključeviPristup
Vaš nalogUključenDatumn●

Isključivanje traži potvrdu: „Isključiti API pristup?” → „Isključi pristup”.

Kontrolna tabla

API ključevi

{count} od 5 aktivnih ključeva

Novi API ključ
Naziv · Ključ · Kanal · Napravljen · Poslednje korišćenje · Ističe · Status · Akcijedigital_academy · ru4m_… · Aktivan · Nikad
Novi API ključ

Naziv

Sajt Digital Academy

Kanal (opciono)

digital_academy

Ističe

Bez isteka ▾
Za 30 dana · Za 90 dana · Za 180 dana · Za 365 dana
Napravi ključ
Vaš novi API ključ

Kopirajte ključ i sačuvajte ga na svom serveru.

Ovaj ključ se prikazuje samo jednom

Kada zatvorite ovaj prozor, ključ više ne može da se prikaže.

<VAŠ_KLJUČ>
KopirajGotovo

Snimci ekrana – uskoro

Ove HTML/CSS ilustracije pokazuju nazive i tok; stvarni snimci ekrana biće dodati kada produkcioni prikaz bude dostupan.

3. Adrese feedova

OkruženjeJSON Feed 1.1RSS 2.0Status
Produkcijahttps://api.ru4m.com/feeds/events.jsonhttps://api.ru4m.com/feeds/events.rssU pripremi — još nije pušteno.
Sandboxhttps://fuzzywork.shop/feed.jsonhttps://fuzzywork.shop/feed.rssZa testiranje; zatražite testni ključ od RU4M tima.

Sandbox je odvojeno testno okruženje sa uzornim događajima. Vraća JSON Feed 1.1 i RSS 2.0 i zahteva testni ključ od RU4M tima. Sandbox prototip može se razlikovati i po nekim nazivima polja ili porukama; produkcijski ugovor i tabele u ovom vodiču prate API kod, ne stariji sandbox payload. API host u produkciji je https://api.ru4m.com; potvrđen je dostupnim API resursom, ali partner feed rute još nisu deploy-ovane.

Podržan je samo parametar locale: sr ili en, podrazumevano sr. Primer: https://api.ru4m.com/feeds/events.json?locale=en. Kanal i organizacija se čitaju iz API ključa; nemojte ih slati u query parametrima. Sačuvajte URL feeda kao podešavanje u svom sistemu, ne kao vrednost ugrađenu u više stranica.

4. Autentifikacija

Ključ se šalje sa svakim serverskim zahtevom. Preporučeno je x-api-key; podržani su i Bearer zaglavlje i query ključ. Prednost ima redom x-api-key, Bearer, pa ?key=.

curl -i -H 'x-api-key: <VAŠ_KLJUČ>' https://api.ru4m.com/feeds/events.json
curl -i -H 'Authorization: Bearer <VAŠ_KLJUČ>' https://api.ru4m.com/feeds/events.json
curl -i 'https://api.ru4m.com/feeds/events.json?key=<VAŠ_KLJUČ>'

Query varijantu koristite samo kada alat ne može da pošalje zaglavlje: ključ u URL-u može završiti u logovima servera/proksija. Nikada ga ne stavljajte u link objavljen na stranici. U aplikaciji stoji upozorenje: „U krajnjoj nuždi možete dodati ?key= sa svojim ključem na URL feeda. Ključ tada završava u logovima servera, proksija i pregledača, zato ga opozovite i pređite na zaglavlje čim budete mogli.”

Ključ držite samo na svom serveru. Nikada ga ne stavljajte u pregledač, mobilnu aplikaciju ili javni repozitorijum. Link svakog događaja iz feeda koristite tačno onakav kakav jeste, bez izmena.
StatusOdgovorNapomena
200JSON Feed 1.1 ili RSS 2.0Primer oblika JSON-a (polja su iz produkcijskog koda; event vrednosti ispod su sandbox primer): {"version":"https://jsonfeed.org/version/1.1","title":"RU4M Partner Events","items":[{"id":"c493a00e-ad7d-45fc-b09a-1dca37d051c1","title":"Career Days Niš","content_text":"Career Days Niš povezuje talente i poslodavce kroz prilike za profesionalni razvoj, zapošljavanje i karijeru.","date_published":"2026-10-10T11:25:11.292Z"}]}
401{"error":"missing_api_key"}Ključ nije poslat. Oba 401 slučaja postavljaju WWW-Authenticate: Bearer realm="ru4m-feed".
401{"error":"invalid_api_key"}Ključ je pogrešan, istekao/opozvan, nema scope-a ili je API pristup isključen. Slučajevi su namerno ujednačeni.
429{"error":"rate_limit_exceeded"}Više od 60 zahteva u 60 sekundi po ključu; odgovor sadrži Retry-After u sekundama.
503{"error":"temporarily_unavailable"}Redis ograničenja zahteva nisu dostupna. Retry-After: 30; pokušajte ponovo posle 30 sekundi.
400{"error":"invalid_locale"}locale nije sr ili en.
500{"error":"feed_unavailable"}Greška pri učitavanju ili sastavljanju feeda.

Uspešan odgovor ima Cache-Control: private, max-age=300. Greške imaju Cache-Control: no-store. Ovaj endpoint ne koristi 403 za nevažeći ključ ili nedostajući feed:read; oba se vraćaju kao 401.

Ključ držite isključivo na serveru: WordPress wp-config.php ili promenljiva okruženja RU4M_FEED_KEY. Nikada u JavaScript-u/HTML-u browsera, mobilnoj aplikaciji, javnom repozitorijumu ili javno objavljenom URL-u. Ako ključ procuri, odmah ga opozovite u „API ključevi” i obavestite RU4M tim.

5. Polja i formati

Produkcijska tabela prati partnerFeedRoutes.ts. Primeri događaja preuzeti su iz sandbox/REST događaja „Career Days Niš”; sandbox i produkcija mogu imati različit tekst poruke dok produkcijski endpoint ne bude objavljen.

JSON Feed 1.1

PutanjaTipPrimer / opisNamena
versionstringhttps://jsonfeed.org/version/1.1Verzija formata.
titlestringRU4M Partner EventsNaziv feeda.
home_page_urlURLRU4M appUrlPočetna URL vrednost iz API runtime konfiguracije.
feed_urlURL{appUrl}/feeds/events.json?locale=srSelf URL sastavljen od runtime konfiguracije i izabranog jezika. Za integraciju koristite objavljenu API adresu iz odeljka 3.
items[]niz objekata, najviše 100Redosled po startDate rastućeJavni, objavljeni događaji koji nisu završeni i dostupni su nalogu.
items[].idstringc493a00e-ad7d-45fc-b09a-1dca37d051c1ID događaja; ne sastavljajte URL od njega.
items[].titlestringCareer Days NišNaslov.
items[].content_textstringCareer Days Niš povezuje talente i poslodavce kroz prilike za profesionalni razvoj, zapošljavanje i karijeru.Opis u običnom tekstu.
items[].date_publishedISO 8601 UTC2026-10-10T11:25:11.292ZVrednost je event.updatedAt.
items[].urlURLIsto kao _ru4m.branchUrlGlavni CTA „Prijavi se”. Branch URL ako postoji sharableUrl; u suprotnom web fallback.
items[].tagsniz stringovaKategorije događajaNaslovi kategorija; u RSS-u postaju ponovljeni <category>.
items[]._ru4m.startDate, endDateISO 8601 UTC string2026-10-14T09:00:00.000Z; 2026-10-14T15:30:00.000ZPočetak i kraj događaja.
items[]._ru4m.timezoneIANA timezoneEurope/BelgradeVremenska zona za prikaz.
items[]._ru4m.locationstring ili nullAleksandra Medvedeva 2a, Niš, SerbiaVrednost lokacije događaja.
items[]._ru4m.addressstringSastavljen od adrese, grada i državePražnjeni delovi se izostavljaju; može biti prazan tekst.
items[]._ru4m.organizerobjekat ili null{"id":1762,"name":"AIESEC","image":"…"}Organizator: ID, naziv i slika.
items[]._ru4m.coverImageURL ili nullNaslovna slika tipa COVERSlika kartice.
items[]._ru4m.branchUrlURLLink za isti događaj kao items[].urlPartner-parametri se dodaju registrovanom Branch linku.
items[]._ru4m.webUrlURLappUrl/{locale}/events/{slug} sa UTM parametrimaDirektna RU4M web stranica za „Više detalja”.
items[]._ru4m.qrUrlURL ili nullURL event QR slikeZvanična QR slika događaja.
items[]._ru4m.priceFrom, currencybroj/string ili nullNajniža cena karata; null kada nema pricing redaNajniža cena i šifra valute.
items[]._ru4m.phaseupcoming ili liveupcominglive kada je startDate već prošao; samo događaji sa budućim endDate ulaze u feed.
items[]._schemaSchema.org Event@type: EventeventStatus je EventInProgress ili EventScheduled; url je webUrl, a ne Branch link.

RSS 2.0

ElementNamespaceMapiranje / značenje
item/title, item/link, item/guidRSS 2.0title, Branch url, id (guid nije permalink).
item/pubDate, item/descriptionRSS 2.0updatedAt u RFC 822; opis u CDATA.
item/categoryRSS 2.0Jedan element po kategoriji, iz tags.
ev:startdate, ev:enddatehttp://purl.org/rss/1.0/modules/event/ISO 8601 početak i kraj.
ev:location, ev:organizer, ev:typeEvent namespaceLokacija, naziv organizatora i kategorije spojene zarezom.
media:content atribut urlhttp://search.yahoo.com/mrss/Naslovna slika; atribut medium="image".
ru4m:webUrl, ru4m:qrUrlhttps://ru4m.com/ns/feedWeb stranica i QR slika.
ru4m:priceFrom, ru4m:currency, ru4m:statusRU4M namespaceCena, valuta i upcoming/live.
atom:linkhttp://www.w3.org/2005/AtomSelf link na RSS feed sa locale query parametrom.

RSS namespace-i su definisani URI-jem, ne samo prefix-om. RSS ne prenosi JSON timezone polje, zato prikažite ISO vreme u očekivanoj vremenskoj zoni ili koristite JSON Feed.

6. Pravila za linkove

Glavni CTA uvek koristi items[].url (RSS <link>). Ne zamenjujte ga sa webUrl.
  • „Prijavi se” koristi feed vrednost url tačno onakvu kakva jeste. Ne skraćujte je, ne provlačite kroz sopstveni redirect i ne sklapajte je od ID-a.
  • „Više detalja” vodi na _ru4m.webUrl.
  • Ako feed url ima registrovani Branch link, on nosi ~channel, app/store fallback i odredišni događaj. Ako event nema Branch link, API koristi web URL kao fallback.
  • Channel ključa postaje Branch ~channel. Ako želite dodatni placement tracking, jedini dodatak je utm_content, dodat URL API-jem, nikada konkatenacijom stringa.
  • target="_blank" je dozvoljen uz rel="noopener"; to sprečava novu stranicu da pristupi window.opener, a ne menja URL.

Preusmeravanje ili menjanje linka može prekinuti Branch otvaranje događaja i atribuciju. QR/CTA koji treba da otvori aplikaciju mora biti napravljen od url, ne od webUrl.

const signupUrl = new URL(item.url);
if (!signupUrl.searchParams.has("utm_content")) signupUrl.searchParams.set("utm_content", "events_list");
const href = signupUrl.toString();

7. Načini integracije

A. WordPress shortcode — preporuka za WordPress

U wp-config.php čuvajte URL i ključ. Postavite produkcijski URL kada endpoint bude objavljen; za sandbox koristite https://fuzzywork.shop/feed.json.

define('RU4M_FEED_URL', 'https://api.ru4m.com/feeds/events.json');
define('RU4M_FEED_KEY', '<VAŠ_KLJUČ>');

Dodajte sledeći kod u functions.php child teme ili u mali plugin; na stranicu dodajte shortcode [ru4m_events]. Keš traje pet minuta, a poslednji ispravan odgovor se čuva kao fallback 30 dana.

function da_partner_get_feed() {
    $cached = get_transient('da_partner_feed');
    if (is_array($cached)) return $cached;

    $response = wp_remote_get(RU4M_FEED_URL, array(
        'timeout' => 5,
        'headers' => array('x-api-key' => RU4M_FEED_KEY),
    ));

    if (!is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response)) {
        $feed = json_decode(wp_remote_retrieve_body($response), true);
        if (is_array($feed) && isset($feed['items']) && is_array($feed['items'])) {
            set_transient('da_partner_feed', $feed, 5 * MINUTE_IN_SECONDS);
            set_transient('da_partner_feed_last_good', $feed, 30 * DAY_IN_SECONDS);
            return $feed;
        }
    }

    $last_good = get_transient('da_partner_feed_last_good');
    return is_array($last_good) ? $last_good : array('items' => array());
}

function da_partner_events_shortcode() {
    $feed = da_partner_get_feed();
    if (empty($feed['items'])) return '<p>Uskoro novi događaji.</p>';
    $html = '<div class="da-ru4m-events">';

    foreach ($feed['items'] as $item) {
        $meta = isset($item['_ru4m']) && is_array($item['_ru4m']) ? $item['_ru4m'] : array();
        $title = $item['title'] ?? '';
        $html .= '<article class="da-ru4m-card">';
        if (!empty($meta['coverImage'])) {
            $html .= '<img src="' . esc_url($meta['coverImage']) . '" alt="' . esc_attr($title) . '" loading="lazy">';
        } else {
            $html .= '<div class="image-fallback" aria-hidden="true"></div>';
        }
        $html .= '<h3>' . esc_html($title) . '</h3>';
        if (($meta['phase'] ?? '') === 'live') $html .= '<strong>U TOKU</strong>';
        if (!empty($meta['startDate'])) {
            $date_label = $meta['startDate'];
            try {
                $date_label = (new DateTimeImmutable($meta['startDate']))
                    ->setTimezone(new DateTimeZone($meta['timezone'] ?? 'Europe/Belgrade'))
                    ->format('d.m.Y. H:i');
            } catch (Exception $error) {
                $date_label = $meta['startDate'];
            }
            $html .= '<p>' . esc_html($date_label) . ' ' . esc_html($meta['timezone'] ?? '') . '</p>';
        }
        if (array_key_exists('priceFrom', $meta)) {
            $price = $meta['priceFrom'];
            $html .= null === $price ? '<p>Besplatno</p>' : '<p>' . esc_html((string) $price . ' ' . ($meta['currency'] ?? '')) . '</p>';
        }
        $html .= '<p>' . esc_html($item['content_text'] ?? '') . '</p>';
        if (!empty($item['url'])) $html .= '<a href="' . esc_url($item['url']) . '" target="_blank" rel="noopener">Prijavi se</a>';
        if (!empty($meta['webUrl'])) $html .= ' <a href="' . esc_url($meta['webUrl']) . '">Više detalja</a>';
        $html .= '</article>';
    }
    return $html . '</div>';
}
add_shortcode('ru4m_events', 'da_partner_events_shortcode');

esc_url() u WordPress core-u dozvoljava ~ i %, pa čuva i literalni tilde i sekvencu %7E. HTML escape ampersanda u atributu je normalan; browser ga pravilno obrađuje. Nemojte ponovo kodirati ceo URL. Minimalni CSS:

.da-ru4m-events{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem}
.da-ru4m-card{overflow:hidden;border:1px solid #ddd;border-radius:12px;padding:1rem}
.da-ru4m-card img{width:100%;aspect-ratio:16/9;object-fit:cover}
.da-ru4m-card .image-fallback{aspect-ratio:16/9;background:#eee}

B. WordPress plugin bez koda

RSS agregator koji ne podržava zaglavlje može koristiti https://api.ru4m.com/feeds/events.rss?key=<VAŠ_KLJUČ>; dok se produkcija ne objavi, zamenite domen sandbox adresom. Ograničenja: plugin možda ne prikazuje datum ili custom namespace polja, a <link> mora ostati neizmenjen Branch URL.

Query ključ je poslednja opcija. URL sa ključem ne sme biti link na objavljenoj stranici, sitemap-u ili javnom podešavanju. Query može biti sačuvan u logovima; čim plugin podrži zaglavlja, pređite na x-api-key.

C. Browser JavaScript preko vašeg backend-a

Browser nikada ne poziva RU4M feed direktno i nikada ne dobija partner ključ. Klijentski kod zove samo vaš endpoint, ovde /wp-json/da/v1/events. Vaš server dodaje x-api-key, kešira pet minuta i može da vrati poslednju uspešnu verziju.

<section id="ru4m-events">
  <h2>Događaji</h2>
  <div class="ru4m-grid" aria-live="polite">
    <article class="ru4m-skeleton" aria-label="Učitavanje događaja"></article>
  </div>
</section>
<script>
const FEED_URL = "/wp-json/da/v1/events";
const grid = document.querySelector("#ru4m-events .ru4m-grid");
function cardFor(item) {
  const meta = item._ru4m || {};
  const card = document.createElement("article");
  card.className = "ru4m-card";
  if (meta.coverImage) {
    const image = document.createElement("img");
    image.src = meta.coverImage; image.alt = item.title; image.loading = "lazy"; card.append(image);
  } else {
    const fallback = document.createElement("div"); fallback.className = "image-fallback"; fallback.setAttribute("aria-hidden", "true"); card.append(fallback);
  }
  const title = document.createElement("h3"); title.textContent = item.title; card.append(title);
  if (meta.phase === "live") { const badge = document.createElement("strong"); badge.textContent = "U TOKU"; card.append(badge); }
  if (meta.startDate) {
    const date = document.createElement("p");
    date.textContent = new Intl.DateTimeFormat("sr-Latn", {dateStyle:"medium",timeStyle:"short",timeZone:meta.timezone || "Europe/Belgrade"}).format(new Date(meta.startDate));
    card.append(date);
  }
  if (Object.hasOwn(meta, "priceFrom")) {
    const price = document.createElement("p");
    price.textContent = meta.priceFrom == null ? "Besplatno" : `${meta.priceFrom} ${meta.currency || ""}`;
    card.append(price);
  }
  const description = document.createElement("p"); description.textContent = item.content_text || ""; card.append(description);
  const signup = document.createElement("a"); signup.href = item.url; signup.textContent = "Prijavi se"; signup.target = "_blank"; signup.rel = "noopener"; card.append(signup);
  if (meta.webUrl) { const more = document.createElement("a"); more.href = meta.webUrl; more.textContent = "Više detalja"; card.append(" ", more); }
  return card;
}
async function loadEvents() {
  const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000);
  try {
    const response = await fetch(FEED_URL, {signal:controller.signal});
    if (!response.ok) throw new Error(`Events HTTP ${response.status}`);
    const feed = await response.json(); grid.replaceChildren();
    if (!Array.isArray(feed.items) || !feed.items.length) { grid.textContent = "Uskoro novi događaji."; return; }
    feed.items.forEach((item) => grid.append(cardFor(item)));
  } catch (error) {
    grid.textContent = "Događaji trenutno nisu dostupni.";
    console.error("RU4M partner feed nije dostupan", error);
  } finally { clearTimeout(timer); }
}
loadEvents();
</script>

Pošto se kartice tek u browseru generišu JavaScript-om, crawler ili preview koji ga ne izvršava možda neće videti događaje; za SEO je bolji serverski render.

Minimalna WordPress REST proxy ruta. Definišite RU4M_FEED_KEY i RU4M_FEED_URL u wp-config.php; ruta je javni endpoint vašeg sajta, ali tajni ključ nikad ne napušta server.

add_action('rest_api_init', function () {
    register_rest_route('da/v1', '/events', array(
        'methods' => 'GET',
        'permission_callback' => '__return_true',
        'callback' => function () {
            $cached = get_transient('da_partner_proxy_feed');
            if (is_array($cached)) return rest_ensure_response($cached);
            $response = wp_remote_get(RU4M_FEED_URL, array(
                'timeout' => 5,
                'headers' => array('x-api-key' => RU4M_FEED_KEY),
            ));
            if (!is_wp_error($response) && 200 === wp_remote_retrieve_response_code($response)) {
                $feed = json_decode(wp_remote_retrieve_body($response), true);
                if (is_array($feed) && isset($feed['items'])) {
                    set_transient('da_partner_proxy_feed', $feed, 5 * MINUTE_IN_SECONDS);
                    set_transient('da_partner_proxy_last_good', $feed, 30 * DAY_IN_SECONDS);
                    return rest_ensure_response($feed);
                }
            }
            $last_good = get_transient('da_partner_proxy_last_good');
            if (is_array($last_good)) return rest_ensure_response($last_good);
            return new WP_Error('partner_feed_unavailable', 'Događaji trenutno nisu dostupni.', array('status' => 503));
        },
    ));
});

Klijentski kod kešira samo u browser memoriji; glavno osvežavanje i fallback radi backend ruta.

D. Serverska strana — Node / Next.js

Next.js server komponenta šalje ključ iz process.env.RU4M_FEED_KEY i revalidira na pet minuta. Ne dodajte "use client" i ne nazivajte promenljivu NEXT_PUBLIC_*.

const RU4M_FEED_URL = process.env.RU4M_FEED_URL || "https://api.ru4m.com/feeds/events.json";
export default async function PartnerEvents() {
  const response = await fetch(RU4M_FEED_URL, {
    headers: {"x-api-key": process.env.RU4M_FEED_KEY},
    next: {revalidate: 300}
  });
  if (!response.ok) return <p>Događaji trenutno nisu dostupni.</p>;
  const feed = await response.json();
  if (!feed.items?.length) return <p>Uskoro novi događaji.</p>;
  return <div>{feed.items.map((item) => <article key={item.id}>
    {item._ru4m.coverImage ? <img src={item._ru4m.coverImage} alt={item.title} /> : <div className="image-fallback" aria-hidden="true" />}
    <h2>{item.title}</h2>
    <p>{new Intl.DateTimeFormat("sr-Latn", {dateStyle:"medium",timeStyle:"short",timeZone:item._ru4m.timezone}).format(new Date(item._ru4m.startDate))}</p>
    {item._ru4m.phase === "live" ? <strong>U TOKU</strong> : null}
    {Object.hasOwn(item._ru4m, "priceFrom") ? <p>{item._ru4m.priceFrom == null ? "Besplatno" : `${item._ru4m.priceFrom} ${item._ru4m.currency || ""}`}</p> : null}
    <a href={item.url} target="_blank" rel="noopener">Prijavi se</a>
    {item._ru4m.webUrl ? <a href={item._ru4m.webUrl}>Više detalja</a> : null}
  </article>)}</div>;
}

Za RSS u Node-u koristite XML parser u serverskom okruženju i zadržite namespace-e. Primer sa fast-xml-parser:

import { XMLParser } from "fast-xml-parser";
const RU4M_FEED_URL = process.env.RU4M_FEED_URL || "https://api.ru4m.com/feeds/events.rss";
const response = await fetch(RU4M_FEED_URL, {headers:{"x-api-key":process.env.RU4M_FEED_KEY}});
if (!response.ok) throw new Error(`RSS HTTP ${response.status}`);
const parser = new XMLParser({ignoreAttributes:false,attributeNamePrefix:"@_",removeNSPrefix:false});
const rss = parser.parse(await response.text());
const raw = rss.rss.channel.item;
const items = Array.isArray(raw) ? raw : [raw];
const events = items.map((item) => ({
  title:item.title, branchUrl:item.link, id:item.guid["#text"] || item.guid,
  startDate:item["ev:startdate"], endDate:item["ev:enddate"],
  location:item["ev:location"], organizer:item["ev:organizer"],
  coverImage:item["media:content"]?.["@_url"], webUrl:item["ru4m:webUrl"],
  qrUrl:item["ru4m:qrUrl"], priceFrom:item["ru4m:priceFrom"],
  currency:item["ru4m:currency"], phase:item["ru4m:status"]
}));

Proverite konkretan oblik guid u izabranom parseru. JSON Feed daje strukturisaniji organizer, tags i Schema.org objekat.

8. Prikaz događaja

  • Preporučena kartica: naslov, cover slika 16:9, datum/vreme, lokacija, organizator, opis i jasna CTA dugmad.
  • phase="live" prikažite kao oznaku „U TOKU”. priceFrom: null znači da nema evidentirane cene karte; prikažite „Besplatno” samo kada je to potvrđeno vašim pravilom prikaza.
  • Feed je sortiran po startDate rastuće i vraća najviše 100 događaja; zadržite redosled.
  • Ako coverImage nedostaje ili se slika ne učita, prikažite neutralnu lokalnu zamenu; ne pravite image URL od ID-a.
  • Prazan feed: „Uskoro novi događaji.” Koristite smislen alt, vidljiv fokus tastature i linkove „Prijavi se” / „Više detalja”.

9. Keširanje i osvežavanje

Uspešan produkcijski odgovor ima Cache-Control: private, max-age=300: osvežava se na pet minuta. Keširajte ga i na svom serveru; ne tražite feed češće od jednom u minuti. Postavite timeout na pet sekundi i pri grešci poslužite poslednju ispravnu verziju.

Greške imaju Cache-Control: no-store. Kod 429 sačekajte Retry-After; kod 503 sa Redis rate-limit nedostupnošću zaglavlje je 30 sekundi. Ne keširajte 400, 401, 429, 500 ili 503 kao feed podatke.

Novi događaj se pojavljuje nakon objave i sledećeg uspešnog osvežavanja. Završeni događaj izlazi kada mu endDate prođe; privatni, nacrt ili obrisani događaji se ne vraćaju. Ako događaj pripada drugom nalogu ili kompaniji u kojoj niste administrator/moderator, neće se pojaviti.

10. QR kodovi

qrUrl je URL zvanične slike QR koda događaja. Za štampu možete koristiti tu sliku. Ona nije partnerski link sa vašim ključnim kanalom. Za QR koji treba da nosi Branch partner atribuciju, generišite QR od url (ili od new URL(item.url) sa utm_content=print dodatim preko URL API-ja). Ne koristite webUrl kao prijavni QR.

11. Analitika i UTM

channel iz API ključa ulazi u Branch ~channel kada događaj ima registrovan Branch link; isti kanal se koristi u web UTM parametrima. Ostavljen prazan kanal API ključa koristi partner_feed. Na svom sajtu zabeležite klik, ID događaja i poziciju:

gtag("event", "ru4m" + "_event_click", {event_id:item.id, placement:"events_list"});

Branch izveštaji mere klikove i prelazak u aplikaciju/prodavnicu; vaša analitika meri prikaz i klik na sajtu. UTM vrednosti opisuju izvor, medijum, kampanju i placement. Izveštaje razmenjujte kroz dogovoreni RU4M dashboard.

12. Testiranje

Koristite sandbox ključ koji dobijete od RU4M tima do produkcijske objave:

curl -i https://fuzzywork.shop/feed.json
curl -i -H 'x-api-key: <VAŠ_KLJUČ>' https://fuzzywork.shop/feed.json
ScenarioUređaj / proveraOčekivani rezultat
Nedostaje ključ / pogrešan ključTerminal401; bez ključa body je missing_api_key.
Važeći ključTerminal200; broj događaja odgovara feedu.
Ključ se koristiKontrolna tabla → API ključeviPoslednje korišćenje se pojavi nakon uspešnog feed zahteva.
Aplikacija je instaliranaiPhone / Android„Prijavi se” otvara tačan događaj u RU4M aplikaciji.
Aplikacija nije instaliranaiPhone / AndroidBranch otvara odgovarajuću prodavnicu aplikacija, pa događaj nakon instalacije.
Desktop QRDesktop + telefonQR se skenira i otvara odgovarajući Branch događaj/link.
In-app browserInstagram / FacebookProveriti otvaranje; po potrebi nastaviti u sistemskom browseru.
Stari ključ posle opozivaTerminal401 invalid_api_key.
  • ☐ Produkcijski URL je zamenjen tek nakon RU4M objave endpointa.
  • ☐ Ključ se čuva samo u server-side tajni.
  • ☐ „Prijavi se” koristi url/<link>, a „Više detalja” webUrl.
  • ☐ Testirani su datum/timezone, prazan feed, 401, 429 i fallback na keš.
  • ☐ Provereno je polje „Poslednje korišćenje” posle uspešnog zahteva.

13. Česte greške i pitanja

Pitanje / greškaOdgovor
401 missing_api_keyNije poslato zaglavlje/query ključ. Preporuka je x-api-key.
Ne vidim API ključevi u menijuAPI pristup je podrazumevano isključen. Pošaljite email RU4M naloga RU4M timu i zamolite da uključi pristup.
Izgubio sam ključKljuč se prikazuje samo jednom. Napravite novi u „API ključevi”, zamenite ga na serveru, pa opozovite stari.
401 invalid_api_keyKljuč je pogrešan, istekao, opozvan, nema feed:read, nalog nije potvrđen ili je API pristup isključen.
403 ForbiddenTrenutna feed ruta ove greške normalizuje na 401; ne očekujte 403.
401 iako je ključ tačanProverite istek, opoziv, feed:read, potvrdu email-a i da li je RU4M isključio API pristup.
Feed je prazanDogađaji mogu biti privatni, nacrti, završeni, obrisani ili van naloga/kompanije kojom upravljate.
429 Too Many RequestsPrekoračeno je 60 zahteva u minuti po ključu. Poštujte Retry-After i povećajte keš.
503 temporarily_unavailableRedis rate-limit servis nije dostupan; sačekajte 30 sekundi iz Retry-After.
Događaj se ne pojavljujeProverite da je javno objavljen, da nije privatan ili završen, da je kreiran vašim nalogom ili kompanijom kojom upravljate, i da pripada prvih 100 rezultata.
Dugme ne otvara aplikacijuProverite da „Prijavi se” koristi feed url / RSS <link>, a ne webUrl. Uklonite shortener i sopstveni redirect.
Ključ je procureoOdmah ga opozovite u RU4M „API ključi” i obavestite RU4M tim.
Pristup je isključenRU4M isključivanje opoziva aktivne ključeve odmah; kontaktirajte RU4M tim da ponovo uključi API pristup.

Za pomoć: [kontakt RU4M tima].