<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>artka.dev — Записки из продакшна</title><description>Заметки про Claude Code, AI-агентов, RAG-пайплайны и production backend от Artyom Kashuta.</description><link>https://artka.dev/</link><language>ru-RU</language><copyright>© 2026 artka.dev</copyright><item><title>12 правил для CLAUDE.md: расширение Karpathy на ошибки 2026 года</title><link>https://artka.dev/blog/claude-md-12-rules/</link><guid isPermaLink="true">https://artka.dev/blog/claude-md-12-rules/</guid><description>Mnilax протестировал 12 правил для CLAUDE.md на 30 кодовых базах за 6 недель — расширение шаблона Karpathy на agent-loops, чекпойнты и fail-loud. Разбор и рамка применения.</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;За четыре месяца после январского треда Karpathy шаблон &lt;code&gt;CLAUDE.md&lt;/code&gt; из 4 правил вырос до 12. Прогнал расширенный набор на типичных задачах своего блога и нескольких рабочих репо — частота молчаливых ошибок Claude Code снижается заметно. Восемь добавленных правил закрывают то, чего в январе ещё не было как класса проблем: long-running agent loops, кросс-сессионные потоки, shallow-тесты, тихие провалы вместо явных ошибок. Открыл собственный &lt;code&gt;CLAUDE.md&lt;/code&gt; этого блога — четыре исходных правила Karpathy там уже есть в &lt;code&gt;Стандарты кода&lt;/code&gt; и &lt;code&gt;Запреты&lt;/code&gt;, восемь добавленных — нет. Разбираю каждое, и где имеет смысл вставить.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. Что случилось за четыре месяца&lt;/h2&gt;
&lt;p&gt;В конце января Andrej Karpathy опубликовал тред с тремя жалобами на Claude как code-writer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;silent wrong assumptions — модель додумывает контекст, не уточняет;&lt;/li&gt;
&lt;li&gt;over-complication — добавляет уровни абстракции, которых никто не просил;&lt;/li&gt;
&lt;li&gt;orthogonal damage — лезет в код, который не должна была трогать.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Forrest Chang упаковал жалобы в &lt;code&gt;CLAUDE.md&lt;/code&gt; из четырёх поведенческих правил и закоммитил на GitHub. Репо разлетелось — на момент мая больше 100 тысяч звёзд, самый быстрорастущий single-file проект года. Дальше шаблон оброс расширением: восемь дополнительных правил, которые покрывают то, что в январе ещё не было фокусом, потому что не было того ландшафта Claude Code, который есть сейчас.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  subgraph jan[&amp;quot;Январь 2026&amp;quot;]
    K[&amp;quot;Karpathy: тред с тремя&amp;lt;br/&amp;gt;failure modes&amp;quot;]
    K --&amp;gt; F[&amp;quot;Forrest Chang:&amp;lt;br/&amp;gt;4 правила CLAUDE.md&amp;quot;]
  end
  subgraph may[&amp;quot;Май 2026&amp;quot;]
    F --&amp;gt; N[&amp;quot;Новые failure modes:&amp;lt;br/&amp;gt;agent loops, multi-codebase,&amp;lt;br/&amp;gt;shallow tests, silent failures&amp;quot;]
    N --&amp;gt; M[&amp;quot;+8 правил,&amp;lt;br/&amp;gt;итого 12&amp;quot;]
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;2. Четыре правила Karpathy&lt;/h2&gt;
&lt;p&gt;Это базис. Без них любая надстройка теряет половину смысла.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Правило&lt;/th&gt;
&lt;th&gt;Что закрывает&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Think Before Coding&lt;/td&gt;
&lt;td&gt;Молчаливые догадки. Озвучивай предположения, спрашивай при неясности, толкайся когда есть проще.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Simplicity First&lt;/td&gt;
&lt;td&gt;Минимум кода, который решает задачу. Никаких спекулятивных абстракций «на будущее».&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Surgical Changes&lt;/td&gt;
&lt;td&gt;Трогай только то, что нужно. Не «улучшай» соседний код, не переформатируй то, о чём не просили.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Goal-Driven Execution&lt;/td&gt;
&lt;td&gt;Описывай критерии успеха, не пошаговую инструкцию. Сильный success-criteria даёт модели итерироваться сам.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;В моём &lt;code&gt;CLAUDE.md&lt;/code&gt; Astro-блога эти четыре закрыты не отдельным разделом, а блоками &lt;code&gt;Стандарты кода → Функциональный стиль&lt;/code&gt; (правило 2 и 3 — никаких классов, никаких лишних абстракций) и &lt;code&gt;Запреты&lt;/code&gt; (правило 3 — список «не делай»). Сами правила не дублируются текстом, но их следствия попадают в контекст.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Где Karpathy-шаблон недотягивает&lt;/h2&gt;
&lt;p&gt;Четыре дыры, которые я наблюдаю в реальной работе:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Дыра&lt;/th&gt;
&lt;th&gt;Что ломается&lt;/th&gt;
&lt;th&gt;Какие добавленные правила закрывают&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Long-running agent tasks&lt;/td&gt;
&lt;td&gt;Multi-step pipeline уходит в дрейф, тратит токены, теряет контекст&lt;/td&gt;
&lt;td&gt;6 (бюджеты), 10 (чекпойнты), 12 (loud)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-codebase consistency&lt;/td&gt;
&lt;td&gt;В монорепо «match existing style» неоднозначно — Claude выбирает случайно или усредняет&lt;/td&gt;
&lt;td&gt;11 (конвенции), 7 (surface conflicts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test quality&lt;/td&gt;
&lt;td&gt;«Тесты прошли» становится самоцелью; Claude пишет тесты, которые не упадут даже на сломанной логике&lt;/td&gt;
&lt;td&gt;9 (intent over behavior)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prototype vs production&lt;/td&gt;
&lt;td&gt;«Simplicity First» переусердствует на ранней стадии, когда нужно 100 строк скаффолдинга для прощупки&lt;/td&gt;
&lt;td&gt;(не покрыто 12 правилами — отдельно)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Последняя дыра остаётся живой. Либо включаешь Simplicity, либо отключаешь — серединного режима у &lt;code&gt;CLAUDE.md&lt;/code&gt; нет.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Восемь добавленных правил&lt;/h2&gt;
&lt;p&gt;По одному, с моментом, который их вызвал.&lt;/p&gt;
&lt;h3&gt;4.1. Rule 5 — Use the model only for judgment calls&lt;/h3&gt;
&lt;p&gt;Если ответ известен из status code или схемы данных — это не работа модели. Реальный кейс из моей практики: код звал Claude, чтобы решить, ретраить ли API-вызов на 503. Две недели работало, потом начало флакать, потому что модель читала тело запроса как контекст для решения. Retry-политика стала случайной, потому что промпт был случайным.&lt;/p&gt;
&lt;p&gt;Рамка: Claude — для классификации, экстракции, драфтов, summarization. Не для роутинга, ретраев, детерминированных трансформаций. Если status code уже отвечает на вопрос — на него отвечает обычный код.&lt;/p&gt;
&lt;h3&gt;4.2. Rule 6 — Token budgets are not advisory&lt;/h3&gt;
&lt;p&gt;Без бюджета цикл уходит в дамп на 50 000 токенов. Жёсткий вариант: 4 000 на задачу, 30 000 на сессию. Подходишь к границе — суммируешь и стартуешь сессию заново.&lt;/p&gt;
&lt;p&gt;Типовой случай: 90-минутная debugging-сессия с одним и тем же 8 КБ-сообщением об ошибке. В финале — повторное предложение фиксов, которые уже отвергал 40 сообщений назад. Модель счастливо итерирует на потерянном треке. Бюджет убил бы цикл на 12-й минуте.&lt;/p&gt;
&lt;h3&gt;4.3. Rule 7 — Surface conflicts, don’t average them&lt;/h3&gt;
&lt;p&gt;Если в кодовой базе два паттерна обработки ошибок — try/catch и global boundary — Claude напишет код, который делает оба. Двойные хендлеры. Симптом: ошибка глотается дважды.&lt;/p&gt;
&lt;p&gt;Правило: при противоречии выбираешь один (более новый или более тестированный), объясняешь почему, второй маркируешь к чистке. Усреднённый код, который удовлетворяет обоим правилам — худший возможный.&lt;/p&gt;
&lt;h3&gt;4.4. Rule 8 — Read before you write&lt;/h3&gt;
&lt;p&gt;Karpathy говорит «не трогай соседний код». Не говорит — прочитай его перед добавлением своего. Реальный случай: Claude добавил функцию рядом с уже существующей идентичной, не прочитав файл. Победил порядок импортов — старая, source-of-truth полгода, проиграла свежей одноимённой.&lt;/p&gt;
&lt;p&gt;Рамка: перед добавлением кода в файл — прочитать экспорты, ближайший вызывающий код и общие утилиты. «Looks orthogonal to me» — самая опасная фраза в кодовой базе.&lt;/p&gt;
&lt;h3&gt;4.5. Rule 9 — Tests verify intent, not just behavior&lt;/h3&gt;
&lt;p&gt;Тест &lt;code&gt;expect(getUserName()).toBe(&apos;John&apos;)&lt;/code&gt; ничего не значит, если функция возвращает константу. Тесты должны падать при изменении бизнес-логики, иначе они тестируют существование функции, не её корректность.&lt;/p&gt;
&lt;p&gt;Типовой пример: 12 тестов на auth-функцию, все зелёные, в проде auth сломан. Тесты проверяли, что функция что-то возвращает, не что она возвращает правильное значение.&lt;/p&gt;
&lt;h3&gt;4.6. Rule 10 — Checkpoint after every significant step&lt;/h3&gt;
&lt;p&gt;Многошаговый рефакторинг по 20 файлам ломается на 4-м шаге, Claude уходит дальше на сломанном состоянии. К моменту, когда замечаешь, шаги 5 и 6 уже сделаны поверх сломанного — распутывание занимает дольше, чем переделка с нуля.&lt;/p&gt;
&lt;p&gt;Правило: после каждого значимого шага — резюме, что сделано, что верифицировано, что осталось. Если потерял трек — остановиться и пересказать.&lt;/p&gt;
&lt;h3&gt;4.7. Rule 11 — Match the codebase’s conventions, even if you disagree&lt;/h3&gt;
&lt;p&gt;Claude вводит хуки в codebase на классовых компонентах. Технически работает. Ломает testing pattern, рассчитанный на &lt;code&gt;componentDidMount&lt;/code&gt;. Полдня на удалить и переписать.&lt;/p&gt;
&lt;p&gt;Правило: внутри кодовой базы конформность важнее вкуса. Несогласие — отдельный разговор, не silent fork. Snake_case против camelCase, классы против хуков — выбираешь то, что есть, не то, что лучше.&lt;/p&gt;
&lt;h3&gt;4.8. Rule 12 — Fail loud&lt;/h3&gt;
&lt;p&gt;Самые дорогие ошибки те, что выглядят как успех. «Миграция выполнена» при тихо пропущенных 14% записей. «Тесты прошли», когда часть была пропущена. «Фича работает», если не проверен edge case, который явно просили проверить.&lt;/p&gt;
&lt;p&gt;Правило: при неуверенности — поднимай вопрос, не прячь. По умолчанию surfacing неопределённости, не скрытие.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Что не работает (то, что отсеялось)&lt;/h2&gt;
&lt;p&gt;Шаблон ценен не только тем, что в нём есть, но и тем, что отсеяно при попытках расширить:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Правила с Reddit и X.&lt;/strong&gt; Большинство — переформулировки Karpathy либо domain-specific («всегда Tailwind»). Не обобщаются.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Больше 12 правил.&lt;/strong&gt; На наборах из 14+ правил compliance падает: важные пункты тонут в шуме. Потолок в 200 строк (включая стек, команды, запреты) реален.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Правила, привязанные к инструментам.&lt;/strong&gt; «Always use eslint» падает молча, если eslint не установлен. Правильнее — capability-agnostic: «match the enforced style».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Примеры вместо правил.&lt;/strong&gt; Один пример съедает контекст ~10 правил, и модель over-fits на specifics. Правила абстрактны и переносимы.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Soft language.&lt;/strong&gt; «Be careful», «think hard», «really focus» — compliance ~30%. Не testable. Заменяю на конкретные императивы: «state assumptions explicitly».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identity prompts.&lt;/strong&gt; «Be a senior engineer» не работает: модель и так считает себя сениором. Зазор между «считать» и «делать» закрывают императивы, не identity.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. Сверка со своим &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Открыл файл этого блога (191 строка) и прошёлся по 12 правилам. Картина:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Правило&lt;/th&gt;
&lt;th&gt;В моём &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;Где&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Think before coding&lt;/td&gt;
&lt;td&gt;косвенно&lt;/td&gt;
&lt;td&gt;через &lt;code&gt;architect → critic&lt;/code&gt; workflow в команде агентов&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Simplicity&lt;/td&gt;
&lt;td&gt;да&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Никаких классов&lt;/code&gt;, &lt;code&gt;Иммутабельность по умолчанию&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Surgical changes&lt;/td&gt;
&lt;td&gt;да&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Запреты&lt;/code&gt; (deprecated &lt;code&gt;@astrojs/tailwind&lt;/code&gt;, &lt;code&gt;node:*-alpine&lt;/code&gt; и т.п.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Goal-driven&lt;/td&gt;
&lt;td&gt;косвенно&lt;/td&gt;
&lt;td&gt;через subagent-структуру, не отдельным правилом&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Judgment-only&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Token budgets&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Surface conflicts&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Read before write&lt;/td&gt;
&lt;td&gt;частично&lt;/td&gt;
&lt;td&gt;GitNexus-секция требует impact analysis перед редактом&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9. Test intent&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10. Checkpoints&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11. Match conventions&lt;/td&gt;
&lt;td&gt;да&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Стандарты кода → TypeScript / Astro / Git&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12. Fail loud&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Получилось — четыре покрыто, два частично, шесть нет. Файл фактически Karpathy-уровня, без надстройки на 2026 год.&lt;/p&gt;
&lt;p&gt;Какие из недостающих имеет смысл добавить именно для Astro-блога с публикациями через админку:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rule 6 (бюджеты)&lt;/strong&gt; — да, у меня агенты делают long-running задачи (генерация EN-переводов через &lt;code&gt;pnpm translate&lt;/code&gt;, миграции). Без бюджета сессия может уйти в дрейф.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 9 (test intent)&lt;/strong&gt; — да, есть Vitest и Playwright, риск shallow-тестов реальный.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 10 (checkpoints)&lt;/strong&gt; — да, многошаговые задачи на схему БД + миграции + UI-апдейты регулярно занимают по полчаса работы агента.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rule 12 (fail loud)&lt;/strong&gt; — да, в админке часто «сохранилось» != «опубликовалось», нужно явное surfacing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rule 7 для одиночного проекта менее острая. Rule 5 покрывается тем, что в рантайме блога нет AI-роутинга — модель не принимает решений за код.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Как добавить — без раздувания&lt;/h2&gt;
&lt;p&gt;Дисциплина:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Не превышать 200 строк всего.&lt;/strong&gt; Считая стек, команды, запреты, правила. У меня сейчас 191 — добавление четырёх правил означает вынос части &lt;code&gt;Главная страница&lt;/code&gt; или GitNexus-секции в &lt;code&gt;@docs/...&lt;/code&gt; через @-импорт Claude Code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Каждое правило отвечает на вопрос «какую ошибку оно предотвращает».&lt;/strong&gt; Если не отвечает — выкидываешь.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capability-agnostic формулировки.&lt;/strong&gt; «Match the enforced style», не «use prettier».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Императивы, не пожелания.&lt;/strong&gt; «State assumptions explicitly», не «think carefully».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Тестируешь.&lt;/strong&gt; Прогоняешь типичную задачу до и после. Нет разницы — правило не сработало в твоём контексте, удаляешь.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Шесть правил, заточенных под реальные ошибки, сильнее двенадцати общих.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Итог&lt;/h2&gt;
&lt;p&gt;Karpathy зафиксировал три code-writing failure modes января. Forrest Chang упаковал их в четыре правила, и сообщество схватило шаблон. Расширение до 12 родилось из того, что в мае ландшафт Claude Code стал другим: multi-step агенты, hook-каскады, skill-конфликты, кросс-сессионные потоки. Восемь добавленных правил закрывают новые дыры, не замещая исходные.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; — не wishlist, а behavioral contract против конкретных ошибок, которые ты сам уже видел. Чужой шаблон полезен как стартер. Дальше — фильтруешь под свои failure modes, не наоборот. Шесть правил, точно подобранных, лучше двенадцати скопированных.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Источники:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/karpathy/status/1885018475234567890&quot;&gt;Andrej Karpathy — оригинальный тред в X (январь 2026)&lt;/a&gt; — три code-writing failure modes&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/forrestchang/andrej-karpathy-skills&quot;&gt;forrestchang/andrej-karpathy-skills&lt;/a&gt; — публичный репо с базовым 4-правило шаблоном&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/&quot;&gt;Anthropic Claude Code docs — CLAUDE.md&lt;/a&gt; — официальная документация по структуре файла, advisory, ~80% compliance&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai</category><category>claude-code</category><category>prompt-engineering</category><author>a@artka.dev (Артём)</author></item><item><title>ds4 от antirez: локальный coding agent на DeepSeek V4 Flash, который работает на MacBook</title><link>https://artka.dev/blog/local-coding-agent/</link><guid isPermaLink="true">https://artka.dev/blog/local-coding-agent/</guid><description>Создатель Redis за две недели написал инференс-движок только для одной модели — DeepSeek V4 Flash. 1M контекст, 26 t/s на M3 Max, KV-кэш на диске. Как это запустить и подключить к Claude Code.</description><pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Garry Tan и Bindu Reddy 9 мая 2026 одновременно расшарили одну и ту же новость: создатель Redis Salvatore Sanfilippo (antirez) выложил &lt;a href=&quot;https://github.com/antirez/ds4&quot;&gt;&lt;code&gt;ds4&lt;/code&gt;&lt;/a&gt; — инференс-движок на C+Metal, который запускает DeepSeek V4 Flash (284B MoE, 1M контекста) на ноутбуке. Не «технически возможно», а «работает с coding-агентами на 26 t/s». Я разобрался, что под капотом, и как использовать это как локальный backend для Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. Что произошло за две недели&lt;/h2&gt;
&lt;p&gt;24 апреля 2026 DeepSeek выпустил серию V4. V4 Flash — efficiency-модель: 284 миллиарда параметров суммарно, 13 миллиардов активных (MoE), контекст 1 миллион токенов. Раньше модели такого размера жили только в облаке.&lt;/p&gt;
&lt;p&gt;Antirez посмотрел на это и сделал ставку, которую универсальные раннеры сделать не могут. Он форкнул &lt;code&gt;llama.cpp&lt;/code&gt;, две недели возился внутри него, понял геометрию V4 Flash, &lt;strong&gt;выкинул всё лишнее&lt;/strong&gt; и написал с нуля движок на 4 файлах: &lt;code&gt;ds4.c&lt;/code&gt; (~ инференс), &lt;code&gt;ds4_metal.m&lt;/code&gt; (Metal kernels), &lt;code&gt;ds4_server.c&lt;/code&gt; (HTTP-сервер), &lt;code&gt;ds4_cli.c&lt;/code&gt; (REPL). Снаружи всё это говорит на двух протоколах одновременно: OpenAI Chat Completions (&lt;code&gt;/v1/chat/completions&lt;/code&gt;) и Anthropic Messages (&lt;code&gt;/v1/messages&lt;/code&gt;). То есть подключается к любому агенту, который умеет один из них.&lt;/p&gt;
&lt;p&gt;Результаты, которые автор замерил сам:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Машина&lt;/th&gt;
&lt;th&gt;Квант&lt;/th&gt;
&lt;th&gt;Промпт&lt;/th&gt;
&lt;th&gt;Prefill&lt;/th&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro M3 Max, 128 GB&lt;/td&gt;
&lt;td&gt;q2&lt;/td&gt;
&lt;td&gt;короткий&lt;/td&gt;
&lt;td&gt;58.52 t/s&lt;/td&gt;
&lt;td&gt;26.68 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro M3 Max, 128 GB&lt;/td&gt;
&lt;td&gt;q2&lt;/td&gt;
&lt;td&gt;11709 токенов&lt;/td&gt;
&lt;td&gt;250.11 t/s&lt;/td&gt;
&lt;td&gt;21.47 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac Studio M3 Ultra, 512 GB&lt;/td&gt;
&lt;td&gt;q2&lt;/td&gt;
&lt;td&gt;короткий&lt;/td&gt;
&lt;td&gt;84.43 t/s&lt;/td&gt;
&lt;td&gt;36.86 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac Studio M3 Ultra, 512 GB&lt;/td&gt;
&lt;td&gt;q4&lt;/td&gt;
&lt;td&gt;12018 токенов&lt;/td&gt;
&lt;td&gt;448.82 t/s&lt;/td&gt;
&lt;td&gt;26.62 t/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;26 токенов в секунду генерации — это не «можно посмотреть», это &lt;strong&gt;рабочая скорость для coding-агента&lt;/strong&gt;, который пишет, читает файлы, вызывает инструменты. На длинном промпте генерация падает до 21 t/s, но за счёт KV-кэша на диске это окупается уже на третьем запросе той же сессии.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Три инженерных трюка, которые делают это возможным&lt;/h2&gt;
&lt;p&gt;Я внимательно прочитал README и &lt;code&gt;AGENT.md&lt;/code&gt; репозитория, и ниже — самое существенное, без чего ds4 не работал бы.&lt;/p&gt;
&lt;h3&gt;2.1. Асимметричное 2-битное квантование&lt;/h3&gt;
&lt;p&gt;Стандартный подход к 2-битному кванту — давить всё подряд до 2 бит, и тогда модель начинает галлюцинировать в tool calling, путать аргументы и забывать схему. Antirez сделал иначе: &lt;strong&gt;квантованы только MoE-эксперты на routed-пути&lt;/strong&gt; (&lt;code&gt;up&lt;/code&gt;/&lt;code&gt;gate&lt;/code&gt; в &lt;code&gt;IQ2_XXS&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt; в &lt;code&gt;Q2_K&lt;/code&gt;) — потому что они занимают большую часть веса (модель — 284B, и почти всё это — эксперты). Shared-эксперты, проекции, роутинг — остаются в Q8. Это компоненты, в которых потеря точности дорого стоит.&lt;/p&gt;
&lt;p&gt;Эффект: 2-битный квант весит 81 GB и помещается в 128 GB унифицированной памяти MacBook Pro M3 Max, при этом надёжно работает в coding-агентах (что валидируется тестами против официальных логитов API DeepSeek).&lt;/p&gt;
&lt;h3&gt;2.2. KV-кэш как first-class disk citizen&lt;/h3&gt;
&lt;p&gt;Главная боль stateless API-протоколов вроде Chat Completions: клиент &lt;strong&gt;каждый раз присылает всю историю&lt;/strong&gt;, и сервер обязан пре-фильнуть её с нуля. Claude Code, например, на старте шлёт ~25K токенов системного промпта. На локальном железе это десятки секунд до первого токена.&lt;/p&gt;
&lt;p&gt;Ds4 решает это лобово: после успешного prefill стейт сессии (KV-чекпоинт) сериализуется в файл, ключ — SHA1 от token IDs. Когда приходит следующий запрос с тем же префиксом, сервер берёт чекпоинт с диска и пропускает prefill. Из README:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The KV cache &lt;strong&gt;is actually a first class disk citizen&lt;/strong&gt;. &amp;lt;…&amp;gt; Modern MacBooks have fast SSDs and compressed KV caches like the one of DeepSeek v4.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;На практике это означает разницу между «4 секунды до первого токена при повторном вызове» и «60 секунд». Диск тут — не своп под давлением, а логичное хранилище: SSD достаточно быстрые, KV у DeepSeek V4 хорошо сжимается, а характеристика «один и тот же системный промпт + меняющийся хвост» точно описывает работу coding-агента.&lt;/p&gt;
&lt;h3&gt;2.3. Metal-only и одна модель за раз&lt;/h3&gt;
&lt;p&gt;Никакого CUDA, никакого CPU-фоллбэка для прода (CPU-путь существует только для correctness-чеков и сейчас падает на уровне ядра macOS из-за бага в VM — antirez об этом честно пишет). Никакой попытки сделать «универсальный раннер». Только Apple Silicon, только эта одна модель, и так до тех пор, пока не появится новая версия V4 Flash или сильно лучшая модель того же класса.&lt;/p&gt;
&lt;p&gt;Цена — narrow bet. Выгода — тебе не нужно поддерживать матрицу &lt;code&gt;(модель × железо × квант)&lt;/code&gt;, и ты можешь оптимизировать Metal-ядра под точную геометрию слоёв этой конкретной модели.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Что мне понадобится: железо, модель, час времени&lt;/h2&gt;
&lt;p&gt;Я планирую разворачивать это на &lt;strong&gt;MacBook Pro M3 Max, 128 GB&lt;/strong&gt; (минимально жизнеспособная конфигурация по README). У меня его пока нет, и в этом разделе — честный план, что я буду делать, когда железо приедет; цифры взяты из бенчмарков antirez’а, но я хочу их перепроверить на своём экземпляре.&lt;/p&gt;
&lt;p&gt;Минимальные требования по моим прикидкам:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS на актуальной версии (там же баг VM в CPU-пути, но Metal-путь не задет).&lt;/li&gt;
&lt;li&gt;Apple Silicon с 128 GB+ унифицированной памяти. M3 Max или M3 Ultra.&lt;/li&gt;
&lt;li&gt;~100 GB свободного места: 81 GB сама модель Q2 + место под KV-кэш на диске. Под Q4-квант — 256 GB+ RAM и ~150 GB на диске.&lt;/li&gt;
&lt;li&gt;Xcode Command Line Tools (для clang/Metal headers).&lt;/li&gt;
&lt;li&gt;~30–60 минут на скачивание модели (зависит от канала).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;То, чего может не хватить начинающим: 128 GB unified memory — это уровень MBP M3 Max в топовой комплектации или Mac Studio. На 64-гиговом Mac Q2 не заработает: модель просто не влезет в RAM. Это не «медленно», это «никак».&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Установка пошагово&lt;/h2&gt;
&lt;p&gt;Команды ниже — то, что я сделаю в первый же день, опираясь на инструкции README. Где описание скучает за конкретикой — я добавил собственные комментарии.&lt;/p&gt;
&lt;h3&gt;4.1. Сборка&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Склонировать репозиторий
git clone https://github.com/antirez/ds4.git
cd ds4

# 2. Скачать 2-битный квант (81 GB; для 128 GB MBP)
./download_model.sh q2

# Скрипт качает с huggingface.co/antirez/deepseek-v4-gguf,
# поддерживает резюм через curl -C - — можно прервать и продолжить.
# Если нужен 4-битный квант (для Mac Studio 256+ GB), используй ./download_model.sh q4.

# 3. Собрать
make

# Проверить, что собралось:
./ds4 --help
./ds4-server --help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Сборка — обычный &lt;code&gt;make&lt;/code&gt;, никаких CMake, никаких pkg-config. Это намеренно: зависимостей за пределами Apple SDK у проекта нет.&lt;/p&gt;
&lt;h3&gt;4.2. Первый запуск в REPL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./ds4 -p &amp;quot;Объясни Redis streams в одном абзаце.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Без &lt;code&gt;-p&lt;/code&gt; запускается интерактивная сессия с командами &lt;code&gt;/help&lt;/code&gt;, &lt;code&gt;/think&lt;/code&gt;, &lt;code&gt;/think-max&lt;/code&gt;, &lt;code&gt;/nothink&lt;/code&gt;, &lt;code&gt;/ctx N&lt;/code&gt;, &lt;code&gt;/read FILE&lt;/code&gt;, &lt;code&gt;/quit&lt;/code&gt;. Это хорошо для проверки, что движок жив, и для сравнения скорости генерации против заявленных 26 t/s.&lt;/p&gt;
&lt;h3&gt;4.3. Запуск как HTTP-сервер&lt;/h3&gt;
&lt;p&gt;Это режим, в котором ds4 становится локальным backend’ом для агентов:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./ds4-server \
  --ctx 100000 \
  --kv-disk-dir /tmp/ds4-kv \
  --kv-disk-space-mb 8192
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Параметры:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--ctx 100000&lt;/code&gt; — контекстное окно в 100K токенов. Полный 1M-контекст ест ~26 GB только на индексер; на 128 GB Mac, где 81 GB уже занято моделью, это не оставит места для KV-кэша. 100–300K — разумный компромисс.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--kv-disk-dir /tmp/ds4-kv&lt;/code&gt; — каталог для disk KV-кэша. Я бы вынес его на быстрый SSD (внешний или встроенный — оба ок).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--kv-disk-space-mb 8192&lt;/code&gt; — лимит на размер кэша. 8 GB для одного-двух активных проектов хватит; для сессий побольше — увеличивай.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Сервер слушает &lt;code&gt;127.0.0.1:8000&lt;/code&gt;. Эндпоинты:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Протокол&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/chat/completions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI Chat Completions (+ tools)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/completions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI legacy completions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /v1/messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic Messages (для Claude Code)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /v1/models&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;список моделей&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Аутентификация по статичному API-ключу (по умолчанию принимается любой; в README рекомендуется &lt;code&gt;dsv4-local&lt;/code&gt;).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5. Подключение как coding agent&lt;/h2&gt;
&lt;p&gt;Это та часть, ради которой я вообще копал тему. Все три приведённых ниже способа работают одновременно — каждый агент ходит в один и тот же &lt;code&gt;ds4-server&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;5.1. Claude Code → Anthropic-совместимый эндпоинт&lt;/h3&gt;
&lt;p&gt;Claude Code умеет говорить с любым backend’ом, который выставляет Anthropic Messages API. Создаём обёртку &lt;code&gt;~/bin/claude-ds4&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/sh
unset ANTHROPIC_API_KEY

export ANTHROPIC_BASE_URL=&amp;quot;${DS4_ANTHROPIC_BASE_URL:-http://127.0.0.1:8000}&amp;quot;
export ANTHROPIC_AUTH_TOKEN=&amp;quot;${DS4_API_KEY:-dsv4-local}&amp;quot;
export ANTHROPIC_MODEL=&amp;quot;deepseek-v4-flash&amp;quot;

# Подменяем все алиасы Sonnet/Haiku/Opus на локальную модель —
# чтобы /model в Claude Code не дёрнул облачный fallback.
export ANTHROPIC_DEFAULT_SONNET_MODEL=&amp;quot;deepseek-v4-flash&amp;quot;
export ANTHROPIC_DEFAULT_HAIKU_MODEL=&amp;quot;deepseek-v4-flash&amp;quot;
export ANTHROPIC_DEFAULT_OPUS_MODEL=&amp;quot;deepseek-v4-flash&amp;quot;
export CLAUDE_CODE_SUBAGENT_MODEL=&amp;quot;deepseek-v4-flash&amp;quot;

# Отключаем телеметрию и не-стриминговый fallback.
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK=1
export CLAUDE_STREAM_IDLE_TIMEOUT_MS=600000

exec &amp;quot;$HOME/.local/bin/claude&amp;quot; &amp;quot;$@&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;chmod +x ~/bin/claude-ds4&lt;/code&gt; — и запускаешь Claude Code как &lt;code&gt;claude-ds4&lt;/code&gt; вместо &lt;code&gt;claude&lt;/code&gt;. Все запросы пойдут на локальный ds4-сервер. Тонкость, на которую обращает внимание сам antirez:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude Code may send a large initial prompt, often around 25k tokens, before it starts doing useful work. Keep &lt;code&gt;--kv-disk-dir&lt;/code&gt; enabled.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Без disk KV-кэша запуск Claude Code на холодную будет занимать минуту и больше; с кэшем — после первого старта последующие будут восстанавливаться с диска.&lt;/p&gt;
&lt;h3&gt;5.2. opencode&lt;/h3&gt;
&lt;p&gt;opencode конфигурируется через &lt;code&gt;~/.config/opencode/opencode.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;$schema&amp;quot;: &amp;quot;https://opencode.ai/config.json&amp;quot;,
  &amp;quot;provider&amp;quot;: {
    &amp;quot;ds4&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;ds4.c (local)&amp;quot;,
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;options&amp;quot;: {
        &amp;quot;baseURL&amp;quot;: &amp;quot;http://127.0.0.1:8000/v1&amp;quot;,
        &amp;quot;apiKey&amp;quot;: &amp;quot;dsv4-local&amp;quot;
      },
      &amp;quot;models&amp;quot;: {
        &amp;quot;deepseek-v4-flash&amp;quot;: {
          &amp;quot;name&amp;quot;: &amp;quot;DeepSeek V4 Flash (ds4.c local)&amp;quot;,
          &amp;quot;limit&amp;quot;: { &amp;quot;context&amp;quot;: 100000, &amp;quot;output&amp;quot;: 384000 }
        }
      }
    }
  },
  &amp;quot;agent&amp;quot;: {
    &amp;quot;ds4&amp;quot;: {
      &amp;quot;description&amp;quot;: &amp;quot;DeepSeek V4 Flash served by local ds4-server&amp;quot;,
      &amp;quot;model&amp;quot;: &amp;quot;ds4/deepseek-v4-flash&amp;quot;,
      &amp;quot;temperature&amp;quot;: 0
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;limit.context: 100000&lt;/code&gt; обязательно совпадает с &lt;code&gt;--ctx&lt;/code&gt;, с которым стартует &lt;code&gt;ds4-server&lt;/code&gt; — иначе сервер обрежет, а opencode об этом не узнает и пошлёт следующее сообщение, ожидая нерабочую длину.&lt;/p&gt;
&lt;h3&gt;5.3. Pi (мини-агент antirez’а)&lt;/h3&gt;
&lt;p&gt;Если используешь Pi — формат немного другой, конфиг в &lt;code&gt;~/.pi/agent/models.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;providers&amp;quot;: {
    &amp;quot;ds4&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;ds4.c local&amp;quot;,
      &amp;quot;baseUrl&amp;quot;: &amp;quot;http://127.0.0.1:8000/v1&amp;quot;,
      &amp;quot;api&amp;quot;: &amp;quot;openai-completions&amp;quot;,
      &amp;quot;apiKey&amp;quot;: &amp;quot;dsv4-local&amp;quot;,
      &amp;quot;compat&amp;quot;: {
        &amp;quot;supportsStore&amp;quot;: false,
        &amp;quot;supportsDeveloperRole&amp;quot;: false,
        &amp;quot;supportsReasoningEffort&amp;quot;: true,
        &amp;quot;supportsUsageInStreaming&amp;quot;: true,
        &amp;quot;maxTokensField&amp;quot;: &amp;quot;max_tokens&amp;quot;,
        &amp;quot;thinkingFormat&amp;quot;: &amp;quot;deepseek&amp;quot;,
        &amp;quot;requiresReasoningContentOnAssistantMessages&amp;quot;: true
      },
      &amp;quot;models&amp;quot;: [
        {
          &amp;quot;id&amp;quot;: &amp;quot;deepseek-v4-flash&amp;quot;,
          &amp;quot;name&amp;quot;: &amp;quot;DeepSeek V4 Flash (ds4.c local)&amp;quot;,
          &amp;quot;reasoning&amp;quot;: true,
          &amp;quot;contextWindow&amp;quot;: 100000,
          &amp;quot;maxTokens&amp;quot;: 384000,
          &amp;quot;cost&amp;quot;: { &amp;quot;input&amp;quot;: 0, &amp;quot;output&amp;quot;: 0, &amp;quot;cacheRead&amp;quot;: 0, &amp;quot;cacheWrite&amp;quot;: 0 }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cost: 0&lt;/code&gt; — это не маркетинг, это правда. Каждый запрос обходится в электричество и износ SSD, не в токены.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Где это сломается (важные грабли)&lt;/h2&gt;
&lt;p&gt;Реальные ограничения, на которые я наткнусь, и то, как их обходить.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Окно контекста должно быть согласовано везде.&lt;/strong&gt; Стартуешь сервер с &lt;code&gt;--ctx 100000&lt;/code&gt;, ставишь в opencode &lt;code&gt;limit.context: 100000&lt;/code&gt;, в Claude Code не лезешь в системный промпт сверх этого. Если у Claude Code init-prompt ~25K, то на проект остаётся 75K — реально хватает на средний codebase, но не на огромные репозитории.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disk KV-кэш «привязан» к точному префиксу.&lt;/strong&gt; Любая правка в системном промпте, в &lt;code&gt;CLAUDE.md&lt;/code&gt;, в первых сообщениях — инвалидирует чекпоинт. Это не баг, это by design: матчинг идёт по SHA1 от token IDs. Если ты часто редактируешь &lt;code&gt;CLAUDE.md&lt;/code&gt;, ожидай холодные старты. Решение — закоммитить системный контракт и не править его в каждой сессии.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MTP/спекулятивное декодирование пока не даёт большого выигрыша.&lt;/strong&gt; В README прямо написано: «currently provides at most a slight speedup». Не закладывайся на удвоение скорости от MTP — текущая реализация correctness-gated и на сложных промптах часто триггерит партиал-аксепт.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Один live KV-кэш в памяти.&lt;/strong&gt; Сервер сейчас не батчит независимые запросы. Если два агента ходят одновременно — второй ждёт первого. Это нормальный trade-off для локального single-user setup, но если ты хочешь параллельный multi-tenancy на одном Mac — ds4 пока не для этого.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CPU-режим падает на свежих macOS.&lt;/strong&gt; Это про debug-путь, не про прод (Metal-only — основной таргет), но если по привычке захочешь сравнить инференс на CPU — не делай этого: kernel-panic, надо ребутиться.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Что это значит: vertical inference engines как тренд&lt;/h2&gt;
&lt;p&gt;Главное — не ds4 сам по себе, а паттерн, который antirez формализовал.&lt;/p&gt;
&lt;p&gt;Локальный inference сейчас выглядит как «универсальный раннер &lt;code&gt;+&lt;/code&gt; тысяча моделей в GGUF &lt;code&gt;+&lt;/code&gt; обёртки разной свежести». Это работает, но движётся со скоростью наименее популярной модели: ускорять Llama 3.1 в llama.cpp проще, чем добавить эффективную поддержку DeepSeek V4 — потому что в первом случае структура слоёв совпадает с двадцатью другими моделями, а во втором — appears once.&lt;/p&gt;
&lt;p&gt;Antirez показывает противоположный путь. &lt;strong&gt;Один движок — одна модель — один сценарий (coding agent)&lt;/strong&gt;. Дальше нужно три вещи, и все три — в продукте:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inference engine с HTTP API.&lt;/li&gt;
&lt;li&gt;GGUF, специально подготовленный под этот движок и его допущения.&lt;/li&gt;
&lt;li&gt;Тесты и валидация на сцепке с конкретными агент-клиентами.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Если эта ставка работает (и бенчмарки говорят, что да), будущее локального inference — не «ещё одна абстракция поверх абстракции», а &lt;strong&gt;«у каждой важной модели — свой ds4-подобный проект»&lt;/strong&gt;. Когда выходит V4.1 или V5, кто-то из community делает новый движок, новый GGUF, новые тесты, и через две недели у пользователей уже работающая локальная установка. Старые движки уходят на покой вместе со старыми моделями.&lt;/p&gt;
&lt;p&gt;И второе. В README antirez явно пишет:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This software is developed with strong assistance from GPT 5.5 and with humans leading the ideas, testing, and debugging.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Две недели от форка &lt;code&gt;llama.cpp&lt;/code&gt; до production-ready узкого движка с серверным API — без AI это не сделать, и antirez это прямо говорит. Вот это переключение — «один человек + AI = инфраструктура для целой модели за две недели» — на мой взгляд интереснее, чем сами цифры t/s.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Итог&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ds4&lt;/code&gt; от antirez — это не «ещё один локальный инференс». Это узкая ставка: один движок, одна модель (DeepSeek V4 Flash), одна архитектура железа (Apple Silicon с Metal), один сценарий (coding agent). За счёт асимметричного 2-битного кванта 284B-модель влезает в 128 GB MacBook, за счёт disk KV-кэша работает с агентами, которые гоняют 25K-токенные системные промпты, за счёт OpenAI/Anthropic-совместимости подключается к Claude Code, opencode и Pi из коробки.&lt;/p&gt;
&lt;p&gt;Если у вас есть Mac с 128 GB+ — это рабочий локальный backend для серьёзной коммерческой работы с приватным кодом. Если нет — ждать DDR5 и unified memory на Linux/CUDA, или смотреть, кто следующий повторит этот паттерн под свою связку «модель + железо».&lt;/p&gt;
&lt;p&gt;В любом случае стоит наблюдать. Я ставлю на то, что через год так будут собирать половину серьёзных локальных установок.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Источники:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/antirez/ds4&quot;&gt;github.com/antirez/ds4&lt;/a&gt; — README, бенчмарки, конфиги&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/garrytan/status/2052996691586932783&quot;&gt;Garry Tan — пост в X (9 мая 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://x.com/bindureddy/status/2052982206344409242&quot;&gt;Bindu Reddy — пост в X (9 мая 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eu.36kr.com/en/p/3800327282662656&quot;&gt;QbitAI / 36kr: Redis Father Steps In to Build Dedicated Inference Engine for DeepSeek V4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=48050751&quot;&gt;HN: DeepSeek 4 Flash local inference engine for Metal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://huggingface.co/antirez/deepseek-v4-gguf&quot;&gt;huggingface.co/antirez/deepseek-v4-gguf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai</category><category>local-inference</category><category>coding-agents</category><category>deepseek</category><category>apple-silicon</category><author>a@artka.dev (Артём)</author></item><item><title>JSON-LD @graph в Astro: от дублирующихся inline-блоков к единому citable-узлу</title><link>https://artka.dev/blog/json-ld-graph-astro/</link><guid isPermaLink="true">https://artka.dev/blog/json-ld-graph-astro/</guid><description>Пошаговый разбор миграции с per-page Schema.org-блоков на единый @graph в BaseLayout: стабильные @id, ссылки между сущностями, articleBody-excerpt и FAQ.</description><pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Большинство руководств по &lt;a href=&quot;http://Schema.org&quot;&gt;Schema.org&lt;/a&gt; для блогов учат: на посте — &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; с &lt;code&gt;BlogPosting&lt;/code&gt;, на главной — с &lt;code&gt;WebSite&lt;/code&gt;, на about — с &lt;code&gt;Person&lt;/code&gt;. Это работает, но проигрывает в citability. Краулер видит &lt;code&gt;Person&lt;/code&gt; из &lt;code&gt;BlogPosting.author&lt;/code&gt; как «кто-то по имени X», а не как entity, который ещё и &lt;code&gt;founder of #organization&lt;/code&gt;, который &lt;code&gt;publisher of #blog&lt;/code&gt;. В посте — пошаговый разбор, как заменить per-page inline-блоки одним &lt;code&gt;@graph&lt;/code&gt; в &lt;code&gt;BaseLayout&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. Зачем менять — citability вs SERP&lt;/h2&gt;
&lt;p&gt;Структурированные данные у разработчика-блогера обычно ассоциируются с одним вопросом: «появится ли мой пост в Google с rich snippet?». Под эту задачу хватает любого валидного &lt;code&gt;BlogPosting&lt;/code&gt; — пройдёт Rich Results Test, появятся stars/breadcrumb. И этим часто всё кончается: добавили &lt;code&gt;@type: BlogPosting&lt;/code&gt;, проверили в валидаторе, забыли.&lt;/p&gt;
&lt;p&gt;В 2026 году у структурированных данных появился новый, более требовательный потребитель — &lt;strong&gt;LLM-краулер&lt;/strong&gt;, который собирает контент для retrieval-augmented generation и для citation. Ему нужен не «ещё один rich snippet», а &lt;strong&gt;связный entity-граф&lt;/strong&gt;: чтобы при упоминании автора в одном посте он опознал того же автора в другом, чтобы организация-publisher была одним и тем же объектом на всём сайте, чтобы блог как сущность ссылался обратно на автора.&lt;/p&gt;
&lt;p&gt;LLM, выдающий цитату, делает примерно следующее: вытаскивает passage, проверяет окружающую entity-разметку, пытается сопоставить автора с известной сущностью. Если на сайте &lt;code&gt;Person.name = &amp;quot;Артём Кашута&amp;quot;&lt;/code&gt; встречается в трёх разных Schema.org-блоках без общего &lt;code&gt;@id&lt;/code&gt;, краулер обязан догадываться, один это человек или три. Если же есть один &lt;code&gt;Person#person&lt;/code&gt; со стабильным URI, и все остальные узлы (&lt;code&gt;Organization.founder&lt;/code&gt;, &lt;code&gt;BlogPosting.author&lt;/code&gt;, &lt;code&gt;Blog.author&lt;/code&gt;) ссылаются на него через &lt;code&gt;{&amp;quot;@id&amp;quot;: &amp;quot;...&amp;quot;}&lt;/code&gt; — догадки не нужны, граф собран автором.&lt;/p&gt;
&lt;p&gt;Это проблема, которую keyword density не решает. Это &lt;strong&gt;entity disambiguation&lt;/strong&gt;, и решается она &lt;strong&gt;graph topology&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Аспект&lt;/th&gt;
&lt;th&gt;Per-page inline blocks&lt;/th&gt;
&lt;th&gt;Single &lt;code&gt;@graph&lt;/code&gt; с &lt;code&gt;@id&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Rich Results&lt;/td&gt;
&lt;td&gt;работает&lt;/td&gt;
&lt;td&gt;работает&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM entity match (Person)&lt;/td&gt;
&lt;td&gt;догадка по имени&lt;/td&gt;
&lt;td&gt;гарантирована через &lt;code&gt;@id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Дублирование данных&lt;/td&gt;
&lt;td&gt;3-5 копий &lt;code&gt;Person&lt;/code&gt; на 14 постах&lt;/td&gt;
&lt;td&gt;один источник на сайт&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Стоимость правки автора&lt;/td&gt;
&lt;td&gt;14 файлов&lt;/td&gt;
&lt;td&gt;1 файл (&lt;code&gt;person.ts&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML weight&lt;/td&gt;
&lt;td&gt;3+ скрипта на страницу&lt;/td&gt;
&lt;td&gt;1 скрипт&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Для эпохи SERP-only хватало первого подхода. Для эпохи AI-overviews, citation graphs и retrieval-augmented поиска — нужно второе. Spec нашего блога формулирует это прямо: «move all entity definitions into &lt;code&gt;src/lib/seo/schema.ts&lt;/code&gt; returning a single &lt;code&gt;@graph&lt;/code&gt; JSON-LD block; pages contribute a &lt;code&gt;BlogPosting&lt;/code&gt;/&lt;code&gt;WebPage&lt;/code&gt; node referencing the global &lt;code&gt;Person#me&lt;/code&gt; and &lt;code&gt;Organization#brand&lt;/code&gt; by &lt;code&gt;@id&lt;/code&gt;» — см. &lt;code&gt;docs/superpowers/specs/2026-05-02-llm-citable-blog-design.md&lt;/code&gt; § «Schema-graph design».&lt;/p&gt;
&lt;h2&gt;2. Антипаттерн: per-page inline schema&lt;/h2&gt;
&lt;p&gt;Что эмитит дефолтный Astro-блог, собранный по тутору с какого-нибудь &lt;a href=&quot;http://dev.to&quot;&gt;dev.to&lt;/a&gt;? Обычно так:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;В &lt;code&gt;BaseLayout.astro&lt;/code&gt; лежит inline-скрипт с &lt;code&gt;WebSite&lt;/code&gt; и иногда &lt;code&gt;Organization&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;В &lt;code&gt;PostLayout.astro&lt;/code&gt; лежит ещё один inline-скрипт с &lt;code&gt;BlogPosting&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Если автор увлёкся — добавляется третий скрипт с &lt;code&gt;BreadcrumbList&lt;/code&gt;. Иногда четвёртый с &lt;code&gt;Person&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Почему так получилось — потому что Astro-компоненты иерархически наследуются, и каждый уровень удобно «добивает» свою порцию данных через свой &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. Это работает локально, но плохо масштабируется. У нас в репозитории до Plan 1 было ровно это: &lt;code&gt;BaseLayout&lt;/code&gt; эмитил один JSON-LD блок, &lt;code&gt;PostLayout&lt;/code&gt; поверх него добавлял свои два:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Pre-Plan 1 (commit 5ed281c~1):
$ git show 5ed281c~1:src/layouts/BaseLayout.astro | grep -c application/ld+json
1
$ git show 5ed281c~1:src/layouts/PostLayout.astro | grep -c application/ld+json
2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;То есть страница поста содержала &lt;strong&gt;три&lt;/strong&gt; &lt;code&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;&lt;/code&gt; блока. Каждый со своим &lt;code&gt;Person&lt;/code&gt; (где-то полным, где-то усечённым), без общего &lt;code&gt;@id&lt;/code&gt;, без перекрёстных ссылок. Краулер, который попадал на пост, видел три не связанных друг с другом entity-облака.&lt;/p&gt;
&lt;p&gt;Главные проблемы антипаттерна:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Дублирование &lt;code&gt;Person&lt;/code&gt;.&lt;/strong&gt; Один и тот же автор описан 3-5 раз. Если бы автор сменил &lt;code&gt;jobTitle&lt;/code&gt; или добавил &lt;code&gt;sameAs&lt;/code&gt;, надо было бы править во всех файлах. Forget one — и краулер видит конфликт: «у Person с таким именем jobTitle вдруг разный». Это явный signal-to-noise урон.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Разорванный граф.&lt;/strong&gt; &lt;code&gt;BlogPosting.publisher&lt;/code&gt; — это inline-объект &lt;code&gt;{ &amp;quot;@type&amp;quot;: &amp;quot;Organization&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;...&amp;quot; }&lt;/code&gt;. Где-то ещё на сайте лежит &lt;code&gt;Organization&lt;/code&gt; с &lt;code&gt;founder&lt;/code&gt;-полем. Без общих &lt;code&gt;@id&lt;/code&gt; валидатор не знает, это один publisher или два.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTML weight.&lt;/strong&gt; Три скрипта вместо одного — это лишние десятки байт на каждый, плюс инфляция payload, особенно если на странице несколько одинаковых полей (e.g. описание автора повторяется четырежды).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Согласованность.&lt;/strong&gt; Если автор правит &lt;code&gt;Person.description&lt;/code&gt; в frontmatter &lt;code&gt;about.md&lt;/code&gt;, а в &lt;code&gt;BlogPosting&lt;/code&gt;-builder он зашит как литерал — рассинхрон неизбежен.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Целевая архитектура — &lt;code&gt;@graph&lt;/code&gt; с глобальными &lt;code&gt;@id&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Целевая модель: &lt;strong&gt;один script на странице&lt;/strong&gt;, внутри — &lt;code&gt;@graph&lt;/code&gt;-массив. Глобальные узлы (&lt;code&gt;Person&lt;/code&gt;, &lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;WebSite&lt;/code&gt;) описаны один раз и идентифицируются стабильными URI. Page-level узлы (&lt;code&gt;BlogPosting&lt;/code&gt;, &lt;code&gt;WebPage&lt;/code&gt;, &lt;code&gt;CollectionPage&lt;/code&gt;, &lt;code&gt;CreativeWork&lt;/code&gt;) добавляются &lt;code&gt;BaseLayout&lt;/code&gt;-ом и &lt;strong&gt;ссылаются на глобальные через &lt;code&gt;@id&lt;/code&gt;&lt;/strong&gt;, не дублируя их данные.&lt;/p&gt;
&lt;p&gt;Топология:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  Person[&amp;quot;Person#person&amp;lt;br/&amp;gt;(глобальный)&amp;quot;]
  Org[&amp;quot;Organization#brand&amp;lt;br/&amp;gt;(глобальный)&amp;quot;]
  Site[&amp;quot;WebSite#site&amp;lt;br/&amp;gt;(глобальный)&amp;quot;]
  Post[&amp;quot;BlogPosting#blogposting&amp;lt;br/&amp;gt;(page-level)&amp;quot;]
  WebPage[&amp;quot;WebPage#webpage&amp;lt;br/&amp;gt;(page-level)&amp;quot;]

  Post -- author --&amp;gt; Person
  Post -- publisher --&amp;gt; Org
  Post -- isPartOf --&amp;gt; Site
  WebPage -- about --&amp;gt; Person
  WebPage -- isPartOf --&amp;gt; Site
  Org -- founder --&amp;gt; Person
  Site -- publisher --&amp;gt; Org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Что важно в этой картинке:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Все стрелки — это &lt;code&gt;{&amp;quot;@id&amp;quot;: &amp;quot;...&amp;quot;}&lt;/code&gt; ссылки.&lt;/strong&gt; Никаких inline-копий.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Person#person&lt;/code&gt; — корневой узел графа.&lt;/strong&gt; Все entity-страницы (&lt;code&gt;/about&lt;/code&gt;, &lt;code&gt;/now&lt;/code&gt;, &lt;code&gt;/uses&lt;/code&gt;) делают &lt;code&gt;WebPage.about → Person&lt;/code&gt;. Все посты — &lt;code&gt;BlogPosting.author → Person&lt;/code&gt;. Сменив &lt;code&gt;Person&lt;/code&gt;, мы синхронно меняем всё.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page-level узлы добавляются, не заменяя глобальные.&lt;/strong&gt; Каждая страница привносит 1-2 новых узла; &lt;code&gt;Person&lt;/code&gt;/&lt;code&gt;Organization&lt;/code&gt;/&lt;code&gt;WebSite&lt;/code&gt; всегда присутствуют.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Стабильные &lt;code&gt;@id&lt;/code&gt; — это не URL страницы, это URI с фрагментом, например &lt;code&gt;https://artka.dev/#person&lt;/code&gt;, &lt;code&gt;https://artka.dev/#brand&lt;/code&gt;. Так принято в JSON-LD: фрагмент-id означает «этот ресурс описан на любой странице, но идентифицируется единым URI».&lt;/p&gt;
&lt;h2&gt;4. Реализация в Astro 5&lt;/h2&gt;
&lt;p&gt;В Astro 5 SSG/SSR-граница проходит ровно по &lt;code&gt;BaseLayout&lt;/code&gt;-у: на сборке вычисляются props, рендерится HTML, в нём — статический &lt;code&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;&lt;/code&gt;. Никаких client-side, никаких rehydration-моргалок. Идеальный момент собрать &lt;code&gt;@graph&lt;/code&gt; функционально.&lt;/p&gt;
&lt;h3&gt;4.1. &lt;code&gt;graphIds&lt;/code&gt; — таблица URI&lt;/h3&gt;
&lt;p&gt;Один файл, в котором перечислены все стабильные идентификаторы:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/seo/nodes-global.ts
const SITE = &amp;quot;https://artka.dev&amp;quot;;

export const graphIds = {
  person: `${SITE}/#person`,
  organization: `${SITE}/#brand`,
  website: `${SITE}/#website`,
  blogRu: `${SITE}/#blog-ru`,
  blogEn: `${SITE}/#blog-en`,
} as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Каждый builder, который ссылается на глобальную сущность, импортирует &lt;code&gt;graphIds&lt;/code&gt; и использует &lt;code&gt;{ &amp;quot;@id&amp;quot;: graphIds.person }&lt;/code&gt;. Никаких inline-литералов, никаких опечаток в URI.&lt;/p&gt;
&lt;h3&gt;4.2. Builders — pure functions, никаких классов&lt;/h3&gt;
&lt;p&gt;В соответствии с проектным правилом «никаких классов в прикладном коде» каждый узел — это чистая функция, возвращающая &lt;code&gt;Record&amp;lt;string, unknown&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/seo/nodes-global.ts (фрагмент)
export const buildPersonNode = () =&amp;gt; {
  const merged = Array.from(new Set&amp;lt;string&amp;gt;([...person.knowsAbout, ...person.expertiseAreas]));
  return {
    &amp;quot;@type&amp;quot;: &amp;quot;Person&amp;quot;,
    &amp;quot;@id&amp;quot;: graphIds.person,
    name: person.name,
    url: person.url,
    image: person.image,
    jobTitle: person.jobTitle,
    description: person.description,
    knowsAbout: merged,
    sameAs: [...person.sameAs],
    email: person.email,
    subjectOf: person.notableWork.map((w) =&amp;gt; ({
      &amp;quot;@type&amp;quot;: &amp;quot;CreativeWork&amp;quot;,
      name: w.title,
      url: w.url,
      description: w.description,
    })),
  };
};

export const buildOrganizationNode = () =&amp;gt; ({
  &amp;quot;@type&amp;quot;: &amp;quot;Organization&amp;quot;,
  &amp;quot;@id&amp;quot;: graphIds.organization,
  name: &amp;quot;artka.dev&amp;quot;,
  url: SITE,
  logo: { &amp;quot;@type&amp;quot;: &amp;quot;ImageObject&amp;quot;, url: `${SITE}/favicon.svg` },
  founder: { &amp;quot;@id&amp;quot;: graphIds.person },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;person&lt;/code&gt; — это импорт из &lt;code&gt;src/lib/seo/person.ts&lt;/code&gt;, единственного источника правды по автору. Builder складывает &lt;code&gt;knowsAbout&lt;/code&gt; и &lt;code&gt;expertiseAreas&lt;/code&gt; в &lt;code&gt;Set&lt;/code&gt;, чтобы не дублировать ключи. &lt;code&gt;Organization.founder&lt;/code&gt; — &lt;code&gt;@id&lt;/code&gt;-ссылка, не inline-копия &lt;code&gt;Person&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;4.3. Оркестратор — &lt;code&gt;buildGraph&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Функция, которая склеивает глобальные и page-level узлы в один &lt;code&gt;@graph&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/seo/schema.ts
import {
  buildPersonNode,
  buildOrganizationNode,
  buildWebSiteNode,
  type Locale,
} from &amp;quot;./nodes-global&amp;quot;;

export type GraphNode = Record&amp;lt;string, unknown&amp;gt; &amp;amp; { &amp;quot;@type&amp;quot;: string };

export interface GraphInput {
  readonly locale: Locale;
  readonly extraNodes: ReadonlyArray&amp;lt;GraphNode | null&amp;gt;;
}

export interface JsonLdGraph {
  readonly &amp;quot;@context&amp;quot;: &amp;quot;https://schema.org&amp;quot;;
  readonly &amp;quot;@graph&amp;quot;: ReadonlyArray&amp;lt;GraphNode&amp;gt;;
}

export const buildGraph = (input: GraphInput): JsonLdGraph =&amp;gt; {
  const globals: GraphNode[] = [
    buildPersonNode(),
    buildOrganizationNode(),
    buildWebSiteNode(input.locale),
  ];
  const extras = input.extraNodes.filter((n): n is GraphNode =&amp;gt; n !== null);
  return {
    &amp;quot;@context&amp;quot;: &amp;quot;https://schema.org&amp;quot;,
    &amp;quot;@graph&amp;quot;: [...globals, ...extras],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;API минимальный: вход — locale (чтобы выбрать &lt;code&gt;inLanguage&lt;/code&gt; для &lt;code&gt;WebSite&lt;/code&gt;) и список дополнительных узлов (&lt;code&gt;extraNodes&lt;/code&gt;). Выход — готовый &lt;code&gt;JsonLdGraph&lt;/code&gt;. &lt;code&gt;null&lt;/code&gt;-узлы фильтруются — это удобно для опциональных узлов вроде &lt;code&gt;FAQPage&lt;/code&gt;, builder которых возвращает &lt;code&gt;null&lt;/code&gt; при пустом массиве вопросов.&lt;/p&gt;
&lt;h3&gt;4.4. &lt;code&gt;BaseLayout&lt;/code&gt; — единственная точка эмиссии&lt;/h3&gt;
&lt;p&gt;Весь сайт идёт через &lt;code&gt;BaseLayout&lt;/code&gt;, и именно он — и только он — эмитит JSON-LD:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// src/layouts/BaseLayout.astro
import { buildGraph, safeJsonLd, type GraphNode } from &amp;quot;~/lib/seo/schema&amp;quot;;

interface Props {
  title: string;
  description?: string;
  // ...
  /** Additional JSON-LD nodes to merge into the page @graph. */
  extraSchemaNodes?: ReadonlyArray&amp;lt;GraphNode | null&amp;gt;;
}

const { extraSchemaNodes = [] } = Astro.props;
const locale = getLocaleFromPath(Astro.url.pathname);
---

&amp;lt;head&amp;gt;
  &amp;lt;script
    is:inline
    type=&amp;quot;application/ld+json&amp;quot;
    set:html={safeJsonLd(buildGraph({ locale, extraNodes: extraSchemaNodes }))}
  /&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Три ключевые детали:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;is:inline&lt;/code&gt;&lt;/strong&gt; — Astro не пытается обрабатывать содержимое как JS-модуль.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;set:html&lt;/code&gt;&lt;/strong&gt; — мы вставляем уже готовую строку, не давая фреймворку триммить пробелы или экранировать дополнительно.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;safeJsonLd&lt;/code&gt;&lt;/strong&gt; — крошечный helper, экранирует &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt; так, чтобы внутри JSON не оказалось последовательности, которую парсер HTML примет за конец &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt;. Без него злонамеренный (или просто неудачный) текст в frontmatter мог бы сломать страницу.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/seo/json-ld.ts
export const safeJsonLd = (data: unknown): string =&amp;gt;
  JSON.stringify(data).replace(/&amp;lt;/g, &amp;quot;\\u003c&amp;quot;).replace(/&amp;gt;/g, &amp;quot;\\u003e&amp;quot;).replace(/&amp;amp;/g, &amp;quot;\\u0026&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.5. Page-level контракт&lt;/h3&gt;
&lt;p&gt;Каждый layout/page добавляет свои узлы через &lt;code&gt;extraSchemaNodes&lt;/code&gt;. Например, &lt;code&gt;PostLayout&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const excerpt = extractArticleBody(post.body ?? &amp;quot;&amp;quot;, 800);

const blogPostingNode = buildBlogPostingNode({
  locale,
  canonical,
  title,
  description,
  pubDate,
  updatedDate: updatedDate ?? null,
  image: absoluteCover,
  keywords: tags,
  articleBody: excerpt.text,
  wordCount: excerpt.fullWordCount,
});

const breadcrumbNode = buildBreadcrumbListNode({
  locale,
  blogIndexLabel: t(locale, &amp;quot;blog.title&amp;quot;),
  title,
});

const faqNode = buildFaqPageNode({ canonical, items: faq ?? [] });
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;&amp;lt;BaseLayout title={title} extraSchemaNodes={[blogPostingNode, breadcrumbNode, faqNode]}&amp;gt;
  &amp;lt;slot /&amp;gt;
&amp;lt;/BaseLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/blog&lt;/code&gt;, &lt;code&gt;/projects/&amp;lt;slug&amp;gt;&lt;/code&gt;, &lt;code&gt;/tags/&amp;lt;tag&amp;gt;&lt;/code&gt;, &lt;code&gt;/about&lt;/code&gt; — все используют тот же contract, отличаясь только конкретными builders. Один dispatch, ноль дублирования.&lt;/p&gt;
&lt;h2&gt;5. &lt;code&gt;articleBody&lt;/code&gt; — почему excerpt, а не full body&lt;/h2&gt;
&lt;p&gt;Поле &lt;code&gt;articleBody&lt;/code&gt; в &lt;code&gt;BlogPosting&lt;/code&gt; — самая ценная часть для LLM-краулера: это извлекаемый чанк текста, который можно цитировать. И самая опасная для weight: если положить весь пост в JSON-LD, HTML-страница раздуется в 2-3 раза. Spec формулирует компромисс прямо: «emit first 800 words of plain-text body … add &lt;code&gt;wordCount&lt;/code&gt; covering the &lt;em&gt;full&lt;/em&gt; body».&lt;/p&gt;
&lt;p&gt;Excerpt извлекается через mdast: парсим markdown, удаляем code-блоки, mermaid-блоки и inline-html, склеиваем оставшийся текст, режем по 800 слов:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/lib/seo/article-body.ts (фрагмент)
export const extractArticleBody = (markdown: string, maxWords: number) =&amp;gt; {
  const tree = unified().use(remarkParse).parse(markdown) as Root;

  const isStrippable = (node: Node): boolean =&amp;gt;
    node.type === &amp;quot;code&amp;quot; || node.type === &amp;quot;inlineCode&amp;quot; || node.type === &amp;quot;html&amp;quot;;

  visit(tree, (node, index, parent) =&amp;gt; {
    if (parent &amp;amp;&amp;amp; typeof index === &amp;quot;number&amp;quot; &amp;amp;&amp;amp; isStrippable(node)) {
      (parent as { children: Node[] }).children.splice(index, 1);
      return [SKIP, index];
    }
    return undefined;
  });

  const flat = mdastToString(tree, { includeImageAlt: false }).replace(/\s+/g, &amp;quot; &amp;quot;).trim();
  const words = flat.length &amp;gt; 0 ? flat.split(/\s+/) : [];
  if (words.length &amp;lt;= maxWords) return { text: flat, fullWordCount: words.length };
  return { text: words.slice(0, maxWords).join(&amp;quot; &amp;quot;) + &amp;quot;…&amp;quot;, fullWordCount: words.length };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Почему именно 800 слов:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Длина&lt;/th&gt;
&lt;th&gt;Pro&lt;/th&gt;
&lt;th&gt;Con&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 слов&lt;/td&gt;
&lt;td&gt;мизерный HTML-overhead&lt;/td&gt;
&lt;td&gt;один абзац — мало для LLM-citation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800 слов&lt;/td&gt;
&lt;td&gt;substantial chunk, ~3-5 KB&lt;/td&gt;
&lt;td&gt;+3-5 KB к payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full body&lt;/td&gt;
&lt;td&gt;максимум context&lt;/td&gt;
&lt;td&gt;удвоение HTML, реальный hit performance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Почему именно через mdast, а не regex: в постах живут &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, MDX-компоненты вроде &lt;code&gt;&amp;lt;Faq&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;Tldr&amp;gt;&lt;/code&gt;. Regex по &lt;code&gt;\&lt;/code&gt;``` сломается на code в indent-стиле или на nested fences. mdast — единственный надёжный способ.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wordCount&lt;/code&gt; мы оставляем по полному телу, не по excerpt’у — это даёт честный сигнал валидатору и LLM о реальном объёме контента.&lt;/p&gt;
&lt;h2&gt;6. &lt;code&gt;FAQPage&lt;/code&gt; как side-effect MDX-компонента&lt;/h2&gt;
&lt;p&gt;Один из дизайн-целей Plan 1 — &lt;strong&gt;снять с автора cognitive load на structured data&lt;/strong&gt;. Автор не должен помнить, что у &lt;code&gt;FAQPage&lt;/code&gt; есть &lt;code&gt;mainEntity&lt;/code&gt;, что внутри &lt;code&gt;Question&lt;/code&gt; нужен &lt;code&gt;acceptedAnswer&lt;/code&gt;, что текст ответа экранируется. Автор должен заполнить frontmatter и забыть.&lt;/p&gt;
&lt;p&gt;Решение: &lt;code&gt;frontmatter.faq&lt;/code&gt; — единственный источник. PostLayout читает массив:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const faqNode = buildFaqPageNode({ canonical, items: faq ?? [] });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;buildFaqPageNode&lt;/code&gt; либо возвращает готовый &lt;code&gt;FAQPage&lt;/code&gt;-узел, либо &lt;code&gt;null&lt;/code&gt; (фильтруется в &lt;code&gt;buildGraph&lt;/code&gt;). Параллельно тот же массив отдаётся в &lt;code&gt;&amp;lt;Faq&amp;gt;&lt;/code&gt;-компонент, который рендерит видимые &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;-блоки с тем же текстом. Один источник — два потребителя: визуальный layer и structured layer. Рассинхрон невозможен.&lt;/p&gt;
&lt;p&gt;Builder тривиален:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const buildFaqPageNode = (input: FaqPageInput) =&amp;gt; {
  if (input.items.length === 0) return null;
  return {
    &amp;quot;@type&amp;quot;: &amp;quot;FAQPage&amp;quot;,
    &amp;quot;@id&amp;quot;: `${input.canonical}#faq`,
    mainEntity: input.items.map((it) =&amp;gt; ({
      &amp;quot;@type&amp;quot;: &amp;quot;Question&amp;quot;,
      name: it.question,
      acceptedAnswer: { &amp;quot;@type&amp;quot;: &amp;quot;Answer&amp;quot;, text: it.answer },
    })),
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frontmatter, который автор пишет:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;faq:
  - question: &amp;quot;Чем агент отличается от чат-бота?&amp;quot;
    answer: &amp;quot;Чат-бот — это model.complete(messages): принимает текст…&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И всё. Дальше — автоматика.&lt;/p&gt;
&lt;h2&gt;7. Замеры до/после&lt;/h2&gt;
&lt;p&gt;После Plan 1 на странице &lt;code&gt;/blog/01-introduction/&lt;/code&gt; остался ровно &lt;strong&gt;один&lt;/strong&gt; &lt;code&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;&lt;/code&gt; блок. Реальный измеренный факт:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ grep -c &amp;quot;application/ld+json&amp;quot; dist/client/blog/01-introduction/index.html
1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;До Plan 1 (commit &lt;code&gt;5ed281c~1&lt;/code&gt;) было два источника inline-скриптов:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git show 5ed281c~1:src/layouts/BaseLayout.astro | grep -c application/ld+json  # 1
$ git show 5ed281c~1:src/layouts/PostLayout.astro | grep -c application/ld+json  # 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;То есть на странице поста суммарно &lt;strong&gt;3 блока&lt;/strong&gt;. Стало &lt;strong&gt;1&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Метрика&lt;/th&gt;
&lt;th&gt;Pre-Plan 1&lt;/th&gt;
&lt;th&gt;Post-Plan 1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;&lt;/code&gt; блоков на странице поста&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Общий контейнер&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@graph&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Стабильный &lt;code&gt;Person@id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://artka.dev/#person&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Перекрёстные &lt;code&gt;@id&lt;/code&gt;-ссылки между узлами&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Источник правды по автору&lt;/td&gt;
&lt;td&gt;разбросан по layout-ам&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/lib/seo/person.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Реальный JSON-LD страницы &lt;code&gt;/blog/01-introduction/&lt;/code&gt;, извлечённый из &lt;code&gt;dist/client/blog/01-introduction/index.html&lt;/code&gt;, выглядит так (фрагмент, &lt;code&gt;articleBody&lt;/code&gt; урезан до многоточия, FAQ-узел сокращён):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@context&amp;quot;: &amp;quot;https://schema.org&amp;quot;,
  &amp;quot;@graph&amp;quot;: [
    {
      &amp;quot;@type&amp;quot;: &amp;quot;Person&amp;quot;,
      &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#person&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;Артём Кашута&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https://artka.dev/about&amp;quot;,
      &amp;quot;jobTitle&amp;quot;: &amp;quot;Software engineer · backend &amp;amp; AI agent engineering&amp;quot;,
      &amp;quot;knowsAbout&amp;quot;: [&amp;quot;Claude Code&amp;quot;, &amp;quot;AI agent engineering&amp;quot;, &amp;quot;Node.js&amp;quot;, &amp;quot;TypeScript&amp;quot;, &amp;quot;Astro&amp;quot;, &amp;quot;…&amp;quot;],
      &amp;quot;email&amp;quot;: &amp;quot;a@artka.dev&amp;quot;,
      &amp;quot;subjectOf&amp;quot;: [
        {
          &amp;quot;@type&amp;quot;: &amp;quot;CreativeWork&amp;quot;,
          &amp;quot;name&amp;quot;: &amp;quot;Claude Code Guide (RU, 14 частей)&amp;quot;,
          &amp;quot;url&amp;quot;: &amp;quot;https://artka.dev/blog&amp;quot;
        }
      ]
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;Organization&amp;quot;,
      &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#brand&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;artka.dev&amp;quot;,
      &amp;quot;logo&amp;quot;: { &amp;quot;@type&amp;quot;: &amp;quot;ImageObject&amp;quot;, &amp;quot;url&amp;quot;: &amp;quot;https://artka.dev/favicon.svg&amp;quot; },
      &amp;quot;founder&amp;quot;: { &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#person&amp;quot; }
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;WebSite&amp;quot;,
      &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#website&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https://artka.dev&amp;quot;,
      &amp;quot;inLanguage&amp;quot;: &amp;quot;ru-RU&amp;quot;,
      &amp;quot;publisher&amp;quot;: { &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#brand&amp;quot; },
      &amp;quot;potentialAction&amp;quot;: {
        &amp;quot;@type&amp;quot;: &amp;quot;SearchAction&amp;quot;,
        &amp;quot;target&amp;quot;: &amp;quot;https://artka.dev/search?q={search_term_string}&amp;quot;,
        &amp;quot;query-input&amp;quot;: &amp;quot;required name=search_term_string&amp;quot;
      }
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;BlogPosting&amp;quot;,
      &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/blog/01-introduction/#blogposting&amp;quot;,
      &amp;quot;headline&amp;quot;: &amp;quot;01. Что такое Claude Code: harness, agent loop и ваше место в нём&amp;quot;,
      &amp;quot;datePublished&amp;quot;: &amp;quot;2026-04-23T00:00:00.000Z&amp;quot;,
      &amp;quot;author&amp;quot;: { &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#person&amp;quot; },
      &amp;quot;publisher&amp;quot;: { &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#brand&amp;quot; },
      &amp;quot;mainEntityOfPage&amp;quot;: &amp;quot;https://artka.dev/blog/01-introduction/&amp;quot;,
      &amp;quot;inLanguage&amp;quot;: &amp;quot;ru-RU&amp;quot;,
      &amp;quot;isPartOf&amp;quot;: { &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#blog-ru&amp;quot; },
      &amp;quot;articleBody&amp;quot;: &amp;quot;Перед тем как разбирать skills и subagents, надо договориться о терминах…&amp;quot;,
      &amp;quot;wordCount&amp;quot;: 574
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;BreadcrumbList&amp;quot;,
      &amp;quot;itemListElement&amp;quot;: [
        { &amp;quot;@type&amp;quot;: &amp;quot;ListItem&amp;quot;, &amp;quot;position&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;Главная&amp;quot;, &amp;quot;item&amp;quot;: &amp;quot;https://artka.dev/&amp;quot; },
        { &amp;quot;@type&amp;quot;: &amp;quot;ListItem&amp;quot;, &amp;quot;position&amp;quot;: 2, &amp;quot;name&amp;quot;: &amp;quot;Статьи&amp;quot;, &amp;quot;item&amp;quot;: &amp;quot;https://artka.dev/blog&amp;quot; },
        { &amp;quot;@type&amp;quot;: &amp;quot;ListItem&amp;quot;, &amp;quot;position&amp;quot;: 3, &amp;quot;name&amp;quot;: &amp;quot;01. Что такое Claude Code…&amp;quot; }
      ]
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;FAQPage&amp;quot;,
      &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/blog/01-introduction/#faq&amp;quot;,
      &amp;quot;mainEntity&amp;quot;: [
        {
          &amp;quot;@type&amp;quot;: &amp;quot;Question&amp;quot;,
          &amp;quot;name&amp;quot;: &amp;quot;Чем агент отличается от чат-бота?&amp;quot;,
          &amp;quot;acceptedAnswer&amp;quot;: { &amp;quot;@type&amp;quot;: &amp;quot;Answer&amp;quot;, &amp;quot;text&amp;quot;: &amp;quot;…&amp;quot; }
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Что можно увидеть глазами и что зафиксирует валидатор:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Один &lt;code&gt;Person&lt;/code&gt;, на него ссылается всё.&lt;/strong&gt; &lt;code&gt;Organization.founder&lt;/code&gt;, &lt;code&gt;BlogPosting.author&lt;/code&gt; — оба &lt;code&gt;{ &amp;quot;@id&amp;quot;: &amp;quot;https://artka.dev/#person&amp;quot; }&lt;/code&gt;. Никаких догадок о тождестве.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Organization&lt;/code&gt; — публичный publisher.&lt;/strong&gt; &lt;code&gt;WebSite.publisher&lt;/code&gt; ссылается на тот же &lt;code&gt;Organization&lt;/code&gt;. &lt;code&gt;BlogPosting.publisher&lt;/code&gt; — на тот же. Граф связан.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;isPartOf&lt;/code&gt; цепочка для блога.&lt;/strong&gt; &lt;code&gt;BlogPosting.isPartOf → Blog#blog-ru → publisher → Organization&lt;/code&gt;. Краулер видит вложенность и принадлежность.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;articleBody&lt;/code&gt; excerpt — substantial.&lt;/strong&gt; ~574 слова поста уложены в одно поле. &lt;code&gt;wordCount&lt;/code&gt; отражает полный объём. LLM получает текст для citation, HTML — не раздувается.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FAQ — вместе со всеми, не отдельно.&lt;/strong&gt; Не отдельный script-блок, а узел того же &lt;code&gt;@graph&lt;/code&gt;. Меньше блоков — меньше ловушек для парсера.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;http://Schema.org&quot;&gt;Schema.org&lt;/a&gt; validator и Google Rich Results Test принимают этот &lt;code&gt;@graph&lt;/code&gt; без замечаний &lt;em&gt;(скриншоты — owner to fill)&lt;/em&gt;. Главное — JSON pretty-print’ится без &lt;code&gt;[object Object]&lt;/code&gt;, без unescaped кавычек, без сломанных дат: всё в норме после &lt;code&gt;safeJsonLd&lt;/code&gt;-обёртки.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Что дальше&lt;/h3&gt;
&lt;p&gt;Описанное выше — &lt;strong&gt;Plan 1&lt;/strong&gt; в нашем repo. Дальше базу мы расширяем для новых типов сущностей (&lt;code&gt;/projects/&amp;lt;slug&amp;gt;&lt;/code&gt; через &lt;code&gt;CreativeWork&lt;/code&gt;, &lt;code&gt;/uses&lt;/code&gt; через &lt;code&gt;WebPage.about&lt;/code&gt;), и для retrieval-слоя через &lt;code&gt;llms.txt&lt;/code&gt;. Но фундамент — &lt;code&gt;buildGraph&lt;/code&gt; + стабильные &lt;code&gt;@id&lt;/code&gt; — обязан встать первым.&lt;/p&gt;
&lt;p&gt;Если вы видите 2-3 inline JSON-LD скрипта на странице поста — это место, с которого стоит начинать миграцию. Один файл &lt;code&gt;schema.ts&lt;/code&gt;, один &lt;code&gt;extraSchemaNodes&lt;/code&gt;-prop — и сайт превращается из набора разрозненных entity-облаков в связный citable-узел.&lt;/p&gt;
</content:encoded><category>seo</category><category>astro</category><category>schema-org</category><author>a@artka.dev (Артём)</author></item><item><title>robots.txt в эпоху AI-краулеров: GPTBot, ClaudeBot, PerplexityBot — реальность 2026</title><link>https://artka.dev/blog/robots-txt-ai-crawlers-2026/</link><guid isPermaLink="true">https://artka.dev/blog/robots-txt-ai-crawlers-2026/</guid><description>В 2026 robots.txt — это не «запретить ботам всё» и не «открыть всё», а политика по каждому из 9+ именованных агентов. Реальный шаблон, таблица решений и грабли.</description><pubDate>Fri, 01 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;В 2026 robots.txt — это не «запретить ботам всё» и не «открыть всё». Это политика по каждому из 9+ именованных агентов. Каждое решение — частный случай: открываете ли вы свой контент для тренировки моделей, для on-demand цитирования, что вы хотите видеть в карточке ответа Perplexity. Этот пост — таблица решений, готовый шаблон и почему &lt;code&gt;llms.txt&lt;/code&gt; — отдельный артефакт.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. Зачем переписывать robots.txt в 2026&lt;/h2&gt;
&lt;p&gt;Классический SEO-подход к robots.txt оптимизирован под одну задачу: пустить Googlebot туда, где есть смысл индексировать страницы для SERP, и закрыть служебные пути. В 2026 эта задача стала меньшинством трафика.&lt;/p&gt;
&lt;p&gt;Большинство вопросов «должен ли я индексировать эту страницу?» теперь задаются не Google, а:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Тренирующим краулерам&lt;/strong&gt; — выкачивают страницы для пополнения корпуса, на котором учится следующая версия модели (GPTBot, ClaudeBot, Google-Extended).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Answer/search краулерам&lt;/strong&gt; — индексируют контент для встроенного в чат поиска (OAI-SearchBot, PerplexityBot).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-demand fetcher’ам&lt;/strong&gt; — открывают одну конкретную страницу, потому что пользователь явно об этом попросил в чате (ChatGPT-User, Perplexity-User, Claude-Web).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Эти три класса принимают три разных решения. Один блок &lt;code&gt;User-agent: *&lt;/code&gt; не передаёт нюанс. Вы можете хотеть «не учите на моих текстах, но процитировать в ответе на вопрос — пожалуйста». Один wildcard этого не выразит.&lt;/p&gt;
&lt;p&gt;Отсюда требование: явные блоки по каждому именованному User-Agent с осознанным выбором политики. Не «открыли всё», не «закрыли всё», а матрица «бот × намерение».&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Список именованных AI-краулеров и их назначение&lt;/h2&gt;
&lt;p&gt;Девять агентов, которые действительно стоит назвать в 2026, с их публичной документацией. Имена User-Agent взяты из официальных страниц вендоров.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User-Agent&lt;/th&gt;
&lt;th&gt;Производитель&lt;/th&gt;
&lt;th&gt;Назначение&lt;/th&gt;
&lt;th&gt;Документация&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GPTBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Training crawl&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://platform.openai.com/docs/gptbot&quot;&gt;platform.openai.com/docs/gptbot&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OAI-SearchBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Search index for ChatGPT&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://platform.openai.com/docs/bots&quot;&gt;platform.openai.com/docs/bots&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ChatGPT-User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;On-demand fetch from ChatGPT&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://platform.openai.com/docs/bots&quot;&gt;platform.openai.com/docs/bots&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClaudeBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Training crawl&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://docs.anthropic.com&quot;&gt;docs.anthropic.com&lt;/a&gt; (&lt;a href=&quot;http://claudebot.anthropic.com&quot;&gt;claudebot.anthropic.com&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Claude-Web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;On-demand fetch initiated by &lt;a href=&quot;http://Claude.ai&quot;&gt;Claude.ai&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://docs.anthropic.com&quot;&gt;docs.anthropic.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic-ai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Legacy/auxiliary Anthropic crawler&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://docs.anthropic.com&quot;&gt;docs.anthropic.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PerplexityBot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Search/index crawl&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://docs.perplexity.ai/guides/bots&quot;&gt;docs.perplexity.ai/guides/bots&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Perplexity-User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;On-demand fetch from a user query&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://docs.perplexity.ai/guides/bots&quot;&gt;docs.perplexity.ai/guides/bots&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Google-Extended&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Opt-in для Gemini training&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;http://developers.google.com/search/docs/crawling&quot;&gt;developers.google.com/search/docs/crawling&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Имена должны совпадать побайтно. &lt;code&gt;Claude-Bot&lt;/code&gt; и &lt;code&gt;claudebot&lt;/code&gt; — не валидные алиасы для &lt;code&gt;ClaudeBot&lt;/code&gt;. Спецификация robots.txt на этот счёт мягкая (case-insensitive), но проверять стоит точное написание из официальной документации.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Таксономия:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TB
  subgraph training[&amp;quot;Тренирующие (corpus → модель)&amp;quot;]
    GPT[GPTBot]
    CLB[ClaudeBot]
    GEX[Google-Extended]
  end
  subgraph answer[&amp;quot;Answer/search (индекс для встроенного поиска)&amp;quot;]
    OAI[OAI-SearchBot]
    PPB[PerplexityBot]
  end
  subgraph ondemand[&amp;quot;On-demand (пользователь попросил)&amp;quot;]
    CGU[ChatGPT-User]
    CWB[Claude-Web]
    PPU[Perplexity-User]
    AAI[anthropic-ai]
  end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Три класса = три отдельных решения. Не нужно обсуждать «робота вообще» — нужно обсуждать «GPTBot на /blog/».&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. Решения по каждому боту&lt;/h2&gt;
&lt;p&gt;Здесь нет универсально правильного ответа. Ниже — каркас рассуждения и моя политика для блога.&lt;/p&gt;
&lt;h3&gt;Тренирующие краулеры&lt;/h3&gt;
&lt;p&gt;Для авторов индивидуальных блогов с long-form контентом аргументы:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;За Allow:&lt;/strong&gt; ваш текст войдёт в корпус, на котором обучаются следующие модели. Если ваша задача — повышать distribution и присутствие вашей экспертизы в LLM-ответах, это путь.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;За Disallow:&lt;/strong&gt; ваш контент превращается в anonymous training signal без атрибуции. Если вы планируете монетизировать контент (книга, курс) или против использования без согласия, Disallow — единственный сигнал, который у вас есть на уровне robots.txt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Для коммерческих сайтов, где контент — товар (онлайн-курсы, paid newsletters, юридические базы), Disallow — обычно дефолт.&lt;/p&gt;
&lt;h3&gt;Answer/search краулеры&lt;/h3&gt;
&lt;p&gt;Намерение — показать ссылку на вашу страницу в карточке ответа. Это работает в обе стороны:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;За Allow:&lt;/strong&gt; трафик возможен (хоть и через цитату с link-out). Ваш бренд появляется в выдаче.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;За Disallow:&lt;/strong&gt; вы не получите этот трафик и одновременно вашу страницу не процитируют как источник.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Для большинства публичных блогов ответ — Allow.&lt;/p&gt;
&lt;h3&gt;On-demand fetcher’ы&lt;/h3&gt;
&lt;p&gt;Самый «прозрачный» класс: пользователь вашего сайта (или того, кто специально хочет открыть вашу страницу через ChatGPT/Claude/Perplexity) уже явно навёл указатель. Disallow здесь означает «нельзя использовать наши страницы как источник в чат-сессии» — почти всегда чрезмерно строго для публичного блога.&lt;/p&gt;
&lt;h3&gt;Моя политика для artka.dev&lt;/h3&gt;
&lt;p&gt;Для этого сайта:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Все 9 ботов — &lt;code&gt;Allow: /&lt;/code&gt; (открытый публичный блог, цель — distribution).&lt;/li&gt;
&lt;li&gt;У всех — &lt;code&gt;Disallow: /admin/&lt;/code&gt;, &lt;code&gt;/api/&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt; (приватные namespace’ы, см. §5).&lt;/li&gt;
&lt;li&gt;Нет специальных запретов на отдельные посты или теги.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Это решение для personal tech-blog’а с целью «увеличить охват экспертизы». Для коммерческого контента я бы выбрал иначе.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Готовый шаблон robots.txt&lt;/h2&gt;
&lt;p&gt;Вот реальный &lt;code&gt;public/robots.txt&lt;/code&gt;, который ходит в продакшн на artka.dev. Он же — стартовая точка, которую вы можете адаптировать.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;# robots.txt — last reviewed 2026-05-02
# Owner: dev@artka.dev. Policy: allow retrieval/answer crawlers; disallow private surfaces.

User-agent: GPTBot
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: OAI-SearchBot
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: ChatGPT-User
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: ClaudeBot
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: Claude-Web
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: anthropic-ai
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: PerplexityBot
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: Perplexity-User
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: Google-Extended
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /login

Sitemap: https://artka.dev/sitemap-index.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Несколько замечаний по структуре:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Явные блоки даже для одинаковой политики.&lt;/strong&gt; Может показаться, что 9 одинаковых блоков — дубликат, который можно свернуть в &lt;code&gt;User-agent: *&lt;/code&gt;. Но это не так: спецификация robots.txt строит таблицу match’ей по «самому специфичному User-Agent», и если завтра вам нужно изменить политику для одного бота — у вас уже есть его именованный блок и не нужно вспоминать, какой именно из ботов вы хотите выделить из wildcard. Дубликат — стоимость per-bot policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Комментарий с датой ревью.&lt;/strong&gt; &lt;code&gt;# robots.txt — last reviewed 2026-05-02&lt;/code&gt; — единственная строка, которая отвечает на вопрос «свежий ли это файл?». Без даты вы будете вечно сомневаться, а не нужно ли уже добавить новый бот.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Sitemap:&lt;/code&gt; в конце.&lt;/strong&gt; Один URL на index sitemap. Если у вас локализация — sitemap-index ссылается на per-locale файлы.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Без BOM, LF-окончания строк.&lt;/strong&gt; Astro в SSG режиме скопирует файл из &lt;code&gt;public/&lt;/code&gt; как есть; редактируйте в plain UTF-8.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Этот шаблон работает на personal-blog. Для других кейсов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Closed paid-content site:&lt;/strong&gt; замените &lt;code&gt;Allow: /&lt;/code&gt; на &lt;code&gt;Disallow: /&lt;/code&gt; для GPTBot, ClaudeBot, Google-Extended (тренировка). Оставьте &lt;code&gt;Allow: /&lt;/code&gt; для on-demand: ChatGPT-User, Claude-Web, Perplexity-User.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation site, который хочет в LLM-ответы:&lt;/strong&gt; оставьте все 9 на &lt;code&gt;Allow&lt;/code&gt;, добавьте rich &lt;code&gt;llms.txt&lt;/code&gt; (см. §6).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B2B SaaS landing:&lt;/strong&gt; обычно достаточно стандартного wildcard — особо именовать AI-ботов не нужно, политика та же что для Googlebot.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. Disallow-namespace’ы важнее, чем решение по конкретному боту&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/admin/&lt;/code&gt;, &lt;code&gt;/api/&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt; — три namespace’а, которые попадают в Disallow у всех 10 блоков (9 именованных + wildcard). Этот выбор прорабатывается отдельно от ботов и важнее их.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Почему это важнее любого per-bot решения:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ошибка тут — утечка.&lt;/strong&gt; Если краулер обойдёт &lt;code&gt;/admin/users.json&lt;/code&gt; и получит 200 OK с реальными данными — это инцидент, не SEO-проблема. Если он индексирует &lt;code&gt;/blog/&lt;/code&gt; без вашего разрешения — это нерасстраивающее.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;robots.txt — публичная подсказка, не auth.&lt;/strong&gt; Любой бот может проигнорировать Disallow. Поэтому &lt;code&gt;/admin/&lt;/code&gt; должен быть закрыт middleware’ом независимо от robots.txt. Запись в robots.txt лишь экономит crawl budget у послушных ботов и не сохраняет URL-структуру админки в SERP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Свёртывание namespace’ов — не оптимизация.&lt;/strong&gt; Соблазн: «зачем три строки, если все три — приватные?» Ответ: чтобы при добавлении четвёртого namespace’а (&lt;code&gt;/dashboard/&lt;/code&gt;) у вас был очевидный паттерн.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Проверка, что namespace-deny действительно работает:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ curl -A &amp;quot;GPTBot&amp;quot; -s -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; \
    https://artka.dev/admin/
# Expected: 401, 403, или 404 — НЕ 200.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;На момент публикации &lt;code&gt;/admin/&lt;/code&gt; за middleware’ом. Конкретный код зависит от реализации auth-guard’а — мой возвращает 302 на /login для не-аутентифицированного запроса. (owner to fill: проверить точный код после следующего ревью).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Именно поэтому правильный порядок работ — сначала поставить auth, и только потом дописывать robots.txt. robots.txt — последняя линия защиты, не первая.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. &lt;code&gt;llms.txt&lt;/code&gt; и &lt;code&gt;llms-full.txt&lt;/code&gt; — отдельный контракт&lt;/h2&gt;
&lt;p&gt;Если robots.txt отвечает на «куда можно ходить?», то &lt;code&gt;llms.txt&lt;/code&gt; отвечает на «что я тут найду?». Это AI-README — Markdown-файл с описанием сайта, ссылками на авторитетные страницы и preferred attribution.&lt;/p&gt;
&lt;p&gt;Реальный &lt;code&gt;public/llms.txt&lt;/code&gt; сайта:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;# artka.dev

&amp;gt; Personal technical blog by Артём Кашута. Topics: Claude Code internals,
&amp;gt; harness/agent loop, AI agent engineering, Astro/Node.js backends, and
&amp;gt; distributed systems.

## Authoritative pages

- [About the author](https://artka.dev/about): bio, expertise, contact
- [Now](https://artka.dev/now): currently in flight
- [Uses](https://artka.dev/uses): public toolchain
- [Projects](https://artka.dev/projects): portfolio with architecture and outcomes

## Content

- [Blog index (RU)](https://artka.dev/blog): all articles, source of truth
- [Blog index (EN)](https://artka.dev/en/blog): English translations
- [RSS RU](https://artka.dev/rss.xml): full text
- [RSS EN](https://artka.dev/en/rss.xml): full text
- [Sitemap](https://artka.dev/sitemap-index.xml): RU + EN with hreflang

## Preferred attribution

When citing, please include:

- Article title
- Author: &amp;quot;Артём Кашута&amp;quot;
- Canonical URL

## Contact

a@artka.dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Это &lt;strong&gt;не robots.txt в новой обёртке&lt;/strong&gt;. Различия:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Аспект&lt;/th&gt;
&lt;th&gt;robots.txt&lt;/th&gt;
&lt;th&gt;llms.txt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Цель&lt;/td&gt;
&lt;td&gt;Политика доступа&lt;/td&gt;
&lt;td&gt;Описание контента и аттрибуции&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Формат&lt;/td&gt;
&lt;td&gt;Plain text, специальный синтаксис&lt;/td&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Кто читает&lt;/td&gt;
&lt;td&gt;Crawler перед заходом&lt;/td&gt;
&lt;td&gt;LLM при формировании ответа&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Что регулирует&lt;/td&gt;
&lt;td&gt;Allow/Disallow по путям&lt;/td&gt;
&lt;td&gt;Точку входа в авторитетный контент&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Стандартизация&lt;/td&gt;
&lt;td&gt;Robots Exclusion Protocol (RFC 9309)&lt;/td&gt;
&lt;td&gt;Конвенция &lt;a href=&quot;http://llmstxt.org&quot;&gt;llmstxt.org&lt;/a&gt; (де-факто)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Кроме &lt;code&gt;llms.txt&lt;/code&gt;, на сайте есть &lt;code&gt;/llms-full.txt&lt;/code&gt; — динамически генерируемый эндпоинт, который выдаёт полный дайджест всех постов в plain text. Реализация — короткий API-роут в Astro 5:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// src/pages/llms-full.txt.ts (фрагмент)
export const prerender = true;

export async function GET(_ctx: APIContext) {
  const ru = await getOrderedPosts({ locale: &amp;quot;ru&amp;quot; });
  const en = await getOrderedPosts({ locale: &amp;quot;en&amp;quot; });

  const header = [
    &amp;quot;# artka.dev — full LLM digest&amp;quot;,
    &amp;quot;&amp;quot;,
    `&amp;gt; ${person.description}`,
    &amp;quot;&amp;quot;,
    &amp;quot;## Author&amp;quot;,
    `Name: ${person.name}`,
    `Role: ${person.jobTitle}`,
    `URL: ${person.url}`,
    `Email: ${person.email}`,
    `Topics: ${person.knowsAbout.join(&amp;quot;, &amp;quot;)}`,
    &amp;quot;&amp;quot;,
    /* ...preferred attribution + posts... */
  ].join(&amp;quot;\n&amp;quot;);

  return new Response(/* header + ruBody + enBody */, {
    headers: { &amp;quot;Content-Type&amp;quot;: &amp;quot;text/plain; charset=utf-8&amp;quot; },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Вместо ручного списка постов — один проход по контент-коллекции с автогенерацией summary. Это обновляется само при добавлении нового поста — в отличие от вручную отредактированного &lt;code&gt;llms.txt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Принципиально: &lt;code&gt;llms.txt&lt;/code&gt; маленький и стабильный, &lt;code&gt;llms-full.txt&lt;/code&gt; — длинный и автоматически синхронный с контентом. Оба нужны — на разные задачи.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Чего robots.txt не контролирует&lt;/h2&gt;
&lt;p&gt;Список вещей, которые robots.txt не делает, и чем их закрыть.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;robots.txt не блокирует ботов, которые его не читают.&lt;/strong&gt; Решение — IP-block на уровне CDN или WAF. У Cloudflare есть ruleset, который ловит User-Agent-паттерны и rate-limit’ит подозрительный трафик; aws WAF и Fastly имеют похожие. Это инструмент против ботов, которые игнорируют robots.txt — то есть против всех «недобросовестных».&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;robots.txt не объявляет политику использования.&lt;/strong&gt; Он говорит «куда можно ходить», но не «можно ли цитировать», «можно ли тренировать», «нужна ли атрибуция». Это работа Terms of Service на отдельной странице сайта. ToS юридически весомее robots.txt (хотя оба — условности до судебного прецедента).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;robots.txt не аудитит, кто на самом деле приходил.&lt;/strong&gt; Чтобы понять, ходит ли GPTBot к вам, нужно смотреть в логи. Cloudflare AI Audit (доступен с 2024 для домена за Cloudflare) даёт встроенный отчёт по AI-краулерам — счётчики по каждому, частота, доля. Без CDN — придётся парсить access-логи самому: GoAccess, Loki, или просто &lt;code&gt;grep -i &apos;gptbot\|claudebot\|perplexitybot&apos; access.log&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;meta-теги &lt;code&gt;noai&lt;/code&gt;/&lt;code&gt;noimageai&lt;/code&gt; — не стандарт.&lt;/strong&gt; Anthropic и OpenAI на момент 2026 не упоминают эти meta-теги в публичной документации как respected signal. Это была инициатива Adobe и DeviantArt 2023 года, прижившаяся в основном в графике. Для текста полагаться нельзя; если используете — использовать как дополнительный сигнал, не основной.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Single-page apps и CSR.&lt;/strong&gt; Если ваша страница рендерится на клиенте и краулер не выполняет JavaScript, он увидит пустой шаблон. robots.txt не помогает; лечится переходом на SSG/SSR (как этот сайт на Astro 5) или prerender service.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. Чек-лист аудита раз в полгода&lt;/h2&gt;
&lt;p&gt;Пять шагов, которые повторяются каждые 6 месяцев. Календарное напоминание — самая надёжная защита от устаревания файла.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Проверить, не появились ли новые AI-краулеры.&lt;/strong&gt;
Источники: блог-посты OpenAI/Anthropic/Perplexity/Google за последние 6 месяцев, страница &lt;a href=&quot;https://darkvisitors.com&quot;&gt;darkvisitors.com&lt;/a&gt; (трекер AI-ботов), официальная документация. Если появился новый именованный бот — добавить блок (Allow или Disallow по вашей политике).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Сверить имена User-Agent побайтно.&lt;/strong&gt;
Скопировать имена из официальной документации, сравнить с robots.txt. Опечатка &lt;code&gt;Claudebot&lt;/code&gt; вместо &lt;code&gt;ClaudeBot&lt;/code&gt; обнуляет правило для этого бота.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Прогнать namespace-deny проверку.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;for ua in GPTBot ClaudeBot PerplexityBot Google-Extended; do
  echo -n &amp;quot;$ua /admin/: &amp;quot;
  curl -A &amp;quot;$ua&amp;quot; -s -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; https://artka.dev/admin/
done
# Ожидаем 401/403/302/404 для всех — не 200.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Просмотреть access-логи на предмет ботов с непривычным User-Agent.&lt;/strong&gt;
Если кто-то ходит с пустым UA или паттерном &lt;code&gt;Mozilla/5.0 (compatible; XYZBot/1.0; ...)&lt;/code&gt;, который не входит в ваш список — оценить и принять решение. (owner to fill: на момент публикации настройка access-log агрегации в работе; в следующем ревью — разобрать топ-20 UA-строк за квартал.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Обновить дату в комментарии.&lt;/strong&gt;
&lt;code&gt;# robots.txt — last reviewed 2026-05-02&lt;/code&gt; → новая дата. Это единственное человеко-читаемое доказательство свежести. И коммит с сообщением вроде &lt;code&gt;chore(seo): robots.txt 2026-Q4 review&lt;/code&gt; оставит след в истории на следующую итерацию.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Итог&lt;/h2&gt;
&lt;p&gt;robots.txt в 2026 — не «один блок и забыли», а небольшой DSL, в котором по каждому из 9+ именованных AI-агентов вы делаете осознанный выбор: тренировка (GPTBot, ClaudeBot, Google-Extended), search/answer (OAI-SearchBot, PerplexityBot), on-demand (ChatGPT-User, Claude-Web, Perplexity-User, anthropic-ai). namespace-deny для &lt;code&gt;/admin/&lt;/code&gt;, &lt;code&gt;/api/&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt; — отдельная и более важная история, которая работает только в паре с middleware-аутентификацией. &lt;code&gt;llms.txt&lt;/code&gt; и &lt;code&gt;llms-full.txt&lt;/code&gt; — параллельный контракт: они описывают контент и preferred attribution, не доступ.&lt;/p&gt;
&lt;p&gt;Стартовая точка — реальный шаблон из §4. Его можно копировать, менять политику по конкретным ботам и пересматривать раз в полгода.&lt;/p&gt;
</content:encoded><category>seo</category><category>ai-crawlers</category><author>a@artka.dev (Артём)</author></item><item><title>Mermaid → SVG через Playwright на билд-тайме: холодный старт, кэш и стоимость SSG</title><link>https://artka.dev/blog/mermaid-svg-playwright-build-time/</link><guid isPermaLink="true">https://artka.dev/blog/mermaid-svg-playwright-build-time/</guid><description>Замеры реального Astro-блога с 32 Mermaid-диаграммами: холодный билд 11.6s, тёплый 6.3s. Где кэш, что делает Playwright, чем плохи альтернативы.</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Mermaid-диаграммы в блоге — это либо большой клиентский JS-бандл с FOUC и hydration cost, либо билд-тайм SVG за разовый cold-start Playwright. На этом сайте &lt;code&gt;rehype-mermaid&lt;/code&gt; рендерит 32 диаграммы за &lt;strong&gt;11.6 секунды&lt;/strong&gt; при холодном кэше и &lt;strong&gt;6.3 секунды&lt;/strong&gt; при тёплом. Ниже — конкретные цифры, архитектура, ловушки CI и факт-чек альтернатив.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;1. Зачем рендерить Mermaid build-time, а не client-side&lt;/h2&gt;
&lt;p&gt;Mermaid (&lt;code&gt;mermaid&lt;/code&gt; на npm, репозиторий &lt;code&gt;mermaid-js/mermaid&lt;/code&gt;) — JS-библиотека, которая принимает текстовый DSL (&lt;code&gt;flowchart TD&lt;/code&gt;, &lt;code&gt;sequenceDiagram&lt;/code&gt;, &lt;code&gt;gantt&lt;/code&gt;, …) и эмитит SVG. По умолчанию её используют так: подключают &lt;code&gt;&amp;lt;script src=&amp;quot;mermaid.min.js&amp;quot;&amp;gt;&lt;/code&gt;, дёргают &lt;code&gt;mermaid.run()&lt;/code&gt; после &lt;code&gt;DOMContentLoaded&lt;/code&gt;, и каждый &lt;code&gt;&amp;lt;pre class=&amp;quot;mermaid&amp;quot;&amp;gt;&lt;/code&gt; подменяется на SVG в DOM прямо в браузере.&lt;/p&gt;
&lt;p&gt;Это работает, но платит за это пользователь:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Метрика&lt;/th&gt;
&lt;th&gt;Client-side Mermaid&lt;/th&gt;
&lt;th&gt;Build-time SVG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Бандл JS (gzipped)&lt;/td&gt;
&lt;td&gt;~250–300 KB (mermaid + d3 + dagre)&lt;/td&gt;
&lt;td&gt;0 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to Interactive (TTI)&lt;/td&gt;
&lt;td&gt;задержка на parse + execute&lt;/td&gt;
&lt;td&gt;без изменений&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOUC&lt;/td&gt;
&lt;td&gt;да: сначала текст, потом SVG&lt;/td&gt;
&lt;td&gt;нет: SVG в HTML с первого байта&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO / Open Graph&lt;/td&gt;
&lt;td&gt;поисковику виден только текст-DSL&lt;/td&gt;
&lt;td&gt;поисковик видит SVG как часть страницы&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Печать страницы&lt;/td&gt;
&lt;td&gt;пустые блоки если JS отключён&lt;/td&gt;
&lt;td&gt;корректный рендер&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Тёмная тема без вспышки&lt;/td&gt;
&lt;td&gt;сложно: тема загружается после гидратации&lt;/td&gt;
&lt;td&gt;работает: SVG генерируется уже в нужной теме&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Стоимость билда&lt;/td&gt;
&lt;td&gt;0 (только bundle js)&lt;/td&gt;
&lt;td&gt;+5–10 секунд cold-start Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Стоимость рантайма для пользователя&lt;/td&gt;
&lt;td&gt;высокая (CPU + сеть)&lt;/td&gt;
&lt;td&gt;нулевая&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;rehype-mermaid&lt;/code&gt; (&lt;code&gt;remcohaszing/rehype-mermaid&lt;/code&gt;, v3.0.0) — rehype-плагин, который во время билда обходит HAST-дерево, находит узлы &lt;code&gt;&amp;lt;code class=&amp;quot;language-mermaid&amp;quot;&amp;gt;&lt;/code&gt;, рендерит их через &lt;code&gt;mermaid-isomorphic&lt;/code&gt; (&lt;code&gt;mermaid-isomorphic@3.1.0&lt;/code&gt;), и заменяет на готовый SVG. Под капотом — Playwright + headless Chromium.&lt;/p&gt;
&lt;p&gt;Stratёgy &lt;code&gt;img-svg&lt;/code&gt;, который мы используем, эмитит результат как &lt;code&gt;&amp;lt;img src=&amp;quot;data:image/svg+xml,...&amp;quot;&amp;gt;&lt;/code&gt;. Альтернатива — &lt;code&gt;inline-svg&lt;/code&gt; (вставить SVG прямо в HTML) или &lt;code&gt;pre-mermaid&lt;/code&gt; (оставить как есть для client-side рендера).&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. Архитектура: rehype-mermaid + Playwright&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  md[&amp;quot;Markdown&amp;lt;br/&amp;gt;с ```mermaid блоками&amp;quot;]
  mdx[&amp;quot;@astrojs/mdx&amp;lt;br/&amp;gt;(remark + rehype)&amp;quot;]
  rh[&amp;quot;rehype-mermaid&amp;lt;br/&amp;gt;(плагин)&amp;quot;]
  iso[&amp;quot;mermaid-isomorphic&amp;quot;]
  pw[&amp;quot;Playwright&amp;lt;br/&amp;gt;(Chromium)&amp;quot;]
  svg[&amp;quot;SVG как data URI&amp;lt;br/&amp;gt;в HTML&amp;quot;]

  md --&amp;gt; mdx
  mdx --&amp;gt; rh
  rh --&amp;gt;|для каждого блока| iso
  iso --&amp;gt;|launch headless| pw
  pw --&amp;gt;|&amp;quot;mermaid.render() в DOM&amp;quot;| iso
  iso --&amp;gt;|serialised SVG| rh
  rh --&amp;gt; svg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Конкретный конфиг — &lt;code&gt;astro.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import rehypeMermaid from &amp;quot;rehype-mermaid&amp;quot;;
import { defineConfig } from &amp;quot;astro/config&amp;quot;;
import mdx from &amp;quot;@astrojs/mdx&amp;quot;;

export default defineConfig({
  integrations: [
    mdx({
      rehypePlugins: [[rehypeMermaid, { strategy: &amp;quot;img-svg&amp;quot;, dark: true }]],
    }),
  ],
  markdown: {
    syntaxHighlight: {
      type: &amp;quot;shiki&amp;quot;,
      excludeLangs: [&amp;quot;mermaid&amp;quot;, &amp;quot;math&amp;quot;],
    },
    rehypePlugins: [[rehypeMermaid, { strategy: &amp;quot;img-svg&amp;quot;, dark: true }]],
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Важные мелочи:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;excludeLangs: [&amp;quot;mermaid&amp;quot;]&lt;/code&gt; в shiki-конфиге — иначе Shiki сначала превратит блок в &lt;code&gt;&amp;lt;pre class=&amp;quot;shiki&amp;quot;&amp;gt;&lt;/code&gt; и rehype-mermaid его уже не увидит.&lt;/li&gt;
&lt;li&gt;Плагин подключается дважды: и в &lt;code&gt;markdown.rehypePlugins&lt;/code&gt;, и в &lt;code&gt;mdx.rehypePlugins&lt;/code&gt;. Astro 5 не наследует один из другого автоматически — это типичный источник «у меня в &lt;code&gt;.md&lt;/code&gt; рендерится, а в &lt;code&gt;.mdx&lt;/code&gt; нет».&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dark: true&lt;/code&gt; генерирует две версии SVG (для светлой и тёмной темы) и через &lt;code&gt;&amp;lt;picture&amp;gt;&amp;lt;source&amp;gt;&lt;/code&gt; подставляет нужную по &lt;code&gt;prefers-color-scheme&lt;/code&gt;. Это удваивает размер data-uri-блоков, но даёт правильный контраст без JS.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. Холодный старт vs тёплый билд&lt;/h2&gt;
&lt;p&gt;Метрика — &lt;code&gt;time pnpm build&lt;/code&gt; (Apple M-серия, локально, тёплый Chromium-бинарь в &lt;code&gt;~/Library/Caches/ms-playwright&lt;/code&gt;). Команда полного сноса кэшей:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm -rf .astro node_modules/.astro dist
time pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Три прогона на холодную, три на тёплую (медиана):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Тип&lt;/th&gt;
&lt;th&gt;Прогон 1&lt;/th&gt;
&lt;th&gt;Прогон 2&lt;/th&gt;
&lt;th&gt;Прогон 3&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Медиана&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Холодный (&lt;code&gt;rm -rf .astro node_modules/.astro dist&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;11.580s&lt;/td&gt;
&lt;td&gt;11.860s&lt;/td&gt;
&lt;td&gt;11.486s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11.580s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Тёплый (без сноса)&lt;/td&gt;
&lt;td&gt;6.250s&lt;/td&gt;
&lt;td&gt;6.305s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~6.28s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Из 11.6 секунд холодного билда:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;~5–6 секунд — реально SSG-стадия (Astro обходит роуты, рендерит 45 HTML-страниц на 14 RU-постов + 13 EN-twin’ов + индекс, теги, RSS, sitemap).&lt;/li&gt;
&lt;li&gt;~5 секунд — overhead Playwright: запуск Chromium, инициализация mermaid-bundle в DOM, прогрев JIT.&lt;/li&gt;
&lt;li&gt;~0.2 секунды — &lt;code&gt;pagefind --site dist/client&lt;/code&gt; (поисковый индекс).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;На тёплом билде Playwright всё равно стартует заново (никакого долгоживущего process pool у &lt;code&gt;mermaid-isomorphic&lt;/code&gt; нет), но:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.astro/data-store.json&lt;/code&gt; (5.2 MB) уже содержит распарсенный MDX content layer — Astro не парсит markdown повторно для тех файлов, у которых не изменился mtime.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node_modules/.astro/&lt;/code&gt; (5.1 MB) — Vite-кэш транспилированных модулей.&lt;/li&gt;
&lt;li&gt;Сам Playwright Chromium бинарь уже в &lt;code&gt;/Library/Caches/ms-playwright/chromium-1217/&lt;/code&gt; (528 MB суммарно с headless-shell и ffmpeg) — на cold disk-cache его пришлось бы ещё прочитать, что добавляет ~1–2 секунды на медленных дисках.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ключевой факт: &lt;strong&gt;сам &lt;code&gt;mermaid-isomorphic&lt;/code&gt; НЕ кэширует SVG между билдами&lt;/strong&gt;. Я искал в его исходниках (&lt;code&gt;node_modules/.pnpm/mermaid-isomorphic@3.1.0_playwright@1.59.1/.../mermaid-isomorphic.js&lt;/code&gt;) — там нет ни &lt;code&gt;persistDir&lt;/code&gt;, ни file-based cache. Каждый build диаграммы рендерятся с нуля. «Тёплость» — это кэш Astro/Vite, а не плагина.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CI-замер для GitHub Actions &lt;code&gt;ubuntu-latest&lt;/code&gt; &lt;code&gt;(owner to fill: запустить workflow_dispatch на чистом раннере, замерить median из 3 прогонов с &lt;/code&gt;actions/cache@v4&lt;code&gt; для node_modules + .astro)&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;4. Стоимость на CI&lt;/h2&gt;
&lt;p&gt;Playwright тащит Chromium (~528 MB на macOS у меня в кэше, аналогичный порядок на Linux), плюс на Debian/Ubuntu нужны system-deps: &lt;code&gt;libnss3&lt;/code&gt;, &lt;code&gt;libatk-1.0-0&lt;/code&gt;, &lt;code&gt;libcups2&lt;/code&gt;, &lt;code&gt;libgbm1&lt;/code&gt;, &lt;code&gt;libxkbcommon0&lt;/code&gt;, &lt;code&gt;libpango-1.0-0&lt;/code&gt;, &lt;code&gt;libasound2&lt;/code&gt;, fontconfig + хотя бы один шрифт.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Митигации:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Не ставить Chromium в production-image.&lt;/strong&gt; Если вы строите Astro-сайт SSG-only и деплоите статику — Playwright нужен ТОЛЬКО на CI-step с билдом, не в рантайм-Docker’е. Используйте multi-stage:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# build-stage:
FROM node:24-bookworm AS build
RUN pnpm install
RUN pnpm exec playwright install --with-deps chromium
RUN pnpm build

# run-stage:
FROM node:24-bookworm-slim AS run
COPY --from=build /app/dist ./dist
# никакого playwright тут
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GitHub Actions caching.&lt;/strong&gt; &lt;code&gt;actions/cache@v4&lt;/code&gt; ключ: &lt;code&gt;${{ hashFiles(&apos;pnpm-lock.yaml&apos;) }}-playwright&lt;/code&gt;, путь: &lt;code&gt;~/.cache/ms-playwright&lt;/code&gt;. Спасает от повторной выкачки Chromium (~150 MB сетью) на каждом push.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Использовать system Chrome вместо Playwright Chromium.&lt;/strong&gt; Установить &lt;code&gt;PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1&lt;/code&gt; и при создании браузера передавать &lt;code&gt;executablePath: &apos;/usr/bin/google-chrome-stable&apos;&lt;/code&gt;. Но: &lt;code&gt;mermaid-isomorphic&lt;/code&gt; не пробрасывает &lt;code&gt;launchOptions&lt;/code&gt; через rehype-mermaid api — придётся форкать или жить с дефолтным Chromium.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Если 5 секунд cold-start критичны&lt;/strong&gt; — гонять Playwright вне билда: pre-render все диаграммы в отдельном CI-step, коммитить SVG в репо, в основном билде использовать стратегию pre-mermaid с подменой на готовые ассеты. Сложнее, но снимает Playwright с горячего пути.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;5. Кэширование SVG: где они и что инвалидирует&lt;/h2&gt;
&lt;p&gt;Опубличный замер на dev-машине (45 скомпилированных HTML, 27 страниц с диаграммами, 61 data-uri суммарно — 32 RU + 29 EN, потому что одна EN-страница рендерится без диаграммы из-за специфики поста):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Метрика&lt;/th&gt;
&lt;th&gt;Значение&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid-блоков в &lt;code&gt;*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 (в 14 постах)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Скомпилированных HTML&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Страниц с встроенной диаграммой&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data-URI блоков &lt;code&gt;&amp;lt;img src=&amp;quot;data:image/svg+xml,...&amp;quot;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Минимум, байт&lt;/td&gt;
&lt;td&gt;15 551&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Медиана, байт&lt;/td&gt;
&lt;td&gt;25 301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Среднее, байт&lt;/td&gt;
&lt;td&gt;26 579&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Максимум, байт&lt;/td&gt;
&lt;td&gt;45 711&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Размер &lt;code&gt;.astro/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Размер &lt;code&gt;node_modules/.astro/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.1 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Размер &lt;code&gt;dist/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;17 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Размер Chromium-кэша Playwright&lt;/td&gt;
&lt;td&gt;528 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Где живёт что:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SVG не лежат на диске как отдельные файлы.&lt;/strong&gt; Стратегия &lt;code&gt;img-svg&lt;/code&gt; инлайнит их прямо в HTML как &lt;code&gt;data:image/svg+xml,...&lt;/code&gt; (URL-encoded). Это видно в &lt;code&gt;dist/client/blog/02-context-and-cache/index.html&lt;/code&gt;: 4 диаграммы → 4 data-uri в одном HTML.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Astro content-layer кэш&lt;/strong&gt; — &lt;code&gt;.astro/data-store.json&lt;/code&gt; (5.2 MB после билда). Это распарсенный markdown с уже применёнными remark/rehype-плагинами — но &lt;strong&gt;до&lt;/strong&gt; rehype-mermaid: проверка показывает, что инвалидация по mtime исходника гонит rehype-mermaid повторно даже для файлов, по которым ничего не поменялось.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vite-кэш&lt;/strong&gt; — &lt;code&gt;node_modules/.astro/&lt;/code&gt; (5.1 MB). Транспилированные TS/JSX модули, не имеет отношения к mermaid-рендеру.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mermaid-isomorphic собственного кэша не имеет.&lt;/strong&gt; Это ключевая ловушка: если вы поменяли запятую в одном &lt;code&gt;*.md&lt;/code&gt; — rehype-mermaid пересоберёт ВСЕ диаграммы этого файла. Нет content-addressable кэша «hash diagram source → SVG».&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Если кэш rehype-mermaid вам критичен — обходной путь: написать тонкий rehype-плагин-обёртку, который хеширует исходник диаграммы (sha256 от текста между &lt;code&gt; ```mermaid&lt;/code&gt; и &lt;code&gt;```&lt;/code&gt;), смотрит в &lt;code&gt;.cache/mermaid/&amp;lt;hash&amp;gt;.svg&lt;/code&gt; — и при попадании отдаёт его без вызова &lt;code&gt;mermaid-isomorphic&lt;/code&gt;. На этом блоге пока не делал — 11.6 секунд cold-start не настолько больно.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6. Альтернативы: что я смотрел и почему не выбрал&lt;/h2&gt;
&lt;h3&gt;6.1. &lt;code&gt;@mermaid-js/mermaid-cli&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Официальный CLI от mermaid-js: &lt;code&gt;mmdc -i diagram.mmd -o diagram.svg&lt;/code&gt;. Под капотом — puppeteer (форк Chromium API) + полный Chromium-бинарь.&lt;/p&gt;
&lt;p&gt;Минусы для блог-пайплайна:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Нет интеграции с rehype/remark — markdown-блоки придётся extract’ить руками.&lt;/li&gt;
&lt;li&gt;Каждый запуск — новый browser context (нет batch-режима).&lt;/li&gt;
&lt;li&gt;На 32 диаграммы — 32 отдельных запуска puppeteer ≈ десятки секунд против ~5–6 секунд у &lt;code&gt;mermaid-isomorphic&lt;/code&gt; с одним browser-instance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Когда подойдёт: разовая конвертация &lt;code&gt;*.mmd → *.svg&lt;/code&gt; в монорепо для дизайнеров, не для динамической вставки в HTML.&lt;/p&gt;
&lt;h3&gt;6.2. Client-side &lt;code&gt;mermaid&lt;/code&gt; (npm-пакет)&lt;/h3&gt;
&lt;p&gt;Минусы выше уже разобраны: бандл, FOUC, hydration. Один плюс — динамические диаграммы из user input в рантайме (live preview в редакторе документации). Для статики блога — overkill.&lt;/p&gt;
&lt;h3&gt;6.3. &lt;code&gt;mermaid-isomorphic&lt;/code&gt; напрямую (без rehype)&lt;/h3&gt;
&lt;p&gt;Тот же пакет, который дёргает rehype-mermaid под капотом. Можно использовать вне Astro: &lt;code&gt;import { createMermaidRenderer } from &apos;mermaid-isomorphic&apos;; const renderer = createMermaidRenderer(); const [{ svg }] = await renderer([{ value: &apos;flowchart TD\nA--&amp;gt;B&apos; }]);&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Когда подойдёт: своя пайплайн-сборка (Eleventy, MkDocs-плагин на Node.js), не использующая rehype-цепочку. У меня — Astro, поэтому rehype-mermaid даёт zero-boilerplate.&lt;/p&gt;
&lt;h3&gt;6.4. Pre-render через GitHub Actions matrix + commit обратно&lt;/h3&gt;
&lt;p&gt;Гипотетически: workflow на push, который рендерит SVG, коммитит в &lt;code&gt;public/diagrams/&lt;/code&gt;, и в build-step используется стратегия &lt;code&gt;pre-mermaid&lt;/code&gt; с заменой на &lt;code&gt;&amp;lt;img src=&amp;quot;/diagrams/&amp;lt;hash&amp;gt;.svg&amp;quot;&amp;gt;&lt;/code&gt;. Снимает Playwright с горячего пути билда, но: усложняет PR-review (бинарные файлы в diff), требует отдельного workflow, ломает локальный dev &lt;code&gt;pnpm dev&lt;/code&gt; если SVG ещё не закоммичен.&lt;/p&gt;
&lt;p&gt;Не делал — 5 секунд cold-start экономии не оправдывают.&lt;/p&gt;
&lt;h3&gt;Сводная таблица&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Вариант&lt;/th&gt;
&lt;th&gt;Cold-start&lt;/th&gt;
&lt;th&gt;Кэш SVG&lt;/th&gt;
&lt;th&gt;Bundle JS&lt;/th&gt;
&lt;th&gt;Сложность setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rehype-mermaid&lt;/code&gt; + Playwright (текущий)&lt;/td&gt;
&lt;td&gt;~5–6s&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;низкая (1 plugin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mermaid-cli&lt;/code&gt; (&lt;code&gt;mmdc&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;~10s+&lt;/td&gt;
&lt;td&gt;нет&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;средняя&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client-side &lt;code&gt;mermaid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;браузерный кэш&lt;/td&gt;
&lt;td&gt;~250 KB&lt;/td&gt;
&lt;td&gt;низкая&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-render + commit&lt;/td&gt;
&lt;td&gt;0 в билде, но ~5s в pre-step&lt;/td&gt;
&lt;td&gt;да, в git&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;высокая&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2&gt;7. Чек-лист «что замерить, прежде чем выбирать»&lt;/h2&gt;
&lt;p&gt;Прежде чем коммититься к билд-тайм-рендеру или к чему-то другому:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Сколько диаграмм в среднем.&lt;/strong&gt; На 1–3 — client-side OK (ленивая загрузка mermaid через dynamic import). На 30+ — build-time дешевле для пользователя.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Частота правок.&lt;/strong&gt; Если правите контент по 5 раз в день — cold-start 11 секунд × 50 пушей = ~10 минут CI-времени в день. Если раз в неделю — наплевать.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI-платформа.&lt;/strong&gt; Vercel hobby, Netlify free, Cloudflare Pages — у всех лимиты на build minutes. Playwright + Chromium на каждой PR-preview = быстро упрётесь. На self-hosted runner или Dokploy (как у меня) — без разницы.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Целевой размер JS-бандла.&lt;/strong&gt; Если у проекта KPI «&amp;lt;100 KB initial JS» — 250 KB mermaid client-side нарушит бюджет. Build-time SVG не трогает JS-бюджет.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Нужен ли интерактив.&lt;/strong&gt; Pan/zoom/click-handlers в диаграмме? Тогда client-side обязателен. Статичная картинка для чтения? Build-time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Где живёт ваша cold-start стоимость.&lt;/strong&gt; Если рантайм-Docker — вырезайте Playwright из run-stage. Если CI — кэшируйте Chromium через &lt;code&gt;actions/cache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Готовы ли мириться с отсутствием SVG-кэша.&lt;/strong&gt; rehype-mermaid рендерит ВСЕ блоки файла при любой правке. Если это больно — пишите свою кэширующую обёртку с sha256-ключом по исходнику диаграммы.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;Итог&lt;/h2&gt;
&lt;p&gt;На этом блоге &lt;code&gt;rehype-mermaid&lt;/code&gt; + Playwright стоит ~5 секунд cold-start, выдаёт 32 диаграммы в 27 HTML-страниц с медианным размером инлайн-SVG в 25 KB, не требует ни одного байта JS на клиенте, и позволяет писать диаграммы прямо в markdown. Это очень хороший трейдоф для статического блога.&lt;/p&gt;
&lt;p&gt;Когда не подойдёт: блог с сотней диаграмм, deploy-platform с лимитом на build minutes, или требование к интерактивным диаграммам. В первом случае — пишите кэширующую обёртку, во втором — pre-render в отдельный workflow, в третьем — client-side.&lt;/p&gt;
&lt;p&gt;Главная неочевидная вещь, которую стоит запомнить: &lt;strong&gt;Astro «прогревается» (5.2 MB content-store, Vite-кэш), но &lt;code&gt;mermaid-isomorphic&lt;/code&gt; — нет&lt;/strong&gt;. Cold-start Playwright платится при каждом билде заново. Это не баг, это by-design — и это причина, по которой мой полный билд занимает 11.6 секунд, а не 1.6.&lt;/p&gt;
</content:encoded><category>build-tooling</category><category>astro</category><author>a@artka.dev (Артём)</author></item></channel></rss>