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.

Komponenta prima samo shopId

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.

1. Div prije skripte

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.

2. Skripta ide u useEffect

<script> napisan u JSX-u React renderira, ali ga preglednik nikad ne izvrši. Ubaci ga imperativno nakon mounta.

3. Drugi mount ide kroz init()

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.

React komponenta
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.

Next.js App Router (osnovna verzija)
// 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:

Next.js App Router (otporno na client-side navigaciju)
"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”.

Prompt 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

Div ostaje prazan
  • 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.
Piše “Recenzije trenutno nisu dostupne”
  • 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.

Započni besplatno