Widget setup za React i Next.js
Na običnom HTML sajtu snippet iz nadzorne ploče radi bez razmišljanja. U Reactu postoji jedna kvaka: skripta se izvrši prije nego React iscrta div. Tri recepta ispod rješavaju točno taj problem.
Nemoj joj dodavati propove za stil ili broj recenzija. Stil prikaza, broj recenzija, filtar izvora, tema, boja i potpis “Powered by Recenzo” mijenjaju se u Dashboard → Widget i vrijede odmah, bez novog deploya.
widget.js skenira elemente s data-shop atributom kad se izvrši i još jednom na DOMContentLoaded. React div stiže kasnije od oba prolaza.
<script> napisan u JSX-u React renderira, ali ga preglednik nikad ne izvrši. Ubaci ga imperativno nakon mounta.
Jednom učitana skripta se ne izvršava ponovno. Kod client-side navigacije pozovi window.Recenzo?.init?.(), opcionalno s containerom kao argumentom.
Recept 1: čisti React (Vite, CRA)
Komponenta iscrta div, a skriptu ubaci tek u useEffect, jer je div tada zajamčeno u DOM-u.
import { useEffect } from "react";
const WIDGET_SRC = "https://www.recenzo.hr/widget.js";
export function RecenzoRecenzije({ shopId }) {
useEffect(() => {
// widget.js traži [data-shop] u trenutku izvršavanja, a React div postoji
// tek nakon mounta, pa skriptu ubacujemo ovdje, a ne u JSX-u.
if (document.querySelector("script[data-recenzo]")) {
// Skripta je već na stranici (druga ruta, drugi widget) i neće se sama
// ponovno izvršiti. Zamoli je za novi mount.
window.Recenzo?.init?.();
return;
}
const script = document.createElement("script");
script.src = WIDGET_SRC;
script.async = true;
script.dataset.recenzo = "true";
document.body.appendChild(script);
}, []);
return <div id="recenzo-widget" data-shop={shopId} />;
}Recept 2: Next.js App Router
next/script sa strategijom afterInteractive učitava skriptu nakon hidracije, pa div uvijek postoji prvi. Ova verzija ostaje Server Component.
// app/components/recenzo-recenzije.tsx
import Script from "next/script";
export function RecenzoRecenzije({ shopId }: { shopId: string }) {
return (
<>
<div id="recenzo-widget" data-shop={shopId} />
{/* afterInteractive: skripta se izvrši nakon hidracije, dakle sigurno
nakon što je div u DOM-u. beforeInteractive bi se izvršio prerano. */}
<Script src="https://www.recenzo.hr/widget.js" strategy="afterInteractive" />
</>
);
}Ako se do stranice s widgetom dolazi client-side navigacijom (klikom na <Link>), next/script skriptu neće izvršiti drugi put. Tada koristi verziju s onReady:
"use client";
import Script from "next/script";
declare global {
interface Window {
Recenzo?: { init: () => void };
}
}
export function RecenzoRecenzije({ shopId }: { shopId: string }) {
return (
<>
<div id="recenzo-widget" data-shop={shopId} />
<Script
src="https://www.recenzo.hr/widget.js"
strategy="afterInteractive"
// next/script skriptu učitava samo jednom, i pri prelasku na drugu rutu
// je ne izvršava ponovno. onReady se okida na svaki mount pa widget
// preživi client-side navigaciju.
onReady={() => window.Recenzo?.init?.()}
/>
</>
);
}Recept 3: daj ovo svom AI asistentu
Ako web održava netko drugi ili radiš s Claudeom, Cursorom ili Copilotom, zalijepi ovaj prompt i zamijeni SHOP_ID svojim ID-em tvrtke. Isti prompt s već upisanim ID-em čeka te u Dashboard → Widget pod gumbom “Kopiraj upute za AI asistenta”.
Dodaj Recenzo widget s recenzijama na moj web. Shop ID: SHOP_ID Skripta: https://www.recenzo.hr/widget.js Puna dokumentacija (pročitaj prvo): https://www.recenzo.hr/llms.txt Markup: <div id="recenzo-widget" data-shop="SHOP_ID"></div> <script src="https://www.recenzo.hr/widget.js" async></script> Pravila: - Div s data-shop mora postojati u DOM-u prije nego se skripta izvrši. U običnom HTML-u to znači da <script> ide ISPOD diva. - U Reactu skriptu ubaci tek nakon mounta (useEffect), a u Next.js App Routeru koristi <Script strategy="afterInteractive" /> iz next/script. - Ako je skripta već na stranici, pozovi window.Recenzo?.init?.() umjesto da je učitavaš ponovno. init prima opcionalni scope (sam container ili njegov predak) i vraća broj montiranih widgeta; već montirani se preskaču. - Ne mijenjaj domenu skripte. Bilo koja druga domena puca na CORS preflightu i forma za slanje recenzije prestaje raditi. - Na div ne dodaji nijedan drugi data atribut. Stil, broj recenzija, izvor, temu i boju postavljam ja u Recenzo nadzornoj ploči i te se promjene primjenjuju bez diranja koda. Atribut poput data-style zamrzne postavku u kodu i od tada nadzorna ploča na nju više ne utječe.
Asistent koji želi cijeli ugovor widgeta (pravilo mount timinga, česte greške i, na kraju, napredni data-* override) neka pročita /llms.txt. To je jedna plain-text datoteka, dovoljna sama za sebe.
Ako se widget ne pojavi
- U konzoli piše “[recenzo] No [data-shop] container found”. Skripta se izvršila prije mounta. Prebaci je u useEffect.
<script>je napisan u JSX-u. React ga renderira, ali ga preglednik ne izvršava.- Drugi dolazak na istu rutu: treba window.Recenzo?.init?.().
- U konzoli baš ništa: skripta se nije ni učitala, provjeri Network tab.
- Widget je montiran, ali API poziv nije prošao.
- Provjeri je li domena skripte prepisana doslovno iz snippeta.
- Ako koristiš dopuštene origine, dodaj domenu u Dashboard → Widget.
- Provjeri da je data-shop tvoj UUID, a ne SHOP_ID placeholder.
Idemo na setup?
Ako još nemaš račun, otvori prvu tvrtku besplatno. Ako si već unutra, skoči ravno na widget snippet i kopiraj kod.