{ "version": "https://jsonfeed.org/version/1", "title": "Josef Rousek", "home_page_url": "https://rousek.name", "feed_url": "https://rousek.name/rss/feed.json", "description": "Blog Josefa Rouska", "icon": "https://rousek.name/favicon.ico", "author": { "name": "Josef Rousek" }, "items": [ { "id": "https://rousek.name/articles/vyber-vydejniho-mista-na-shopify-jake-jsou-moznosti", "content_html": "

V Evropě dominují výdejní místa – jsou levnější, pohodlnější a bezpečnější než doručování domů. Shopify na to reaguje novou funkcí, která výběr výdejního místa posouvá o úroveň výše. Ale než se k této novince dostaneme, pojďme si nejdřív rozebrat, jak vlastně výdejní místa na Shopify fungují.

\n

Proč jsou výdejní místa tak populární?

\n

Výdejní místa se v posledních letech stala běžnou součástí online nakupování, zejména v Evropě. A není se čemu divit.

\n\n

Z pohledu e-shopu navíc přinášejí výdejní místa i nižší chybovost v doručování, méně vratek a méně komplikací s nedoručenými zásilkami. Jenže na Shopify to stále není pro běžné obchody ideální. Shopify Plus a jeho Enterprise plán totiž umožňuje vložit rozšíření do checkoutu, které ostatní plány nemají.

\n

Výběr výdejního místa pro obchodníky bez Plus plánu

\n

Většina obchodníků dnes využívá specializované aplikace, které výdejní místa zprostředkují. Mezi nejznámější patří:

\n\n

Tyto aplikace fungují víceméně podle stejného scénáře:

\n
    \n
  1. Zákazník v checkoutu zvolí způsob dopravy, např. "Výdejní místo – Packeta".
  2. \n
  3. Po dokončení objednávky si může vybrat výdejní místo na Thank you page. Jestli výběr provede přímo na stránce nebo na samostatné stránce záleží na aplikaci.
  4. \n
  5. Vybere si konkrétní výdejní místo (dle PSČ, mapy nebo názvu pobočky).
  6. \n
  7. Aplikace uloží údaje o výdejním místě do objednávky – nejčastěji jako poznámku nebo vlastní pole.
  8. \n
\n

A kde je háček?

\n

Z pohledu uživatele i vývojáře má toto řešení několik slabin:

\n\n

Jeden z důvodů, proč přicházíte o konverze

\n

Zákaznická zkušenost je klíčem k úspěchu. A pokud při nákupu narazí na rušivý moment – třeba když musí čekat, než se načte mapa výdejních míst, nebo když si nemůže zvolit pobočku hned – snadno košík opustí.

\n

Navíc je zde otázka důvěry: pokud uživatel nevidí, kam mu balík přijde ještě během nákupu, může váhat s dokončením objednávky. A v době, kdy konkurence nabízí pohodlný výběr přímo při nákupu, může být rozdíl pár kliknutí rozhodující.

\n

Jak funguje výběr výdejního místa pro Shopify plus obchodníky:

\n\n

Blýská se na lepší časy pro všechny?

\n

Výdejní místa přímo v checkoutu bez nutnosti zadání adresy

\n

Shopify testuje dlouho očekávanou novinkou – plně integrovaný výběr výdejního místa přímo v rámci checkoutu. Jde o zásadní posun v tom, jak zákazníkům zpříjemnit nákupní zážitek. A pro evropské trhy, kde pickup pointy hrají hlavní roli, je to velká věc.

\n

Co znamená "nativní"?

\n

Slovo "nativní" v tomto případě neznamená, že výdejní místa fungují bez aplikace. Znamená, že jejich výběr je zabudován přímo do prostředí checkoutu bez použití checkout extensions. Tyto checkout extensions jsou právě funkce pouze pro Shopify plus a je tedy šance, že Shopify později zpřístupní výběr výdejního místa v checkoutu pro všechny.

\n

Zákazník:

\n\n

Obchodník:

\n\n

Pro koho je tato novinka určená?

\n

Bohužel – a to je třeba zdůraznit – nativní pickup pointy nejsou zatím dostupné pro všechny. Shopify je spustil zatím jen v režimu early access a pro Shopify Plus obchodníky.

\n

Hlavní výhody nativního řešení

\n

1. Checkout bez přerušení

\n

Výběr výdejního místa bez nutnosti zadat adresu, přesně jak zákazník očekává.

\n

2. Výběr výdejního místa i bez zadání adresy

\n

Dříve musel zákazník nejprve zadat adresu, podle které se zobrazila výdejní místa. Teď to funguje obráceně – výběr místa je první krok.

\n

3. Plná integrace s objednávkou

\n

Zvolený pickup point se uloží jako součást doručovacích údajů: adresa pobočky, kód výdejního místa, dopravce. Vývojáři mají k těmto údajům přístup přes API.

\n

4. UX v souladu s celým e-shopem

\n

Žádné překvapivé přechody nebo odlišný vzhled. Výběr výdejního místa vypadá a funguje stejně jako ostatní části checkoutu.

\n

Jak to vypadá v praxi? Příklad z českého e-shopu Yoggies

\n

Český e-shop Yoggies prodává zdravé krmivo pro psy a kočky. Novou funkci využívá jako jeden z prvních v ČR. Díky nativnímu výběru výdejního místa zákazník vidí výdejní místa rovnou při objednávce, aniž by musel zadávat adresu. Tím se snižuje míra opuštěných košíků, zvyšuje důvěra zákazníka a zlepšuje se celkový zážitek z nákupu.

\n\n

Co očekávat do budoucna?

\n

Je velmi pravděpodobné, že se tato funkce časem rozšíří i na další tarify Shopify. Tímto krokem Shopify ukazuje, že výdejní místa vnímá jako standardní součást doručování, a to především v Evropě. Lze očekávat:

\n\n

Nová funkce výběru výdejního místa přímo v checkoutu představuje zásadní zjednodušení nákupního procesu. Umožňuje obchodníkům nabídnout zákazníkům to, co chtějí – pohodlí, přehlednost a rychlost.

\n

Nejčastější otázky

\n

Jak aktivuji nativní výběr výdejního místa?

\n

Je potřeba Shopify Plus, vlastní aplikace a přístup přes early access program Shopify.

\n

Může zákazník zvolit výdejní místo bez zadání adresy?

\n

Ano. To je hlavní rozdíl oproti původnímu řešení – adresu nemusí zadávat vůbec.

\n

Umožňuje Shopify změnu výdejního místa po objednávce?

\n

V rámci nativního řešení ano, v ostatních případech to musí podporovat aplikace.

\n

Co když mám tarif Shopify Basic?

\n

Bohužel, prozatím musíte výdejní místa řešit pomocí externích aplikací na děkovací stránce a sledování stavu objednávky.

\n

Pomocí aplikace Nouta.cz můžete implementovat výdejní místa PPL a Zásilkovny do svého e-shopu. Pro informace ohledně implementace nativních výdejních míst napište na: josef@rousek.name.

", "url": "https://rousek.name/articles/vyber-vydejniho-mista-na-shopify-jake-jsou-moznosti", "title": "Výběr výdejního místa na Shopify – jaké jsou možnosti", "summary": "V Evropě dominují výdejní místa – jsou levnější, pohodlnější a bezpečnější než doručování domů. Shopify na to reaguje novou funkcí, která výběr výdejního místa posouvá o úroveň výše.", "date_modified": "2025-07-03T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/dobirka-v-roce-2025", "content_html": "

Je platba na dobírku přežitek? Pro a proti této platební metody

\n

Platba na dobírku – dříve standardní součást online nakupování, dnes často diskutovaný relikt minulosti. Zatímco jedni ji vnímají jako bezpečný způsob nákupu, jiní se jí vyhýbají kvůli administrativě, rizikům a nákladům. E-commerce se mění a s ní i platební metody. Znamená to, že dobírka už nemá své místo? Nebo existují situace, kdy je stále výhodná?

\n

Co znamená platba na dobírku a proč byla dříve tak populární?

\n

Dobírka v kostce: stručný princip jejího fungování

\n

Dobírka je způsob doručení zásilky, při kterém zákazník platí za objednávku až při převzetí zboží – hotově, kartou nebo QR kódem u dopravce. Peníze jsou následně převedeny zpět prodejci. Z pohledu zákazníka jde o platbu při převzetí, která odstraňuje obavu, že zboží nedorazí. Zejména v počátcích e-commerce, kdy důvěra k e-shopům teprve vznikala, byla dobírka jedním z hlavních taháků.

\n

Historie a důvody, proč dobírka vznikla a proč se udržela až dodnes

\n

Platba na dobírku vznikla jako praktické řešení pro zemi, kde důvěra ve vzdálený obchod nebyla samozřejmostí. V Česku, podobně jako v dalších postkomunistických zemích, hrála zásadní roli v růstu e-shopů od počátku 21. století. Zákazník nechtěl riskovat – dobírka mu poskytovala klid. Přestože dnes většina zákazníků platí online, dobírka si stále drží své místo. A právě to nutí e-shopy přemýšlet: má cenu ji ještě nabízet, nebo je to přežitek, který spíš škodí než pomáhá?

\n

Výhody platby na dobírku pro zákazníky

\n

Důvěra ve zboží a obava z podvodů

\n

Jedním z hlavních důvodů, proč zákazníci stále volí dobírku, je nedůvěra. Platit předem neznámému e-shopu? Ne každý je ochotný to risknout. Zvlášť u starších generací nebo zákazníků, kteří nakupují online výjimečně.

\n

Psychologický komfort při nákupu

\n

Dobírka působí jako pojistka – zákazník má pocit, že „drží trumfy". Zboží uvidí, osahá si ho (alespoň balík), a teprve pak platí. Tato kontrola nad nákupem je pro určité typy lidí klíčová.

\n

Platba při převzetí jako argument pro nerozhodné zákazníky

\n

Nabídka dobírky může fungovat jako konverzní záchrana – zákazník váhá, ale když vidí možnost platby až při doručení, dokončí objednávku. Pro nové e-shopy může být dobírka psychologickým argumentem pro získání první objednávky.

\n

Výhody dobírky pro e-shopy

\n

Získání zákazníků, kteří odmítají platbu předem

\n

Některé skupiny zákazníků prostě neplatí předem – ať už kvůli nedůvěře, technickým možnostem nebo zvyku. Nabídkou dobírky může e-shop oslovit právě tyto zákazníky a rozšířit svou cílovou skupinu, aniž by musel zásadně měnit obchodní model.

\n

Konkurenční výhoda pro specifické segmenty

\n

V některých segmentech, jako je second hand, bazary, hobby zboží nebo niche trhy, kde zákazníci často hledají alternativy mimo zavedené značky, může dobírka představovat konkurenční výhodu. Zvlášť pokud cílová skupina není digitálně zdatná nebo důvěřivá.

\n

Nevýhody dobírky pro e-shopy

\n

Vysoká míra nevyzvednutých zásilek

\n

Zásadním problémem dobírky je riziko, že zákazník zásilku nepřevezme. Ať už z rozmaru, zapomnětlivosti nebo neochoty zaplatit. Podle dostupných statistik se míra nevyzvednutých balíků pohybuje mezi 5–15 %, v některých případech i více. To znamená ztrátu za dopravu, vratku, skladování a znehodnocení zboží.

\n

Náklady na dobírku: cena dobírky a její dopad

\n

Dobírka bývá zpoplatněná – běžně 20–50 Kč navíc oproti klasickému doručení. Tento poplatek může:

\n\n

Z pohledu cash flow může být platba na dobírku značně nevýhodná, zvlášť pokud se zpracovává ručně nebo přes méně moderní dopravce.

\n

Administrativa a zpětné toky peněz

\n

Zatímco online platby jsou okamžité, peníze z dobírky se k e-shopu dostanou často až po několika dnech. Navíc vyžadují účetní zpracování, párování plateb, kontrolu reklamací a případných vratných procesů. Pro menší e-shop to může být neúměrná zátěž.

\n

Přehled dopravců a jejich podmínek

\n

Zásilkovna a platba na dobírku

\n

Zásilkovna umožňuje platbu na dobírku jak hotově, tak kartou, přičemž peníze obvykle odesílá e-shopu do 3 pracovních dnů. Dobírkový poplatek se pohybuje kolem 20–30 Kč, v závislosti na objemu a smluvních podmínkách. Výhodou je rychlé zpracování a dostupnost poboček.

\n

Česká pošta a balík na dobírku

\n

Česká pošta stále nabízí dobírku jako jednu ze svých klíčových služeb. Přestože je dražší (dobírkový poplatek často přesahuje 40 Kč), pro některé zákazníky zůstává tradičním a důvěryhodným dopravcem. Výplata probíhá obvykle do 5 pracovních dnů.

\n

Platby na dobírku s PPL

\n

PPL nabízí dobírku s možností platby v hotovosti i kartou. Výhodou je rychlé doručení i převod dobírkové částky – zpravidla do 2–3 pracovních dnů. Služba PPL Parcel Connect navíc umožňuje dobírku i v rámci zahraničních zásilek (např. Slovensko, Polsko). Poplatek za dobírku se pohybuje okolo 30–40 Kč, podle tarifu.

\n

Platby na dobírku s DPD

\n

U DPD je dobírka dostupná jak pro doručení na adresu, tak pro výdejní místa DPD Pickup. Platba je možná kartou nebo hotově, peníze jsou standardně převáděny do 3 pracovních dnů. Služba se hodí pro e-shopy, které chtějí nabídnout komfortní doručení s profesionálním vystupováním kurýrů.

\n

Platby na dobírku s GLS

\n

GLS nabízí dobírku s výplatou na bankovní účet, často během 1–2 dnů. Podporuje platbu kartou i hotově, v závislosti na oblasti.

\n

Platby na dobírku s WE|DO

\n

WE|DO umožňuje platbu na dobírku v rámci výdejních boxů i na adresu, podmínky jsou podobné jako u konkurence.

\n

Jak poslat balík na dobírku: Praktický návod

\n

Krok za krokem: Dobírka přes dopravce

\n

Poslat balík na dobírku není složité, ale je dobré mít přehled o postupu, abyste předešli chybám:

\n
    \n
  1. Výběr dopravce – Zvolte dopravce, který dobírku podporuje (Zásilkovna, Česká pošta, PPL, DPD apod.).
  2. \n
  3. Vyplnění údajů – V e-shopovém systému nebo aplikaci dopravce nastavíte:\n\n
  4. \n
  5. Způsob platby – Ověřte, zda je možná i platba kartou při převzetí.
  6. \n
  7. Odeslání zásilky – Označte balík jako „dobírkový" dle instrukcí dopravce.
  8. \n
  9. Sledování a výplata – Sledujte doručení a výplatu dobírkové částky (běžně do 2–5 dní).
  10. \n
\n

Dobře nastavený systém dokáže celý proces automatizovat. Pro větší e-shopy jsou ideální napojení na API dopravců nebo automatizace přes externí služby.

\n

Statistiky: Kolik zákazníků ještě platí na dobírku?

\n

Trendy posledních let v ČR a EU

\n

Zatímco dříve tvořila dobírka většinu online plateb, dnes je její podíl v Česku podle různých studií přibližně 15–25 %. Závisí však na typu zboží a věkové struktuře zákazníků.

\n\n

Kdy je dobírka stále relevantní?

\n\n

Alternativní platební metody a jejich výhody

\n

Platby kartou online, Apple Pay, bankovní převod

\n

Dnes mají zákazníci k dispozici řadu moderních platebních metod, které jsou rychlé, bezpečné a pohodlné. Z pohledu e-shopu tyto metody přinášejí nižší náklady, rychlé získání peněz, eliminaci nevyzvednutých zásilek a méně administrativy. Patří mezi ně:

\n\n

Možnosti "Buy Now, Pay Later"

\n

Stále populárnější možností jsou také služby typu "kup teď, zaplať později", které nabízí např. Twisto, Skip Pay nebo Mall Pay. Výhoda? Zákazník získá komfort jako u dobírky (neplatí hned), ale e-shop dostane peníze okamžitě a bez rizika. Tyto služby se rychle šíří zejména v segmentech s mladší cílovou skupinou nebo dražším zbožím.

\n

Shrnutí: Má dobírka v roce 2025 ještě smysl?

\n

Dobírka v dnešní době není mrtvá – rozhodně už ale není pro každého. Pro některé zákazníky představuje jistotu, pro e-shopy ale i značné náklady a rizika. Zvažovat by se měla vždy s ohledem na typ zboží, cílovou skupinu, obchodní model a marže i technické možnosti e-shopu a dopravců.

\n

Doporučení pro majitele e-shopů

\n

Kdy se dobírka ještě vyplatí

\n\n

Kdy je lepší ji nahradit moderními metodami

\n\n

Platba na dobírku už dávno není nutnost

\n

V roce 2025 je to volitelná služba, kterou e-shop může – ale nemusí – nabízet. Může být mostem ke konverzi, ale také zdrojem frustrace a zbytečných nákladů. Záleží na tom, koho obsluhujete a jak chcete podnikat. Přemýšlejte strategicky: kde dobírka pomáhá, tam ji využijte. Kde škodí, tam ji bez výčitek zrušte.

\n

Nejčastější otázky

\n

Co znamená platba na dobírku?

\n

Je to způsob platby, kdy zákazník hradí zboží až při jeho převzetí od dopravce.

\n

Jaké jsou hlavní výhody dobírky pro zákazníky?

\n

Zákazníci se cítí bezpečněji, protože platí až po doručení. Nemusí mít obavy z podvodu.

\n

Jaká je cena dobírky u dopravců?

\n

Zpravidla 20–50 Kč, záleží na dopravci a smluvních podmínkách s e-shopem.

\n

Jak poslat balík na dobírku?

\n

Stačí nastavit dobírkovou částku při zadávání zásilky u dopravce a zajistit výplatu na váš účet.

\n

Je dobírka vhodná pro každý e-shop?

\n

Ne. Využijte ji tam, kde má přínos – u důvěryhodnosti, nových zákazníků nebo konzervativního segmentu.

", "url": "https://rousek.name/articles/dobirka-v-roce-2025", "title": "Dobírka v roce 2025: Ano, nebo ne?", "summary": "Platba na dobírku – výhody, nevýhody a aktuální statistiky. Zjistěte, kdy se dobírka e-shopům vyplatí a kdy je lepší ji zrušit.", "date_modified": "2025-06-28T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/upravy-produktove-stranky-konverze", "content_html": "

Možná to znáte – máte skvěle nastavenou reklamu, návštěvnost roste, ale prodeje ne a ne vyskočit. Často to není o nedostatku lidí, ale o tom, co vidí, když na stránku přijdou. Právě produktová stránka totiž výrazně ovlivňuje, jestli návštěvník nakoupí, nebo odejde jinam. V tomto článku se podíváme na osm konkrétních úprav, které vycházejí z principů UX, behaviorální psychologie i reálných zkušeností z e-commerce. A co je nejlepší? Není k tomu potřeba více návštěvníků. Jen lépe pracovat s těmi, kteří už na stránce jsou.

\n

Název produktu: první řádek, který rozhoduje

\n

Jak psát názvy, které zaujmou a pomohou ve vyhledávání

\n

Název produktu je první věc, kterou návštěvník na stránce vnímá. Nejde jen o informaci – je to argument. Slova v názvu musí odpovídat tomu, co zákazník hledá, zároveň ale musí vytvářet důvěru a stručně vysvětlit, proč právě tento produkt stojí za kliknutí, přečtení – a nákup. Na Shopify je běžnou chybou, že název obsahuje jen interní pojmenování (např. Tričko 1324 M zelená). Takový název nepomáhá zákazníkovi ani vyhledávačům. Naopak: popisný, konkrétní a srozumitelný název dokáže zvýšit konverze i organickou návštěvnost.

\n

Jak na to:

\n\n

Dobrý název zvyšuje kliknutí i na stránkách kolekcí, kde zákazník často porovnává více produktů naráz. Navíc se název automaticky propisuje i do title tagu produktové stránky – má tedy vliv i na SEO.

\n

Tip: úpravy názvů v Shopify Admin nebo pomocí tagů

\n

V administraci Shopify přejděte na Produkty → konkrétní produkt → Název. Úprava se projeví napříč celým e-shopem – na stránkách kolekcí, v detailech produktu, ale i v košíku a e-mailech. Pokud používáte appky pro filtrování (např. Search & Discovery od Shopify), můžete si navíc pomoci tagy a metafieldy, které vám umožní přidat "marketingový název" nebo doplnit název o klíčovou vlastnost bez narušení základní struktury.

\n

Fotky, které prodávají: co očekávají zákazníci na Shopify stránce

\n

Podpora 3D modelů a videí ve výchozích šablonách

\n

Vizuální obsah je na produktové stránce zásadní – a u Shopify to platí dvojnásob. Platforma nabízí výborné možnosti práce s multimédiem: podporuje fotky ve vysokém rozlišení, videa, a dokonce 3D modely (GLB nebo USDZ formáty). V šablonách jako Dawn nebo Craft je navíc možné přidat video nebo interaktivní prvek bez zásahu do kódu. Zákazníci chtějí vidět produkt "na vlastní oči". Nestačí jeden hezký snímek zepředu. Co chtějí doopravdy?

\n\n

Shopify umožňuje tato média vkládat přímo při editaci produktu. Přidání videa nebo 3D modelu výrazně zvyšuje důvěru a snižuje počet vratek, protože zákazník lépe ví, co kupuje.

\n

Doporučení na styl fotografií pro mobilní vs. desktop uživatele

\n

Přes 70 % zákazníků na e-shopech prochází produktové stránky z mobilu. Tomu by měly odpovídat i fotky:

\n\n

Zkontrolujte si také, zda Shopify šablona neomezuje počet náhledových fotek. V některých případech je vhodné použít appku typu TinyIMG nebo Photo Resize, které optimalizují velikost bez ztráty kvality.

\n

Cenová strategie: jak využít badge a slevy v šablonách Shopify

\n

V příkladech níže narazíte často na šablonu Dawn – není to náhoda. Jde o výchozí šablonu, kterou Shopify automaticky nabízí od roku 2021 a která slouží jako základ většině e-shopů. Je moderní, flexibilní a postavená na Online Store 2.0, takže podporuje vše, co aktivní e-shop potřebuje – od dynamických sekcí přes snadnou práci s médii až po pokročilé úpravy bez programování.

\n

Všechny uvedené principy a úpravy však platí i pro další šablony na platformě Shopify, jako jsou Sense, Craft, Studio, Refresh a další. Pokud používáte upravený theme nebo vlastní šablonu, pravděpodobně i tak využíváte základní stavební prvky, které z Dawn vycházejí.

\n

Práce se štítky jako "Sleva", "Výhodná cena", "Doporučujeme"

\n

Cena je jedním z největších spouštěčů nákupního rozhodnutí. Není to jen o číslech – je to o vnímání hodnoty. Pokud zákazník cítí, že dostává více, než kolik platí, konverze roste.\nShopify s tímto principem dobře pracuje. Ve výchozích šablonách jako Dawn, Sense nebo Studio se automaticky zobrazuje porovnání původní a zlevněné ceny. Ale co opravdu funguje?

\n\n

Tyto prvky zvyšují důvěryhodnost, urychlují rozhodování a navozují dojem exkluzivity.\nV šabloně Dawn (a většině dalších) můžete zobrazovat slevu automaticky:

\n\n

Pro zobrazení vlastního odznaku s výší slevy ve tvaru kolečka (např. -15 %) lze upravit šablonu, nebo použít předpřipravený kód v Liquid. Alternativně lze využít appku jako Sale & Discount Badges, která nevyžaduje znalost kódování.

\n\n

CTA tlačítko, které vede k akci

\n

Změny barvy, textu a umístění v rámci Theme Editoru

\n

CTA ("Call to Action" neboli výzva k akci) je nejdůležitější tlačítko celé stránky – a zároveň prvek, který bývá nejčastěji přehlížen. Pokud zákazník nepochopí, kam má kliknout, nebo se mu tlačítko ztratí v designu, vaše produktová stránka konverze generovat nebude, i kdyby byla sebelépe napsaná.

\n

Na Shopify se CTA obvykle zobrazí jako tlačítko "Přidat do košíku" nebo "Buy now". Zní to jako samozřejmost, ale právě styl, barva, velikost i pozice CTA mají zásadní vliv na to, zda zákazník vůbec dokončí nákup.

\n

Většina moderních šablon (např. Dawn, Sense, Refresh) umožňuje:

\n\n

CTA by mělo být jasné, kontrastní a srozumitelné. Zapomeňte na obecné fráze typu "Pokračovat" nebo "Zobrazit více". Dejte zákazníkovi přesně vědět, co se stane:

\n

➡️ "Koupit teď"\n➡️ "Vložit do košíku"\n➡️ "Objednat s dopravou zdarma"

\n

Hodnocení produktů: jak je sbírat a zobrazit na Shopify

\n

Doporučené aplikace (Loox, Judge.me, Yotpo)

\n

Hodnocení a recenze od reálných zákazníků patří mezi nejúčinnější prvky sociálního důkazu. Zákazník, který uvažuje o koupi, si často chce potvrdit, že nejde o zbytečné riziko – a právě pozitivní zkušenosti jiných ho mohou přesvědčit. Shopify sice nemá nativně zabudovaný systém recenzí, ale existuje řada kvalitních aplikací, které jejich sběr i zobrazování vyřeší elegantně:

\n\n

Všechny tři appky lze snadno integrovat přímo z Shopify App Store. Po aktivaci se recenze propisují na produktovou stránku, obvykle hned pod název nebo popis produktu – místo, kde mají největší vliv.

\n

Využití recenzí jako sociálního důkazu přímo na produktové stránce

\n

Získané recenze nestačí jen zobrazit – je třeba je také správně prezentovat:

\n\n

Na Shopify lze přizpůsobit vzhled recenzí pomocí widgetů v Theme Editoru (pokud appka tuto možnost nabízí) nebo lehkou úpravou Liquid kódu. Loox a Judge.me například umožňují přidat vlastní nadpisy, změnit barvy i řazení.

\n

Nezapomeňte také na možnost propojit recenze s e-mailem po nákupu – zákazníci často rádi pomohou, když je o to jasně a jednoduše požádáte.

\n

Důvěryhodnost a garance: co pomáhá v rozhodovacím procesu

\n

Doprava zdarma, výměna zdarma, garance – jak je zobrazit v šabloně

\n

Nákup online je pro zákazníka vždy spojen s určitou nejistotou. Nevidí produkt naživo, nemůže si ho osahat a často nezná vaši značku. Právě proto hrají garance, výhody a důvěryhodnostní prvky klíčovou roli v rozhodování. Na Shopify produktové stránce je ideální přidat k produktu tzv. "value props" – tedy výčet důvodů, proč nakoupit právě u vás. Patří sem například:

\n\n

Tyto benefity můžete zobrazit několika způsoby:

\n\n

Shopify vám umožňuje pracovat s těmito prvky snadno. Můžete použít:

\n\n

Horizon šablony: budoucnost Shopify témat s AI na palubě

\n

Horizon představuje novou vlajkovou šablonu Shopify, která postupně nahradí dosavadní Dawn theme. V rámci Edition Summer 2025 Shopify oznámil, že Horizon se stane novým designovým základem postavených na flexibilních blocích. Přestože Dawn zůstává funkční a bude i nadále dostávat opravy kritických chyb, nové funkce už se do něj přidávat nebudou.

\n

Co dělá Horizon výjimečným:

\n\n

Doporučení pro vaši volbu:

\n\n

Horizon skutečně představuje budoucnost Shopify témat, ale stejně jako u každé nové technologie je rozumné počkat na stabilizaci, pokud provozujete kritický e-shop s komplexními úpravami.

\n

Ikony, které přidáte bez znalosti kódu (např. pomocí metafields)

\n

Chcete přidat ikonky jako "🚚 Doprava zdarma", "✔️ Bezpečná platba", "💬 Podpora 24/7"? Nemusíte umět kód. V moderních šablonách Shopify je můžete přidat přes:

\n\n

Důležité je, kde tyto výhody zobrazíte – ideální je místo v blízkosti ceny nebo tlačítka, kde zákazník právě zvažuje, zda pokračovat v nákupu.

\n

Popis produktu, který je čitelný a orientovaný na výhody

\n

Strukturování textu: od nadpisů po tabulky specifikací

\n

Popis produktu není technický manuál. Je to nástroj, jak zbavit zákazníka nejistoty a přivést ho k rozhodnutí. Bohužel mnoho e-shopů popisek buď odbývá („černé tričko z bavlny, velikost M"), nebo přetěžuje informacemi bez kontextu. Silný popis by měl odpovědět na tři otázky:

\n
    \n
  1. Co to je?
  2. \n
  3. Proč by mě to mělo zajímat?
  4. \n
  5. Co z toho budu mít já?
  6. \n
\n

Na Shopify doporučujeme strukturovat popis do jasných bloků. Například:

\n\n

Shopify umožňuje využít bohatý textový editor nebo pracovat s metafieldy, které přidávají různé části textu (např. technický popis zvlášť, marketingový zvlášť). Díky tomu můžete obsah přizpůsobit podle typu produktu.

\n

Kam vložit dlouhý popis a kam krátké shrnutí

\n

Většina moderních Shopify šablon zobrazuje buď:

\n\n

U šablon jako Dawn, Sense nebo Refresh můžete popisek rozdělit na více částí pomocí appky jako Easy Tabs nebo pomocí vlastních sekcí v kódu šablony.

\n

Doporučení:

\n\n

Bonus: Zvažte přidání příběhu produktu nebo značky – co ho dělá výjimečným? Takové texty pomáhají budovat vztah a přeměňují návštěvníky v zákazníky.

\n

Technická a vizuální důvěryhodnost stránky

\n

Platební metody, zabezpečení, recenze obchodu

\n

Zákazníci dnes očekávají, že e-shop bude bezpečný, důvěryhodný a transparentní. I když nabízíte kvalitní produkty, pokud stránka nevypadá profesionálně a postrádá důležité vizuální signály důvěry, může to zásadně snížit váš konverzní poměr. Co přesně zákazníci vnímají jako důvěryhodnostní prvky?

\n\n

Na Shopify je možné tyto prvky snadno přidat:

\n\n

Badge důvěry: kde je získat a jak je na Shopify snadno přidat

\n

Badge (neboli odznaky důvěry) plní dvě funkce: okamžitě signalizují profesionalitu a snižují obavy z nákupu. Čím více máte objednávek, tím víc by se měly tyto prvky na stránce objevovat.

\n

Tipy:

\n\n

Na Shopify lze badge přidat buď ručně (obrázky v sekci) nebo pomocí appky, která vám umožní přidat kódově připravený widget.

\n

Shrnutí: Co všechno může ovlivnit konverzní poměr

\n

Vylepšení produktové stránky není o náhodě. Naopak – vychází z konkrétních principů UX, psychologie prodeje a dat. A právě Shopify vám dává skvělé nástroje, jak tato zlepšení jednoduše aplikovat. Zopakujme si 8 úprav, které mohou vystřelit konverze vašeho e-shopu:

\n
    \n
  1. Název produktu, který prodává
  2. \n
  3. Kvalitní fotografie a videa
  4. \n
  5. Chytré zobrazení ceny a slevy
  6. \n
  7. CTA tlačítko, které vede k akci
  8. \n
  9. Recenze a hodnocení zákazníků
  10. \n
  11. Výhody a garance nákupu
  12. \n
  13. Promyšlený a čitelný popis produktu
  14. \n
  15. Vizuální a technická důvěryhodnost stránky
  16. \n
\n

K tomu přidejte jednoduché A/B testování a pravidelnou práci s daty – a získáte výkonnou produktovou stránku, která už nebude jen hezká, ale hlavně zisková.

\n

Nejčastější otázky

\n

Jaký je ideální konverzní poměr pro Shopify e-shop?\nZáleží na oboru, ale v průměru se konverzní poměr pohybuje mezi 1–3 %. U dobře optimalizovaných stránek může přesáhnout i 5 %.

\n

Jak mohu sledovat konverzní poměr ve svém e-shopu?\nNejlépe přes Google Analytics 4, Shopify Analytics nebo nástroje jako Hotjar. Nejde jen o samotné konverze, ale i o to, kde zákazníci nejčastěji odcházejí.

\n

Co mám udělat jako první, pokud chci zvýšit konverze?\nZačněte s tým, co má největší vliv: výzva k akci, kvalita vizuálů a sociální důkaz (recenze). Tyto prvky často přinášejí nejrychlejší výsledky.

\n

Je možné mít na produktové stránce video nebo 3D model?\nAno. Shopify podporuje MP4 videa i 3D soubory GLB a USDZ, které lze nahrát přímo do galerie produktu.

", "url": "https://rousek.name/articles/upravy-produktove-stranky-konverze", "title": "Produktová stránka: 8 úprav pro vyšší konverzní poměr", "summary": "Zvyšte konverze e-shopu pomocí osmi osvědčených změn produktové stránky. Praktické tipy pro majitele Shopify.", "date_modified": "2025-05-15T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/water-consumption-monitoring-with-multical21-and-home-assistant", "content_html": "

My water meter is located quite far from the house, which made this project particularly interesting. Here's how I got it working.

\n

The Hardware Setup

\n\n

The Multical21 water meter transmits data using the Wireless M-Bus protocol. Since my meter is far from the house, I needed to place the receiver (iM871A-USB dongle) closer to the meter. The Raspberry Pi Zero was perfect for this setup due to its small size and low power consumption.

\n

Detailed Installation Guide

\n

1. Prepare the Raspberry Pi

\n
    \n
  1. Flash Raspberry Pi OS Lite to your SD card
  2. \n
  3. Enable SSH and configure WiFi before first boot
  4. \n
  5. Connect to the Pi via SSH
  6. \n
\n

2. Install Required Dependencies

\n
# Update system packages\nsudo apt update\nsudo apt upgrade -y\n\n# Install required dependencies\nsudo apt install -y git\nsudo apt install -y librtlsdr-dev libusb-dev\nsudo apt install -y libxml2-dev\nsudo apt install -y mosquitto-clients\n\n### 3. Install wmbusmeters\n\n```bash\n# Download and extract wmbusmeters\nwget https://api.github.com/repos/wmbusmeters/wmbusmeters/tarball/master\ntar -xvzf master\ncd wmbusmeters-wmbusmeters-*\n\n# Compile and install\n./configure\nmake\nsudo make install\n\n# If make fails, ensure all dependencies are installed and try again\n
\n

4. Configure wmbusmeters

\n
    \n
  1. Create the main configuration file:
  2. \n
\n
sudo nano /etc/wmbusmeters.conf\n
\n
    \n
  1. Add the following configuration:
  2. \n
\n
loglevel=normal\ndevice=auto:c1\ndonotprobe=/dev/ttyAMA0\nlogtelegrams=false\nformat=json\nmeterfiles=/var/log/wmbusmeters/meter_readings\nmeterfilesaction=overwrite\nmeterfilesnaming=name-id\nlogfile=/var/log/wmbusmeters/wmbusmeters.log\nshell=HOME=/home/wmbusmeters/ mosquitto_pub -h YOUR_HOME_ASSISTANT_IP -u water_meter -P YOUR_PASSWORD -t wmbusmeters/$METER_ID -m "$METER_JSON"\n
\n
    \n
  1. Create meter configuration directory and file:
  2. \n
\n
sudo mkdir -p /etc/wmbusmeters.d\nsudo nano /etc/wmbusmeters.d/vodomer\n
\n
    \n
  1. Add meter configuration:
  2. \n
\n
name=tapwater\nid=YOUR_METER_ID\nkey=YOUR_METER_KEY\ndriver=multical21\n
\n

5. Set up logging directory

\n
# Create logging directories\nsudo mkdir -p /var/log/wmbusmeters/meter_readings\n
\n

6. Start and Enable Service

\n
# Reload systemd to recognize the service\nsudo systemctl daemon-reload\n\n# Start wmbusmeters service\nsudo systemctl start wmbusmeters\n\n# Enable service to start on boot\nsudo systemctl enable wmbusmeters\n\n# Check service status\nsudo systemctl status wmbusmeters\n
\n

7. Monitor Logs and Troubleshooting

\n
# View live logs\ntail -f /var/log/wmbusmeters/wmbusmeters.log\n\n# Check meter readings\ntail -f /var/log/wmbusmeters/meter_readings/tapwater-YOUR_METER_ID\n\n# Restart service after config changes\nsudo systemctl restart wmbusmeters\n
\n

8. Home Assistant Configuration

\n

Add the following to your Home Assistant's configuration.yaml:

\n
mqtt:\n  sensor:\n    - name: "Tapwater total"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      device_class: water\n      state_class: total\n      value_template: "{{ (value_json.total_m3 * 1000) }}"\n      unit_of_measurement: L\n\n    - name: "Tapwater target"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      unit_of_measurement: "m³"\n      device_class: water\n      state_class: total\n      value_template: "{{ value_json.target_m3 }}"\n\n    - name: "Tapwater temperature"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      unit_of_measurement: "°C"\n      device_class: temperature\n      state_class: measurement\n      value_template: "{{ value_json.flow_temperature_c }}"\n\n    - name: "Tapwater external temperature"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      unit_of_measurement: "°C"\n      device_class: temperature\n      state_class: measurement\n      value_template: "{{ value_json.external_temperature_c }}"\n\n    - name: "Tapwater rssi"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      unit_of_measurement: "dBm"\n      device_class: signal_strength\n      state_class: measurement\n      value_template: "{{ value_json.rssi_dbm }}"\n\n  binary_sensor:\n    - name: "Tapwater status dry"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      device_class: problem\n      payload_on: "True"\n      payload_off: "False"\n      value_template: "{{ 'DRY' in value_json.current_status }}"\n\n    - name: "Tapwater status reversed"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      device_class: problem\n      payload_on: "True"\n      payload_off: "False"\n      value_template: "{{ 'REVERSED' in value_json.current_status }}"\n\n    - name: "Tapwater status leak"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      device_class: problem\n      payload_on: "True"\n      payload_off: "False"\n      value_template: "{{ 'LEAK' in value_json.current_status }}"\n\n    - name: "Tapwater status burst"\n      state_topic: "wmbusmeters/YOUR_METER_ID"\n      device_class: problem\n      payload_on: "True"\n      payload_off: "False"\n      value_template: "{{ 'BURST' in value_json.current_status }}"\n
\n

Troubleshooting Tips

\n
    \n
  1. \n

    If the dongle isn't detected:

    \n\n
  2. \n
  3. \n

    If no readings appear:

    \n\n
  4. \n
  5. \n

    If MQTT publishing fails:

    \n\n
  6. \n
  7. \n

    Common log messages:

    \n\n
  8. \n
", "url": "https://rousek.name/articles/water-consumption-monitoring-with-multical21-and-home-assistant", "title": "Water Consumption Monitoring with Multical21 and Home Assistant", "summary": "How I set up a Raspberry Pi Zero with wmbusmeters to remotely monitor my Multical21 water meter using an iM871A-USB dongle and forward the readings to Home Assistant.", "date_modified": "2024-11-03T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/building-a-shed", "content_html": "

When I decided to build a shed, I saw it as a perfect opportunity to put leftover CLT (cross-laminated timber) panels from my house construction to good use. They were already lying under the tarp for almost a year and I didn't want them to rot or make firewood from them.

\n

I was really intimidated by the project at first, but it turned out to be more manageable than I thought. The hardest was the start. I learned a lot! From working with new materials to experimenting with 3D modeling and fighting with my fear of heights, every step of the way was a learning experience.

\n\n

This article documents my process, from planning and construction to the lessons learned along the way. It’s not just a guide, but a reflection on the challenges and rewards of taking on a DIY project of this scale, especially when you’re not a professional builder. Hopefully, my experience will encourage you to dive into your own projects, whether it’s a small shed or something more ambitious.

\n

Making Use of Leftover Materials

\n

The main material for the shed came from leftover CLT panels, which had originally been used for my house. I had enough to use for the walls of the shed of the size 3 x 4m, and the dimensions were roughly 4.2 cm thick, 80 cm wide, and between 220 and 240 cm tall.

\n

For the foundation, I used ground screws and wooden beams.

\n

As for the tools, my toolbox isn’t very extensive, but it turned out to be enough. The main tools I relied on were:

\n\n

For the floor, I opted for 22mm OSB boards and 18mm OSB boards for the roof.

\n

For the roofing, I chose trapezoidal (corrugated) metal sheets, which offered durability and a good level of water resistance. Finally, I used a standard roofing membrane to protect OSB boards from condensation.

\n\n

Planning and Modeling with Precision

\n

Before any physical work began, I took the time to plan everything using SketchUp, a 3D modeling tool. Initially, I was quite intimidated by the idea of using 3D software. However, after watching just two YouTube videos, I picked up the basics and started to enjoy the process. Having the ability to experiment with panel arrangment and shed sizes was crucial to me.

\n

The 3D model became invaluable later in the project. It allowed me to plan all my cuts in advance, get a clear idea of how the structure would come together, and make sure I wasn’t wasting any materials. Without it, I would have had to calculate and manually draw all the angles and measurements, which would have made the whole process much more time-consuming and prone to errors.

\n

Laying a Solid Foundation

\n

Once the planning was done, the next step was to lay the foundation. I decided to use ground screws and wooden beams. Ground screws were a practical choice, as they eliminated the need for concrete and provided a sturdy base while being easier to install on uneven ground. The beams were treated with a mixture of pine tar, tung oil, and turpentine to protect them from the elements. This traditional wood treatment not only sealed the wood against moisture but also gave the beams some insect-repellent properties.

\n

The biggest challenge during this phase was getting the foundation both level and square. It took a lot of patience and small adjustments to make sure everything was aligned correctly. Any errors here would have caused bigger problems down the line, so I made sure to double-check all my measurements with a speed square and laser level before moving on.

\n\n

Building a Strong Floor

\n

With the foundation in place, I moved on to the floor. I chose 22mm OSB boards for this part. Then I started second guessing myself and did some research. I discovered that there's no right answer and it depends on your needs and how sturdy you want the floor to feel. Since the spacing between the foundation beams was about 90 cm, I decided to add some extra braces in between the beams to give the floor additional support.

\n\n

Cutting and fitting the OSB boards was straightforward. The braces really helped strengthen the floor, and once everything was in place, it felt solid enough.

\n\n

Cutting and Assembling the Panels

\n

Once the floor was complete, the next step was cutting the CLT panels to size. This was where the 3D model truly came into play. Having everything pre-planned meant I could follow the dimensions and angles with precision, avoiding guesswork. Since I was working mostly during evenings after my regular day, the process was slow but steady. It took about two weeks of cutting in my free time to get all the panels sized properly.

\n\n

After cutting, it was time to install the panels. I took some vacation days and, with help from my partner, started putting them up. The large panels were a breeze to manipulate with two people, and after a few hours, we had the main walls standing. It was incredibly satisfying to see the structure start taking shape.

\n

The panels were secured to the floor and to each other with 140mm screws and galvanized connection plates, which helped ensure everything stayed firmly in place.

\n

There was a small section of about 1 meter that I had to fill with off-cuts because there wasn’t enough large pannels. I used pieces around 30 cm wide to fill this gap.

\n\n

Tackling the Rafters and Roof Frame

\n

Once the walls were up, it was time to tackle the rafters and roof frame. I used the same spacing for the rafters as I had for the foundation beams — about 90 cm apart. However, one of the beams I had planned to use for the rafters was severely warped, so I couldn't use it. To get around this, I joined two shorter beams together, which worked out surprisingly well. This was actually my initial plan because I wanted to use the beams I already have but in order to make things quicker and easier I decided to just buy longer beams.

\n\n

To fill the gaps between the rafters I used off-cuts from the CLT panels, not only to seal the building but also to help stabilize the walls. I had plenty of 140mm screws on hand, so I used those to secure everything tightly.

\n\n

Constructing the Roof without Confidence

\n

With the rafters in place things started to get challenging. The roof boards were significantly larger than the ones used for the floor(18 x 1250 x 2500 mm) and getting them up in one piece was not an option. I had cut them so that the boards were supported by the rafters. I then laid down a roofing membrane and installed metal sheets.

\n\n

This phase was challenging due to my fear of heights, and I made a few decisions I might need to go back to in the future. For example, I overlapped the membrane edges instead of taping them, which should be fine on the sides and top edge but could need monitoring. Also I taped the membrane to the drip edge instead of the boards, this left the tape exposed to sunlight and OSB board edge exposed. So I had to tape over it to protect it from splashing water from the gutter.

\n

Protecting and Preparing the Walls

\n

With the roof in place, the next step was to protect the walls. Since I didn’t made the final decision on the cladding, I covered the walls with the same roofing membrane. This provides protection from the weather until I can install the final cladding. The plan is to use vertical slatted cladding.

\n\n\n

There's always something more to do

\n

The shed stands solid, and I’m looking forward to finishing the cladding soon. Also as you can see the membrane is not finished around the doorframe yet and I'm not really sure how to finish this detail. But the ultimate goal having a dry space for a lawnmower, bike and other stuff is finished.

\n

Overall, building this lean-to shed was a huge learning experience. From mastering 3D modeling to handling CLT panels, every step taught me something new. While there were some mistakes—like the roof membrane choices and dealing with warped beams—it was all part of the learning curve.

", "url": "https://rousek.name/articles/building-a-shed", "title": "Panel by Panel: My Hands-On Shed Building Adventure", "summary": "Building a shed might seem like a simple weekend project, but when I set out to build mine using leftover CLT panels from my house, it quickly evolved into a multi-month journey.", "date_modified": "2024-10-02T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/what-is-cls-and-how-to-improve-it-in-shopifynot-only-apps", "content_html": "

Shopify has begun to measure Web Vitals for embedded apps. Prioritizing web app performance can be challenging, so it's encouraging to see that they're pushing app developers to do the right thing. However, improving Web Vitals can be difficult on Single Page Applications (SPA) as it was primarily designed for web content. The first metric is called the Largest Contentful Paint, and it's important to remember that this metric is influenced by everything between your code and the user.

\n

Firstly, ensure that everything is set up correctly on the backend side.

\n\n

How is the metric measured in the browser

\n

LCP represents the time it takes for the largest image or text block to become visible to the user.

\n
    \n
  1. Images - This includes both images loaded via the <img> tag and images as part of a CSS background (loaded with the background-image property).
  2. \n
  3. Image elements inside an SVG document.
  4. \n
  5. Video elements - For videos, it is the poster image that's considered, not the actual video itself.
  6. \n
  7. Block-level elements containing text nodes - or other inline-level text elements children. It means any text wrapped in elements like <p>, <h1>, <div>, etc.
  8. \n
\n

This means that your dashboard table is likely not considered content, but your banner with a sentence or two might be. If your largest content is delayed, you can fix that by adding a paragraph of text or an image to the part of the page that's visible upon loading. Use the "Largest Contentful Paint Sub-Parts (LCP)" from https://github.com/nucliweb/webperf-snippets to determine what qualifies as contentful paint in your app.

", "url": "https://rousek.name/articles/what-is-cls-and-how-to-improve-it-in-shopifynot-only-apps", "title": "What is CLS and how to improve it in Shopify(not only) apps", "summary": "Shopify has begun to measure Web Vitals for embedded apps. Prioritizing web app performance can be challenging, so it's encouraging to see that they're pushing app developers to do the right thing.", "date_modified": "2023-07-04T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/svg-sparklines-with-no-dependencies", "content_html": "

In one of our projects, we used Highcharts to display a sparkline. However, when I began to explore the bundle contents to improve our Largest Contentful Paint (LCP), I noticed that Highcharts was fairly large - around 200kb before being gzipped.

\n

Previously, I had used Pygal to generate SVG charts on the server side. To my delight, I discovered that it supported sparklines. However, after giving it a whirl, I found that the resulting SVG was larger than I had hoped. It was bloated with a lot of unnecessary CSS and JS. I tried wrangling the library to fit my needs for some time, but eventually decided to explore other options.

\n

I then turned my attention to the SVG produced by Highcharts. Interestingly, I noticed two visible path elements - one for the area below the line and one for the line itself. With the assistance of ChatGPT, I managed to gain insight into the structure of the path data and asked it to write a function that would generate these paths.

\n
\n

To my surprise, the function worked flawlessly on the first attempt! Of course, I had to make a few tweaks to support both the line and the area below it, and to handle any edge cases that might come up. So, in the end, the lesson learned was that sometimes it's best to roll up your sleeves and get down to creating exactly what you need.

\n
import React, { FC } from 'react';\n\nconst WIDTH = 150;\nconst HEIGHT = 25;\n\ninterface SparklineProps {\n  data: number[];\n}\n/*\n\nThe calculation HEIGHT - ((value - minimum) / (maximum - minimum)) * HEIGHT is used to normalize and scale the values of the data set within a certain range. In this case, it is scaling the values within the range of the height of the SVG canvas. Here's how it works:\n\n1. (value - minimum) / (maximum - minimum): This part of the equation normalizes the value. This means it adjusts the value to be on a scale from 0 to 1, where 0 corresponds to the minimum value in the dataset and 1 corresponds to the maximum value. This is achieved by subtracting the minimum value from the current value (making the minimum value now 0), and then dividing by the range of the dataset, which is (maximum - minimum).\n\n2. The normalized value is then multiplied by the HEIGHT. This scales the normalized value to the range of the height of the SVG canvas. This value now represents a position on the y-axis of the SVG canvas, but from the top down (because in SVG coordinates, 0,0 is at the top left).\n\n3. Finally, HEIGHT - ... is used to invert the value. This is done because SVG coordinates start from the top left corner, meaning higher y-values are lower on the canvas. This step ensures that higher data values are plotted higher on the canvas.\n\n*/\nexport const generateSparkline = (data: number[], area = false): string => {\n  // We don't want to display single point sparklines\n  if (data.length <= 1) {\n    return '';\n  }\n\n  let minimum = Math.min(...data);\n  let maximum = Math.max(...data);\n\n  // Prevent division by zero when values are constant\n  if (minimum === maximum) {\n    maximum += 1;\n  }\n  let deltaXincrementPerDataPoint = WIDTH / (data.length - 1); // Calculate the x-axis increment for each data point\n\n  const path = data.map((value, i) => {\n    // Scale the value to fit within the height\n    let scaledValue =\n      HEIGHT - ((value - minimum) / (maximum - minimum)) * HEIGHT;\n\n    let mode: 'M' | 'L';\n    if (i === 0 && !area) {\n      // For line path move to the first point and start there\n      mode = 'M';\n    } else {\n      // in every other case draw line\n      mode = 'L';\n    }\n\n    return `${mode}${i * deltaXincrementPerDataPoint} ${scaledValue}`;\n  });\n\n  if (area) {\n    path.unshift(`M0 ${HEIGHT}`); // Start at the bottom left\n    path.push(`L${WIDTH} ${HEIGHT}`); // Line to the last point\n  }\n  return path.join(' ');\n};\n\nconst Sparkline: FC<SparklineProps> = ({ data }) => {\n  let areaPath = generateSparkline(data, true);\n  let linePath = generateSparkline(data);\n\n  return (\n    <svg\n      version="1.1"\n      xmlns="http://www.w3.org/2000/svg"\n      width={WIDTH}\n      height={HEIGHT}\n      viewBox={`0 0 ${WIDTH} ${HEIGHT}`}\n    >\n      <path fill="#47C1BF1A" d={areaPath}></path>\n      <path\n        fill="none"\n        d={linePath}\n        stroke="#47C1BF"\n        strokeWidth="1"\n        strokeLinejoin="round"\n        strokeLinecap="round"\n      ></path>\n    </svg>\n  );\n};\n\nexport default Sparkline;\n
", "url": "https://rousek.name/articles/svg-sparklines-with-no-dependencies", "title": "SVG Sparklines with no dependencies", "summary": "Discover how to create SVG sparklines with no dependencies, improving performance and reducing bundle size.", "date_modified": "2023-06-29T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/unmasking-ui-glitches-in-react", "content_html": "

While working on a project, I encountered a peculiar behavior that initially stumped me. I was aiming to improve Cumulative Layout Shift (CLS) by introducing a skeleton loader. However, after implementing this loader, I noticed the User Interface (UI) was occasionally jumpy, leading to a surge in CLS.

\n

Upon reviewing screen recordings, I discovered the issue: the skeleton loader would disappear for a few milliseconds, causing the content to shift upward. Immediately after, a banner would render and push the content back down. The effect was more noticeable with CPU slowdown enabled.

\n

The code seemed fine, so I was unsure about the root cause. The implementation wasn't optimal, but there was no clear reason for this behavior given the simplicity of the output variable.

\n

Here's the code I was dealing with:

\n
const { settings, onAction: handleAction } = useSettings();\n\nconst showSettingsBanner = settings.data?.blockSupported && settings.data?.isAppRemoved;\n\nconst tutorial = useGetTutorialQuery();\nconst tutorialDismissed = tutorial.data?.appTutorial?.dismissed;\n\nconst {\n  data: defaultData,\n  error: defaultDataError,\n  isSuccess: defaultDataIsSucess,\n  isLoading: defaultDataIsLoading\n} = useIsDefaultQuery();\n\nconst showPermissionsBanner = (isApp1() || isApp2()) && !defaultDataIsLoading && !defaultDataError && defaultData && !defaultData.isDefault && offerExists;\n\nconst showTutorialBanner = ((isApp1() || isApp2()) && tutorialDismissed === true) || tutorialDismissed === undefined ? false : true;\n\nconst showFreeBanner = isApp2() && someBillingConditions\n\nlet bannerToDisplay = (() => {\n  if (\n    (!settings.isSuccess && !settings.isError) ||\n    (!tutorial.isSuccess && !tutorial.isError) ||\n    (!defaultDataIsSucess && !defaultDataError)\n  ) {\n    return 'skeleton';\n  }\n  if (showFreeBanner) {\n    return 'criticalFreeBanner';\n  } else if (showSettingsBanner) {\n    return 'criticalSettingsBanner';\n  } else if (showPermissionsBanner) {\n    return getWarningBanner(showPermissionsBanner);\n  } else if (showTutorialBanner) {\n    return getTutorialBanner(showTutorialBanner);\n  }\n})();\n
\n

I began to suspect that the issue might be beyond my grasp, but I decided to try a performance optimization technique using React's useMemo hook—and it worked!

\n
const { settings, onAction: handleAction } = useSettings();\nconst tutorial = useGetTutorialQuery();\nconst {\n  data: defaultData,\n  error: defaultDataError,\n  isSuccess: defaultDataIsSucess,\n} = useIsDefaultQuery();\n\nconst bannerToDisplay = useMemo(() => {\n  const showSettingsBanner = settings.data?.blockSupported && settings.data?.isAppRemoved;\n\n  const tutorialDismissed = tutorial.data?.appTutorial?.dismissed;\n\n  const showPermissionsBanner = (isApp1() || isApp2()) && !defaultDataIsLoading && !defaultDataError && defaultData && !defaultData.isDefault && offerExists;\n\n  const showTutorialBanner = ((isApp1() || isApp2()) && tutorialDismissed === true) || tutorialDismissed === undefined ? false : true;\n\n  const showFreeBanner = isApp2() && someBillingConditions\n\n  if (\n    (!settings.isSuccess && !settings.isError) ||\n    (!tutorial.isSuccess && !tutorial.isError) ||\n    (!defaultDataIsSucess && !defaultDataError)\n  ) {\n    return 'skeleton';\n  }\n  if (showFreeBanner) {\n    return 'criticalFreeBanner';\n  } else if (showSettingsBanner) {\n    return 'criticalSettingsBanner';\n  } else if (showPermissionsBanner) {\n    return getWarningBanner(showPermissionsBanner);\n  } else if (showTutorialBanner) {\n    return getTutorialBanner(showTutorialBanner);\n  }\n})\n
\n

This surprising issue sparked my curiosity about how heavy computations and the way React handles rendering can impact performance and UI stability, especially under heavy load or on slower CPUs.

\n

React function components execute from top to bottom on each render. Thus, if you perform heavy computations directly in the render function, these computations will be re-run each time the component re-renders. Although React employs a mechanism called Virtual DOM to optimize this process, running these computations repeatedly can slow down the rendering process.

\n

One way to optimize heavy computations is by using React's built-in hooks, useMemo and useCallback. These hooks memoize computations and functions, preventing unnecessary recalculations when their dependencies have not changed.

\n

useMemo is used to remember the result of a function, re-computing the memoized value only when one of its dependencies changes. It can prevent the need to re-run an expensive computation on each render, resulting in performance improvements.

\n

However, it's crucial to understand that overusing memoization can introduce unnecessary complexity into your codebase and sometimes lead to more overhead than benefits. So, it's essential to identify any actual performance bottlenecks before optimizing.

\n

Another aspect that could lead to UI glitches is layout thrashing, where repeated read/write operations to the DOM force the browser to perform multiple layout calculations before a page is visually updated. In my case, alternating between showing the skeleton loader and rendering the banner might have caused the browser to recalculate the layout more frequently than necessary, leading to the jumpy UI.

\n

React mitigates some of these effects by batching updates to the Virtual DOM. Still, direct DOM manipulations or frequent layout changes in components can sometimes cause layout thrashing and degrade performance.

\n

Understanding these behaviors allows us to write more efficient and performant React applications, ensuring a smoother user experience even under heavy loads or on slower CPUs. As React developers, we need to be mindful of potential pitfalls and take advantage of the tools and techniques React provides to optimize performance.

", "url": "https://rousek.name/articles/unmasking-ui-glitches-in-react", "title": "Unmasking UI Glitches in React: How useMemo Saved the Day", "summary": "While working on a project, I encountered a peculiar behavior that initially stumped me. I was aiming to improve Cumulative Layout Shift (CLS) by introducing a skeleton loader.", "date_modified": "2023-05-22T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/dynamic-blocks-with-streamfield", "content_html": "

As a developer, I always strive to find ways to make my projects more friendly and customizable for the end user. Recently, I created an online store called passionbox.cz for my partner using Django. In this blog post, I'll share how I used StreamField, a library that allows the creation of dynamic blocks on the page, to create a flexible and easy-to-manage interface for the store.

\n

StreamField: What and Why?

\n

StreamField is a library that provides a growable value of elements that can be of different types, making it possible to create dynamic content blocks on the page. It was inspired by the StreamField in Wagtail CMS, and like Wagtail, stores block data in a JSONField. The library is designed to integrate seamlessly with the Django admin interface, making it easy to manage the site's content from the backend.

\n

Implementing StreamField in the Product and Page Models

\n

To implement StreamField in the Product and Page models, I started by defining the different block types I wanted to use. In the Product model, I created a DetailsBlock class, which includes a title, description, images, and corresponding descriptions for each image.

\n
class DetailsBlock(blocks.StructBlock):\n    title = blocks.RawHTMLBlock()\n    description = blocks.RawHTMLBlock()\n    image1 = blocks.TextBlock()\n    description1 = blocks.RawHTMLBlock()\n    image2 = blocks.TextBlock()\n    description2 = blocks.RawHTMLBlock()\n
\n

I then added a StreamField to the Product model, specifying the DetailsBlock as a block type.

\n
class Product(models.Model):\n    ...\n    blocks = StreamField(\n        block_types=[\n            ("details", DetailsBlock()),\n        ],\n        verbose_name="Product block",\n        default=list,\n        blank=True,\n    )\n
\n

For the Page model, I created several block types, such as AccordionBlock, CallToActionBlock, and ProductTestimonialBlock. Each block type consists of different content elements, such as questions and answers, button text and links, testimonials, and images.

\n
class AccordionItemBlock(blocks.StructBlock):\n    question = blocks.CharBlock()\n    answer = blocks.RawHTMLBlock()\n\nclass AccordionBlock(blocks.StructBlock):\n    items = blocks.ListBlock(AccordionItemBlock)\n\nclass CallToActionBlock(blocks.StructBlock):\n    title = blocks.RawHTMLBlock()\n    description = blocks.RawHTMLBlock()\n    button_text = blocks.TextBlock()\n    button_link = blocks.RelURLBlock()\n\nclass ProductTestimonialBlock(blocks.StructBlock):\n    title = blocks.RawHTMLBlock()\n    testimonial = blocks.RawHTMLBlock()\n    image = blocks.TextBlock()\n    button_link = blocks.RelURLBlock()\n    items = blocks.ListBlock(blocks.TextBlock())\n
\n

After defining the block types, I added a StreamField to the Page model, making these block types available for use on the site.

\n
class Page(models.Model):\n    ...\n    blocks = StreamField(\n        block_types=[\n            ("accordion", AccordionBlock()),\n            ("cta", CallToActionBlock()),\n            ("product_testimonial", ProductTestimonialBlock()),\n            ("raw_html", blocks.RawHTMLBlock()),\n        ],\n        verbose_name="Page block",\n        default=list,\n        blank=True,\n    )\n
\n

Result: A Flexible and Easy-to-Manage Online Store

\n

By implementing StreamField in the Product and Page models, I was able to create an online store that is not only dynamic but also easy to manage through the Django admin interface. The different block types allow my partner to customize the content and layout of the store, tailoring it to the specific needs of the business without requiring any coding knowledge.

\n

The use of StreamField has also made it easier to maintain the site's content and structure, as it provides a clear separation between the different content elements. This modular approach allows for more efficient updates and modifications, as my partner can edit or rearrange the blocks without affecting the rest of the page.

\n

Moreover, StreamField's ability to store block data in a JSONField makes it simple to work with the data programmatically. This flexibility opens up possibilities for further customization and automation, such as generating dynamic meta tags or performing advanced filtering based on block content.

\n

Conclusion

\n

In conclusion, StreamField has proven to be a valuable addition to the passionbox.cz project, enabling the creation of a dynamic and easy-to-manage online store using Django. By implementing various block types in the Product and Page models, I've provided my partner with a flexible and user-friendly interface to manage the site's content and layout.

\n

If you're looking to build a customizable and dynamic website or application, I highly recommend giving StreamField a try. Its seamless integration with Django admin and flexible content management capabilities make it a powerful tool for both developers and non-technical users alike.

", "url": "https://rousek.name/articles/dynamic-blocks-with-streamfield", "title": "Dynamic Blocks with StreamField: How I Built an Online Store with Django", "summary": "As a developer, I always strive to find ways to make my projects more friendly and customizable for the end user. Recently, I created an online store called passionbox.cz for my partner using Django. In this blog post, I'll share how I used StreamField, a library that allows the creation of dynamic blocks on the page, to create a flexible and easy-to-manage interface for the store.", "date_modified": "2023-05-11T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/fighting-email-spoofing-a-step-by-step-guide-to-setting-up-dmarc-for-your-domain", "content_html": "

In this blog post, let's dive into the world of email spoofing frauds and learn how to protect your domain. I learned about the topic from risky.biz podcast.

\n

Email spoofing is a common problem in the digital world, where attackers can send fraudulent emails that appear to be from a legitimate source. DMARC, or Domain-based Message Authentication, Reporting and Conformance, is a protocol designed to combat email spoofing by verifying the authenticity of emails. In this blog post, we'll go over how to set up DMARC and provide some examples of DMARC configuration.

\n
    \n
  1. Start by creating a DMARC record DMARC uses a DNS TXT record to specify email authentication policies for your domain. The record will include the following elements:
  2. \n
\n\n

Here's an example of a basic DMARC record:

\n
v=DMARC1; p=none; pct=100; rua=mailto:re+dkmhabc123@dmarc.postmarkapp.com; sp=none; aspf=r;\n
\n
    \n
  1. Add Forensic Reporting Forensic Reporting is an optional feature of DMARC that provides detailed information about emails that fail authentication checks. It is recommended to enable this feature to help identify the source of the problem and fix it. To add Forensic Reporting to your DMARC record, modify the record as follows:
  2. \n
\n\n

Here's an example of a DMARC record with Forensic Reporting enabled:

\n
v=DMARC1; p=none; pct=100; rua=mailto:re+dkmhabc123@dmarc.postmarkapp.com; ruf=mailto:forensic-reports@example.com; sp=none; aspf=r; fo=1;\n
\n
    \n
  1. Monitor and Test Your DMARC Configuration To ensure that DMARC is working correctly, it is recommended to monitor and test your configuration. Postmark offers a free weekly email service to help monitor and implement DMARC.
  2. \n
\n

You can also use learndmarc.com to test your DMARC configuration. The DMARC analyzer will send a test email to your email address and provide you with a report on the DMARC compliance status of your domain.

\n

In conclusion, DMARC is a protocol that helps prevent email spoofing and increases the security of email communication. By following the steps outlined in this blog post, you can set up DMARC with recommended settings and tools for testing. Remember to tighten your DMARC policy as you become more confident in your configuration.

", "url": "https://rousek.name/articles/fighting-email-spoofing-a-step-by-step-guide-to-setting-up-dmarc-for-your-domain", "title": "Fighting Email Spoofing: A Step-by-Step Guide to Setting Up DMARC for Your Domain", "summary": "In this blog post, let's dive into the world of email spoofing frauds and learn how to protect your domain. I learned about the topic from risky.biz podcast.", "date_modified": "2023-03-19T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/shopify-like-product-variants-in-django", "content_html": "

In this blog post, we will discuss how to create Shopify-like product variants in Django. Our goal was to have a product with multiple variants, each with its own price. We liked the approach used by Shopify, which uses product options and variants.

\n

A product option represents a choice that a customer can make when purchasing a product. For example, a t-shirt product might have the option of "Size" with the values "Small", "Medium", and "Large". A product variant is a specific combination of options that a product can have. In the example of the t-shirt, a variant would be "Small - Red" or "Large - Blue".

\n

One of the key components of this approach is the use of the save_related method in the product admin class. This method is called after the parent product and its related options and variants have been saved, and it allows us to perform additional actions on the related objects. In our case, we use save_related to automatically create variants for each option value, and to delete any variants that are no longer associated with the product.

\n

Here's an example of how we implemented this in our project:

\n
class ProductAdmin(admin.ModelAdmin):\n    list_display = ("title", "price", "order")\n    list_display_links = ("title",)\n    prepopulated_fields = {"slug": ("title",)}\n    inlines = (ProductOptionInline, ProductVariantInline)\n    form = ProductAdminForm\n\n    def save_related(self, request, form, formsets, change):\n        super().save_related(request, form, formsets, change)\n        instance = form.instance\n        instance.refresh_from_db()\n        if instance.option1:\n            for option_value in instance.option1.values:\n                instance.variants.get_or_create(option1=option_value, defaults={"price": instance.price})\n\n            instance.variants.exclude(option1__in=instance.option1.values).delete()\n
\n

In this example, we've overridden the save_related method in the ProductAdmin class. The super().save_related(request, form, formsets, change) line calls the parent method so that the parent product and related options and variants are saved as usual. Then, we refresh the instance from the database using instance.refresh_from_db(). Next, we use the get_or_create method to create a variant for each option value, with the price set to the product's price. We also use the exclude method to delete any variants that are no longer associated with the product.

\n

With this approach, we were able to easily create and manage product variants with different prices, pretty close to Shopify. Below you can see all the parts.

\n", "url": "https://rousek.name/articles/shopify-like-product-variants-in-django", "title": "Shopify-like product variants in Django online store", "summary": "In this blog post, we will discuss how to create Shopify-like product variants in Django. Our goal was to have a product with multiple variants, each with its own price. We liked the approach used by Shopify, which uses product options and variants.", "date_modified": "2023-01-26T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/pivot-table-in-django-admin-using-postgres-crosstab", "content_html": "

As a person responsible for the process that runs every week for multiple merchants, I wanted to gain insight into the reliability of our solution. Specifically, I wanted to ensure that we had not missed any runs. To accomplish this, I decided to create a pivot table that would allow me to look back a few weeks and see all of the runs that had occurred during that time period.

\n

The query to generate pivot table looks like this and consists of following sections.

\n
SELECT final_result.* FROM crosstab(\n$$\n    SELECT\n        merchant.myshopify_domain,\n        EXTRACT('week' FROM (analysis."created" + interval '13 hour') AT TIME ZONE 'Europe/Prague'),\n        MAX(analysis."created")\n    FROM\n        merchant_admin_analysis analysis\n        left join merchant_admin_authappshopuser merchant on analysis.shop_id = merchant.id\n    WHERE analysis."created" > (NOW() - interval '7 weeks')\n    GROUP BY\n    1,\n    2;\n$$,\n$$\n    SELECT extract('week' from generate_series((NOW() - interval '7 week'), now(), '1 week')::date)::int;\n$$\n) AS final_result(shop_id varchar, "48" timestamp with time zone,"49" timestamp with time zone,"50" timestamp with time zone,"51" timestamp with time zone,"52" timestamp with time zone,"1" timestamp with time zone,"2" timestamp with time zone,"3" timestamp with time zone);\n
\n
    \n
  1. The first section of this query is the main SELECT statement which selects the final result of a crosstab query. The crosstab function is used to create a pivot table from the results of a SELECT statement.
  2. \n
  3. The first argument of the crosstab function is a SELECT statement that retrieves data from the "merchant_admin_analysis" table and joins it with the "merchant_admin_authappshopuser" table on the "shop_id" column. The query extracts the week from the "created" column, adds 13 hours to it, and converts the time zone to 'Europe/Prague'. This is to make sure we're covering entire world. I'm sure there's a better way to do this. It also gets the maximum "created" value for each merchant and week, where the analysis "created" is greater than 7 weeks ago. The query groups the results by the merchant's myshopify_domain and the extracted week.
  4. \n
  5. The second argument of the crosstab function is another SELECT statement that generates a series of weeks starting from 7 weeks ago until now, and extracts the week number from it. This is used as the column names in the resulting crosstab table.
  6. \n
  7. The final result of the crosstab query is given the column names "shop_id" of type varchar, and "48" to "3" of type timestamp with time zone. These column names correspond to the weeks in the generate series of the second argument of the crosstab function and represent the maximum created column of the first query in each week.
  8. \n
\n

One of the challenges I encountered in this endeavor was that we are using Postgres as our database, and the crosstab function built into Postgres does not support dynamic columns. To work around this limitation, I decided to split the select into two. First I generate a list of weeks, and then use that list to generate the crosstab query in Python.

\n
import logging\n\nfrom django.db import connection\nfrom django.shortcuts import render\nfrom django.views import View\n\nlogger = logging.getLogger(__name__)\n\n\ndef generate_crosstab_query(weeks):\n    columns = ",".join(f'"{week}" timestamp with time zone' for week in weeks)\n    return f"""\n    SELECT final_result.* FROM crosstab(\n    $$\n        SELECT\n            merchant.myshopify_domain,\n            EXTRACT('week' FROM (analysis."created" + interval '13 hour') AT TIME ZONE 'Europe/Prague'),\n            MAX(analysis."created")\n        FROM\n            merchant_admin_analysis analysis\n            left join merchant_admin_authappshopuser merchant on analysis.shop_id = merchant.id\n        WHERE analysis."created" > (NOW() - interval '7 weeks')\n        GROUP BY\n        1,\n        2;\n    $$,\n    $$\n        SELECT extract('week' from generate_series((NOW() - interval '7 week'), now(), '1 week')::date)::int;\n    $$\n    ) AS final_result(shop_id varchar, {columns});\n    """\n\n\nWEEKS_QUERY = """\nSELECT extract('week' from generate_series((NOW() - interval '7 week'), now(), '1 week')::date)::int;\n"""\n\n\ndef use_raw_query(query):\n    with connection.cursor() as cursor:\n        cursor.execute(query)\n        rows = cursor.fetchall()\n    return rows\n\n\ndef auto_analysis_report():\n    weeks = use_raw_query(WEEKS_QUERY)\n    weeks = [row[0] for row in weeks]\n    report = use_raw_query(generate_crosstab_query(weeks))\n    return weeks, report\n\n\nclass AutoAnalysisReportView(View):\n    admin_site = None\n\n    def get(self, request):\n        context = dict(self.admin_site.each_context(request))\n        weeks, report = auto_analysis_report()\n        context |= {"report": report, "weeks": weeks}\n\n        return render(request, "admin/report.html", context)\n\n
\n

The template is simple table extending {% extends "admin/base_site.html" %}.

\n
{% extends "admin/base_site.html" %} {% block title %} Order statuses {{\nblock.super }} {% endblock %} {% block breadcrumbs %}\n<div class="breadcrumbs">\n  <a href="{% url 'admin:index' %}">Home</a>\n</div>\n{% endblock %} {% block content %}\n<div id="content-main">\n  <hr style="margin: 2vh 0 2vh 0;" />\n  <table>\n    <thead>\n      <th>Shop</th>\n      {% for week in weeks %}\n      <th>{{week}}</th>\n      {% endfor %}\n      <th>inactive more than 180 days</th>\n    </thead>\n    <tbody>\n      {% for row in report %}\n      <tr>\n        {% for item in row %}\n        <td>{{ item|default:"-" }}</td>\n        {% endfor %}\n      </tr>\n      {% endfor %}\n    </tbody>\n  </table>\n</div>\n{% endblock %}\n
\n

Then to add the view to Django Admin sidebar. There's no easy/standard/right way to do this. I kinda liked the custom admin site approach i found at StackOverflow.

\n
\nfrom django.contrib import admin\nfrom django.urls import path, reverse\n\nimport merchant_admin.admin_views as views\n\n\nclass CustomAdminSite(admin.AdminSite):\n\n    def get_urls(self):\n        urls = super().get_urls()\n\n        custom_urls = [\n            path(\n                "auto-analysis-report",\n                self.admin_view(views.AutoAnalysisReportView.as_view(admin_site=self)),\n                name="auto_analysis_report",\n            ),\n        ]\n        return custom_urls + urls\n\n    def _build_app_dict(self, request, label=None):\n        # we create manually a dict to fake a model for our view 'auto_analysis_report'\n        # this is an example how the dict should look like, i.e. which keys\n        # should be present, the actual values may vary\n        stats = {\n            "name": "Auto analysis report",\n            "admin_url": reverse("admin:auto_analysis_report"),\n            "object_name": "Auto analysis report",\n            "perms": {"delete": False, "add": False, "change": False},\n            "add_url": "",\n        }\n        # get the app dict from the parent method\n        app_dict = super()._build_app_dict(request, label)\n        # check if there is value for label, then the app_index will be rendered\n        if label:\n            # append the manually created dictionary 'stats'\n            app_dict["models"].append(stats)\n        else:\n            app = app_dict.get("merchant_admin", None)\n            # if an entry for 'merchant_admin' has been found\n            # we append our manually created dictionary\n            if app:\n                app["models"].append(stats)\n        return app_dict\n\n
", "url": "https://rousek.name/articles/pivot-table-in-django-admin-using-postgres-crosstab", "title": "Pivot Table in Django Admin using Postgres crosstab", "summary": "As a person responsible for the process that runs every week for multiple merchants, I wanted to gain insight into the reliability of our solution.", "date_modified": "2023-01-22T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/codespaces", "content_html": "

I ended up writing online store almost from scratch because of product and market restrictions and then stuck with it because price and freedom it provides. Meaning some features like live preview are not available. This was possible thanks to my partner knowing basics of HTML.

\n

With Github Codespaces being released to all developers I though I could use it to spin up development environment with lowest amount of manual steps required.

\n

The project is Django application using PostgreSQL as database and Cloudinary to host images. I'm also using Tailwind UI which comes with CLI to build CSS. Codespaces come with ready made docker-compose.yml and Docker for Python and Node.js development so I didn't have to touch Dockerfile.

\n

I started by putting sensitive ENV variables like mail provider and payment gateway credentials to secrets. Then I used docker-compose.yml to store static non-sensitive variables and containerEnv in devcontainer.json to store dynamic variables like PUBLIC_URL.

\n

.devcontainer/docker-compose.yml

\n
version: '3.8'\n\nservices:\n  app:\n    build:\n      context: ..\n      dockerfile: .devcontainer/Dockerfile\n      args:\n        # Update 'VARIANT' to pick a version of Python: 3, 3.10, 3.9, 3.8, 3.7, 3.6\n        # Append -bullseye or -buster to pin to an OS version.\n        # Use -bullseye variants on local arm64/Apple Silicon.\n        VARIANT: 3.10-bullseye\n        # Optional Node.js version to install\n        NODE_VERSION: '18'\n\n    environment:\n      - DEFAULT_MANAGER_EMAIL=josef@rousek.name\n      - DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres\n      - ECOMAIL_LIST_ID=2\n
\n

.devcontainer/devcontainer.json

\n
  // Use 'postCreateCommand' to run commands after the container is created.\n  "postCreateCommand": "pipenv install --dev && pip install pre-commit && pre-commit install && pipenv run migrate && pipenv run loaddata && npm install",\n\n  // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.\n  "remoteUser": "vscode",\n  "portsAttributes": {\n    "8000": {\n      "label": "web"\n    }\n  },\n\n  "containerEnv": {\n    "PUBLIC_URL": "https://${localEnv:CODESPACE_NAME}-8000.${localEnv:GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}",\n    "PAYMENT_HOST": "${localEnv:CODESPACE_NAME}-8000.${localEnv:GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}",\n    "PAYMENT_RETURN_HOST": "https://${localEnv:CODESPACE_NAME}-8000.${localEnv:GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"\n  }\n
\n

devcontainer.json also contains postCreateCommand that I used to do following steps:

\n\n

I'm using pipenv to manage dependencies and virtual environments. One of features of Pipenv I like are scripts. They provide a way to create alias for complicated or long commands. Most of the commands are Django management commands. The only exception being dev command. This command starts two separate processes using tool called honcho. Honcho is a Python library that makes it easy to run multiple processes in a Unix environment. The processes are defined in Procfile.dev.

\n

Pipfile

\n
...\n[scripts]\ndev = "honcho -f Procfile.dev start"\ntest = "python manage.py test"\nrunserver = "python manage.py runserver"\nmakemigrations = "python manage.py makemigrations"\nmigrate = "python manage.py migrate"\nloaddata = "python manage.py loaddata categories products pages images"\n
\n

This way I could use VS Code Tasks to start both Django and CSS minifier.

\n

.vscode/tasks.json

\n
{\n  "version": "2.0.0",\n  "tasks": [\n    {\n      "type": "shell",\n      "command": "pipenv run dev",\n      "problemMatcher": [],\n      "label": "Run webserver",\n      "detail": ""\n    }\n  ]\n}\n
\n

In conclusion, I was able to use Github Codespaces to spin up a development environment for my Django application with minimal manual steps. Overall, Github Codespaces proved to be a valuable tool for the project.

", "url": "https://rousek.name/articles/codespaces", "title": "Using Github Codespaces for an Efficient Django Development Environment", "summary": "I ended up writing online store almost from scratch because of product and market restrictions and then stuck with it because price and freedom it provides. Meaning some features like live preview are not available. This was possible thanks to my partner knowing basics of HTML.", "date_modified": "2022-12-14T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/faas-on-fly-io-with-fastapi", "content_html": "

Fly.io recently came with a very intriguing service called Machines. Machines are designed to quickly(in around 400ms) start a docker image when a request arrives and turn off when the process exits. This is amazing because it allowed me to run getimages.social without worrying about the costs. I only pay for storage when a machine isn't running, which is around $0.15/mo for a 1GB Docker image. Here's a snippet of log messages related to one start/stop sequence.

\n
2022-07-06T07:29:07.991 proxy[39080523a19875] fra [info] Machine started in 454.077286ms\n2022-07-06T07:29:08.794 app[39080523a19875] fra [info] INFO: Started server process [509]\n2022-07-06T07:29:08.794 app[39080523a19875] fra [info] INFO: Waiting for application startup.\n2022-07-06T07:29:08.794 app[39080523a19875] fra [info] INFO: Application startup complete.\n2022-07-06T07:29:08.804 app[39080523a19875] fra [info] INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n2022-07-06T07:29:09.172 proxy[39080523a19875] fra [info] Machine became reachable in 1.180665787s\n2022-07-06T07:29:09.186 app[39080523a19875] fra [info] INFO: 27.50.59.67:0 - "GET / HTTP/1.1" 200 OK\n2022-07-06T07:29:38.796 app[39080523a19875] fra [info] before condition self.last_request=datetime.datetime(2022, 7, 6, 7, 29, 9, 186501)\n2022-07-06T07:29:38.797 app[39080523a19875] fra [info] self.last_request < datetime.now() - timedelta(seconds=25)=True\n2022-07-06T07:29:38.871 app[39080523a19875] fra [info] INFO: Shutting down\n2022-07-06T07:29:38.972 app[39080523a19875] fra [info] INFO: Waiting for application shutdown.\n2022-07-06T07:29:38.972 app[39080523a19875] fra [info] INFO: Application shutdown complete.\n2022-07-06T07:29:38.972 app[39080523a19875] fra [info] INFO: Finished server process [509]\n
\n

From my experience machine starts in 300-500ms. 1GB docker image then becomes reachable in around 1400ms. More lightweight image(200MB) starts in 500-800ms.

\n

I'm using async web framework FastAPI so I created a simple coroutine that will tell the process to terminate when there were no requests in the last 30 seconds. So I added middleware that updates the last request time and coroutine regularly checks when was the last request made. I'm using uvicorn as a web server and the best way to self-terminate I found is to send SIGTERM.

\n
class ActivityWatcher:\n    last_request: datetime\n\n    def update_last_request(self):\n        self.last_request = datetime.now()\n\n    async def start(self):\n        self.last_request = datetime.now()\n        while True:\n            await asyncio.sleep(30)\n            print(f"before condition {self.last_request=}")\n            print(f"{self.last_request < datetime.now() - timedelta(seconds=25)=}")\n            if self.last_request < datetime.now() - timedelta(seconds=25):\n                import os\n                import signal\n\n                os.kill(os.getpid(), signal.SIGTERM)\n\n\nactivity_watcher = ActivityWatcher()\n\n\n@app.on_event("startup")\nasync def startup_event():\n    asyncio.create_task(activity_watcher.start())\n\n\n@app.middleware("http")\nasync def update_last_request_middleware(request: Request, call_next):\n    response = await call_next(request)\n    activity_watcher.update_last_request()\n    return response\n
\n

Setting up a Machine is more involved than classic Fly Virtual Machines as it's currently available only via API and requires you to build and push the docker image. But it's not that big of a problem as entire process is well documented.

\n

Want to try it for yourself? Go ahead and try my demo app.

", "url": "https://rousek.name/articles/faas-on-fly-io-with-fastapi", "title": "Functions-as-a-Service on Fly.io with FastAPI", "summary": "Fly.io recently came with a very intriguing service called Machines. Machines are designed to quickly(in around 400ms) start a docker image when a request arrives and turn off when the process exits. This is amazing because it allowed me to run getimages.social without worrying about the costs.", "date_modified": "2022-07-06T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/plotting-strava-rides-with-cartopy", "content_html": "

For a long time I was thinking of improving my GitHub profile. I wanted to show something cool. Maybe map of my last ride would do the trick. Last time I was playing with Strava data I used interactive maps but this time I wanted to create nice static image.

\n\n

This led me to cartopy. Cartopy is a Python package designed for geospatial data processing in order to produce maps. Comming from academia this package is not easy to use and required quite a lot tweaking to produce maps I wanted.

\n

Drawing the path itself is quite simple, the problematic part are the tiles. I needed to determine apropriate zoom level for the ride I was trying to display. Another constraint is that some tiles are provided with restricted set of zoom levels when rendering outside of city. For example stamen.

\n

These are the steps required draw a map with a route.

\n
    # Configure tiler to render map tiles on background\n    tiler = GoogleTiles()\n    mercator = tiler.crs # CRS means coordinate reference system\n    fig = plt.figure()\n    ax = fig.add_subplot(1, 1, 1, projection=mercator)\n\n    # Resolution refers to google maps zoom level\n    ax.add_image(tiler, params["resolution"])\n\n    # Set map bounds\n    bounding_box = [\n        parsed_activity["min_lon"],\n        parsed_activity["max_lon"],\n        parsed_activity["min_lat"],\n        parsed_activity["max_lat"],\n    ]\n    ax.set_extent(bounding_box, crs=ccrs.PlateCarree())\n\n    # Prepare lists of longitudes and latitudes\n    ride_longitudes = [coordinate[1] for coordinate in parsed_activity["coordinates"]]\n    ride_latitudes = [coordinate[0] for coordinate in parsed_activity["coordinates"]]\n\n    ax.plot(\n        ride_longitudes,\n        ride_latitudes,\n        color="#fc5200",\n        alpha=1,\n        linewidth=0.5,\n        transform=ccrs.PlateCarree(),\n        antialiased=True,\n        path_effects=[\n            # White "border"\n            pe.Stroke(linewidth=params["linewidth"] * 2.5, foreground="w"),\n            # "Strava orange" route line\n            pe.Normal(),\n        ]\n    )\n\n    output_path = root / "output.png"\n    # To remove border I had to set `bbox_inches='tight'` and negative padding\n    plt.savefig(output_path, dpi=400, bbox_inches='tight', pad_inches=-0.02)\n    plt.show()\n
\n

Setting up the environment

\n

Cartopy was not easy to install for me. It has a bunch of C dependencies and it took me several tries to do it right. On the first try I was getting strange tuple errors and it took me few hours to realize it's because version mismatch. Before I managed to install latest version I had to learn about Conda Forge first. But after picking the right docker image it all started working.

\n

The code is at https://github.com/stlk/strava_cartopy

", "url": "https://rousek.name/articles/plotting-strava-rides-with-cartopy", "title": "Plotting Strava rides with Cartopy", "summary": "For a long time I was thinking of improving my GitHub profile. I wanted to show something cool. Maybe map of my last ride would do the trick.", "date_modified": "2022-06-29T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/shopify-a-cookie-lista-v-roce-2022", "content_html": "

Od 1. ledna 2022 se mění pravidla při práci s cookies. Je v tom trochu zmatek a konkrétní technické přístupy se velmi liší. Více informací k této problematice najdete v článku od Pavla Ungra, já se budu soustředit pouze na implementaci v rámci Shopify.

\n

Shopify připravilo Customer Privacy API, která celý proces velmi usnadňuje. Customer Privacy API totiž plně zprostředkovává správu Google a Facebook tracking mechanismů. V nastavení Online store sales channelu je možné aktivovat "Limit data collection for EU buyers". Pak už jen stačí použít funkce na objektu window.Shopify.customerPrivacy.

\n

shouldShowGDPRBanner slouží pro zjištění, jestli bychom měli cookie lištu zobrazit.\nsetTrackingConsent pak použijeme pro nastavení, jestli návštěvník povolil trackovaní.\nuserCanBeTracked pak můžeme použít pro ovládání dalších trackovacích skriptů.

\n

Shopify zvolilo přístup, který se liší od běžné praxe v Čechách. Pokud návštěvník odmítne trackovaní, Shopify kompletně vypne Google Analytics a FB pixel. U nás jsem slyšel spíše o přechodu do režimu bez cookies. Stejně tak se liší přistup k vlastním cookies. Pokud zákazník nedá souhlas s trackovaním, Shopify používá session cookies místo persistentních.

\n

Pro Upřimný triko jsem udělal minimalistickou cookie lištu. Nabízí jednoduché a rychlé potvrzení a nekomplikuje odmítnutí. Na Githubu najdete zdrojáky a návod na implementaci.

\n", "url": "https://rousek.name/articles/shopify-a-cookie-lista-v-roce-2022", "title": "Shopify a cookie lišta v roce 2022", "summary": "Od 1. ledna 2022 se mění pravidla při práci s cookies. Je v tom trochu zmatek a konkrétní technické přístupy se velmi liší.", "date_modified": "2021-12-31T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/podcasts-i-listen-to", "content_html": "

I love podcasts, I've been listening to them for years now. Here are the one where I listened almost every episode.

\n

Entertainment

\n

Oh No, Ross and Carrie!

\n

Two former christians exploring various spiritual movements.

\n

Behind the Bastards

\n

Background of how the most terrible people came to be and what influenced their actions.

\n

Q Clearance: The Hunt for QAnon

\n

No need to describe this one.

\n

Pratchat

\n

Terry Pratchett book club.

\n

Philosophize This!

\n

Yes, philosophy.

\n

Business

\n

Bootstrapped Web

\n

Two hosts sharing their progress while building async communication tool and e-commerce checkout.

\n

The Bootstrapped Experience

\n

Two guys building Shopify apps.

\n

Out of Beta

\n

Two technical founders documenting their way while building form builder tool and business forecast tool.

\n

The Art of Product

\n

Peek into how the best online pair programming tool is made. Another host has meeting reservation tool.

\n

Software Social

\n

Two indie SaaS founders—one just getting off the ground, and one with an established profitable business.

\n

Startups For the Rest of Us

\n

One of the original found updates podcasts, now changed format to more informational style.

\n

Slow & Steady

\n

Co-founder of Userlist and Gatsby consultant.

\n

Rogue Startups Podcast

\n

Founder of podcast hosting platform and founder of abandoned cart recovery email solution for Magento and Shopify sharing their progress.

\n

Product Journey

\n

They're too much into crypto, but still like it.

\n

Technical

\n

The Bike Shed

\n

Lovely show discussing issues and questions that come up when becoming a better developer.

\n

ShopTalk Show

\n

Must-listen show for web developers.

\n

Notes On Work - by Caleb Porzio

\n

Personal musings of author of Alpine.js.

\n

The Real Python Podcast

\n

Well, the podcast about Python.

\n

The only infosec podcast I listen to

\n

Risky Business

\n

The ones I sometimes listen to

\n

Syntax

\n

Indie Hackers

\n

Podcasts in Czech

\n

Patrickův Podcast

", "url": "https://rousek.name/articles/podcasts-i-listen-to", "title": "Podcasts I listen to", "summary": "I love podcasts, I've been listening to them for years now. Here are the one where I listened almost every episode.", "date_modified": "2021-12-19T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/inertiajs-react-a-django", "content_html": "

Po přechodu na single-page aplikaci (přibližně před rokem) se naše produktivita nezlepšila i přesto, že jsme tým prakticky zdvojnásobili. Říkal jsem si, že se to časem srovná. K přechodu nás donutil vývoj okolo 3rd party cookies, protože naše aplikace běží v iframe v administraci e-shopu.

\n

Pak přišel Hotwire od lidí okolo Rails, všimnul jsem si HTMX. Tyto knihovny používají HTML over the wire přístup, který pro nás není vhodný. A proto mě zaujala Inertia.js, která se od nich odlišuje. Snaží se totiž definovat protokol, jak by spolu měli React, Vue nebo Svelte aplikace a backend komunikovat. Díky tomu můžeme vytvářet JavaScriptové SPA bez nutnosti psát API a client-side routování. Na serveru jsou oficiálně podporované Laravel a Rails. Pro Django existuje neoficiální balíček.

\n

Inertia funguje velmi podobně jako klasická webová aplikace. Stále píšeme controllery, čteme data pomocí ORM a renderujeme views. V případě Inertia jsou ale views JavaScriptové komponenty. Takové řešení vytváří velmi úzkou provázanost mezi backendem a frontendem, což je v případě klasických MVC aplikací běžná praxe a podle autora knihovny i jejich benefit.

\n

Tento přístup je pro nás zajímavý hlavně kvůli Polaris - Design Systému od Shopify. Většinu HTML a CSS si nepíšeme sami, ale používáme React komponenty připravené Shopify.

\n

Ve view pouze definuju, jaká komponenta se má použít a jaké props dostane. render_inertia rozhodne, jestli vrátí JSON nebo HTML odpověď.

\n
def home(request):\n    props = {\n        "events": [{\n            "id": 1,\n            "title": "Title",\n            "description": "description, wow"\n        }],\n    }\n    return render_inertia(request, "Home", props)\n
\n

Pro přechod mezi stránkami existuje komponenta Link. Díky ní Inertia načte pouze data a nedochází k full-page refresh. Ineria totiž pošle požadavek s hlavičkou X-Inertia. Server v takovém případě pošle JSON místo HTML.

\n

Nedílnou součástí každé aplikace jsou formuláře. Inertia proto nabízí helper funkce pro všechny podporované frameworky.

\n

Samotný kód vypadá následovně. V případě Reactu má hook useForm na starosti uchování stavu, odeslání dat na server a validace. Způsob zpracování požadavku je velmi podobný klasickému odeslání formuláře. Pokud je potřeba uživateli zobrazit validační hlášku, stačí do props přidat klíč errors. Inertia helper useForm potom zpřístupní pomocí stejnojmenného klíče.

\n
import React from 'react'\nimport { Link, useForm } from '@inertiajs/inertia-react'\n\nexport default function Create() {\n    const { data, setData, post, processing, errors } = useForm({\n        email: '',\n        password: '',\n        remember: false,\n    })\n\n    function submit(e) {\n        e.preventDefault()\n        post('/inertia/create')\n    }\n\n    return (\n        <div>\n<h2>Create event</h2>\n<link href="/inertia"/>Back\n            <form onsubmit="{submit}">\n<input =="" onchange="{e" type="text" value="{data.email}"/> setData('email', e.target.value)} />\n                {errors.email &amp;&amp; <div>{errors.email}</div>}\n                <input =="" onchange="{e" type="password" value="{data.password}"/> setData('password', e.target.value)} />\n                {errors.password &amp;&amp; <div>{errors.password}</div>}\n                <input =="" checked="{data.remember}" onchange="{e" type="checkbox"/> setData('remember', e.target.checked)} /> Remember Me\n                <button disabled="{processing}" type="submit">Login</button>\n</form>\n</div>\n    )\n}\n
\n

Zpracování POST requestu na serveru.

\n
from inertia.views import render_inertia\nfrom inertia.share import share, share_flash\n\ndef event_create(request):\n    if request.method == "POST":\n        data = json.loads(request.body)\n        if not data["email"]: # Just so there's some validation\n            share_flash(request, error=True, errors={"email": ["email can't be empty"]})\n        else:\n            share_flash(request, success=f"event {data['email']} created")\n            return redirect(reverse("inertia_react:index"))\n\n    return render_inertia(request, "Events.Create", {})\n
\n

Inertia představuje velmi tenkou vrstvu mezi tradičními webovými frameworky a moderními frontendovými frameworky. Možnost použít React místo klasických šablon bez nutnosti vybudovat nejdříve API, je pro mě velmi lákavá. Nemusí to být ani konečný stav. Inertia může být krok mezi jednotlivými komponentami a SPA.

\n

Momentálně převádíme jednu aplikaci na Inertia a zatím z toho mám dobrý pocit. Jsem velmi zvědavý, jak se osvědčí.

", "url": "https://rousek.name/articles/inertiajs-react-a-django", "title": "Inertia.js, React a Django", "summary": "Po přechodu na single-page aplikaci (přibližně před rokem) se naše produktivita nezlepšila i přesto, že jsme tým prakticky zdvojnásobili. Říkal jsem si, že se to časem srovná. K přechodu nás donutil vývoj okolo 3rd party cookies, protože naše aplikace běží v `iframe` v administraci e-shopu.", "date_modified": "2021-08-21T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/you-dont-need-a-spa", "content_html": "

I'm building Shopify apps using Django and React. When I first created Candy Rack it was server rendered HTML with single or multiple React components on a page. Since then the web ecosystem evolved. The most important change are the restrictions around 3rd party cookies. To address this problem Shopify came with an alternative authentication method that doesn't rely on cookies called session tokens. Instead of cookies Shopify provides a Javascript function that returns a token. As a result all authenticated requests must be made from Javascript. While SPA is fine for larger apps and teams, we have some smaller apps and rewriting them to SPA isn't viable.

\n

Thankfully a whole new approach to interactive apps is starting to get a widespread attention.

\n

The one I have some experience with is Hotwire, successor of Turbolinks. I created an example Shopify app based on Shopify's tutorial Authenticate server-side rendered embedded apps using Rails and Turbolinks. Hotwire works well, gets the job done. But there are two other tools I would love to try.

\n

htmx is similar to Hotwire and I'm seeing more of it in Python community.

\n

Inertia.js is taking slightly different approach. It serves as client-side router for React, Vue and Svelte apps. The most interesting feature is that Inertia uses a protocol that defines how are data passed from the server and based on this data it renders a component. If you would like to learn more I recommend Episode 291: All Things Inertia.js with Jonathan Reinink of The Bike Shed podcast.

\n

I'm very excited about the direction these tools are going and will keep writing about my experience with them.

", "url": "https://rousek.name/articles/you-dont-need-a-spa", "title": "You don't need a SPA", "summary": "I'm building Shopify apps using Django and React. When I first created Candy Rack it was server rendered HTML with single or multiple React components on a page. Since then the web ecosystem evolved. The most important change are the restrictions around 3rd party cookies. To address this problem Shopify came with an alternative authentication method that doesn't rely on cookies called session tokens.", "date_modified": "2021-06-12T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/websockets-on-django-and-react", "content_html": "

For our new Shopify app I needed to create a websocket server that broadcasts a message when model is updated. I ended up with this solution. Start with installing channels and channels-redis to you Django app.

\n

Add channels to INSTALLED_APPS and configure ASGI_APPLICATION and CHANNEL_LAYERS.

\n

myproject/settings.py

\n
INSTALLED_APPS = [\n    ...\n    "channels",\n    ...\n]\n\nWSGI_APPLICATION = "myproject.wsgi.application"\nASGI_APPLICATION = "myproject.asgi.application"\n\nCHANNEL_LAYERS = {\n    "default": {\n        "BACKEND": "channels_redis.core.RedisChannelLayer",\n        "CONFIG": {\n            "hosts": [os.environ.get("REDIS_URL")],\n        },\n    },\n}\n
\n

Setup the ASGI application so it's production ready. Based on How to deploy with ASGI I replaced waitress with daphne.

\n

collection_sorting/asgi.py

\n
import os\n\nfrom django.core.asgi import get_asgi_application\n\n# Fetch Django ASGI application early to ensure AppRegistry is populated\n# before importing consumers and AuthMiddlewareStack that may import ORM\n# models.\nos.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")\ndjango_asgi_app = get_asgi_application()\n\nfrom channels.auth import AuthMiddlewareStack\nfrom channels.routing import ProtocolTypeRouter, URLRouter\n\nimport myapp.routing\n\napplication = ProtocolTypeRouter(\n    {\n        "http": django_asgi_app,\n        "websocket": AuthMiddlewareStack(URLRouter(myapp.routing.websocket_urlpatterns)),\n    }\n)\n
\n

Create routing.py to hook up the consumer.

\n

myapp/routing.py

\n
from django.urls import re_path\n\nfrom . import consumers\n\nwebsocket_urlpatterns = [\n    # We use re_path() due to limitations in URLRouter.\n    re_path(r"ws/job-status/$", consumers.JobStatusConsumer.as_asgi()),\n]\n
\n

What is a consumer? This is a description form Django Channels documentation.

\n
\n

When Django accepts an HTTP request, it consults the root URLconf to lookup a view function, and then calls the view function to handle the request. Similarly, when Channels accepts a WebSocket connection, it consults the root routing configuration to lookup a consumer, and then calls various functions on the consumer to handle events from the connection.

\n
\n

This consumer accepts connections when user is signed in and adds them to their own group. When it receives propagate_status message it forwards it to all subscribers.

\n

myapp/consumers.py

\n
import json\n\nfrom channels.generic.websocket import AsyncWebsocketConsumer\n\n\nclass JobStatusConsumer(AsyncWebsocketConsumer):\n    async def connect(self):\n        if self.scope["user"].is_anonymous:\n            await self.close()\n            return\n\n        self.user = self.scope["user"]\n        self.group_name = f"job-posting-{self.user.id}"\n\n        await self.channel_layer.group_add(self.group_name, self.channel_name)\n        await self.accept()\n\n    async def propagate_status(self, event):\n        if not self.scope["user"].is_anonymous:\n            message = event["message"]\n            await self.send(text_data=json.dumps(message))\n
\n

Now in model's save method we call group_send to publish the update.

\n

myapp/models.py

\n
import channels.layers\nfrom asgiref.sync import async_to_sync\n\nclass JobPosting(models.Model):\n\n    ...\n\n    def save(self, *args, **kwargs):\n    super().save(*args, **kwargs)\n    channel_layer = channels.layers.get_channel_layer()\n    group = f"job-posting-{self.user.id}"\n    async_to_sync(channel_layer.group_send)(\n        group,\n        {\n            "type": "propagate_status",\n            "message": {"id": self.id, "state": self.state},\n        },\n    )\n
\n

Now when everything is wired up I created very simple client to make sure messages are being received.

\n
const updatesSocket = new WebSocket(\n  `ws://${window.location.host}/ws/job-status/`\n)\n\nupdatesSocket.onmessage = function (e) {\n  const data = JSON.parse(e.data)\n  console.log(data)\n}\n\nupdatesSocket.onclose = function (e) {\n  console.error('Chat socket closed unexpectedly')\n}\n
\n

When the server side was working I used react-use-websocket to add live updates to React.

\n
import React, { useState } from 'react';\nimport useWebSocket from 'react-use-websocket';\n\nexport default function List() {\n  const [data, setData] = useState([]);\n\n  useWebSocket(`wss://${window.location.host}/ws/job-status/`, {\n    onMessage: (e) => {\n      const message = JSON.parse(e.data);\n      setData((data) => (data.map((item) => {\n          if (item.id === message.id) {\n            item.state = message.state;\n          }\n          return item;\n        }));\n    },\n    shouldReconnect: (closeEvent) => true,\n  });\n\n  return (<div></div>);\n}\n\n
", "url": "https://rousek.name/articles/websockets-on-django-and-react", "title": "Websockets on Django and React", "summary": "For our new Shopify app I needed to create a websocket server that broadcasts a message when model is updated. I ended up with this solution. Start with installing `channels` and `channels-redis` to you Django app.", "date_modified": "2021-03-13T00:00:00.000Z", "author": { "name": "Josef Rousek" } }, { "id": "https://rousek.name/articles/high-speed-request-filtering-using-cloudflare-workers", "content_html": "

Recently I was facing very interesting challenge. We distribute our app's javascript using Cloudflare. Sometimes the <script> tag stays on the site after the app is uninstalled resulting in unnecessary load on our server. This means that eventually we'll have to quickly determine whenever should the script run or not.

\n

Quite a few CDN providers now support running our code on CDN edge. I'm using it to return empty JS file for clients that uninstalled our app. Based on URL https://my-awesome-app.com/static/main.js?shop=example-client.com I can use Cloudflare Workers and KV store to make this decision in around 1ms.

\n

I wrote following worker and assigned it to my-awesome-app.com/static/main.js*.

\n
async function getBlockedHostname(myshopify_domain) {\n  if (!myshopify_domain) {\n    return\n  }\n\n  return await KV_NAMESPACE.get(myshopify_domain);\n}\n\nasync function handleRequest(request) {\n  let url = new URL(request.url)\n  let myshopify_domain = url.searchParams.get("shop")\n\n  let blocked_hostname = await getBlockedHostname(myshopify_domain)\n  // if KV contains key myshopify_domain we'll return empty response instead \n  if (blocked_hostname) {\n    return new Response("",\n      {\n        headers: {\n          'content-type': 'application/javascript',\n        }\n      })\n  }\n\n  // else return regular response\n  return fetch(request)\n}\n\naddEventListener('fetch', event => {\n  event.respondWith(handleRequest(event.request))\n})\n
", "url": "https://rousek.name/articles/high-speed-request-filtering-using-cloudflare-workers", "title": "Hi-speed request filtering using Cloudflare Workers", "summary": "Recently I was facing very interesting challenge. We distribute our app's javascript using Cloudflare. Sometimes the