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í.
- 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. - Respektujte
list_mode. V režimusame_listsexistuje jeden společný seznam kategorií a produktů, ze kterého zákazník vybírá do všech slotů. V režimuseparate_listsmá každý slot vlastní nabídku — do slotu N patří jen kategorie, které majícolumn_number === N. - Kategorie si seřaďte sami. Pořadí, ve kterém dorazí, není součástí kontraktu; řaďte podle
column_numbera pak podlenameslocaleCompare(…, 'cs'), jinak se sloty mezi načteními přehazují. - 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.
- Do košíku vkládejte výhradně přes
shoptet.cartShared.addToCarts kódem sady. Nativní funkce řeší CSRF token, přepočet košíku i UI feedback šablony; vlastnífetchna košíkové akce Shoptet blokuje. - Závazná cena je
pricez 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. - 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. - 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: |
| 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:
- Vyprodaný produkt je v nabídce zablokovaný a nejde ho dostat do sady.
- Produkt bez ceny v dané cenové hladině se nezobrazuje jako „0 Kč" a nekazí součet.
- Na pomalé síti (Slow 4G) je vidět skeleton, ne prázdno ani holý text shortcodu.
- Když API nedopoví při načtení, je vidět srozumitelná chyba a tlačítko „Zkusit znovu".
- Když selže odeslání, tlačítko je znovu klikatelné a výběr zůstal zachovaný.
- Rychlý dvojklik na „Koupit" odešle jeden požadavek a do košíku spadne jedna položka.
- Nekompletní výběr drží tlačítko
disableda je vidět, kolik ještě chybí. - Změna výběru po zobrazení ceny přepočítá souhrn, nezůstane viset stará hodnota.
- Přihlášený zákazník s cenovou hladinou vidí ceníkové ceny; ověřte i odhlášení.
- XSS test: dočasně přejmenujte testovací produkt na
Test <img src=x onerror=alert(1)> "uvozovky" & ampersanda projděte celý flow. - Mobil 360 px — vejde se vše, sticky lišta nepřekrývá poslední slot.
- Klávesnice a čtečka — projde se Tabem, sloty mají
<label>, chyba je varia-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
- Dokumentace vlastního frontendu na GitHubu — kompletní kontrakt, API reference a okrajové případy.
- Startovací implementace
examples/starter— funkční widget k rozebrání. - Sestav si vlastní balíček — co doplněk umí, ceník a živá ukázka.
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.