# Универсальное ТЗ для SEO-апгрейда — v9.0

> **Версия:** 9.0
> **Дата:** 2026-09-15
> **Область:** Все домены экосистемы Dragon Education
> **Целевая архитектура:** Cloudflare → прокси OpenResty → Lovable
> **Среда проверки:** macOS / BSD utils (GNU-синтаксис `grep -P` НЕ применять)
> **Предыдущая версия:** v8.0 от 2026-03-25 (Cloudflare Worker v6.5)
> **Автор:** AI-SEO архитектор Dragon Education

---

## Содержание

- [§0. Что изменилось v8 → v9](#0-что-изменилось-v8--v9)
- [§1. Архитектура: три файла, три роли](#1-архитектура-три-файла-три-роли)
- [§2. Слои инъекции: где что живёт](#2-слои-инъекции-где-что-живёт)
- [§3. ai-seo.json — данные для серверной инъекции](#3-ai-seojson--данные-для-серверной-инъекции)
- [§4. ai-context.json — контекст для LLM](#4-ai-contextjson--контекст-для-llm)
- [§5. seoConfig.ts, Seo-компонент, per-page метатеги](#5-seoconfigts-seo-компонент-per-page-метатеги)
- [§6. index.html, noscript, JSON-LD](#6-indexhtml-noscript-json-ld)
- [§7. sitemap.xml + robots.txt + cache busting](#7-sitemapxml--robotstxt--cache-busting)
- [§8. Sync-протокол SACMISL](#8-sync-протокол-sacmisl)
- [§9. Кеширование: Cloudflare, OpenResty, purge](#9-кеширование-cloudflare-openresty-purge)
- [§10. Чеклист проверки — BSD-совместимый](#10-чеклист-проверки--bsd-совместимый)
- [§11. Канон-соответствие публичных данных](#11-канон-соответствие-публичных-данных)
- [§12. Правила по умолчанию](#12-правила-по-умолчанию)
- [§13. Диагностическое дерево отказов](#13-диагностическое-дерево-отказов)

---

## §0. Что изменилось v8 → v9

| Проблема в v8 | Исправление в v9 |
|---|---|
| Вся архитектура привязана к Cloudflare Worker v6.5 | Worker снят, введён прокси OpenResty. §2 описывает слои независимо от реализации |
| Все curl-проверки на GNU-синтаксисе (`grep -oP`) | **Весь §10 переписан под BSD/macOS.** На Mac `grep -P` не существует |
| Нет проверки метатегов как отдельного слоя | §5.4: запрещённые формулировки живут в `<meta>` и **не находятся** ни Ctrl+F, ни поиском конструктора, ни DevTools по DOM |
| Нет требования уникальности description по маршрутам | §5.3: одинаковый description на всех страницах — частый отказ SPA. Отдельный критерий приёмки |
| Cache-Control описан только как код внутри Worker | §9: три независимых способа — Cloudflare Cache Rules, OpenResty, mini-worker. При снятии Worker TTL пропадает молча |
| Нет разделения «кеш» vs «отсутствие данных» | §13: дерево диагностики. Одинаковый тег на всех путях ≠ кеш |
| Нет контроля достоверности фактов в JSON | §11: стоп-лист канон-нарушений. `ai-context.json` читают LLM — ошибка тиражируется в ответах ИИ |
| Дублирование данных об ИС по страницам | §11.3: единый типизированный источник `ipRegistry.ts`, ручное дублирование запрещено |
| Нет протокола приоритета источников при расхождении | §10.0: при конфликте «обход ассистента» vs «curl владельца» побеждает curl |

---

## §1. Архитектура: три файла, три роли

### `ai-seo.json` — данные для серверной инъекции

| Параметр | Значение |
|---|---|
| Потребитель | слой инъекции (Worker / OpenResty / prerender) |
| Задача | подмена `<title>`, `<meta description>`, `<link canonical>`, OG, JSON-LD в HTML для ботов |
| Формат | `{ default: {...}, pages: [{path, title, description, canonical, ogImage, jsonLd}] }` |
| Расположение | `public/ai-seo.json` → `https://{ORIGIN}.lovable.app/ai-seo.json` |
| Читает | только слой инъекции |

⚠️ **Новое в v9:** без работающего слоя инъекции этот файл — мёртвый груз.
Наполнять его всё равно нужно (это подготовка), но эффекта на индексацию не будет,
пока §2 не закрыт. Проверять живость слоя обязательно — §10.3.

### `ai-context.json` — контекст для LLM

| Параметр | Значение |
|---|---|
| Потребитель | PerplexityBot, GPTBot, ClaudeBot, Anthropic-AI и др. |
| Задача | объяснить ИИ-системам, что это за сайт, какие страницы, какова экосистема |
| Формат | `{ version, lastUpdated, site, ecosystem, pages[], organization, services[], intellectual_property, aiInstructions }` |
| Расположение | `public/ai-context.json` → `https://DOMAIN/ai-context.json` |
| Читает | LLM напрямую через HTTP GET |

⚠️ **Новое в v9:** этот файл **не зависит** от слоя инъекции и работает всегда.
Следствие: при утрате инъекции он остаётся единственным достоверным каналом к LLM —
и одновременно единственным местом, где ошибка в факте попадает прямо в ответы ИИ.
Приоритет исправления данных здесь **выше**, чем у метатегов.

### `seoConfig.ts` — источник для клиентского рендера

| Параметр | Значение |
|---|---|
| Потребитель | React через `<Seo />` + Helmet |
| Задача | метатеги для браузеров с JS |
| Расположение | `src/data/seoConfig.ts` |
| Читает | клиент после загрузки JS |

### Критическое правило

> Три файла — три разных потребителя. `title` и `description` в `seoConfig.ts` и
> `ai-seo.json` обязаны быть **идентичны**. Рассогласование = разные метатеги
> для людей и ботов.

### Дополнительные файлы

| Файл | Роль |
|---|---|
| `ai-context.md` | человекочитаемый аналог `ai-context.json` |
| `ipRegistry.ts` | **новое в v9:** единый источник данных по объектам ИС |
| `ai-lovable-guide.md` | инструкция для AI-агента по домену (опционально) |
| `ai-lovable-update.md` | журнал изменений для AI-агента (опционально) |

---

## §2. Слои инъекции: где что живёт

### Схема запроса в текущей архитектуре

```
Запрос → Cloudflare (CDN, кеш, TTL)
       → прокси OpenResty (маршрутизация, заголовки)
       → Lovable origin (SPA, статика)
```

### Что чем обслуживается

| Слой | Ответственность | Признак в ответе |
|---|---|---|
| Cloudflare | кеш, TTL, purge | `cf-ray`, `cf-cache-status`, `age` |
| OpenResty | заголовки, маршрутизация, возможная подмена `<head>` | `server: openresty` |
| Lovable | HTML, статика, `public/*` | контент |
| React/Helmet | метатеги для JS-браузеров | различие BOT vs HUMAN отсутствует |

Определить архитектуру домена:

```bash
curl -sI https://DOMAIN/ | grep -iE 'server|cf-ray|via|x-powered'
```

### Варианты реализации инъекции

| Вариант | Где | Плюс | Минус |
|---|---|---|---|
| A. Cloudflare Worker + HTMLRewriter | CDN-слой | код v6.5 готов и проверен | два слоя поверх прокси |
| B. OpenResty `sub_filter` / Lua | прокси | один слой, полный контроль | требует разработки |
| C. Prerender для бот-агентов | отдельный сервис | решает и JS-проблему | дороже всех |
| D. Только клиентский рендер | React | ничего не делать | **боты без JS не видят метатеги** |

Вариант D — это состояние отказа, а не решение.

### BOT_AGENTS — список для любого варианта

```
Googlebot, Bingbot, YandexBot, DuckDuckBot, Baiduspider,
Twitterbot, facebookexternalhit, LinkedInBot, WhatsApp,
Slackbot, Telegrambot, Discordbot, Applebot, Applebot-Extended,
GPTBot, ChatGPT-User, Claude-Web, ClaudeBot, Anthropic-AI,
anthropic-ai, PerplexityBot, Google-Extended, Amazonbot,
CCBot, AI2Bot, Cohere-AI, Bytespider, meta-externalagent
```

⚠️ **Новое в v9:** при миграции с Worker на прокси этот список **не наследуется
автоматически**. Проверять каждого агента — §10.4.

---

## §3. ai-seo.json — данные для серверной инъекции

### Структура

```json
{
  "default": {
    "title": "DOMAIN — Главный заголовок",
    "description": "Описание до 160 символов",
    "ogImage": "https://DOMAIN/og-image.jpg",
    "locale": "ru_RU",
    "siteName": "Название сайта"
  },
  "pages": [
    {
      "path": "/",
      "title": "DOMAIN — Главная (до 60 символов)",
      "description": "Уникальное описание до 160 символов",
      "canonical": "https://DOMAIN/",
      "ogImage": "https://DOMAIN/og-image.jpg",
      "jsonLd": { }
    }
  ]
}
```

### Правила полей

| Поле | Правило |
|---|---|
| `path` | строгое соответствие маршруту SPA; без `/index`, без trailing slash (кроме `/`) |
| `title` | до ~60 символов, **уникален для каждого маршрута** |
| `description` | до ~160 символов, **уникально для каждого маршрута** |
| `canonical` | полный URL: `https://DOMAIN` + `path` |
| `ogImage` | осмысленное изображение |
| `noindex` | `true` только для админок и служебных страниц |
| `jsonLd` | Schema.org; обязателен для содержательных страниц |

### JSON-LD по типам страниц

| Тип | Рекомендуемые `@type` |
|---|---|
| Главная / Hub | `WebPage` + `EducationalOrganization` |
| Статья / лендинг | `Article` (`headline`, `author`, `publisher`, `datePublished`, `dateModified`) |
| Курс | `Course` (`provider`, `educationalLevel`, `timeRequired`) |
| Каталог | `CollectionPage` или `ItemList` |
| О проекте | `EducationalOrganization` + `sameAs` |

### StableSEO-страницы

При `stableSEO: true` в `ai-context.json`: нельзя менять **смысл** title и description;
можно улучшать форму (орфография, длина, ясность); обязательно синхронизировать
`canonical` и `ogImage` с реальным URL.

---

## §4. ai-context.json — контекст для LLM

### Обязательные поля верхнего уровня

```json
{
  "version": "3.0",
  "lastUpdated": "2026-09-15",
  "site": { "name": "", "domain": "", "type": "", "language": "ru",
            "targetAudience": [], "uniqueSellingPoints": [] },
  "ecosystem": { },
  "pages": [ { "path": "/", "type": "landing", "title": "", "description": "" } ],
  "organization": { },
  "services": [ ],
  "intellectual_property": { },
  "aiInstructions": { }
}
```

### Правила

1. `lastUpdated` — обновлять при **каждом** деплое.
2. `pages[]` — каждый публичный маршрут SPA.
3. `ecosystem` — все домены с актуальными ролями.
4. `description` здесь может быть длиннее 160 символов.
5. Закрытые страницы включать с `"type": "restricted"`.

### Новое в v9: блок `intellectual_property`

Объекты ИС описываются машиночитаемо, с типом объекта и фактическим статусом:

```json
"intellectual_property": {
  "invention_application": {
    "number": "", "title": "", "filing_date": "", "priority_date": "",
    "status": "экспертиза по существу", "ipc": []
  },
  "pct_application": {
    "number": "", "international_filing_date": "", "priority_date": "",
    "isr_date": "", "status": "международная фаза"
  },
  "software_certificate": {
    "number": "", "title": "", "registration_date": "", "status": "зарегистрировано"
  },
  "industrial_design_application": {
    "number": "", "title": "", "filing_date": "", "locarno": "",
    "status": "экспертиза по существу"
  },
  "trademark": {
    "number": "", "classes": [], "registration_date": "",
    "expiry_date": "", "status": "зарегистрирован"
  }
}
```

**Правило типизации:** тип объекта в имени ключа обязателен.
Смешение `software_certificate` и `industrial_design_application` — типовая ошибка,
приводящая к юридически некорректным публичным утверждениям.

**Правило статуса:** «зарегистрировано» допустимо только для объектов с полученным
охранным документом. Для заявок — «экспертиза по существу», «формальная экспертиза
пройдена», «международная фаза».

### Новое в v9: блок `aiInstructions` с терминологией

```json
"aiInstructions": {
  "summarization": "",
  "keyTopics": [],
  "contentContext": "",
  "terminology_rules": {
    "forbidden": ["запатентовано", "получен патент", "..."],
    "correct": { "ip_status": "", "technology": "", "status_program": "" }
  },
  "method_description": ""
}
```

Смысл: LLM читает файл напрямую, поэтому список запрещённых и корректных формулировок
доставляется прямо в источник, а не остаётся во внутреннем каноне.

### ai-context.md

Человекочитаемый аналог: версия и дата, описание платформы, таблица экосистемы,
описание разделов с путями, контакты, пометка «для AI-систем».
**Синхронизация обязательна** при каждом изменении JSON.

---

## §5. seoConfig.ts, Seo-компонент, per-page метатеги

### 5.1 seoConfig.ts

```typescript
export const seoConfig: Record<string, PageSeoConfig> = {
  "/": { title: "", description: "", keywords: "", canonicalPath: "/",
         type: "website", jsonLd: {} },
};
```

Правила: каждый маршрут имеет запись; для динамических — фабрика
(`getLessonSeo(lesson)`); захардкоженные `<title>` и `<meta>` в компонентах
запрещены; все страницы используют `<Seo {...seoConfig[path]} />`.

### 5.2 Seo.tsx

```tsx
<Helmet>
  <title>{title}</title>
  <meta name="description" content={description} />
  <meta name="robots" content={noindex ? "noindex, nofollow" : "index, follow"} />
  <link rel="canonical" href={canonicalUrl} />
  {/* OG, Twitter, JSON-LD */}
</Helmet>
```

### 5.3 Новое в v9: уникальность description как критерий приёмки

Типовой отказ SPA: `index.html` содержит одно статическое
`<meta name="description">`, Helmet подменяет его только после загрузки JS.
Для ботов без JS все страницы имеют одинаковое описание.

Обязательная проверка:

```bash
for P in / /technology /gos-kurs /rationale; do
  echo "=== $P"
  curl -s "https://DOMAIN$P" | grep -o 'name="description" content="[^"]*"'
done
```

**Критерий:** четыре разные строки. Одинаковые — отказ, а не кеш (см. §13).

В `index.html` дефолтное описание должно быть нейтральным, уровня организации,
не привязанным к конкретной странице.

### 5.4 Новое в v9: метатеги невидимы для поиска по контенту

Запрещённая формулировка может жить в `<meta name="description">` внутри `<head>`.
Такой текст **не находится**:

- поиском Ctrl+F по странице;
- поиском конструктора по контенту проекта;
- поиском в DevTools по DOM-дереву;
- визуальным просмотром страницы.

И **находится** краулером, превью-сервисом, LLM.

Обязательная проверка при каждом аудите терминологии:

```bash
for P in / /technology /gos-kurs /rationale /mission; do
  curl -s "https://DOMAIN$P" | grep -o 'name="description" content="[^"]*"'
  curl -s "https://DOMAIN$P" | grep -o 'property="og:description" content="[^"]*"'
done
```

Правится в `seoConfig.ts` и `ai-seo.json`, **не в тексте страницы**.

---

## §6. index.html, noscript, JSON-LD

### Требования к `<head>`

```html
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
<link rel="canonical" href="https://DOMAIN/" />
<link rel="icon" href="/favicon.png" type="image/png" />
```

### Статические JSON-LD

Минимум: `EducationalOrganization` (с `@id`, `name`, `url`, `logo`, `sameAs`),
`WebSite` (`name`, `url`, `publisher`, `inLanguage`), `BreadcrumbList`.
Опционально `Course`.

### noscript — AI-friendly fallback

Содержит: ровно один `<h1>`; полную навигацию на все публичные страницы;
текстовое описание каждого раздела; корректные статусы объектов ИС.

⚠️ **Новое в v9:** при утрате слоя инъекции `noscript` становится **единственным**
источником контента для ботов без JS. Приоритет поднимается с P2 до P1.

**Критерии:** более 3000 байт; ссылок не меньше числа публичных маршрутов;
ровно один `<h1>`; содержимое не противоречит `ai-context.json`.

---

## §7. sitemap.xml + robots.txt + cache busting

### sitemap.xml

```xml
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://DOMAIN/</loc>
    <lastmod>2026-09-15</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://DOMAIN/ai-context.json?v=20260915</loc>
    <lastmod>2026-09-15</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.3</priority>
  </url>
</urlset>
```

Правила: каждый публичный маршрут (кроме `noindex`); `lastmod` — реальная дата
изменения; AI-файлы включать; `priority` — главная 1.0, основные 0.8, прочие 0.6–0.7.

### Новое в v9: cache busting через параметр версии

Когда доступа к purge нет (например, правки делает конструктор, а CDN администрирует
другой человек), AI-файлы публикуются в sitemap с параметром `?v=YYYYMMDD`,
обновляемым при каждом деплое.

**Ограничение:** боты, запрашивающие файл без параметра, получат кешированную
версию до истечения TTL. Это смягчение, не замена §9.

### robots.txt

```
User-agent: Googlebot
Allow: /

User-agent: GPTBot
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: *
Allow: /

Disallow: /admin/
Disallow: /corp?key=
Disallow: /offer?key=

Sitemap: https://DOMAIN/sitemap.xml
Host: DOMAIN
```

Обязательно: разрешить всех агентов из списка §2; запретить закрытые разделы;
указать Sitemap и Host.

---

## §8. Sync-протокол SACMISL

При добавлении или изменении страницы — строго в этом порядке:

```
ШАГ 1. seoConfig.ts     → запись маршрута: title, description, keywords,
                           canonicalPath, type, jsonLd
ШАГ 2. ai-seo.json      → скопировать title/description/canonical из seoConfig.ts,
                           добавить ogImage и jsonLd. Данные ИДЕНТИЧНЫ шагу 1
ШАГ 3. ai-context.json  → запись в pages[]: path, type, title, description
                           (может быть длиннее). Обновить lastUpdated
ШАГ 4. ai-context.md    → описание раздела, версия, дата
ШАГ 5. index.html       → ссылка в noscript-навигации + <section> с описанием
ШАГ 6. sitemap.xml      → <url> с loc, lastmod, changefreq, priority
ШАГ 7. lastUpdated      → финальная сверка дат во всех файлах
ШАГ 8. purge / TTL      → НОВОЕ В v9: сброс кеша по §9. Без этого шаги 1–7
                           не видны внешним потребителям
```

**Мнемоника:** `SACMISL + P` — Seo → Ai-seo → Context → Md → Index → Sitemap →
LastUpdated → **Purge**.

---

## §9. Кеширование: Cloudflare, OpenResty, purge

### Проблема

CDN кеширует `.json`, `.md`, `.xml` с длинным TTL. После деплоя боты читают старую
версию часами или сутками. **Ключевое наблюдение v9:** в архитектуре v8 заголовок
`Cache-Control: max-age=300` ставился кодом внутри Worker. При снятии Worker он
**пропадает молча** — ошибок нет, деплой проходит, но обновления не доходят
до потребителей.

### Вариант А — Cloudflare Cache Rules (без кода)

Dashboard → зона → Caching → Cache Rules → Create rule:

```
Если:    URI Path содержит /ai-context.json, /ai-context.md,
         /ai-seo.json, /sitemap.xml, /robots.txt
Тогда:   Cache eligibility → Eligible for cache
         Edge TTL    → Override origin → 300 seconds
         Browser TTL → Override origin → 300 seconds
```

Создавать в каждой зоне отдельно.

### Вариант Б — OpenResty / nginx на прокси (предпочтительно)

```nginx
location ~* \.(json|md|xml|txt)$ {
    add_header Cache-Control "public, max-age=300" always;
}
```

Надёжнее варианта А: не зависит от настроек CDN и версионируется вместе с конфигом.

### Вариант В — минимальный Worker только для заголовков

```javascript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    const response = await fetch(request);
    if (/\.(json|md|xml|txt)$/.test(url.pathname)) {
      const headers = new Headers(response.headers);
      headers.set('Cache-Control', 'public, max-age=300');
      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers
      });
    }
    return response;
  }
};
```

15 строк, без HTMLRewriter. Инъекция метатегов решается отдельно (§2).

### Purge после деплоя

Dashboard → Caching → Configuration → Purge Cache → **Custom Purge → By URL**:

```
https://DOMAIN/ai-context.json
https://DOMAIN/ai-context.md
https://DOMAIN/ai-seo.json
https://DOMAIN/sitemap.xml
https://DOMAIN/robots.txt
https://DOMAIN/
https://DOMAIN/technology
```

⚠️ **Не использовать Purge Everything** — сбрасывает кеш изображений и статики,
скорость просядет на часы.

### Purge через API

Токен с правом `Zone.Cache Purge`:

```bash
export CF_TOKEN="ТОКЕН"
ZONE=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=DOMAIN" \
  -H "Authorization: Bearer $CF_TOKEN" | python3 -c "
import json,sys; print(json.load(sys.stdin)['result'][0]['id'])")

curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/purge_cache" \
  -H "Authorization: Bearer $CF_TOKEN" -H "Content-Type: application/json" \
  --data '{"files":["https://DOMAIN/ai-context.json","https://DOMAIN/ai-context.md"]}'
```

---

## §10. Чеклист проверки — BSD-совместимый

### §10.0 Приоритет источников при расхождении

Внешние обходы (в том числе ассистентами и SEO-сервисами) возвращают **кешированный
снимок индекса**, а не живой ответ. `curl` с машины владельца возвращает живой ответ.

> **При расхождении побеждает `curl` владельца.**

Само расхождение — полезный сигнал: оно означает, что правка сделана, но не дошла
до потребителей, то есть требуется §9.

### §10.1 Уникальность метатегов

```bash
for P in / /technology /gos-kurs /rationale; do
  echo "=== $P"
  curl -s --max-time 15 "https://DOMAIN$P" | grep -o 'name="description" content="[^"]*"'
done
```

Ожидаемо: разные строки. Одинаковые → §5.3.

### §10.2 Матрица BOT / HUMAN / 404

```bash
T() { curl -s --max-time 15 ${2:+-H "User-Agent: $2"} "$1" | sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' | head -1; }

for D in DOMAIN1 DOMAIN2; do
  echo "=== $D"
  for P in / /technology /nonexistent-xyz; do
    B=$(T "https://$D$P" "PerplexityBot")
    H=$(T "https://$D$P" "")
    [ "$B" = "$H" ] && V="СОВПАДАЕТ (инъекции нет)" || V="РАЗЛИЧАЕТСЯ (инъекция есть)"
    printf "  %-20s %s\n" "$P" "$V"
    printf "     BOT: %s\n" "${B:-ПУСТО}"
  done
done
```

### §10.3 Живость данных инъекции

```bash
for O in ORIGIN1 ORIGIN2; do
  printf "%-24s ai-seo.json: %s\n" "$O" \
    "$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://$O.lovable.app/ai-seo.json)"
done
```

`200` — данные целы, нужна настройка инъекции. `404` — пересобрать из `seoConfig.ts`.

### §10.4 Полнота набора тегов для бота

```bash
curl -s --max-time 15 -H "User-Agent: PerplexityBot" "https://DOMAIN/technology" > /tmp/bot.html
for TAG in '<title>' 'name="description"' 'rel="canonical"' 'og:title' 'og:description' \
           'og:url' 'og:image' 'application/ld+json' 'name="robots"'; do
  printf "%-28s %s\n" "$TAG" "$(grep -c "$TAG" /tmp/bot.html)"
done
```

### §10.5 Проверка по списку бот-агентов

```bash
for UA in Googlebot Bingbot YandexBot Baiduspider PerplexityBot GPTBot ClaudeBot \
          Applebot Twitterbot facebookexternalhit Telegrambot Slackbot \
          Discordbot CCBot Bytespider meta-externalagent Amazonbot; do
  printf "%-22s %s\n" "$UA" \
    "$(curl -s --max-time 15 -H "User-Agent: $UA" https://DOMAIN/technology | sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p' | head -1)"
done
```

### §10.6 ai-context.json — свежесть и полнота

```bash
curl -s --max-time 15 "https://DOMAIN/ai-context.json" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('lastUpdated:', d.get('lastUpdated','НЕТ'))
print('pages:', len(d.get('pages',[])))
print('ip objects:', len(d.get('intellectual_property',{})))
print('stableSEO:', sum(1 for p in d.get('pages',[]) if p.get('stableSEO')))
"
```

### §10.7 Сверка списков маршрутов

```bash
curl -s "https://DOMAIN/ai-context.json" | python3 -c "
import json,sys; print('\n'.join(p['path'] for p in json.load(sys.stdin)['pages']))" | sort > /tmp/ctx.txt
curl -s "https://DOMAIN/sitemap.xml" | grep -o '<loc>[^<]*' | sed 's|<loc>https://DOMAIN||' | sed 's|?v=[0-9]*||' | sort > /tmp/map.txt
diff /tmp/ctx.txt /tmp/map.txt
```

Ожидаемо: diff пустой.

### §10.8 Кеширование

```bash
for F in ai-context.json ai-context.md sitemap.xml robots.txt; do
  printf "%-20s " "$F"
  curl -sI --max-time 15 "https://DOMAIN/$F" | grep -iE 'cache-control|cf-cache-status|^age:' | tr -d '\r' | tr '\n' ' '
  echo
done
```

Критерий: `max-age` ≤ 600 для json/md/xml.

### §10.9 noscript

```bash
N=$(curl -s --max-time 15 "https://DOMAIN/" | sed -n '/<noscript>/,/<\/noscript>/p')
printf "байт=%s ссылок=%s h1=%s\n" \
  "$(printf '%s' "$N" | wc -c | tr -d ' ')" \
  "$(printf '%s' "$N" | grep -o '<a ' | wc -l | tr -d ' ')" \
  "$(printf '%s' "$N" | grep -o '<h1' | wc -l | tr -d ' ')"
```

### §10.10 Архитектура и техническая гигиена

```bash
echo -n "стек:        "; curl -sI --max-time 15 "https://DOMAIN/" | grep -iE 'server|cf-ray' | tr -d '\r' | tr '\n' ' '; echo
echo -n "404:         "; curl -s -o /dev/null -w '%{http_code}\n' --max-time 15 "https://DOMAIN/nonexistent-xyz"
echo -n "www:         "; curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' --max-time 15 "https://www.DOMAIN/"
echo -n "http:        "; curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' --max-time 15 "http://DOMAIN/"
echo -n "TTFB:        "; curl -s -o /dev/null -w '%{time_starttransfer}s\n' --max-time 20 "https://DOMAIN/"
echo -n "HSTS:        "; curl -sI --max-time 15 "https://DOMAIN/" | grep -ic 'strict-transport-security'
```

⚠️ SPA часто отдаёт `200` на любой путь — для поисковика это бесконечные дубли.
Фиксировать и решать отдельно.

### §10.11 Браузерная проверка

Открыть те же URL без спец-UA: layout не сломан, React загружается, навигация
работает, консоль без ошибок.

---

## §11. Канон-соответствие публичных данных

**Новый раздел v9.** Технически исправный SEO, раздающий недостоверные факты,
вреднее сломанного: ошибка тиражируется эффективнее.

### 11.1 Приоритет выше технических правок

`ai-context.json` читают LLM напрямую. Ошибка в факте попадает в ответы ИИ-сервисов
и живёт там дольше, чем в поисковом индексе, — кеши моделей не сбрасываются purge.

**Порядок:** сначала достоверность данных, потом видимость.

### 11.2 Стоп-лист — автоматическая проверка

```bash
STOP='Сколково с 2019|Запатентовано|запатентованн|получен патент|0,003%|IRR|NPV|ROI|founded.*200[0-9]'
for D in DOMAIN1 DOMAIN2; do
  for F in ai-context.json ai-context.md ai-seo.json robots.txt sitemap.xml; do
    OUT=$(curl -s --max-time 15 "https://$D/$F" | grep -inE "$STOP")
    [ -n "$OUT" ] && { echo "НАРУШЕНИЕ $D/$F:"; echo "$OUT"; }
  done
done
echo "ожидаем: пусто"
```

### 11.3 Единый источник данных по ИС

Данные об объектах ИС дублируются на страницах, в JSON и метатегах. При изменении
статуса правки расходятся.

**Требование:** `src/data/ipRegistry.ts` — единственный типизированный источник.
Компонент `<IpRegistrySection />` рендерит из него. Блок `intellectual_property`
в `ai-context.json` формируется из него же. Ручное дублирование номеров и статусов
запрещено.

### 11.4 Правила формулировок о статусе ИС

| Состояние объекта | Допустимо | Запрещено |
|---|---|---|
| Заявка подана, экспертиза идёт | «заявка на изобретение № N», «проходит экспертизу» | «патент», «запатентовано» |
| Охранный документ получен | «зарегистрировано», «свидетельство № N» | — |
| Формальная экспертиза пройдена | «формальная экспертиза пройдена» | «зарегистрировано» |
| Международная фаза PCT | «международная заявка PCT/…, подана DD.MM.YYYY» | «международный патент» |

**Правило дат PCT:** дата международной подачи и дата пересылки регистрационного
экземпляра в МБ ВОИС — **разные даты**. Публиковать первую.

### 11.5 Единообразие формулировок между страницами

```bash
for P in / /technology /gos-kurs /rationale; do
  echo -n "$P : "
  curl -s --max-time 15 "https://DOMAIN$P" | grep -o 'КЛЮЧЕВОЕ_СЛОВО[^.<]\{0,40\}' | sort -u | head -3
  echo
done
```

Критерий: одна формулировка на всех страницах всех доменов.

---

## §12. Правила по умолчанию

### Принцип «не навреди»

1. Сначала прочитать `ai-context.json` и `ai-context.md` домена.
2. `stableSEO: true` — менять только форму, не смысл.
3. Предыдущий вариант SEO-записи сохранять как backup.

### Принцип «не ломай»

1. Не ломать layout, дизайн, React-логику.
2. Не создавать альтернативные SEO-системы вместо `seoConfig.ts` + `ai-seo.json`.
3. Не дублировать слой инъекции — один активный вариант из §2.

### Принцип «всегда синхронизируй»

1. Любое SEO-изменение — все файлы по §8.
2. `lastUpdated` при каждом деплое.
3. После деплоя — purge по §9.
4. **Новое:** проверка метатегов через `curl`, не через поиск по контенту (§5.4).
5. **Новое:** проверка стоп-листа по §11.2 перед закрытием задачи.

### Порядок создания файлов для нового домена

```
1. public/ai-seo.json        ← данные для инъекции
2. src/data/seoConfig.ts     ← фронтовый конфиг, идентичен п.1 по title/description
3. src/data/ipRegistry.ts    ← НОВОЕ: единый источник по ИС
4. public/ai-context.json    ← для LLM, включая intellectual_property и aiInstructions
5. public/ai-context.md      ← человекочитаемый
6. public/sitemap.xml        ← карта + AI-файлы с ?v=
7. public/robots.txt         ← агенты из §2
8. index.html (noscript)     ← fallback
9. §9 — TTL и purge          ← вне конструктора, руками
```

---

## §13. Диагностическое дерево отказов

**Новый раздел v9.** Симптомы похожи, причины разные.

### Симптом: `<title>` одинаков на всех путях

```
BOT == HUMAN == /nonexistent, все пути → один title
  └─ слой инъекции не работает (§2). Вариант D — состояние отказа
     Проверить: §10.3 — жив ли ai-seo.json
       ├─ 200 → данные целы, настроить инъекцию
       └─ 404 → пересобрать ai-seo.json из seoConfig.ts
```

### Симптом: `description` одинаков на всех путях

```
Не кеш. Отсутствие per-page метатегов в статике (§5.3)
  └─ index.html содержит одно статическое описание,
     Helmet подменяет только после JS
     Решение: заполнить seoConfig.ts + ai-seo.json, восстановить инъекцию
```

### Симптом: правка сделана, но внешние сервисы показывают старое

```
curl владельца → новое  |  внешний обход → старое
  └─ залипший кеш (§9). Порядок:
     1. проверить Cache-Control (§10.8)
     2. если max-age отсутствует → §9 вариант А/Б/В
     3. выполнить purge (§9)
     4. перепроверить через 5–10 мин
```

### Симптом: запрещённая формулировка «не находится» в конструкторе

```
Поиск по контенту, Ctrl+F, DevseTools по DOM → не найдено
curl → найдено
  └─ формулировка в <meta> внутри <head> (§5.4)
     Правится в seoConfig.ts и ai-seo.json, не в тексте страницы
```

### Симптом: LLM воспроизводят устаревший факт

```
  └─ ai-context.json содержит ошибку или кеш модели хранит старое
     1. исправить данные (§11)
     2. purge (§9)
     3. поднять lastUpdated
     4. добавить aiInstructions.terminology_rules (§4)
     Важно: кеши моделей purge не сбрасывает — срок разброса недели
```

### Симптом: команда проверки падает с ошибкой

```
grep: invalid option -- P
  └─ GNU-синтаксис на macOS. BSD grep не поддерживает -P
     Использовать sed -n 's/.*<title>\(.*\)<\/title>.*/\1/p'
     или grep -o '<title>[^<]*' | sed 's/<title>//'
     Либо brew install grep → ggrep -oP
```

---

*Инструкция v9.0 применяется ко всем доменам экосистемы Dragon Education.
Архитектура: Cloudflare → прокси OpenResty → Lovable. Среда проверки: macOS/BSD.
Предыдущая версия v8.0 (Worker v6.5) сохраняется как исторический референс —
её §1, §2, §8, §9 и §9-чеклист неприменимы к текущей архитектуре.*

---

# Результат реализации на Dragon Education Technology

**Дата сверки:** 15.09.2026  
**Канонический домен:** https://dragon-education.ru  
**Версия AI-контура:** 9.0

## Принятые решения

- Статус Сколково: «Участник проекта Сколково с 2023 года, ID 1125355».
- Ограничение 53 символа применяется к `title`; `description` остаётся содержательным, до 160 символов.
- Отдельная страница `/technology/en` не создаётся; синхронизирована существующая английская научная публикация.
- Маркетинговая формулировка: «собственная защищённая технология». Слово «патент» используется только фактологически: заявка РФ № 2025106036 проходит экспертизу по существу, патент не выдан.
- Каноническое имя автора: Тимофеев Вячеслав Анатольевич / Тимофеев В.А.; Vyacheslav Timofeev / V. A. Timofeev; ORCID https://orcid.org/0009-0008-7546-7680.
- Каноническая метрика: 17 тыс.+ оплативших клиентов; общий охват — 22 тыс.+; исследования и разработка — 12 лет, с 2014 года.

## Реализовано

1. Создан единый типизированный реестр пяти объектов интеллектуальной собственности и подключён к Technology и RU/EN научной публикации.
2. Синхронизированы статусы заявки РФ, PCT, свидетельства на программу, заявки на промышленный образец и товарного знака.
3. Все статические SEO-title сокращены до 53 символов; descriptions — до 160 символов, без дублей.
4. `ai-seo.json` и маршрутные данные `ai-context.json` синхронизируются из единого SEO-источника перед каждой сборкой.
5. `ai-context.md`, полнотекстовый поиск и защитный SEO-аудит пересобираются автоматически перед сборкой.
6. В sitemap добавлены версионированные AI-адреса `?v=20260915` для сброса промежуточного кэша.
7. Размещён полный исходный файл задания `/ai-seo-universal-tz-9.md` и этот объединённый файл «ТЗ + результат».
8. Защитный аудит проверяет 78 публичных маршрутов, title ≤53, description ≤160, идентичность SEO/AI-метаданных, юридические формулировки, имя автора и ключевые метрики.

## Публичные AI-адреса после публикации

- https://dragon-education.ru/ai-context.json?v=20260915
- https://dragon-education.ru/ai-context.md?v=20260915
- https://dragon-education.ru/ai-seo.json?v=20260915
- https://dragon-education.ru/ai-seo-universal-tz-9.md?v=20260915
- https://dragon-education.ru/AI-SEO-v9-TZ-AND-RESULT.md?v=20260915

## Проверка

```bash
npm run seo:sync
npm run ai:context
npm run search:index
npm run seo:audit
npm run seo:audit -- --live https://dragon-education.ru
```

Локальный критерий приёмки: 78 маршрутов, 0 ошибок, 0 предупреждений. Внешняя проверка выполняется отдельно после публикации: до неё канонический домен продолжает отдавать предыдущую опубликованную сборку и её кэш.
