+420 728 089 029EN

Vlastní frontend balíčku: návod pro agentury

7 min čtení

Většina doplňků pro Shoptet přichází s hotovým widgetem. Ten widget vypadá, jak vypadá, a když se klientovi nelíbí, začíná kolotoč: přebíjení stylů !important, hledání selektorů v cizím markupu a doufání, že příští verze doplňku nic nerozbije.

„Sestav si vlastní balíček" jde jinou cestou. Doplněk žádný widget neservíruje. Backend garantuje dvě věci — data, ze kterých zákazník vybírá, a závaznou cenu výsledné sady. Všechno mezi tím je vaše.

Tenhle text je pro agentury a vývojáře, kteří si nad doplňkem staví vlastní widget pro Shoptet na míru klientově šabloně. Průběžně používáme smyšlený e-shop www.example.cz, projectId 123456 a konfiguraci „Kávový balíček" — 3 sloty, sleva 15 %.

Proč headless a co z toho má agentura

Nikdo cizí nesahá do kódu klienta. Nasazujete dva vlastní soubory (JS a CSS) a jednu HTML vložku. Do šablony nevstupuje žádný externí skript, který byste neviděli, a my do vašeho kódu nevidíme. Když klient řeší bezpečnostní audit nebo má vlastní build pipeline, tohle je rozdíl mezi „jde to" a „nejde to".

Design je váš, ne náš. Konfigurátor můžete postavit jako tři sloty vedle sebe, jako průvodce po krocích i jako mřížku s počítadlem. Sedne do brand manuálu klienta, ne do našeho.

Žádný cizí objekt v window. Nemáme globální proměnnou, kterou byste museli respektovat, ani API, které by vás nutilo do konkrétní struktury DOM. Zbývá jediná závislost — dvě HTTP volání a jedna nativní funkce Shoptetu.

Cena té svobody je jednoduchá: musíte dodržet kontrakt níž.

Závazný kontrakt: co musí splnit každá implementace

Osm bodů. Cokoli mimo ně je vaše rozhodnutí.

  1. Počet slotů berte z product_count. Konfigurace může mít 2 až 5 slotů a obchodník ji může kdykoli změnit v administraci — natvrdo zadrátovaná trojka vám při změně tiše rozbije widget.
  2. Respektujte list_mode. V režimu same_lists existuje jeden společný seznam kategorií a produktů, ze kterého zákazník vybírá do všech slotů. V režimu separate_lists má každý slot vlastní nabídku — do slotu N patří jen kategorie, které mají column_number === N.
  3. Kategorie si seřaďte sami. Pořadí, ve kterém dorazí, není součástí kontraktu; řaďte podle column_number a pak podle name s localeCompare(…, 'cs'), jinak se sloty mezi načteními přehazují.
  4. GUIDy před odesláním seřaďte. Backend rozpozná už vytvořenou sadu podle spojených GUIDů a ten klíč je citlivý na pořadí — stabilní řazení tedy zajistí, že stejná kombinace nezaloží v katalogu druhý produkt.
  5. Do košíku vkládejte výhradně přes shoptet.cartShared.addToCart s kódem sady. Nativní funkce řeší CSRF token, přepočet košíku i UI feedback šablony; vlastní fetch na košíkové akce Shoptet blokuje.
  6. Závazná cena je price z odpovědi POSTu. Cokoli spočítáte na frontendu, je orientační náhled — po odeslání zobrazenou cenu přepište hodnotou z odpovědi, aby zákazník nikdy neviděl jiné číslo, než jaké dostane v košíku.
  7. Data z API escapujte před vložením do DOM. Názvy produktů a kategorií plní lidé v administraci e-shopu a klidně obsahují &, < nebo uvozovky.
  8. Kód sady neukládejte na později a posílejte jen GUIDy z konfigurace. Kód platí pro aktuální nákup; pro opakovaný nákup si vyžádejte nový POSTem se stejným (seřazeným) výběrem.

Kompletní popis polí, tvarů odpovědí a okrajových případů je v dokumentaci na GitHubu — sem ho neopisujeme, protože je to živý dokument.

Co je naopak čistě na vás

Oblast Doporučení z praxe
Layout Sloty vedle sebe pro 2–3 krátké seznamy, průvodce po krocích při 20+ produktech na slot, mřížka s počítadlem pro same_lists.
Zobrazení úspory Nestačí finální cena. Funguje trojice: 917 Kč 779,45 Kč, ušetříte 137,55 Kč (15 %). Slevu dejte i jako badge nad konfigurátor.
Obrázky produktů API vrací název souboru, URL skládáte z hostu e-shopu a varianty Shoptet CDN. Vždy nastavte width/height nebo aspect-ratio (CLS) a mějte placeholder pro prázdný slot.
Hodnocení Hvězdičky zobrazujte jen když je počet hodnocení větší než nula — „0 hvězdiček" vypadá jako špatná recenze, ne jako chybějící údaj.
Dostupnost > 5 → „Skladem", 1–5 → „Skladem N ks" (vytváří tlak), ≤ 0 → „Vyprodáno" a produkt ve výběru zablokovat.
Stepper množství Řeší počet balíčků, ne kusů uvnitř. Není povinný; když ho máte, klampujte hodnotu na 1..N a ošetřete ruční zápis do inputu.
Animace Nejsilnější efekt má vizualizace balíčku, která se plní — tři rámečky, do kterých po výběru naskočí obrázek. Držte se pod 250 ms a respektujte prefers-reduced-motion.
Prázdné a chybové stavy Skeleton při načítání, prázdný slot jako pozvánka („Vyberte kávu"), chyba se srozumitelnou hláškou a tlačítkem „Zkusit znovu". Nikdy jen do konzole.
Mobil Tři sloty se na 360 px nevejdou — stack nebo carousel se snapem, sticky souhrn s tlačítkem u spodní hrany, cíle minimálně 44 × 44 px.

Startovací implementace

V repozitáři je funkční starter: examples/starter. Vanilla ES6+, žádné závislosti, žádný build step — nahrajete dva soubory a jede to. Zvládá 2 až 5 slotů, oba režimy seznamů, ceny podle měny a ceníku, loading i chybové stavy.

Není to povinný základ. Berte ho jako referenci, ze které si klidně vezmete jen datovou vrstvu.

Co upravujete Co se tím mění Jak často
Blok MULTIPACK_CONFIG Shortcode, selektor obsahu, varianta obrázků z CDN, práh skladu, maximální množství Vždy
Blok MULTIPACK_TEXTS Všechny texty pro zákazníka včetně pluralizace a popisků pro čtečky Skoro vždy
CSS proměnné --wtm-* Barvy, rádiusy, mezery, stíny. Primární barva se čte jako var(--color-primary, …), takže na většině šablon sedne sama Skoro vždy
Metody render*() Samotný markup — renderSlot(), renderCard(), renderSummary(), renderRating(). Vrací HTML řetězce Když chcete jiný layout

Konfigurace je celá na jednom místě na začátku souboru:

const MULTIPACK_CONFIG = {
    baseUrl: 'https://shoptet.webotvurci.cz',
    shortcode: 'w-multipack-ABC12',   // váš shortcode, bez hranatých závorek
    contentSelector: '.content-inner',
    imageCdnBase: 'https://cdn.myshoptet.com/usr',
    imageVariant: 'orig',
    stockThreshold: 5,
    disableSoldOut: true,
    useApiPriceInSummary: true,
};

Shortcode v konfiguraci je jen výchozí hodnota — když je ve stránce [w-multipack-ABC12], vyhrává ID ze stránky. Jedním nahraným skriptem tak obsloužíte libovolný počet stránek s různými konfiguracemi, což je při správě většího e-shopu zásadní úspora.

Markup jednoho slotu vypadá takhle. Všimněte si, že každý řetězec z API prochází helperem esc() a že se kategorie vykreslují jako <optgroup> — pokud přepnete roletky za dlaždice, měníte jen tuhle metodu, zbytek widgetu o tom neví:

renderSlot(slot, index) {
    const selectId = `${this.uid}-slot-${index}`;
    return `
        <div class="wt-multipack__slot">
            <label class="wt-multipack__slot-label" for="${selectId}">${esc(slot.label)}</label>
            <div class="wt-multipack__select-wrap">
                <select class="wt-multipack__select" id="${selectId}" data-slot-select="${index}">
                    <option value="">${esc(this.texts.selectPlaceholder)}</option>
                    ${this.renderOptions(slot.categories)}
                </select>
            </div>
            <div class="wt-multipack__card" data-slot-card="${index}">${this.renderCard(index)}</div>
        </div>`;
}

Celý zdroják včetně komentářů je v examples/starter/multipack.js.

Doporučené vzory

Stav držte v objektu, ne v DOM. Jeden objekt s výběrem, množstvím, stavem a chybou, ze kterého renderujete. Jakmile začnete stav číst zpátky z <select>ů a textContent, přijdou nekonzistence — zákazník klikne během requestu, šablona vám prvek překreslí, jiný doplněk přepíše kontejner.

Event delegation. Jeden listener na kořeni konfigurátoru místo N listenerů na dlaždicích. Přežije překreslení obsahu a nemusíte nic odvěšovat.

Orientační cena lokálně, závazná z odpovědi. Během výběru počítejte náhled na frontendu, ať má zákazník okamžitou zpětnou vazbu, a po úspěšném POSTu ho přepište:

async function submit() {
    setState({ status: 'submitting', error: null });
    const data = await createSet(guidsSorted());
    setState({ status: 'ready', setPrice: data.price });   // závazná cena přepíše odhad
    shoptet.cartShared.addToCart({ productCode: data.code, amount: state.quantity });
}

Zobrazujte state.setPrice ?? estimate() a při každé změně výběru setPrice vynulujte.

Ochrana proti dvojkliku. Každé odeslání zakládá v e-shopu reálný produkt, takže single-flight guard není volitelný — druhý klik má dostat tentýž Promise:

let inFlight = null;

async function createSet(guids) {
    if (inFlight) return inFlight;
    inFlight = fetch(URL_PRODUCT_SET, { /* … */ })
        .then((res) => { if (!res.ok) throw new Error('multipack:' + res.status); return res.json(); })
        .finally(() => { inFlight = null; });
    return inFlight;
}

Samotné disabled na tlačítku nestačí — mezi kliknutím a nastavením atributu je několik milisekund. Doplňte ho o viditelný spinner.

UX checklist před spuštěním

Odklikejte na testovací konfiguraci, ideálně přímo na e-shopu klienta:

  1. Vyprodaný produkt je v nabídce zablokovaný a nejde ho dostat do sady.
  2. Produkt bez ceny v dané cenové hladině se nezobrazuje jako „0 Kč" a nekazí součet.
  3. Na pomalé síti (Slow 4G) je vidět skeleton, ne prázdno ani holý text shortcodu.
  4. Když API nedopoví při načtení, je vidět srozumitelná chyba a tlačítko „Zkusit znovu".
  5. Když selže odeslání, tlačítko je znovu klikatelné a výběr zůstal zachovaný.
  6. Rychlý dvojklik na „Koupit" odešle jeden požadavek a do košíku spadne jedna položka.
  7. Nekompletní výběr drží tlačítko disabled a je vidět, kolik ještě chybí.
  8. Změna výběru po zobrazení ceny přepočítá souhrn, nezůstane viset stará hodnota.
  9. Přihlášený zákazník s cenovou hladinou vidí ceníkové ceny; ověřte i odhlášení.
  10. XSS test: dočasně přejmenujte testovací produkt na Test <img src=x onerror=alert(1)> "uvozovky" & ampersand a projděte celý flow.
  11. Mobil 360 px — vejde se vše, sticky lišta nepřekrývá poslední slot.
  12. Klávesnice a čtečka — projde se Tabem, sloty mají <label>, chyba je v aria-live.

FAQ pro agentury

Můžeme použít React nebo Vue? Ano, API je framework-agnostické — dva HTTP endpointy a jedna funkce Shoptetu. Ohlídejte si ale velikost bundle: konfigurátor běží uvnitř cizí šablony, která už načítá jQuery, slider, analytiku a další doplňky. React + ReactDOM navíc na landing page, kde jde o konverzi, se na mobilu pozná. Preact, Svelte, Alpine nebo prostě vanilla dávají v tomhle kontextu větší smysl. Když framework použijete, mountujte ho do vlastního elementu a nesahejte na okolní DOM.

Musíme použít váš starter? Ne. Je to ukázka, ne závazek — nemusíte z něj přebírat strukturu, názvy CSS tříd ani texty. Když si napíšete vlastní implementaci a dodržíte kontrakt výš, funguje to úplně stejně. Když si ho forknete, je váš.

Jak testovat bez zásahu do produkce? Vyžádejte si u nás testovací konfiguraci — vlastní shortcode nad testovacími produkty s izolovaným obsahem a bez dopadu na ostrou landing page. Frontend si přitom můžete vyvíjet lokálně na localhost, CORS je otevřený a proxy nepotřebujete.

Co když Shoptet změní šablonu? Naše API je nezávislé na šabloně i na verzi Shoptetu, takže se tím nemění. Shoptet dataLayer a shoptet.cartShared jsou navíc oficiální rozhraní Shoptetu pro doplňky — nemění se svévolně a případné změny Shoptet komunikuje předem. Reálné riziko je jinde: v selektorech, kterými se váš kód váže na markup šablony. Držte jich co nejmíň, mějte je na jednom místě v konfiguraci a ošetřete případ, kdy prvek neexistuje.

Můžeme mít na jedné stránce dvě konfigurace? Ano, jsou to dva nezávislé shortcody. Ve svém kódu ale musíte iterovat přes všechny nálezy a držet stav per instanci, ne v globálních proměnných.

Kam dál

Stavíte balíček pro klienta a chcete si předem projít zadání nebo si vyžádat testovací konfiguraci? Napište nám na jsme@webotvurci.cz — rádi vám k tomu sedneme.