Na SEOlogeru o indexování JavaScriptových stránek Tomáš Kapler poukázal na to, že ve zdrojovém kódu Zboží.cz by měl být nevyplněný TITLE a meta description a prázdné BODY. Nevyplněný TITLE, description a body jsou obvyklý případ, ale to se nám na Zboží.cz nehodilo a mě napadlo vám vysvětlit proč ten index.html obsahuje to, co obsahuje.
Tl;dr
- Zboží je v Angularu a kvůli SEO renderuje HTML pomocí PhantomJS. Cacheuje se do Couchbase.
- Předvyplněný TITLE a meta description je pro roboty, kteří neumí spustit JavaScript, ani AJAX Crawling (třeba aplikace – Nuzzel, Pinterest a tak)
- Index.html máme dlouhý proto, že je tam fůra věcí pro roboty bez podpory JS a taky si to díky cacheování můžeme dovolit.
- Detailně se rozepisuji o Zboží.cz. Úplně stejně fungují i Firmy.cz. Bez cache renderujeme i Sreality, Sauto, Stream a Lidé.
—
Zboží.cz
… je Single Page Aplikace (SPA) a je napsané v AngularJS. AngularJS je JavaScriptový framework od Google a funguje tak, že kdo chce vidět obsah stránek, ten musí umět spustit JavaScript. Pokud neumíte spustit JavaScript, tak vidíte jen prázdnou stránku. Takhle by valná většina robotů (vyhledávačů, crawlerů, atd.) a lidé s vypnutým JavaScriptem viděli na Zboží.cz bílou stránku bez obsahu – bez výpisu produktů, bez obrázků, bez hodnocení, bez popisů produktů, atd.
Jak je Zboží.cz indexované?
Teď vám přiblížím řešení, které aktuálně používá Zboží.cz, Firmy.cz, Sreality.cz, Lide.cz, Stream.cz a Sauto.cz. Je důležité, abyste věděli, že tohle řešení Google označil jako deprecated už v říjnu 2015 !! Proto to doporučuji NEpoužívat pro nové projekty. Pokud už AJAX crawling používáte, tak začněte uvažovat, jak přejít na Progressive Enhancement. O možnostech indexování JavaScriptových stránek napíšu později.
V roce 2014, když vznikalo nové Zboží.cz, jsme v Seznamu dělali hlavně JavaScriptové aplikace. Těžili jsme z výhod JS aplikací: relativně rychlý vývoj, rychlé odezvy aplikace pro uživatele. Když jsme přepisovali Zboží.cz na JavaScriptovou aplikaci, tak vyhledávače neuměly spustit JavaScript. Seznam.cz to neuměl vůbec a Google to neuměl spolehlivě. Aby na Zboží mohly přicházet milióny návštěv z vyhledávačů, potřebovali jsme, aby Google i Seznam viděli textový obsah stránek a ne jen prázdnou bílou stránku, kterou vidí všichni, kdo neumí spustit JavaScript. Jedinou možností bylo použití AJAX crawling s meta fragment a _escaped_fragment_=. Seznam.cz i Google to uměli. AJAX crawling jsme uměli – běžel už na Firmách a měli jsme i farmu serverů s PhantomJS. Takže šlo jen o to držet se pár pravidel a napojit se na farmu s PhantomJS. PhantomJS je headless prohlížeč spuštěný na serveru a překládá JavaScriptové stránky do HTML.
Jak AJAX crawling funguje?
- robot, který umí AJAX crawling, přijde na URL https://www.zbozi.cz/telefony-navigace/mobilni-telefony/
- zbožácký nginx server vrátí malou HTML stránku (v Seznamu jí říkáme „šablona index.html„), která je stejná pro všechny requesty .. píšu o ní v další kapitole.
- robot si v HTML najde <meta name=“fragment“ content=“!“ /> a podle toho ví, že na téhle stránce není žádný obsah a že má udělat 2. request na server na trochu upravenou URL: https://www.zbozi.cz/telefony-navigace/mobilni-telefony/?_escaped_fragment_= (jen se do querystringu přidává proměnná _escaped_fragment_= . V tomto případě s prázdnou hodnotou.)
- robot tedy udělá druhý request na https://www.zbozi.cz/telefony-navigace/mobilni-telefony/?_escaped_fragment_=
- zbožácký nginx server z URL pozná, že má vydat HTML stránku s obsahem – výpisem mobilů, filtrů, drobečkové navigace, obrázky, nastavenými titulky, popisky, OG tagy, atd.
- nginx z URL ořízne „_escaped_fragment_=“ a pošle URL na PhantomJS farmu.
- PhantomJS dostane URL https://www.zbozi.cz/telefony-navigace/mobilni-telefony/, stáhne si všechny JavaScripty, CSS soubory a obrázky, které se na dané stránce vyskytují a spustí JavaScript jako by to udělal běžný prohlížeč. Hotový HTML kód pak pošle zpátky na nginx.
- Na PhantomJS se děje více věcí, než pro jednoduchost popisuji. Nejdůležitější je nastavování HTTP kódů (200, 301, 404, 50X, atd.). Jako poslední věc v běhu JavaScriptu děláme to, že nastavujeme hodnotu HTTP kódu do proměnné „szn-status“ do meta tagu. Podle toho PhantomJS pozná, že stránka je již vykreslená celá a může vrátit HTML na nginx. Vědět, kdy je stránka hotová a vykreslená je velká výhoda. Můžete tak vracet HTML výrazně rychleji, než kdybyste čekali nějaký pevně daný čas.
- nginx vrátí robotovi HTML kód stránky s obsahem a nastaví HTTP status kód podle toho, co najde v szn-status.
PhantomJS je relativně pomalý – vykreslit stránku mu trvá 1-5 vteřin a to je strašně moc. Proto máme ještě cache, kde kešujeme všechny důležité stránky. Děláme to jednoduše. Parsujeme sitemap.xml a v cca 40 vláknech posíláme URL ze sitemapy na PhantomJS a vyrenderované HTML zagzipujeme a uložíme do Couchbase. Celou sitemap.xml umíme takto zakešovat za pár dní. A pak jede další kolečko s občerstvováním. Dokumenty z cache vydáváme v desítkách milisekund. Dokumenty, které nejsou v cache jdou na PhantomJS (obvykle výsledky hledání, stránkování, různé kombinace filtrů, atd.) a to trvá 1-5 sekund.
Šablona index.html
Aby celé indexování s „dvojím stahováním URL“ bylo co nejrychlejší, tak se hodí mít co nejjednodušší, gzipovaný a minifikovaný a kešovaný „index.html“. Roboti i lidé za den stáhnou tento soubor v řádech desítek až stovek milionů. Ideál by byl, kdyby index.html obsahoval jen toto:
1 | <!doctype html> |
2 | <html> |
3 | <head> |
4 | <meta charset=“utf-8„> |
5 | <meta name=“fragment“ content=“!“ /> |
6 | <meta name=“viewport“ content=“width=device-width, initial-scale=1“ /> |
7 | <title></title> |
8 | <script src=“/js/all.js?6.14.0″></script> |
9 | <link rel=“stylesheet“ href=“/css/all.css?6.14.0″/> |
10 | </head> |
11 | <body> |
12 | <script>Zbozi.init();</script> |
13 | </body> |
14 | </html> |
Index.html je pro každou URL stejný. Je jedno, jestli přijdete na adresu www.zbozi.cz, nebo www.zbozi.cz/hledani/?q=papir-na-piskvorky, nebo https://www.zbozi.cz/telefony-navigace/mobilni-telefony/?vyrobce=samsung&platforma=android, vždy dostanete stejný index.html. Až po spuštění JavaScriptu se v HTML nastaví titulek, popisek, OG tagy, RSS, obsah HEAD i BODY a další informace.
Roboti, kteří neumí JavaScript
Jenže – předchozí řešení je blbé pro roboty, kteří neumí JavaScript ani _escaped_fragment_=. Takový robot pak dostane prázdnou stránku a nemůže ji ani vyfotit, ani z ní vyparsovat TITLE a meta description pro náhledy – například Facebook, Pinterest, LinkedIn, Nuzzle (už je mrtvý?), atd.
Někteří roboti jsou pro nás důležití. Třeba Facebot, WhatsApp bot, Google+ bot, atd. Oni totiž dělají důležité náhledy stránek sdílených na sociálních sítích, nebo v aplikacích (Messenger, WhatsApp, Skype, atd.). V Seznamu chceme, aby naše webovky měly hezký náhledy, proto se staráme i o to, co vidí tito roboti.
Známé tzv. VIP roboty (Facebook, Google+, Tumblr) posíláme na PhantomJS, aby dostali správně nastavené titulky, popisky, OG tagy, obrázky a jiný obsah, ze kterého mohou vytvořit hezké náhledy. Podle user-agenta poznáme pro nás důležitého robota a rovnou ho pošleme na PhantomJS (nebo vydáme HTML stránku z keše). Ano, to je cloackování – ale to robotům sociálních sítí a mobilních aplikací nevadí (na rozdíl od vyhledávačů).
Renderování na PhantomJS je relativně drahé a nechceme tam posílat každého neznámého robota. Na druhou stranu chceme mít alespoň neprázdné náhledy v nově vznikajících aplikacích. Proto pro všechny ostatní roboty máme v index.html natvrdo nastavený TITLE a meta description na obecný text popisující danou webovku. Je to takový fallback pro situace, kdy nějaká služba je už celkem používaná, ale my ji ještě nepřidali na seznam „VIP robotů“.
Ten seznam vypadá zhruba takto :
- .*Nuzzel.*
- .*LinkedInBot.*
- .*vkShare.*
- .*Twitterbot.*
- .*BaiduSpider.*
- .*Pinterest.*
- .*Tumblr.*
- .*WhatsApp.*
- .*Facebot.*
- .*Slackbot-LinkExpanding.*
- .*Twitterbot.*
- .*Google-HTTP-Java-Client.*
- .*Instagram.*
- .*Feedfetcher-Google.*
- .*WhatsApp.*
- .*HTTP_Request2.*
- .*MetaURI API.*
- .*Jakarta Commons-HttpClient.*
- .*Skype.*
- .*facebookexternalhit.*
- .*Exabot.*
- .*SZN-Image-Resizer.*
- .*facebookexternalhit.*
- .*Google (+https://developers.google.com/+/web/snippet/).*
Ideální index.html v reálu neexistuje
Aktuálně je v index.html tohle:
1 | <!doctype html> | |
2 | <html> | |
3 | <head> | |
4 | <meta charset=“utf-8„> | |
5 | <meta http-equiv=“X-UA-Compatible“ content=“IE=Edge“ /> | statické info pro IE, nemění se, tak je tady |
6 | ||
7 | <title>Zboží.cz • Tisíce obchodů na jednom místě</title> | pro všechny roboty, kteří neumí JS a nemáme je na seznamu VIP robotů |
8 | <meta name=“description“ content=“Na Zboží.cz jsou produkty včetně popisů, recenzí, příslušenství a návodů. Ceny si navíc můžete srovnat od těch nejlevnějších.„> | pro všechny roboty, kteří neumí JS a nemáme je na seznamu VIP robotů |
9 | <noscript><meta http-equiv=“refresh“ content=“0;url=?_escaped_fragment_=“/></noscript> | uživatele s vypnutým JavaScriptem redirectujeme na ?_escaped_fragment_= URL, aby dostali obsah i když mají vypnutý JavaScript. Tohle řešení je ale nedotažené. Ignoruje parametry v QueryString. |
10 | <meta name=“fragment“ content=“!“ ng-if=“showMetaFragment“ /> | Informace pro roboty o tom, že stránka umí AJAX crawling přes _escaped_fragment_= |
11 | <meta name=“viewport“ content=“width=device-width, initial-scale=1“ /> | statické, může to být v index.html |
12 | ||
13 | <meta name=“seznam-wmt“ content=“GYLHTGLKyxpiKgg2XIGG6w8J3XY7mm3X„> | Ano, Seznam Webmaster Tools se blíží 🙂 A ověřovací robot neumí JavaScript. |
14 | ||
15 | <link rel=“shortcut icon“ href=“/img/favicon/favicon.ico?6.14.0“ type=“image/x-icon“ /> | Favicony jsou statický obsah, takže je máme zde i pro neznámé roboty. |
16 | <link rel=“apple-touch-icon“ sizes=“57×57“ href=“/img/favicon/apple-touch-icon-57×57.png?6.14.0„> | |
17 | <link rel=“apple-touch-icon“ sizes=“60×60“ href=“/img/favicon/apple-touch-icon-60×60.png?6.14.0„> | |
18 | <link rel=“apple-touch-icon“ sizes=“72×72“ href=“/img/favicon/apple-touch-icon-72×72.png?6.14.0„> | |
19 | <link rel=“apple-touch-icon“ sizes=“76×76“ href=“/img/favicon/apple-touch-icon-76×76.png?6.14.0„> | |
20 | <link rel=“apple-touch-icon“ sizes=“114×114“ href=“/img/favicon/apple-touch-icon-114×114.png?6.14.0„> | |
21 | <link rel=“apple-touch-icon“ sizes=“120×120“ href=“/img/favicon/apple-touch-icon-120×120.png?6.14.0„> | |
22 | <link rel=“apple-touch-icon“ sizes=“144×144“ href=“/img/favicon/apple-touch-icon-144×144.png?6.14.0„> | |
23 | <link rel=“apple-touch-icon“ sizes=“152×152“ href=“/img/favicon/apple-touch-icon-152×152.png?6.14.0„> | |
24 | <link rel=“apple-touch-icon“ sizes=“180×180“ href=“/img/favicon/apple-touch-icon-180×180.png?6.14.0„> | |
25 | <meta name=“msapplication-TileColor“ content=“#666666„> | |
26 | <meta name=“msapplication-config“ content=“/img/favicon/browserconfig.xml?6.14.0„> | |
27 | <meta name=“msapplication-TileImage“ content=“/img/favicon/mstile-144×144.png?6.14.0„> | |
28 | ||
29 | <link rel=“stylesheet“ href=“/css/all.css?6.14.0„/> | CSS. „lazy load“ by šel, ale neřešíme ho, takže link na CSS je tady. |
30 | ||
31 | <!–[if lte IE 8]><link rel=“stylesheet“ href=“/css/all.ie8.css?6.14.0″><![endif]–> | tyhle ify bylo jednodušší vložit staticky do HTML, než je vkládat JavaScriptem a doufat, že se k nim IE8 a 9 dostanou i z JS. |
32 | <!–[if lte IE 9]> | |
33 | <script src=“/js/lib/polyfills/ie8/es5.js?6.14.0″></script> | |
34 | <script src=“/js/lib/polyfills/ie8/js16.js?6.14.0″></script> | |
35 | <script src=“/js/lib/polyfills/ie8/html5shiv.min.js?6.14.0″></script> | |
36 | <script src=“/js/lib/polyfills/ie8/respond.min.js?6.14.0″></script> | |
37 | <![endif]–> | |
38 | ||
39 | <script src=“https://login.szn.cz/js/api/login.js„></script> | JS pro login, asi by mohl být donačítaný v JS. |
40 | <script src=“//1.im.cz/software/promo/promo-sbrowser.js“ crossorigin></script> | JS pro promo Seznam.cz prohlížeč, asi by mohl být donačítaný v JS. |
41 | ||
42 | <script src=“/js/conf/config.js?6.14.0„></script> | JS s nastavením aplikace Zboží. Ten může být součástí all.js, ale není 🙂 |
43 | <script src=“/js/all.js?6.14.0„></script> | Tohle je nejdůležitější JavaScript na Zboží. Ten dělá „magic“. Ten tu musí být. |
44 | </head> | |
45 | <body data-dot=“zbozi„> | |
46 | <ng-view ng-if=“!error“ data-autoscroll=““></ng-view> | Statický content, nechali jsme to v index.html, ale být to tady nemusí. |
47 | <ng-include ng-if=“error“ src=“‚/app/errors/templ/errors.html‘„></ng-include> | Statický content, nechali jsme to v index.html, ale být to tady nemusí. |
48 | <erotic-disclaimer ng-if=“showEroticDisclaimer„></erotic-disclaimer> | Statický content, nechali jsme to v index.html, ale být to tady nemusí. |
49 | <script src=“//i.imedia.cz/js/im3.js“ crossorigin></script> | Statický content, nechali jsme to v index.html, ale být to tady nemusí. |
50 | <script src=“//h.imedia.cz/js/dot-small.js“ crossorigin></script> | Statický content, nechali jsme to v index.html, ale být to tady nemusí. |
51 | <script src=“//h.imedia.cz/js/sid.js“ crossorigin></script> | Statický content, nechali jsme to v index.html, ale být to tady nemusí. |
52 | <script>Zbozi.init();</script> | Tohle spouští JavaScript. To tu v podstatě musí být. |
53 | </body> | |
54 | </html> |
Pro dnešek vše. Příště se podíváme na to, jaké jsou možnosti indexace JavaScriptu ve vyhledávačích a zajímavých robotech.
Jestli vás zajímá něco konkrétního, tak se nebojte v komentářích zeptat.