Roboti, robots.txt a sitemapy#
Co smí robot číst a co ne, jak se mu to řekne a jak se vyhledávači předá seznam adres.
Tři věci, které spolu souvisí a které dřív nefungovaly: roboti spouštěli akční odkazy,
robots.txt byl statický soubor s cizí sitemapou a sitemapa stránek byla vždy prázdná.
Co robot smí#
Robot smí číst každou obsahovou stránku. Klikat (spouštět signál ?do=…) nesmí:
oblíbené, košík, hodnocení článku, hlášení příspěvku. Roboti by takové adresy volali
ve velkém a plnili databázi i relace.
| Vrstva | Co dělá | Kde |
|---|---|---|
#[Requires(ajax: true)] |
zapisující handler odmítne holý GET (403); AJAX z JavaScriptu projde | Rating…HandleTrait, Discussion VoteTrait (hlas, nahlášení) |
CrawlerDetector |
signál od klienta, který se hlásí jako robot, končí 403 dřív, než se cokoli načte | Front\BasePresenter::startup() |
rel="nofollow" |
50 akčních odkazů v šablonách má rel="nofollow" |
šablony témat |
robots.txt |
zakazuje akční adresy | Seo\Presenters\RobotsPresenter |
Oblíbené a košík zůstávají GET
Zapisují jen do session, takže je hlídá CrawlerDetector a robots.txt. Funguje to
lidem s JavaScriptem i bez něj. Převod na POST/AJAX-only je další krok.
CrawlerDetector#
App\Core\Utils\Helpers\CrawlerDetector::isCrawler(?string $userAgent): bool. Poznává
bot, crawl, spider, slurp, scrap, fetch, headless, preview, monitor,
archiver, jména crawlerů, které tyhle značky nemají (Facebook, Bing preview,
Google-InspectionTool, ChatGPT-User, OAI-SearchBot, Perplexity, Bytespider, PetalBot,
ia_archiver), a obecné klienty (curl, wget, python-requests, go-http-client,
okhttp, libwww). Prázdný User-Agent je vždy robot.
Je to zdvořilostní filtr, ne bezpečnostní hranice
Zastaví klienty, kteří se sami hlásí. Cokoli, co zapisuje do databáze, musí mít vlastní
ochranu (oprávnění, #[Requires(ajax: true)], isSameSite()).
Při testování signálů přes curl posílej prohlížečový User-Agent
Jinak dostaneš 403 a budeš hledat chybu v kódu. curl/… a prázdná hlavička jsou pro
detektor robot.
Čítače zobrazení#
Zobrazení výpisu a detailu (inzeráty a katalog firem, příspěvky diskuze, články) robot
nezvyšuje: sedm míst ve Front presenterech volá CrawlerDetector::isCrawlerRequest()
a zápis čítače přeskočí. Čítač kliků zůstal beze změny. Přidáváš-li nový čítač zobrazení,
obal zápis stejnou podmínkou.
Postup: přidávám zapisující signál na frontu#
- Handler označ
#[\Nette\Application\Attributes\Requires(ajax: true)]— holý GET je pak 403 a odkaz nepouštějí roboti ani předběžné načtení. - Odkaz na signál dej
rel="nofollow". - Má-li akce fungovat i bez JavaScriptu, nepoužívej
ajax: true; spoléhej naCrawlerDetectorarobots.txt, a zapisuj jen do session. - Hodnoty, které zapisuješ do databáze, ověř v manageru; ID z adresy se jen porovnává
s tím, co patří komponentě (vzor: hlášení příspěvku
handleReport()). - Zkus signál z AJAXu, bez AJAXu a s
curl/…— dostaneš postupně ✓, 403 a 403.
robots.txt#
Generuje ho App\UI\Seo\Presenters\RobotsPresenter (routa robots.txt v
RouterDispatcher). Je to obyčejný Nette presenter — bez motivu, lokalizace a databáze.
Statický www/robots.txt byl odstraněn: Sitemap: potřebuje absolutní adresu a jeden
kód obsluhuje víc domén (starý soubor ukazoval všechny weby na cizí host).
Pevná pravidla:
User-agent: *
Allow: /
Disallow: /*?do=
Disallow: /*&do=
Disallow: /*?_fid=
Disallow: /*&_fid=
Disallow: /admin/
Disallow: /api/
Disallow: /cron/
Řádek Sitemap: <host>/sitemaps/sitemap.xml se přidá jen tehdy, když soubor indexu
skutečně existuje (www/sitemaps/sitemap.xml, adresář z parametru sitemapDir). Host se
bere z aktuálního požadavku.
Bez indexu sitemapy robots.txt na sitemapu neukazuje
Dokud na serveru neproběhne /cron/system/sitemap/index?save=1, v souboru řádek
Sitemap: není. Nic neselže, jen vyhledávač nemá odkaz.
Sitemapy#
Generují je cron presentery (app/UI/Cron/<Modul>/Presenters/SitemapPresenter.php) přes
SitemapBuilderTrait. Bez ?save=1 vrátí JSON náhled (počty), s ?save=1 zapíše soubor do
www/sitemaps/.
Adresy v sitemapě staví SitemapBuilderTrait přes službu SiteBaseUrl: je-li vyplněné
nastavení site.baseUrl, platí vždy ono, ne Host, pod kterým cron web zavolal. Prázdné
nastavení znamená adresu požadavku (viz Odesílání e-mailů).
| Úloha | Co dělá |
|---|---|
/cron/system/sitemap/generate |
stránky všech jazykových variant (hreflang) |
/cron/blog/sitemap/{article,news,event} a …-category |
blog |
/cron/bazaar/…, /cron/comcat/…, /cron/discussion/… |
moduly |
/cron/eshop/sitemap/category |
všechny kategorie e-shopu |
/cron/eshop/sitemap/product |
produkty, které ukazuje výpis (ProductSetEligibilityFilters), do souborů podle rozsahů ID (velikost viz níže) |
/cron/system/sitemap/index |
index všech sitemap-*.xml; spustit až po ostatních |
Úlohy se plánují crontabem serveru a samy neběží, viz Naplánované úlohy. Frekvence: denně v noci.
Adresář www/sitemaps/ patří do .gitignore i do deploy/rsync-exclude.txt
Soubory vznikají za běhu na serveru. Kdyby je rsync při nasazení (--delete) viděl
jako cizí, smazal by je a robots.txt by zůstal bez sitemapy do další noci. Obojí je
už zapsáno; při nové instalaci to ověř.
Index ze starých souborů
Index skládá odkazy z toho, co v adresáři právě je. Poběží-li před ostatními úlohami, sestaví se z výsledku minulého běhu.
Dělení do souborů#
Velké sady (inzeráty, firmy, produkty, články…) se zapisují po dávkách podle id rovnou do
souboru (KeysetBatches, SitemapBucketWriter, SitemapUrlsetWriter), ne přes pole v paměti.
Do jednoho souboru patří 45000 / počet jazyků položek (SitemapBucketWriter::bucketSizeFor()),
protože každá položka dává jednu adresu na jazyk a soubor sitemapy smí mít nejvýš 50 000 adres.
Soubor se jmenuje <předpona>-<bucket>.xml, bucket = intdiv(id - 1, velikost); hranice id
jsou pevné, takže smazání položky ostatní mezi soubory neposune. Kdyby další položka přetáhla
pojistku (50 000 adres nebo 45 MiB na soubor), pokračuje se v <předpona>-<bucket>-1.xml,
-2, … Změna počtu jazyků změní velikost bucketu, a tím rozdělení id do souborů. Staré číslované
soubory sady, které nová generace nepřepsala, se smažou (vzor <předpona>-[0-9]*.xml, takže
cizí soubor se stejným začátkem, např. soubor kategorií, zůstane).
Zápis je atomický přes StagedFileSet:
- každý soubor se píše do náhodného
.tmpvedle cíle (reserve(),stage()) — stejný filesystem, takžerenameje atomický; commit()teprve po zapsání všech souborů přejmenuje dočasné na finální jména, nastaví0664(web server je vydává staticky i přes umask cronu) a smaže staré soubory podle vzoru;abort()(i selhání zápisu) smaže jen vlastní dočasné soubory; stará sada zůstane.
Robot tak u každého jména vidí buď celou starou, nebo celou novou verzi.
Nemaž starou sadu předem a nepiš rovnou do finálního jména
Selhání druhého souboru by nechalo web bez sitemapy a robot čtoucí soubor během zápisu by
dostal useknuté XML. Vlastní generátor souborů pro roboty publikuj přes StagedFileSet.
Položky musí jít vzestupně podle id
SitemapBucketWriter::add() při id menším nebo stejném jako předchozí vyhodí výjimku.
Loader pro KeysetBatches proto řadí ORDER BY id a vrací jen id > poslední.
Postup: přidat sitemapu nového modulu#
- V cron presenteru modulu použij
SitemapBuilderTrait(buildSitemapXml(),respondSitemap(), u velkých sadrespondBucketedSitemap()). - Cíl odkazu skládej přes routing (
presenter->link(…)/buildPageUrl()), ne ručně. Sitemapa stránek byla dřív prázdná, protože cíl odkazu byl složený jakoFront:Front:System:Pagea neexistoval; po opravě je v ní kolem stovky adres. - Do crontabu přidej úlohu před
index. - Ověř: bez
saveJSON s počtem, ssave=1soubor;curl …/robots.txtukážeSitemap:; soubor se servíruje sapplication/xml.
Jedna kanonická adresa#
www/.htaccess sjednocuje adresy na jednu variantu (301, cesta i dotaz se zachovají):
| Vstup | Výsledek |
|---|---|
www.<host>/… |
<host>/… |
/www/… (doslovný prefix složky) |
/…; existující soubory se nepřesměrují |
Cíl přesměrování se skládá z $1 pravidla, ne z %{REQUEST_URI}
Web běží přes kořenový .htaccess (^(.*)$ → /www/$1), takže REQUEST_URI už nese
vnitřní /www/. První verze proto přesměrovávala na /www/inzerce. Duplicitní cestu
/www/… rozlišuje %{THE_REQUEST} — původní řádek požadavku, který vnitřní přepis
nemění.
Web, který má žít na www, tyto dva řádky smaže
Přesměrování www → bez www je ve výchozím .htaccess zapnuté. Na webu, který má
kanonický host www, se musí odstranit.
Kam sáhnout#
| Chci | Kde |
|---|---|
| detekci robotů | app/Core/Utils/Helpers/CrawlerDetector.php |
| bránu v prezenteru | app/Core/Base/Front/BasePresenter.php → startup() |
robots.txt |
app/UI/Seo/Presenters/RobotsPresenter.php, routa v RouterDispatcher |
| generování sitemap | app/Core/Traits/Cron/Presenters/SitemapBuilderTrait.php |
| e-shopové sitemapy | app/UI/Cron/Eshop/Presenters/SitemapPresenter.php |
| dělení a atomický zápis souborů | app/Core/Utils/Helpers/Sitemap/, app/Core/Utils/Helpers/StagedFileSet.php |
| kanonický host | www/.htaccess |
Související: CORS, rate limit a audit · Naplánované úlohy · Routování