<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eugene</title>
    <description>The latest articles on DEV Community by Eugene (@numart).</description>
    <link>https://dev.to/numart</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1890960%2F3d7830be-51c1-4206-a6e6-c36e70b22279.png</url>
      <title>DEV Community: Eugene</title>
      <link>https://dev.to/numart</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/numart"/>
    <language>en</language>
    <item>
      <title>Single-flight caching: how one hot key stops taking the database down</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Mon, 17 Aug 2026 23:21:16 +0000</pubDate>
      <link>https://dev.to/numart/single-flight-caching-how-one-hot-key-stops-taking-the-database-down-3ae5</link>
      <guid>https://dev.to/numart/single-flight-caching-how-one-hot-key-stops-taking-the-database-down-3ae5</guid>
      <description>&lt;p&gt;On podbor-minuta.ru some database queries are heavy, so we keep their results in a cache: apartment lists, aggregate numbers for listing pages. While the value is in the cache, everything is fast. The trouble starts the moment it expires.&lt;/p&gt;

&lt;p&gt;One day under load we saw a batch of 500 responses timing out. The database was alive, but queries to it suddenly queued up and hit the connection limit. The answer was in how we refreshed the cache.&lt;/p&gt;

&lt;p&gt;The logic was naive: if the value is not in the cache, go to the database, compute it, put it in the cache. For a single request that is correct. But imagine the cache has just expired and in the same millisecond two hundred requests arrive for the same page. Each one sees an empty cache and each one goes to the database to compute the same thing. Two hundred identical heavy queries at once. The connection pool drains, the remaining requests get no connection, they wait and drop on timeout.&lt;/p&gt;

&lt;p&gt;This is called a cache stampede. Caching itself does not help here, because the hole opens exactly when the value is missing, and that is exactly when the load arrives.&lt;/p&gt;

&lt;p&gt;The fix is the idea of "one for all". If a computation for a key is already running, everyone else who comes for the same key does not start their own, they wait for that same result. One query goes to the database, and two hundred callers get the same value.&lt;/p&gt;

&lt;p&gt;In practice this is done with a shared in-flight promise. We store not only finished values but also promises of results that are still in progress. The first caller creates a promise and puts it in a map by key. The others see that the promise already exists and simply await it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// leave the fast hit path untouched&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already computing - await the same result&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// release the key no matter what&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things matter here. First: the cache-hit path stays fast, we added nothing to it, no maps and no extra checks. Second: we clear the in-flight map in finally, not in then. If the computation fails, the key still has to be released, otherwise the next caller will wait forever on a dead promise. Third: this works inside one process. If you run several replicas of the service, each collapses its own concurrent calls, but there is no protection between replicas. For our load that is enough, because the stampede happens inside one process on a hot key. If you need it stricter, you add a shared lock through Redis, but that is a different cost and a different complexity.&lt;/p&gt;

&lt;p&gt;After this change the same load stopped taking the database down. Two hundred requests for a hot key produce one database query instead of two hundred, and the connection pool no longer drains for no reason.&lt;/p&gt;

&lt;p&gt;What we took from this. A cache does not protect you from load by itself. The dangerous point is the miss, and if many identical requests arrive at that moment, you get a stampede. It is fixed not by a bigger pool and not by a longer timeout, but by letting concurrent computations of one key share a single unit of work. The fast path must stay untouched, and the key must be released even on an error.&lt;/p&gt;

&lt;p&gt;The site is podbor-minuta.ru, daily price monitoring for Moscow new builds.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Single-flight кэш: как один горячий ключ не роняет базу</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Mon, 17 Aug 2026 23:21:15 +0000</pubDate>
      <link>https://dev.to/numart/single-flight-kesh-kak-odin-ghoriachii-kliuch-nie-roniaiet-bazu-4ic9</link>
      <guid>https://dev.to/numart/single-flight-kesh-kak-odin-ghoriachii-kliuch-nie-roniaiet-bazu-4ic9</guid>
      <description>&lt;p&gt;На podbor-minuta.ru есть тяжёлые запросы к базе, результат которых мы держим в кэше: списки квартир, сводные цифры по подборкам. Пока значение в кэше есть, всё быстро. Проблема начинается в момент, когда оно протухает.&lt;/p&gt;

&lt;p&gt;Однажды под нагрузкой мы увидели пачку ответов с ошибкой 500 по таймауту. База при этом была жива, но запросы к ней вдруг встали в очередь и упёрлись в лимит соединений. Разгадка оказалась в том, как мы обновляли кэш.&lt;/p&gt;

&lt;p&gt;Логика была наивная: если значения в кэше нет, идём в базу, считаем, кладём в кэш. Для одного запроса это правильно. Но представьте, что кэш протух, и в эту же миллисекунду прилетело двести запросов на одну и ту же страницу. Каждый из них видит пустой кэш и каждый идёт в базу считать одно и то же. Двести одинаковых тяжёлых запросов разом. Пул соединений опустошается, остальным запросам соединения не достаётся, они ждут и отваливаются по таймауту.&lt;/p&gt;

&lt;p&gt;Это называют набегом на кэш. Само по себе кэширование тут не помогает, потому что дыра открывается ровно в момент, когда значения нет, а именно в этот момент нагрузка и приходит.&lt;/p&gt;

&lt;p&gt;Чинится это идеей "один за всех". Если по ключу уже идёт вычисление, все остальные, кто пришёл за тем же ключом, не запускают своё, а ждут тот же самый результат. В базу уходит один запрос, а двести вызовов получают одно и то же значение.&lt;/p&gt;

&lt;p&gt;На практике это делается через общий незавершённый промис. Мы храним не только готовые значения, но и обещания результата, которые ещё в процессе. Первый вызов создаёт промис и кладёт его в карту по ключу. Остальные видят, что промис уже есть, и просто дожидаются его.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// быстрый путь не трогаем&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// уже считается - ждём тот же результат&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// освобождаем ключ в любом случае&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;inFlight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Здесь важны три вещи. Первая: путь с попаданием в кэш остаётся быстрым, мы ничего к нему не добавили, там нет ни карт, ни лишних проверок. Вторая: карту незавершённых промисов чистим в finally, а не в then. Если вычисление упало с ошибкой, ключ всё равно надо освободить, иначе следующий вызов будет вечно ждать мёртвый промис. Третья: это работает в пределах одного процесса. Если у вас несколько реплик сервиса, каждая схлопнет свои параллельные вызовы, но между репликами защиты нет. Для нашей нагрузки этого хватает, потому что набег случается внутри одного процесса на горячем ключе. Если нужно строже, добавляют общий замок через Redis, но это уже другая цена и другая сложность.&lt;/p&gt;

&lt;p&gt;После этой правки та же нагрузка перестала ронять базу. Двести запросов на горячий ключ дают один запрос к базе вместо двухсот, а пул соединений больше не опустошается на ровном месте.&lt;/p&gt;

&lt;p&gt;Что мы вынесли. Кэш сам по себе не спасает от нагрузки. Опасен именно момент промаха, и если в этот момент приходит много одинаковых запросов, вы получаете набег. Лечится он не увеличением пула и не более длинным таймаутом, а тем, что параллельные вычисления одного ключа делят одну работу на всех. Быстрый путь при этом трогать нельзя, а ключ надо освобождать даже при ошибке.&lt;/p&gt;

&lt;p&gt;Сайт - podbor-minuta.ru, ежедневный мониторинг цен на новостройки Москвы.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Keeping junk out of programmatic geo pages: an allow list for metro, a block list for districts</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:54:11 +0000</pubDate>
      <link>https://dev.to/numart/keeping-junk-out-of-programmatic-geo-pages-an-allow-list-for-metro-a-block-list-for-districts-4adk</link>
      <guid>https://dev.to/numart/keeping-junk-out-of-programmatic-geo-pages-an-allow-list-for-metro-a-block-list-for-districts-4adk</guid>
      <description>&lt;p&gt;On podbor-minuta.ru we have listing pages grouped by metro station and by district. The idea is simple: every station and every district gets its own page with the new-build apartments near it. These pages are built ahead of time from the database, each with its own URL, title and a real list of apartments. The problem was the data we built those groups from.&lt;/p&gt;

&lt;p&gt;The metro and district values come from a scraper, and it copies them from other sites as they are. In the metro field, instead of a clean station name, we often had strings like "Tushinskaya (13 min)". The walking time was glued onto the station. For a person that is nothing, but for grouping it is a disaster: "Tushinskaya", "Tushinskaya (5 min)" and "Tushinskaya (13 min)" count as three different values. One station turned into several near-identical pages.&lt;/p&gt;

&lt;p&gt;District was worse. The scraper dumped anything into that field: project names ("ZhK Sezar City"), marketing fragments ("multifunctional complex..."), and sometimes whole sentences from the description, with prices, areas and build stages. Real districts were less than half of it.&lt;/p&gt;

&lt;p&gt;Grouping naively on these fields produced about 193 thin, near-duplicate pages. On a young domain that is a direct risk: search engines dislike a pile of empty lookalike pages and can push the whole site down. We wanted only pages with real stations and real districts, and we wanted variants of one station to collapse into one page.&lt;/p&gt;

&lt;p&gt;The first thing we understood: metro and district are two different cases, and they need different medicine.&lt;/p&gt;

&lt;p&gt;For metro we have a full reference of Moscow stations. So we can check against an allow list: if the cleaned value is in the reference, keep it, if not, drop it. And to collapse the variants, the page URL is built not from the raw value but from the normalized name. First we strip the walking-time tail, get a clean "Tushinskaya", and build the URL from that. All variants with different times land on the same URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "Tushinskaya (13 min)" -&amp;gt; "Tushinskaya" -&amp;gt; tushinskaya&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;station&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeMetroValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isKnownMetroStation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KNOWN_TRANSIT_LINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lineKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not a real station - no page&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transliterateSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separately we kept transit lines as their own kind of page: MCD, MCK, BKL (Moscow's suburban and ring lines). They are not stations, but they have real search demand ("new builds near MCD") and thousands of apartments under them. So next to the station allow list we keep a small list of lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_TRANSIT_LINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцк&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;бкл&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;District did not work this way, and here is why. We do not have a full district reference. Our districts table is missing the okrugs and the towns of the Moscow region. Those are real, living groups we do not want to lose. If we made an allow list, we would throw out half of the good data along with the junk.&lt;/p&gt;

&lt;p&gt;So for district we went the other way: we do not describe what is good, we describe what is clearly bad and reject that. The bad is well defined here. A project name almost always starts with "ZhK", "MFK", "residential complex", "tower", "residences". A fragment of a description almost always has numbers with "million", "thousand", "rub", "sq m", or "min", or words about construction like "being built", "handed over", "phase". A real district never looks like that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JUNK_DISTRICT_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;жилой&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;комплекс|квартал&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;\s)(?:&lt;/span&gt;&lt;span class="sr"&gt;жк|мфк|башня&lt;/span&gt;&lt;span class="se"&gt;)(?:\s&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;\s)(?:&lt;/span&gt;&lt;span class="sr"&gt;апарт|резиденц&lt;/span&gt;&lt;span class="se"&gt;)[&lt;/span&gt;&lt;span class="sr"&gt;а-яё&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*|&lt;/span&gt;&lt;span class="se"&gt;\d[\d&lt;/span&gt;&lt;span class="sr"&gt;.,&lt;/span&gt;&lt;span class="se"&gt;\s]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;млн|тыс|руб|кв&lt;/span&gt;&lt;span class="se"&gt;\.?\s?&lt;/span&gt;&lt;span class="sr"&gt;м|мин&lt;/span&gt;&lt;span class="se"&gt;\b)&lt;/span&gt;&lt;span class="sr"&gt;|возвод|строит|сдан|очеред&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isJunkDistrictValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// a district is never a full sentence&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JUNK_DISTRICT_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that are easy to trip on. In JavaScript the "\w" class does not include Cyrillic, so word continuations had to be spelled out with "[а-яё]*". And stems must be bound to a word boundary: if you just search for "dom" (house), the regex will hit "Domodedovo" and kill a real district. So short words require a space or a line end around them, while prefixes like "apart" and "residenc" match the project forms directly.&lt;/p&gt;

&lt;p&gt;Both checks meet in one function. It takes a raw value and either returns a pair of "display name plus slug" or null, in which case the page simply does not exist. This is the single point through which geo reaches the catalog and the sitemap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveSeoGeoLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawDisp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawDisp&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dim&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;station&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeMetroValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isKnownMetroStation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KNOWN_TRANSIT_LINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isJunkDistrictValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transliterateSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_GEO_SLUG_LEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that there is one more simple safety margin. We capped the slug length at 64 characters. Each of these pages becomes a directory on disk at build time, and a directory name in the filesystem cannot be longer than 255 bytes. A single dirty value the length of a whole sentence once crashed our entire build. We have a separate article about that story, and here we just do not let long values through.&lt;/p&gt;

&lt;p&gt;And the last filter is count. We build a page only if it has at least a threshold of apartments under it. In SQL that is one line, "HAVING COUNT(*) &amp;gt;= minLots". This drops groups of one or two apartments, which would also be thin pages.&lt;/p&gt;

&lt;p&gt;What we took from this. Where you have a full reference, use an allow list: keep only what is in it. Where you have no reference but the junk is clear, use a block list: drop what is clearly bad and keep the rest. This is not symmetry for looks, it is about what you actually know about the data. And always clean the raw value before it becomes part of a URL, or one station variant will multiply into a dozen empty pages.&lt;/p&gt;

&lt;p&gt;The site is podbor-minuta.ru, daily price monitoring for Moscow new builds.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Как мы держим мусор подальше от программных гео-страниц: белый список метро и чёрный список районов</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:54:11 +0000</pubDate>
      <link>https://dev.to/numart/kak-my-dierzhim-musor-podalshie-ot-proghrammnykh-ghieo-stranits-bielyi-spisok-mietro-i-chiornyi-spisok-raionov-45i5</link>
      <guid>https://dev.to/numart/kak-my-dierzhim-musor-podalshie-ot-proghrammnykh-ghieo-stranits-bielyi-spisok-mietro-i-chiornyi-spisok-raionov-45i5</guid>
      <description>&lt;p&gt;На podbor-minuta.ru есть страницы-подборки квартир, сгруппированные по метро и по району. Идея простая: у каждой станции метро и у каждого района своя страница со списком новостроек рядом. Такие страницы собираются заранее из базы, у каждой свой адрес, заголовок и реальный список квартир. Проблема была в том, из чего мы эти группы строили.&lt;/p&gt;

&lt;p&gt;Данные о метро и районе приходят от скрейпера, а он берёт их с чужих сайтов как есть. В поле "метро" вместо чистого названия станции лежали строки вроде "Тушинская (13 мин.)". То есть к станции приклеено время пешком. Для человека это мелочь, а для группировки - катастрофа: "Тушинская", "Тушинская (5 мин.)", "Тушинская (13 мин.)" считаются тремя разными значениями. Одна станция превращалась в несколько почти одинаковых страниц.&lt;/p&gt;

&lt;p&gt;С районом было хуже. В поле "район" скрейпер сваливал что попало: названия жилых комплексов ("ЖК Сезар Сити"), рекламные куски ("многофункциональный комплекс..."), а иногда целые предложения из описания - с ценами, площадями и стадиями стройки. Настоящих районов там было меньше половины.&lt;/p&gt;

&lt;p&gt;Наивная группировка по этим полям дала около 193 тонких страниц, почти дублей. Для молодого домена это прямой риск: поисковики не любят пачку пустых похожих страниц и могут понизить весь сайт. Нам нужны были только страницы с настоящими станциями и настоящими районами, и чтобы варианты одной станции схлопывались в одну.&lt;/p&gt;

&lt;p&gt;Первое, что мы поняли: метро и район - это два разных случая, и лечить их надо по-разному.&lt;/p&gt;

&lt;p&gt;Для метро у нас есть полный справочник станций Москвы. Значит, можно проверять по белому списку: если после чистки значение есть в справочнике - берём, если нет - выкидываем. А чтобы схлопнуть варианты, адрес страницы строится не из сырого значения, а из нормализованного названия. Сначала убираем хвост со временем пешком, получаем чистую "Тушинскую", и уже из неё делаем адрес. Все варианты с разным временем сходятся в один и тот же адрес.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "Тушинская (13 мин.)" -&amp;gt; "Тушинская" -&amp;gt; tushinskaya&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;station&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeMetroValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isKnownMetroStation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KNOWN_TRANSIT_LINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lineKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// не настоящая станция - страницы не будет&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transliterateSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Отдельно мы оставили линии как самостоятельный вид страницы: МЦД, МЦК, БКЛ. Это не станции, но у них есть реальный спрос в поиске ("новостройки у МЦД") и под ними тысячи квартир. Поэтому рядом с белым списком станций держим маленький список линий.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KNOWN_TRANSIT_LINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцк&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;мцд-5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;бкл&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;С районом так не получилось, и вот почему. Полного справочника районов у нас нет. В нашей таблице районов не хватает округов и городов Московской области. А это настоящие, живые группы, которые мы не хотим терять. Если сделать белый список, мы выкинем вместе с мусором и половину нормальных данных.&lt;/p&gt;

&lt;p&gt;Поэтому для района мы пошли от обратного: не описываем, что хорошо, а описываем, что заведомо плохо, и это отбраковываем. Плохое здесь хорошо определено. Название жилого комплекса почти всегда начинается с "ЖК", "МФК", "жилой комплекс", "башня", "резиденции". Кусок описания почти всегда содержит цифры с "млн", "тыс", "руб", "кв. м" или "мин", либо слова про стройку - "возводится", "сдан", "очередь". Настоящий район не выглядит так никогда.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JUNK_DISTRICT_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;жилой&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;комплекс|квартал&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;\s)(?:&lt;/span&gt;&lt;span class="sr"&gt;жк|мфк|башня&lt;/span&gt;&lt;span class="se"&gt;)(?:\s&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;\s)(?:&lt;/span&gt;&lt;span class="sr"&gt;апарт|резиденц&lt;/span&gt;&lt;span class="se"&gt;)[&lt;/span&gt;&lt;span class="sr"&gt;а-яё&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*|&lt;/span&gt;&lt;span class="se"&gt;\d[\d&lt;/span&gt;&lt;span class="sr"&gt;.,&lt;/span&gt;&lt;span class="se"&gt;\s]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;млн|тыс|руб|кв&lt;/span&gt;&lt;span class="se"&gt;\.?\s?&lt;/span&gt;&lt;span class="sr"&gt;м|мин&lt;/span&gt;&lt;span class="se"&gt;\b)&lt;/span&gt;&lt;span class="sr"&gt;|возвод|строит|сдан|очеред&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isJunkDistrictValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// район не бывает длиной в предложение&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JUNK_DISTRICT_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Пара тонкостей, на которых легко обжечься. В JavaScript класс "\w" не включает кириллицу, поэтому продолжения слов пришлось писать явно через "[а-яё]*". И стемы надо привязывать к границе слова: если просто искать "дом", регулярка поймает "Домодедово" и убьёт настоящий район. Поэтому короткие слова требуют пробела или конца строки вокруг, а префиксы вроде "апарт" и "резиденц" ловят проектные формы напрямую.&lt;/p&gt;

&lt;p&gt;Обе проверки сходятся в одной функции. Она получает сырое значение и либо возвращает пару "показываемое имя плюс адрес", либо null, и тогда страницы просто не будет. Это единственная точка, через которую гео попадает в каталог и в карту сайта.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveSeoGeoLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawDisp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawDisp&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dim&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;station&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeMetroValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isKnownMetroStation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KNOWN_TRANSIT_LINES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;station&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isJunkDistrictValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transliterateSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_GEO_SLUG_LEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Сверху идёт ещё один простой запас прочности. Мы ограничили длину адреса в 64 символа. Каждая такая страница при сборке становится папкой на диске, а имя папки в файловой системе не может быть длиннее 255 байт. Одно грязное значение длиной в целое предложение когда-то уронило нам всю сборку. Про эту историю у нас есть отдельная статья, а здесь просто не пускаем длинные значения дальше.&lt;/p&gt;

&lt;p&gt;И последний фильтр - количество. Страницу строим только если под ней набирается хотя бы порог квартир. В SQL это одна строчка "HAVING COUNT(*) &amp;gt;= minLots". Так отсекаются группы из одной-двух квартир, которые тоже дают тонкие страницы.&lt;/p&gt;

&lt;p&gt;Что мы вынесли из этого. Там, где у вас есть полный справочник, используйте белый список: берём только то, что в нём есть. Там, где справочника нет, а мусор понятный, используйте чёрный список: выкидываем то, что заведомо плохо, и оставляем остальное. Это не симметрия ради красоты, это про то, что вы реально знаете о данных. И всегда чистите сырое значение до того, как оно станет частью адреса, иначе один вариант станции размножится в десяток пустых страниц.&lt;/p&gt;

&lt;p&gt;Сайт - podbor-minuta.ru, ежедневный мониторинг цен на новостройки Москвы.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>postgres</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>ENAMETOOLONG: how one dirty database field crashed our static build</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:52:48 +0000</pubDate>
      <link>https://dev.to/numart/enametoolong-how-one-dirty-database-field-crashed-our-static-build-2312</link>
      <guid>https://dev.to/numart/enametoolong-how-one-dirty-database-field-crashed-our-static-build-2312</guid>
      <description>&lt;p&gt;At podbor-minuta.ru about 600 apartment listing pages are built ahead of time into static HTML. Each page address is built from data: city, district or metro, room count, price. For example, /novostroyki/rayon/nagatino/2-komnatnye.&lt;/p&gt;

&lt;p&gt;One day the build crashed with ENAMETOOLONG, file name too long.&lt;/p&gt;

&lt;p&gt;Here is how it happened. We collect apartment data automatically from several sites. In one record the district field held a whole sentence from a description instead of a district name, about 295 characters, something like "prices in nearby projects start from 8.9 million for a one-room with finishing". Our page generator took that field and made it part of an address. The static builder (Vike) creates a directory on disk for each address. A directory name in the filesystem cannot be longer than 255 bytes. 295 characters, once encoded, is well past that. mkdir fails, and the whole build fails with it.&lt;/p&gt;

&lt;p&gt;Why we did not catch it right away. The build runs in Docker. The static-build layer was cached, so for a while the step was skipped and the error was invisible. Locally the build sometimes did not reach that point because the data fetch stalled and the broken page never made it into the set. The error only showed up on a clean build in CI.&lt;/p&gt;

&lt;p&gt;What we fixed. Two things.&lt;/p&gt;

&lt;p&gt;First, a length guard right in the address generator. Any address segment longer than a sane limit is dropped and the page is not created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSafeSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;120 characters is well under the filesystem limit, and real district and station names are shorter.&lt;/p&gt;

&lt;p&gt;Second, cleaning at the source. We did not want to just hide bad data, so we filtered the junk where the district is read from the database: a value that looks like a description sentence (too long, with digits and words like "rubles", "apartments", "finishing") never enters the listings. This also removed dozens of thin empty pages that such junk produced.&lt;/p&gt;

&lt;p&gt;What to take from this. In a static site any field from your database eventually becomes part of a file name. Data you do not control (from scraping, from users) has to be checked and trimmed before it turns into an address or a path. And one bad record should not crash the whole build, better to skip it than to fall over completely.&lt;/p&gt;

&lt;p&gt;The site is podbor-minuta.ru, daily price monitoring for Moscow new builds.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>webdev</category>
      <category>seo</category>
      <category>debugging</category>
    </item>
    <item>
      <title>ENAMETOOLONG: как одно грязное поле в базе уронило сборку статики</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:52:47 +0000</pubDate>
      <link>https://dev.to/numart/enametoolong-kak-odno-ghriaznoie-polie-v-bazie-uronilo-sborku-statiki-1o4c</link>
      <guid>https://dev.to/numart/enametoolong-kak-odno-ghriaznoie-polie-v-bazie-uronilo-sborku-statiki-1o4c</guid>
      <description>&lt;p&gt;У нас на podbor-minuta.ru около 600 страниц-подборок квартир собираются заранее в готовый HTML. Адрес каждой страницы строится из данных: город, район или метро, число комнат, цена. Например, /novostroyki/rayon/nagatino/2-komnatnye.&lt;/p&gt;

&lt;p&gt;Однажды сборка упала с ошибкой ENAMETOOLONG - имя файла слишком длинное.&lt;/p&gt;

&lt;p&gt;Как это вышло. Данные о квартирах мы собираем автоматически с разных сайтов. В одной записи поле "район" вместо названия района содержало целое предложение из описания, около 295 символов, что-то вроде "цены на квартиры в соседних жк начинаются от 8,9 млн рублей за однушку с отделкой". Наш генератор страниц взял это поле и сделал из него кусок адреса. А сборщик статики (Vike) на каждый адрес создаёт папку на диске. Имя папки в файловой системе не может быть длиннее 255 байт. 295 символов кириллицы после кодирования - это заметно больше. mkdir падает, и вместе с ним падает вся сборка.&lt;/p&gt;

&lt;p&gt;Почему нашли не сразу. Сборка идёт в Docker. Слой со сборкой статики кэшировался, и какое-то время шаг просто пропускался, ошибки не было видно. А локально сборка иногда не доходила до этого места, потому что запрос к API за данными подвисал, и страница со сломанным полем просто не попадала в набор. Ошибка вылезала только на чистой сборке в CI.&lt;/p&gt;

&lt;p&gt;Что чинили. Две вещи.&lt;/p&gt;

&lt;p&gt;Первое - предохранитель по длине прямо в генераторе адресов. Любой сегмент адреса длиннее разумного предела отбрасывается, страница не создаётся:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSafeSegment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;120 символов с запасом меньше лимита файловой системы, а реальные названия районов и станций короче.&lt;/p&gt;

&lt;p&gt;Второе - чистка на источнике. Прятать плохие данные не хотелось, поэтому мусор отфильтровали ещё там, где район берётся из базы: значение, похожее на предложение из описания (слишком длинное, с цифрами и словами вроде "рублей", "квартир", "отделкой"), в подборки не попадает. Заодно это убрало десятки тонких страниц-пустышек, которые такой мусор порождал.&lt;/p&gt;

&lt;p&gt;Что из этого забрать. В статическом сайте любое поле из базы рано или поздно становится частью имени файла. Данные, которые вы не контролируете (со скрейпинга, от пользователей), надо проверять и обрезать до того, как они превратятся в адрес или путь. И одна плохая запись не должна ронять всю сборку - лучше пропустить её, чем упасть целиком.&lt;/p&gt;

&lt;p&gt;Сайт - podbor-minuta.ru, ежедневный мониторинг цен на новостройки Москвы.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>webdev</category>
      <category>seo</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How a login wall hid 600 pages from Google</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Wed, 12 Aug 2026 19:21:21 +0000</pubDate>
      <link>https://dev.to/numart/how-a-login-wall-hid-600-pages-from-google-2h9c</link>
      <guid>https://dev.to/numart/how-a-login-wall-hid-600-pages-from-google-2h9c</guid>
      <description>&lt;p&gt;podbor-minuta.ru has about 600 listing pages for apartments, grouped by metro, district, room count and price. They are built ahead of time into ready HTML so search engines can read them without running JavaScript. Each has its own title, description and a real list of apartments.&lt;/p&gt;

&lt;p&gt;In Google Search Console these pages were marked as indexed. Over 90 days they got zero impressions. Zero. Other sections, building cards and articles, did get impressions.&lt;/p&gt;

&lt;p&gt;I chased the wrong ideas first: thin content? crawl budget on a young domain? duplicates? The titles and descriptions were fine and the pages were in the index. So I looked at what search engines actually receive - the ready HTML.&lt;/p&gt;

&lt;p&gt;I fetched a page and stripped the head and scripts to see only the visible text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://podbor-minuta.ru/novostroyki/metro/domodedovskaya/do-10-mln &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;script.*script&amp;gt;//g'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a list of apartments there was one thing: "Log in with Telegram to use the service." A sign-in form. It went straight into the ready HTML. No page title, no list, just a prompt to sign in.&lt;/p&gt;

&lt;p&gt;The reason is how rendering works. All content is wrapped in an access check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When pages are built ahead of time there is no user and no session. For pages treated as non-public, the check shows the sign-in form. That form is what gets saved to the file. The title and description are prepared separately, so they stayed correct. That is why the pages looked healthy in the index while the search engine read the sign-in form as their content.&lt;/p&gt;

&lt;p&gt;The check itself is a list of public paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATH_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/project/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/discounts/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRouteTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_PATH_PREFIXES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The paths /novostroyki/ and /developer/ were never added. An unknown path is treated as closed, which means the sign-in form.&lt;/p&gt;

&lt;p&gt;The fix is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATH_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/project/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/discounts/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/novostroyki/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/developer/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a rebuild all these pages return the real title, intro and apartment list instead of the sign-in form.&lt;/p&gt;

&lt;p&gt;What I took from this.&lt;/p&gt;

&lt;p&gt;Prebuilt pages inherit your access check. They are built as a guest. What a logged-out visitor sees is what gets frozen into the file, and that is what the search engine reads.&lt;/p&gt;

&lt;p&gt;If a whole section gets zero impressions, it is almost always technical, not content. One page low in results is competition. A whole pattern at zero - look at the ready HTML, not the page in your browser where you are logged in and everything looks fine.&lt;/p&gt;

&lt;p&gt;The title and the body can disagree. A correct title and description can hide a broken body.&lt;/p&gt;

&lt;p&gt;The reliable guard is not the list entry but a build-time check: no prebuilt page should contain the sign-in form. Then this kind of bug cannot ship silently.&lt;/p&gt;

&lt;p&gt;600 pages were invisible to search for months. The fix was two lines. The expensive part was trusting the titles instead of looking at the page.&lt;/p&gt;

&lt;p&gt;The site is podbor-minuta.ru, daily price monitoring for Moscow new builds.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>react</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Как логин-стена спрятала 600 страниц от Google</title>
      <dc:creator>Eugene</dc:creator>
      <pubDate>Wed, 12 Aug 2026 17:49:58 +0000</pubDate>
      <link>https://dev.to/numart/the-auth-gate-that-hid-600-pages-from-google-3g7n</link>
      <guid>https://dev.to/numart/the-auth-gate-that-hid-600-pages-from-google-3g7n</guid>
      <description>&lt;p&gt;У нас на сайте podbor-minuta.ru около 600 страниц-подборок квартир: по метро, району, числу комнат и цене. Они собираются заранее в готовый HTML, чтобы поисковики видели их без запуска JavaScript. У каждой свой заголовок, описание и реальный список квартир.&lt;/p&gt;

&lt;p&gt;В Google Search Console эти страницы числились проиндексированными. За 90 дней они не получили ни одного показа. Ноль. При этом другие разделы, карточки жилых комплексов и статьи, показы получали.&lt;/p&gt;

&lt;p&gt;Сначала я думал не туда: мало контента? маленький бюджет обхода у молодого домена? дубли? Заголовки и описания были в порядке, страницы в индексе. Тогда я посмотрел на то, что на самом деле уходит поисковику - на готовый HTML.&lt;/p&gt;

&lt;p&gt;Я скачал страницу и убрал из неё head и скрипты, чтобы увидеть только видимый текст:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://podbor-minuta.ru/novostroyki/metro/domodedovskaya/do-10-mln &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/&amp;lt;script.*script&amp;gt;//g'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Вместо списка квартир там было одно: "Войдите через Telegram, чтобы пользоваться сервисом". Форма входа. Она попала прямо в готовый HTML. Ни заголовка страницы, ни списка, только предложение авторизоваться.&lt;/p&gt;

&lt;p&gt;Причина в том, как устроен показ. Весь контент оборачивается в проверку доступа:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Когда страницы собираются заранее, пользователя нет и сессии нет. Для страниц, которые считаются не публичными, проверка показывает форму входа. И именно она сохраняется в файл. А заголовок и описание готовятся отдельно, поэтому остались правильными. Из-за этого страницы выглядели здоровыми в индексе, но поисковик читал как их содержимое форму входа.&lt;/p&gt;

&lt;p&gt;Сама проверка - это список публичных путей:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATH_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/project/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/discounts/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRouteTier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_PATH_PREFIXES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Пути /novostroyki/ и /developer/ в список не добавили. Незнакомый путь считается закрытым, а значит показывается форма входа.&lt;/p&gt;

&lt;p&gt;Починка - две строки:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PUBLIC_PATH_PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/project/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/discounts/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/novostroyki/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/developer/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;После пересборки все эти страницы стали отдавать реальный заголовок, вводный текст и список квартир вместо формы входа.&lt;/p&gt;

&lt;p&gt;Что я вынес.&lt;/p&gt;

&lt;p&gt;Готовые страницы наследуют вашу проверку доступа. Их собирают от лица гостя. Что видит незалогиненный посетитель, то и замораживается в файл, и то же читает поисковик.&lt;/p&gt;

&lt;p&gt;Если целый раздел получает ноль показов, это почти всегда техническая причина, а не контент. Одна страница внизу выдачи - это конкуренция. Весь шаблон на нуле - смотрите готовый HTML, а не страницу в браузере, где вы залогинены и всё выглядит нормально.&lt;/p&gt;

&lt;p&gt;Заголовок и тело страницы могут расходиться. Правильные заголовок и описание могут прятать сломанное тело.&lt;/p&gt;

&lt;p&gt;Надёжная защита - не запись в списке, а проверка на сборке: ни одна готовая страница не должна содержать форму входа. Тогда такая ошибка не пройдёт незаметно.&lt;/p&gt;

&lt;p&gt;600 страниц были невидимы для поиска несколько месяцев. Починка заняла две строки. Дорого обошлось то, что я верил заголовкам вместо того, чтобы посмотреть на саму страницу.&lt;/p&gt;

&lt;p&gt;Сайт - podbor-minuta.ru, ежедневный мониторинг цен на новостройки Москвы.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>react</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
