Jak jsme přestavěli javascriptovou aplikaci, aby jí i roboti rozuměli

Popíšeme vám, jak jsme náš projekt původně postavený na Angularu upravili, aby byl snadno dostupný i pro roboty.
Text vyšel původně na medium.com.
Je tomu už přes rok, co jsme v Zonky produkčně spouštěli tržiště optimalizované pro vyhledávací roboty. Motivace byla jasná — nejsme vidět ve výsledcích vyhledávačů a přitom máme v příbězích obsah, který pomůže projekt zviditelnit. V následujících odstavcích společně projdeme zadání, volbu technologií, a uvedení do produkčního provozu.
Co bylo naším úkolem?
Úkolem bylo vytvořit na tržišti samostatné podstránky, které budou rozdělené podle kategorií. Každá kategorie má mít svůj název, např. “Půjčky na domácnost” a specifickou URL, např. https://app.zonky.cz/pujcky-na-domacnost. S takto zvoleným názvem a URL budeme pro užitele, kteří hledají “Půjčky na domácnost”, vyhledávačem zvýhodněni.
Jednotlivé stránky příběhů budou mít ve svém názvu kromě nadpisu také přezdívku a účel půjčky Spokojené bydlení ve vlastním - zonky242602 - Půjčky na domácnost
, a URL https://app.zonky.cz/domacnost/spokojene-bydleni-ve-vlastnim-214608. Stránka s příběhem pak bude zpětně odkazovat na podstránky s kategoriemi.

Jak na to?
V prvním kroku bylo potřeba rozhodnout, jak tržiště zpřístupnit pro roboty. Stávající aplikace je postavena na technologii AngularJS, a staticky dodávána z webového serveru NGINX. V hledáčku jsme měli tři možnosti:
- prohánět tržiště nástrojem PhantomJS (ať už interně nebo s využitím externí služby) a generovat statické stránky,
- implementovat separátní tržiště na serveru pouze pro roboty,
- přepsat tržiště od základu s podporou pro SSR (“server-side rendering”).
První varianta nebyla vhodná kvůli nutnosti řešit cacheování (TwoHardThings) — technické problémy s (pře)generováním stránek při vystavení nového příběhu, či změně stavu nebo obsahu již existujícího. Druhou variantu jsme zavrhli pro udržování dvou samostatných verzí aplikace a generování shodného výstupu pro roboty i uživatele. Zvolili jsme poslední variantu — krom jiného pro jeden zdrojový kód, shodný HTML výstup a rychlejší vizuální odezvu, a také možnosti použít nový způsob psaní frontendové aplikace běžící i na serveru.
Přepsaná část aplikace je postavena na frameworku Ember.js s rozšířením FastBoot, které přidává podporu pro SSR. FastBoot běží na platformě Node.js jako middleware do Express.js serveru. FastBoot funguje tak, že při spuštění serveru se načtou JavaScriptové soubory do V8 Virtual Machine kontextu a vytvoří se část aplikace, která je odpovědná primárně za její konfiguraci. Jednotlivé HTTP dotazy pak přes Express.js protékají do VM kontextu, kde Ember vytvoří instanci aplikace pro zpracování dotazu, navštíví se požadovaná cesta, poté se počká na vykreslení HTML obsahu, výsledek se odešle do prohlížeče klienta a instance se zahodí.

Pro vzájemnou komunikaci mezi serverem a prohlížečem slouží:
- cookies, které se používají například pro správu přihlášeného uživatele.
- Ve FastBootu lze také použít
shoebox
API, které umožňuje na serveru uložit data ve formátu JSON dometa
tagů a v prohlížeči načíst do aplikace. Tento způsob používáme pro předání IP adresy klienta externí službě LaunchDarkly, která nám spravujefeature flags
pro fázované nasazení nových funkcí.
getPublicIp() {
let isFastBoot = this.get('fastboot.isFastBoot');
let shoebox = this.get('fastboot.shoebox');
if (isFastBoot) {
let headers = this.get('fastboot.request.headers');
let publicIpAddress = headers.get('X-Forwarded-For');
shoebox.put('public-ip', publicIpAddress);
return publicIpAddress;
} else {
return shoebox.retrieve('public-ip');
}
}
Zavrhli jsme možnost přepsat celou aplikaci najednou jako “big bang”, proto bylo potřeba vyřešit přepínání mezi stávajícím frameworkem AngularJS a novým frameworkem Ember. Napřed jsme přesunuli Angular část z /
(rootu) do podsložky /public
. V Angular používáme UI Router, který spravuje stav aplikace v hash
části, např. původní dashboard půjčovače byl dostupný na https://app.zonky.cz/#/dashboard/borrower
, nová adresa je tedy na https://app.zonky.cz/public/index.html#/dashboard/borrower
.
Bylo nutné zajistit přesměrování z původních URL adres na nové. Protože se hash
část neposílá na server, je potřeba přesměrovat až v prohlížeči. V Ember části je malý skript, který řeší přechod na nové URL adresy.
Postupně migrujeme stránku po stránce. Když dojde k přepsání stránky, upraví se router
v AngularJS tak, aby přesměrovával na novou URL do Emberu. V případě zmiňovaného dashboardu půjčovače by vypadal kód následovně:
onStateChangeStart: (event, toState) =>
if toState.name is "dashboard.borrower"
event.preventDefault();
return window.location = "#{ENV.emberAppUrl}/moje-pujcky"
Mimo jiné bylo potřeba implementovat meta tagy pro navádění robotů při stránkování na tržišti, např. pro druhou stránku takto:
<link rel="canonical" href="https://app.zonky.cz/?page=1">
<link rel="prev" href="https://app.zonky.cz/">
<link rel="next" href="https://app.zonky.cz/?page=2">
A pravidelně přegenerovávat mapu stránek složenou z jednotlivých příběhů (dynamická čast) a kategorií (statická část), k čemuž jsme použili Lambda funkce.
Jdeme na produkci
Z jednoduchého statického serveru jsme se rozhodli přejít do oblak na AWS platformu. Používáme CloudFront jako CDN pro distribuci statického obsahu, který se natahuje z S3 bucketů -jeden bucket /assets
pro Ember a druhý /public
pro Angular. Dynamická část běží v Elastic Beanstalk s load balancerem a EC2 instancemi s FastBootem.
Při rozjezdu aplikace jsme chtěli mít možnost postupně navyšovat počet uživatelů, a nebo se při chybě vrátit zpět na původní variantu (čehož jsme několikrát využili). Před CloudFront byla umístěna EC2 instance s HAProxy, která se starala o přesměrování požadavků buď na CloudFront nebo NGINX s původní aplikací.

Napřed jsme na nové tržiště vpouštěli pouze uživatele z firemní sítě a až poté jsme začali postupně přidávat uživatele i z internetu. HAProxy nám kromě prvotního přechodu donedávna sloužila i pro nastavení HTTP hlaviček — pravidla pro zabezpečení, kešování, atd. Služba S3, měla v té době pouze omezenou sadu editovatelných hlaviček. Dnes již používáme pro nastavení hlaviček Lambda@Edge funkce, které jsou propojené s CloudFrontem a plně tak nahradily HAProxy.
Na závěr
Použití Ember.js s FastBootem nám umožnilo implementaci SEO požadavků a zároveň mít společný kód jak pro uživatele, tak roboty. Získané zkušenosti s přechodem na jiný framework a s migrací na AWS se nám budou hodit i pro přepis naší back-office aplikace a mohou být inspirací i pro ostatní, kteří jsou postaveni před podobný problém.
Jaký robot byl konkrétně cílem? Googlebot nemá s aplikacemi problém proto JAM stack tak stoupá v oblibě. Teď už aplikaci budete těžko servovat z cdn. Otázka je jestli by se to vůbec vyplatilo v lokálním měřítku.
Kromě robota od Google bylo potřeba podporovat i robota od Seznamu. Toho času pro aplikaci CDN používáme, týká se to statických souborů, dynamické cesty pro SSR skrz CDN pouze protíkají. Kromě případu pro roboty slouží SSR i pro rychlejší vizuální odevzu. U čistě statického HTML je ze začátku vidět povětšinou prázdná stránka, případně nějaký „app shel“.
Pre web typu trziste by bol JAM stack nezmysel, preto je irelevantné, že s inými typmi webov funguje.
Typický příklad špatně zvolené technologie na začátku projektu a následné řešení z toho plynoucích problémů způsobem „škrábání se levou rukou v pravém uchu“. Jinak řečeno: 1.chyba byla ve výběru technologie pro veřejný komerční projekt, která má potíže s vyhledávacími roboty (javascript v prohlížeči). 2.chyba je zásadní předělání celého projektu, ale přitom ponechání původní problémové technologie, která je pouze znásilněna k účelu, ke kterému nebyla původně určena (javascript na serveru), takže se domnívám, že nové problémy na sebe nenechají dlouho čekat (výkon, zdroje, poruchovost).
ad 1) U prvotního vývoje aplikace (prototypu) nebylo SEO požadováno, protože se ani nevědělo, jestli bude služba jako taková fungovat a dožije se druhého dne (trochu se to rezebíralo i tady).
ad 2) Subjektivně mi nepřijde jako problémové použít JavaScript na serveru (Node.js), jeho jednovláknové a neblokující zpracování má oproti vícevláknovým aplikacím výhody, ale záleží samozřejmě na použítí.
Nehnevaj sa Gazda, ale nevieš o čom hovoríš. V dnešnej dobe môže použitie JS na serveri považovať za znásilnenie len úplný ignorant. A aj v bode 1. si mimo, crawlers nemajú problém s JS všeobecne, len s niektorými spôsobmi jeho použitia.
Pekny clanek,
mohli byste se vice rozeptat proc jste zrovna zvolili Ember a na zaklade ceho jste se rozhodovali?
Proc ne treba: Angular 6, VueJs nebo React vse SSR podporuje.
Diku
Na přepisu aplikace s podporou SSR jsme začali dělat v létě 2016. Tou dobou byl samotný Angular 2 IIRC v beta fázi. Pro vybudování komplexní aplikace nad Reactem je podle nás nutné mít někoho zkušenějšího, v našem týmu byla ale znalost téměř nulová. Ember jsme jak znali, tak mohli použít i SSR.
Toto je jediná zmysluplná otázka. Premýšľali ste nad tým ako tú apku urobiť SEO friendly bez toho aby ste sa škriabali okolo hlavy, preto ste vypustili riešenie 1. s PhantomJS a vyriešili ste to tak, že … sa škriabete okolo hlavy s EmberJS? Jop, to má logiku… A ešte sa tu s tým aj pochválite, hoci to silne zaváňa nekompetentnosťou? Jediným zmysluplným riešením, keď už je postavená nad Angularom, bolo ostať nad Angularom, akurát ju zmodernizovať do latest verzie a urobiť ju SSR s Angular Universal. Mrzí ma to, ale váš článok je odstrašujúci príklad, nie inšpirácia…
Z čeho usuzujete, že se škrábeme okolo hlavy s Ember.js?
V našem případě byl rozdíl mezi Angular 1 řešením a nastupujícím Angular 2+ takový, že by se jednalo o přepis tak jako tak, a tím pádem jsme stáli před přepisem do čehokoliv.
Uz chapu zvolenou technologii pro prepis vzhledem k terminu realizace prepisu app je pravda ze Angular 2 a SSR bylo zatim v nedohlednu, takze zvolili pro prepis vhodnou technologii z pohledu moznosti SSR vcetne znalosti vyvojaru. Predpokladam ze nyni by rozhodovani nemeli lehke vzhledem k dostupnosti VueJS, Angular 6, atd. ktere podporuji SSR.