
Yandex Webmaster
- 72 installs
- 177 repo stars
- Updated May 10, 2026
- artwist-polyakov/polyakov-claude-skills
yandex-webmaster is a Claude skill that manages sites via the Yandex Webmaster API v4, covering indexing, search queries, sitemaps, recrawl, links, and Alice Share-of-Voice scraping.
About
This skill manages sites through the Yandex Webmaster API v4, covering indexing history, search queries, sitemaps, page recrawl, links, and diagnostics. A developer uses it to monitor how a site performs in Yandex search and to submit URLs for recrawl. It is read-and-add only and additionally scrapes the Alice Share-of-Voice section, which has no public API.
- Manages sites through the Yandex Webmaster API v4: indexing, search queries, sitemaps, and recrawl
- Reports SQI history, popular queries, diagnostics, and internal/external link issues
- Scrapes the Alice / Share of Voice section which has no public API
Yandex Webmaster by the numbers
- 72 all-time installs (skills.sh)
- Ranked #1,217 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
yandex-webmaster capabilities & compatibility
Requires a Yandex Webmaster OAuth token; the Webmaster API is free, and Alice SoV also needs a browser SESSION_ID cookie.
- Capabilities
- seo
- Use cases
- seo · web search
- Pricing
- Bring your own API key
What yandex-webmaster says it does
Работа с Yandex Webmaster API v4. Управление сайтами, анализ индексации, поисковые запросы, переобход страниц, ссылки, фиды.
**No destructive ops** — скилл не удаляет сайты, сайтмапы и фиды. Только чтение и добавление.
npx skills add https://github.com/artwist-polyakov/polyakov-claude-skills --skill yandex-webmasterAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 72 |
|---|---|
| repo stars | ★ 177 |
| Last updated | May 10, 2026 |
| Repository | artwist-polyakov/polyakov-claude-skills ↗ |
What it does
Monitor and manage a site in Yandex Webmaster: indexing, search queries, sitemaps, recrawl, and Share of Voice.
Who is it for?
Monitoring Yandex indexing, search queries, sitemaps, and recrawl for a site.
Skip if: Deleting sites, sitemaps, or feeds; the skill is read-and-add only.
When should I use this skill?
You need to manage a site in Yandex Webmaster, analyze indexing or search queries, submit recrawls, or check Alice Share of Voice.
What you get
Indexing, query, sitemap, link, and Share-of-Voice data is retrieved and URLs are submitted for recrawl.
By the numbers
- over 20 bundled scripts across sites, indexing, queries, links, and sitemaps
Files
yandex-webmaster
Работа с Yandex Webmaster API v4. Управление сайтами, анализ индексации, поисковые запросы, переобход страниц, ссылки, фиды.
Config
Требуется YANDEX_WEBMASTER_TOKEN в config/.env. Scope: webmaster:hostinfo + webmaster:verify. Инструкция: config/README.md.
Для alice.sh (Share of Voice, без публичного API) дополнительно нужен SESSION_ID — cookie из браузера. Подробности в config/README.md и references/ALICE_EFFICIENCY.md.
Philosophy
1. Cache-first — список сайтов, user_id кешируются надолго. Отчёты кешируются по ключу host+params. Диагностика, квоты, статусы — всегда live. 2. Context window hygiene — stdout ограничен 30 строками. Полные данные в TSV/файл. Кеш доступен через grep/rg. 3. Host resolution — все скрипты принимают --host <domain> (поиск по кешу hosts.tsv) или --host-id <id> (прямой ID). Первый вызов hosts.sh автоматически кеширует список. 4. No destructive ops — скилл не удаляет сайты, сайтмапы и фиды. Только чтение и добавление.
Workflow
STOP! Перед любым анализом:
1. Получи список сайтов:
bash scripts/hosts.sh2. Спроси пользователя (если сайт не очевиден из контекста):
"О каком сайте идёт речь?
Укажите домен или host_id из списка."Для поиска по кешу:
bash scripts/hosts.sh --search "example"3. Получи сводку по сайту:
bash scripts/summary.sh --host example.com4. Запускай нужные отчёты по задаче пользователя.
Scripts
Общий паттерн вызова:
bash scripts/<script>.sh --host <domain> [--action <action>] [params...]Управление сайтами
| Script | Description | Key params |
|---|---|---|
hosts.sh | Список сайтов | --search "text", --no-cache |
host_info.sh | Инфо о сайте + владельцы | — |
add_site.sh | Добавить сайт | --url <url> |
verify.sh | Верификация сайта | `--action get\ |
Статистика сайта
| Script | Description | Key params |
|---|---|---|
summary.sh | Сводка: SQI, страницы, проблемы | — |
sqi_history.sh | История SQI | --date-from, --date-to |
diagnostics.sh | Проблемы сайта (live) | — |
Поисковые запросы
| Script | Description | Key params |
|---|---|---|
popular_queries.sh | Топ запросов | --order-by, --device, --limit |
queries_history.sh | История запросов | --query-id (опц.), --device |
query_analytics.sh | Расширенная аналитика (POST) | --text-indicator, --filter-*, --region-ids |
Индексация
| Script | Description | Key params |
|---|---|---|
indexing.sh | История/сэмплы индексации | `--action history\ |
important_urls.sh | Важные URL | `--action list\ |
archive_export.sh | Экспорт всех страниц | `--action start\ |
Страницы в поиске
| Script | Description | Key params |
|---|---|---|
insearch.sh | Страницы в выдаче | `--action history\ |
search_events.sh | Появление/исчезновение | `--action history\ |
Переобход
| Script | Description | Key params |
|---|---|---|
recrawl.sh | Переобход URL | `--action submit\ |
Ссылки
| Script | Description | Key params |
|---|---|---|
internal_links.sh | Битые внутренние ссылки | `--action samples\ |
external_links.sh | Внешние ссылки | `--action samples\ |
Сайтмапы
| Script | Description | Key params |
|---|---|---|
sitemaps.sh | Управление сайтмапами | `--action list\ |
Alice / Share of Voice (SSR scraping)
| Script | Description | Key params |
|---|---|---|
alice.sh | Эффективность в Алисе: SoV timeline, конкуренты, запросы где сайт есть/нет | `--action summary\ |
⚠ Нет публичного API — данные парсятся из window._initData HTML-страницы.ТребуетSESSION_IDcookie вconfig/.env. См. references/ALICE_EFFICIENCY.md.
Фиды и PRO
| Script | Description | Key params |
|---|---|---|
feeds.sh | YML-фиды | `--action list\ |
search_export.sh | PRO SERP экспорт | `--action dates\ |
regions.sh | Справочник регионов | --filter "москва" |
Общие параметры
| Param | Description |
|---|---|
--host <domain> | Домен/URL сайта (поиск по hosts.tsv) |
--host-id <id> | Прямой host_id (формат: http:example.com:80) |
--action <act> | Подкоманда скрипта |
--date-from | Начало периода YYYY-MM-DD (history: default 90 дней назад) |
--date-to | Конец периода YYYY-MM-DD |
--limit N | Число записей |
--offset N | Смещение |
--no-cache | Пропустить кеш |
Кеш-стратегия
Кеш в cache/:
user_id.txt— ID пользователя (permanent)hosts.json+hosts.tsv— список сайтов (permanent, инвалидируется при add/verify)host_*/queries/*.tsv— результаты запросов (session, hash-keyed)host_*/indexing/*.tsv— данные индексации (session)host_*/insearch/*.tsv— данные о поиске (session)host_*/links/*.tsv— данные о ссылках (session)host_*/alice/init.json— распарсенный alice объект, переиспользуется всеми action'амиalice.sh(refresh:--no-cacheили--action fetch)- Диагностика, квоты, статусы переобхода — не кешируются (always live)
Расширенные сценарии
- Добавление и верификация сайтов
- Поисковые запросы и аналитика
- Управление сайтмапами
- Статистика сайта и индексация
- Страницы в поиске
- Переобход страниц
- Битые внутренние ссылки
- Внешние ссылки
- YML-фиды
- PRO SERP экспорт
- Alice / Share of Voice (scraping)
- Расписание врачей (спецификация)
Лимиты API
- Query Analytics: 10 000 запросов/час на домен
- Скрипты автоматически обрабатывают 429 (Retry-After ≤ 60s → retry, иначе fail)
- Лимит сайтов: 1703 на аккаунт
- Лимит фидов: 5000 на сайт, 50 в batch
- Лимит export: макс. 100 комбинаций дат+URL
config/.env
cache/*
!cache/.gitkeep
# OAuth token for Yandex Webmaster API v4 (scopes: webmaster:hostinfo, webmaster:verify)
YANDEX_WEBMASTER_TOKEN="your_oauth_token_here"
# Session_id cookie из браузера (yandex.ru) — нужен для scripts/alice.sh
# (data SoV/Alice не имеет публичного API, парсится из window._initData).
# Берём в DevTools → Application → Cookies → yandex.ru → Session_id (httpOnly).
# Кавычки обязательны: значение содержит '|'.
SESSION_ID="paste_session_id_here"
Получение токена Yandex Webmaster API
Шаг 1: Зарегистрируйте приложение
1. Перейдите на https://oauth.yandex.ru/client/new 2. Укажите название приложения (например, "Claude Webmaster") 3. В разделе "Платформы" выберите "Веб-сервисы" 4. В "Доступы" добавьте ОБА scope:
webmaster:hostinfo— чтение данных о сайтахwebmaster:verify— управление сайтами (добавление, верификация, переобход)
5. Сохраните и запишите client_id
Подробнее: https://yandex.ru/dev/id/doc/ru/register-client
Шаг 2: Получите OAuth токен
Откройте в браузере:
https://oauth.yandex.ru/authorize?response_type=token&client_id=ВАШ_CLIENT_IDПосле авторизации токен будет в URL:
https://oauth.yandex.ru/#access_token=ВАШТОКЕН&token_type=bearer&expires_in=31536000Скопируйте значение access_token.
Шаг 3: Настройте токен
cp config/.env.example config/.envВставьте токен:
YANDEX_WEBMASTER_TOKEN=ваш_токен_здесьПроверка
bash scripts/hosts.shДолжен показать список ваших сайтов в Вебмастере.
Срок жизни токена
Токен действует 1 год. После истечения получите новый по той же ссылке.
SESSION_ID — для Alice (Share of Voice)
Данные раздела «Эффективность → Алиса» не доступны через Webmaster API v4 — они отдаются только при SSR-рендере страницы. Чтобы их парсить (scripts/alice.sh), нужна cookie Session_id авторизованного пользователя.
Где взять: 1. Откройте https://webmaster.yandex.ru в обычном залогиненном Chrome. 2. DevTools (F12) → Application → Cookies → https://yandex.ru 3. Найдите строку Session_id (httpOnly) и скопируйте Value целиком. 4. Вставьте в config/.env в кавычках (значение содержит |):
SESSION_ID="3:1775...|142834...|3:11824..."Cookie живёт долго (несколько месяцев), но при логауте/смене пароля протухает — тогда нужно достать заново.
Документация
- Webmaster API: https://yandex.ru/dev/webmaster/doc/ru/
- OAuth: https://yandex.ru/dev/id/doc/ru/
Alice efficiency / Share of Voice
Раздел Webmaster «Эффективность → Алиса (нейросетевой поиск)»: показывает долю показов сайта в ответах Алисы (Share of Voice), список конкурентов в нише, а также примеры запросов — где наш сайт присутствует и где отсутствует.
Особенность: нет публичного API
Endpoint'ов в Webmaster API v4 для этих данных нет (проверено: /v4/..., /gate/alice/, /gate/sov/ — все 404 либо отсутствуют в схеме).
Данные приходят только при SSR-рендере HTML-страницы:
https://webmaster.yandex.ru/site/{host_id}/efficiency/alice/В HTML встроен <script>window._initData = {...}</script> — там лежит весь объект alice целиком. Параметры query (tab, tableType, onlyWithMySites) управляют только клиентским отображением — переключение вкладок не делает дополнительных запросов. Поэтому одного fetch'а достаточно, дальше парсим четыре среза локально.
Авторизация
Через cookie Session_id обычной браузерной сессии Яндекса. См. установку в config/README.md. OAuth-токен Webmaster для Alice не подходит.
Использование
# Короткая сводка (по умолчанию)
bash scripts/alice.sh --host metallik.ru
# Share of Voice — 12 недельных точек
bash scripts/alice.sh --host metallik.ru --action sov
# Топ сайтов в нише по версии Алисы
bash scripts/alice.sh --host metallik.ru --action competitors
# Запросы где наш сайт встречается в ответах
bash scripts/alice.sh --host metallik.ru --action with-site
# Запросы где наш сайт НЕ встречается (= упущенный спрос)
bash scripts/alice.sh --host metallik.ru --action without-site
# Принудительно перекачать страницу (минуя кеш)
bash scripts/alice.sh --host metallik.ru --action fetch
bash scripts/alice.sh --host metallik.ru --action sov --no-cacheПринимает либо --host <domain> (поиск по cache/hosts.tsv), либо --host-id <id> (формат https:metallik.ru:443). Если используете --host — сначала выполните bash scripts/hosts.sh хотя бы один раз, чтобы наполнился hosts кеш.
Кеш
Скрипт делает один сетевой запрос на хост и сохраняет распарсенный объект alice в:
cache/host_<host_id>/alice/init.jsonВсе последующие действия (sov, competitors, with-site, without-site, summary) читают из кеша. Чтобы перекачать — добавь --no-cache или вызови --action fetch.
TSV-файлы (генерируются по запросу действия):
sov.tsv—date_from / date_to / share / share_pctcompetitors.tsv—rank / urlwith_site.tsv—query / rank / host / url / titlewithout_site.tsv—query / rank / host / url / title
Структура _initData.alice
{
"viewModel": { "tableType": "EXAMPLES", "onlyWithMySites": "ON" },
"alertType": "TOP3", // тип бейджа
"sov": [
{
"dateFrom": "2026-01-12",
"dateTo": "2026-01-18",
"sharePercent": 0.6659699980555219 // доля 0..1
}
// ... 12 недельных точек, ~3 месяца
],
"queries": {
"GENERAL": [ // топ-10 сайтов в нише
{ "url": "https://www.ivd.ru", "favicon": "..." }
],
"EXAMPLES": {
"hasOwnExamples": [ // запросы где есть наш сайт
{
"query": "шибер это что",
"urls": [
{
"url": "https://metallik.ru/articles/...",
"title": "Шибер для дымохода ...",
"host": "https://metallik.ru",
"favicon": "..."
}
]
}
],
"noOwnExamples": [ /* такая же форма */ ]
}
}
}Что считать ошибкой
| Симптом | Причина |
|---|---|
Error: HTTP 4xx ... от urlopen | network/proxy/DNS |
userIsAuth=false ... | Session_id протух — обновить из браузера |
init._initData.alice missing — page changed or wrong host | host_id не существует, либо у сайта нет данных Alice (новый сайт), либо вёрстка изменилась |
window._initData not found | сильное изменение HTML или антибот-страница |
Хрупкость и план миграции
Это scraping SSR-страницы, не API. Любое изменение вёрстки Webmaster ломает парсер. Для повышения надёжности нужно: повторные попытки с jitter, обработка антибот-капчи, сохранение сырого HTML рядом с JSON для отладки, а в перспективе — переезд на тот же webmaster_raw_get стиль (curl + temp file + retry на 429), как сделано для остальных скриптов скилла. Issue по миграции заведено в репозитории.
Расписание врачей (спецификация данных)
Это НЕ endpoint Webmaster API. Это спецификация формата данных, который владелец медицинского сайта должен реализовать на своём сервере. Яндекс периодически обращается к этому API для получения актуального расписания.
Endpoint на стороне сайта
GET /api/slots?page={N}Требования к реализации
- HTTP 200 OK (без редиректов)
- Content-Type:
application/json; charset=utf-8 - Минимум 3 RPS
- Ответ за ≤ 10 секунд
- Полный экспорт за ≤ 10 минут
- Макс. 500 врачей на страницу
Формат ответа
{
"Total": 150,
"Url": "https://example.com/api/slots?page=2",
"DoctorList": [
{
"Id": 12345,
"Price": 2500,
"Slots": {
"clinic_001": [
{
"AmenityIds": [1, 2],
"StartTime": "2025-03-22T10:00:00+03:00",
"FinishTime": "2025-03-22T10:30:00+03:00",
"Data": {}
}
]
}
}
]
}Поля:
Total— общее число врачейUrl— ссылка на следующую страницу (пустая = последняя)DoctorList[].Id— ID врачаDoctorList[].Price— цена приёма (опционально)DoctorList[].Slots— расписание по клиникам- Ключ — ID клиники
StartTime/FinishTime— ISO 8601 с таймзонойAmenityIds— ID удобств (опционально)
Скилл
Для этой спецификации нет скрипта в скилле, так как это не API Вебмастера, а требования к API на стороне владельца сайта.
Внешние ссылки
По умолчанию history запрашивает последние 90 дней. Для полной истории укажите --date-from явно.Сэмплы внешних ссылок
bash scripts/external_links.sh --host example.com --action samples --limit 30Каждая запись: URL-источник (внешний сайт), URL-назначение (ваш сайт), дата обнаружения.
История количества внешних ссылок
bash scripts/external_links.sh --host example.com --action history --date-from 2025-03-01Единственный доступный индикатор: LINKS_TOTAL_COUNT.
Анализ ссылочного профиля
Рекомендуемый сценарий: 1. Получить сэмплы для понимания источников 2. Проверить историю на аномалии (резкий рост/падение) 3. Сопоставить с SQI историей (sqi_history.sh) для корреляции
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/links/external/samples |
| GET | /v4/user/{uid}/hosts/{hid}/links/external/history |
Требуется OAuth scope: webmaster:hostinfo.
YML-фиды
Обзор
YML-фиды используются для улучшенного представления товаров, услуг, объектов недвижимости и др. в поиске Яндекса. Только HTTPS.
Поддерживаемые форматы файлов: .xml, .yml, gzip-архивы.
Типы фидов
| Тип | Описание |
|---|---|
REALTY | Недвижимость |
VACANCY | Вакансии |
GOODS | Товары |
DOCTORS | Врачи |
CARS | Автомобили |
SERVICES | Услуги |
EDUCATION | Образование |
ACTIVITY | Мероприятия |
Список фидов
bash scripts/feeds.sh --host example.com --action listДобавление фида
bash scripts/feeds.sh --host example.com --action add \
--url "https://example.com/feed.yml" \
--type GOODS \
--region-ids "225,213"Регион по умолчанию: 225 (Россия).
Загрузка асинхронная. Проверка статуса:
bash scripts/feeds.sh --host example.com --action add-status --request-id <id>Статусы: OK, IN_PROGRESS.
Изменение регионов
bash scripts/feeds.sh --host example.com --action change \
--url "https://example.com/feed.yml" \
--region-ids "213,2"Основные регионы
| ID | Регион |
|---|---|
| 225 | Россия |
| 213 | Москва |
| 2 | Санкт-Петербург |
| 54 | Екатеринбург |
| 43 | Казань |
| 66 | Нижний Новгород |
| 56 | Новосибирск |
Полный список: Справочник регионов
Лимиты
- Макс. 5000 фидов на сайт
- Макс. 50 фидов в batch-запросе
- Макс. 50 одновременных асинхронных загрузок
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/feeds/list |
| POST | /v4/user/{uid}/hosts/{hid}/feeds/add/start |
| GET | /v4/user/{uid}/hosts/{hid}/feeds/add/info |
| POST | /v4/user/{uid}/hosts/{hid}/feeds/batch/add |
| POST | /v4/user/{uid}/hosts/{hid}/feeds/change |
Битые внутренние ссылки
По умолчанию history запрашивает последние 90 дней. Для полной истории укажите --date-from явно.Сэмплы битых ссылок
bash scripts/internal_links.sh --host example.com --action samples --limit 30Фильтр по типу проблемы:
bash scripts/internal_links.sh --host example.com --action samples --indicator SITE_ERRORТипы индикаторов:
SITE_ERROR— ошибки сайта (404, 500 и т.д.)DISALLOWED_BY_USER— заблокированы пользователем (robots.txt, nofollow)UNSUPPORTED_BY_ROBOT— не поддерживается роботом
Каждая запись содержит: URL-источник, URL-назначение, дату обнаружения.
История битых ссылок
bash scripts/internal_links.sh --host example.com --action history --date-from 2025-03-01Разбивка по всем трём индикаторам за каждую дату.
Рекомендации по исправлению
1. SITE_ERROR — проверить, что целевые страницы доступны (не 404/500) 2. DISALLOWED_BY_USER — убрать из robots.txt или убрать ссылки на заблокированные страницы 3. UNSUPPORTED_BY_ROBOT — проверить формат ссылок (JavaScript, фреймы)
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/links/internal/broken/samples |
| GET | /v4/user/{uid}/hosts/{hid}/links/internal/broken/history |
Требуется OAuth scope: webmaster:hostinfo.
Страницы в поиске
По умолчанию history-скрипты запрашивают последние 90 дней. Для полной истории укажите --date-from явно.История количества страниц в поиске
bash scripts/insearch.sh --host example.com --action history --date-from 2025-03-01Показывает, сколько страниц сайта находится в поисковой выдаче Яндекса на каждую дату.
Сэмплы страниц в поиске
bash scripts/insearch.sh --host example.com --action samples --limit 50Список URL, находящихся в поиске, с заголовками и датой последнего обращения. Макс. 50 000 URL через API.
События поиска
История появлений/исчезновений
bash scripts/search_events.sh --host example.com --action history --date-from 2025-03-01Два индикатора:
APPEARED_IN_SEARCH— новые страницы в поискеREMOVED_FROM_SEARCH— удалённые из поиска
Сэмплы событий
bash scripts/search_events.sh --host example.com --action samples --limit 50Для каждого URL показывает тип события, дату и причину исключения (для удалённых).
Причины исключения (excluded_url_status):
DUPLICATE— дубликатLOW_QUALITY— низкое качествоREDIRECT_NOTSEARCHABLE— редиректROBOTS_URL_ERROR— заблокирован robots.txtNO_INDEX— noindexHTTP_ERROR— HTTP ошибкаHOST_ERROR— ошибка хостаNOT_CANONICAL— не каноничный URLCLEAN_PARAMS— очистка параметров
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/search-urls/in-search/history |
| GET | /v4/user/{uid}/hosts/{hid}/search-urls/in-search/samples |
| GET | /v4/user/{uid}/hosts/{hid}/search-urls/events/history |
| GET | /v4/user/{uid}/hosts/{hid}/search-urls/events/samples |
Переобход страниц
Квота
bash scripts/recrawl.sh --host example.com --action quotaПоказывает дневную квоту и остаток. Данные всегда live (без кеша).
Отправка URL на переобход
bash scripts/recrawl.sh --host example.com --action submit --url "https://example.com/updated-page"Ответ включает task_id и quota_remainder.
Возможные ошибки:
INVALID_URL— невалидный URLURL_ALREADY_ADDED— URL уже в очередиQUOTA_EXCEEDED— дневная квота исчерпана
Статус задачи
bash scripts/recrawl.sh --host example.com --action status --task-id <id>Состояния:
IN_PROGRESS— в процессеDONE— завершеноFAILED— ошибка
Список задач
bash scripts/recrawl.sh --host example.com --action list --limit 20
bash scripts/recrawl.sh --host example.com --action list --date-from 2025-03-01Приоритетный переобход сайтмапа
Отдельная функция — см. SITEMAPS.md (--action recrawl и --action recrawl-limit).
API endpoints
| Метод | Endpoint |
|---|---|
| POST | /v4/user/{uid}/hosts/{hid}/recrawl/queue |
| GET | /v4/user/{uid}/hosts/{hid}/recrawl/queue/{tid} |
| GET | /v4/user/{uid}/hosts/{hid}/recrawl/queue |
| GET | /v4/user/{uid}/hosts/{hid}/recrawl/quota |
PRO SERP экспорт
Экспорт данных по запросам из поисковой выдачи. Часть PRO-функциональности Вебмастера.
Доступные даты
bash scripts/search_export.sh --host example.com --action datesПоказывает, за какие даты доступны данные для экспорта.
Лимиты и квоты
bash scripts/search_export.sh --host example.com --action limitsПоказывает доступные квоты (бесплатные и PRO), использование, период действия.
Инициализация экспорта
bash scripts/search_export.sh --host example.com --action start \
--dates '"2025-03-01","2025-03-02"' \
--paths '"/","/catalog/"' \
--region-ids "213"Ограничение: макс. 100 комбинаций дат + URL.
Параметры:
--dates— даты в формате JSON-строк (из--action dates)--paths— пути страниц (должны начинаться с/)--region-ids— опционально, пустой = все регионы
Проверка статуса
bash scripts/search_export.sh --host example.com --action status --task-id <id>Статусы:
IN_PROGRESS— генерацияSUCCESS— готово, ссылка доступнаFAILED— ошибка
Ссылка на скачивание действительна 24 часа.
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/pro/serp/dates |
| GET | /v4/user/{uid}/hosts/{hid}/pro/limits |
| POST | /v4/user/{uid}/hosts/{hid}/pro/serp/queries/download/ |
| GET | /v4/user/{uid}/hosts/{hid}/pro/serp/queries/download/{tid} |
Поисковые запросы
Популярные запросы
bash scripts/popular_queries.sh --host example.com --order-by TOTAL_CLICKS --limit 100Возвращает топ-3000 запросов за последнюю неделю (макс. 500 за запрос).
Индикаторы:
TOTAL_SHOWS— показы в выдачеTOTAL_CLICKS— кликиAVG_SHOW_POSITION— средняя позиция показаAVG_CLICK_POSITION— средняя позиция клика
Фильтр по устройствам: ALL, DESKTOP, MOBILE_AND_TABLET, MOBILE, TABLET.
История запросов
По умолчанию запрашиваются последние 90 дней. Для другого диапазона укажите --date-from явно.Агрегат по всем запросам
bash scripts/queries_history.sh --host example.com --date-from 2025-03-01 --date-to 2025-03-15Конкретный запрос
bash scripts/queries_history.sh --host example.com --query-id <id> --date-from 2025-03-01query_id берётся из вывода popular_queries.sh.
По умолчанию возвращает данные за последние 7 дней.
Query Analytics (расширенная аналитика)
POST-endpoint с фильтрами. Данные за последние 14 дней (фиксированный период).
bash scripts/query_analytics.sh --host example.com \
--text-indicator QUERY \
--filter-text "купить" \
--filter-impressions ">100" \
--filter-position "<10" \
--device MOBILE \
--limit 50Типы фильтров
Текстовые (по --filter-text):
TEXT_CONTAINS(по умолчанию)TEXT_MATCH— точное совпадениеTEXT_DOES_NOT_CONTAIN— исключение
Статистические (формат: ">значение", "<значение", ">=значение"):
IMPRESSIONS— показыCLICKS— кликиPOSITION— позицияCTR— кликабельностьDEMAND— спрос
text_indicator
QUERY— группировка по поисковым запросамURL— группировка по URL-адресам страниц
search_location
WEB_LOCATION— обычный поиск (по умолчанию)ALL_LOCATIONS— все типы поискаALL_LOCATIONS_ORGANIC— только органикаDYNAMIC_LOCATION_ALL— все динамические блокиDYNAMIC_LOCATION_BASIC— основные динамическиеDYNAMIC_LOCATION_ADDITIONAL— дополнительные динамические
Регионы
Для фильтрации по региону используйте --region-ids. ID регионов можно получить:
bash scripts/regions.sh --host example.com --filter "москва"Лимиты
- 10 000 запросов/час на домен
- Макс. 500 результатов за запрос
- Данные доступны за последние 14 дней
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/search-queries/popular |
| GET | /v4/user/{uid}/hosts/{hid}/search-queries/all/history |
| GET | /v4/user/{uid}/hosts/{hid}/search-queries/{qid}/history |
| POST | /v4/user/{uid}/hosts/{hid}/query-analytics/list |
Добавление и верификация сайтов
Добавление сайта
bash scripts/add_site.sh --url https://example.comAPI создаёт запись и возвращает host_id (формат: http:example.com:80). После добавления сайт не верифицирован — данные недоступны до прохождения верификации.
Лимит: 1703 сайта на аккаунт.
Верификация
Шаг 1: Получить код верификации
bash scripts/verify.sh --host example.com --action getВернёт verification_uin (код) и доступные методы.
Шаг 2: Разместить подтверждение
Три метода:
| Метод | Что делать |
|---|---|
DNS | Добавить TXT-запись: yandex-verification: <код> |
HTML_FILE | Создать файл yandex_<код>.html в корне сайта |
META_TAG | Добавить <meta name="yandex-verification" content="<код>" /> в <head> |
Шаг 3: Запустить проверку
bash scripts/verify.sh --host example.com --action start --method DNSВозможные состояния:
VERIFIED— сайт подтверждёнIN_PROGRESS— проверка идётVERIFICATION_FAILED— запись не найденаINTERNAL_ERROR— ошибка на стороне Яндекса
При VERIFICATION_FAILED проверьте, что DNS-запись распространилась (DNS propagation может занять до 72 часов).
Владельцы сайта
bash scripts/host_info.sh --host example.comПоказывает всех владельцев с типом верификации и датой.
Информация о сайте
Поле host_data_status:
OK— данные загружены, всё доступноNOT_LOADED— данные ещё загружаютсяNOT_INDEXED— сайт не проиндексирован
API endpoints
| Метод | Endpoint |
|---|---|
| POST | /v4/user/{uid}/hosts — добавить сайт |
| GET | /v4/user/{uid}/hosts/{hid} — инфо о сайте |
| GET | /v4/user/{uid}/hosts/{hid}/verification — код верификации |
| POST | /v4/user/{uid}/hosts/{hid}/verification?verification_type=DNS — начать проверку |
| GET | /v4/user/{uid}/hosts/{hid}/owners — владельцы |
Статистика сайта и индексация
Сводка по сайту
bash scripts/summary.sh --host example.comВозвращает:
- SQI (Site Quality Index) — индекс качества сайта
- Searchable pages — страницы, доступные в поиске
- Excluded pages — исключённые страницы
- Problems по уровням серьёзности:
FATAL— может привести к исключению из поискаCRITICAL— SSL, битые ссылки, время ответаPOSSIBLE_PROBLEM— сайтмапы, robots.txt, дублиRECOMMENDATION— рекомендации по оптимизации
История SQI
По умолчанию все history-скрипты запрашивают последние 90 дней. Для полной истории укажите --date-from явно.bash scripts/sqi_history.sh --host example.com --date-from 2025-01-01История индекса качества за последний год.
Диагностика проблем
bash scripts/diagnostics.sh --host example.comВсегда live (без кеша). Возвращает все проблемы с severity и state (PRESENT/ABSENT/UNDEFINED).
Типичные проблемы:
- FATAL:
SITE_ACCESS_FAILED,DNS_ERROR,ROBOTS_BLOCKED,SECURITY_THREAT - CRITICAL:
SSL_ERROR,SLOW_RESPONSE,HTTP_4XX,DUPLICATE_CONTENT - POSSIBLE:
NO_SITEMAP,ROBOTS_ERROR,SOFT_404
История индексации
bash scripts/indexing.sh --host example.com --action history --date-from 2025-03-01Разбивка по HTTP-кодам: 2xx, 3xx, 4xx, 5xx, OTHER.
Сэмплы индексации
bash scripts/indexing.sh --host example.com --action samples --limit 50Список URL с их статусом индексации. Макс. 50 000 URL через API.
Важные URL
bash scripts/important_urls.sh --host example.com --action listОтслеживаемые URL с изменениями. Для истории конкретного URL:
bash scripts/important_urls.sh --host example.com --action history --url "https://example.com/page"Индикаторы изменений: INDEXING_HTTP_CODE, SEARCH_STATUS, TITLE, DESCRIPTION.
Статусы исключения из поиска: DUPLICATE, LOW_QUALITY, REDIRECT_NOTSEARCHABLE, ROBOTS_URL_ERROR, NO_INDEX и другие.
Экспорт всех страниц
bash scripts/archive_export.sh --host example.com --action start
bash scripts/archive_export.sh --host example.com --action status --task-id <id>Генерация занимает от 10 секунд до 3 минут. Ссылка на скачивание действительна 24 часа.
API endpoints
| Метод | Endpoint |
|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/summary |
| GET | /v4/user/{uid}/hosts/{hid}/sqi-history |
| GET | /v4/user/{uid}/hosts/{hid}/diagnostics |
| GET | /v4/user/{uid}/hosts/{hid}/indexing/history |
| GET | /v4/user/{uid}/hosts/{hid}/indexing/samples |
| GET | /v4/user/{uid}/hosts/{hid}/important-urls |
| GET | /v4/user/{uid}/hosts/{hid}/important-urls/history |
| POST | /v4/user/{uid}/hosts/{hid}/indexing/archive/ |
| GET | /v4/user/{uid}/hosts/{hid}/indexing/archive/{tid} |
Управление сайтмапами
Список всех сайтмапов
bash scripts/sitemaps.sh --host example.com --action list --limit 50Показывает все обнаруженные сайтмапы (из robots.txt, добавленные пользователем, индексные).
Источники (sources): ROBOTS_TXT, WEBMASTER, INDEX_SITEMAP. Типы: SITEMAP, INDEX_SITEMAP.
Пользовательские сайтмапы
bash scripts/sitemaps.sh --host example.com --action user-listТолько добавленные вручную через Вебмастер.
Детали сайтмапа
bash scripts/sitemaps.sh --host example.com --action info --sitemap-id <id>Добавление сайтмапа
bash scripts/sitemaps.sh --host example.com --action add --url https://example.com/sitemap.xmlПриоритетный переобход сайтмапа
Проверка лимитов
bash scripts/sitemaps.sh --host example.com --action recrawl-limitПоказывает месячный лимит, использованные запросы и ближайшую доступную дату.
Отправка на переобход
bash scripts/sitemaps.sh --host example.com --action recrawl --sitemap-id <id>Важно: использует API v4.1 (не v4). Ограничение — месячная квота на приоритетный переобход.
API endpoints
| Метод | Endpoint | Версия |
|---|---|---|
| GET | /v4/user/{uid}/hosts/{hid}/sitemaps | v4 |
| GET | /v4/user/{uid}/hosts/{hid}/sitemaps/{sid} | v4 |
| GET | /v4/user/{uid}/hosts/{hid}/user-added-sitemaps | v4 |
| GET | /v4/user/{uid}/hosts/{hid}/user-added-sitemaps/{sid} | v4 |
| POST | /v4/user/{uid}/hosts/{hid}/user-added-sitemaps | v4 |
| GET | /v4.1/user/{uid}/hosts/{hid}/sitemaps/recrawl | v4.1 |
| POST | /v4.1/user/{uid}/hosts/{hid}/sitemaps/{sid}/recrawl | v4.1 |
#!/bin/sh
# Add a site to Yandex Webmaster
# Usage: add_site.sh --url <site_url>
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
URL=""
while [ $# -gt 0 ]; do
case "$1" in
--url) URL="$2"; shift 2 ;;
*) shift ;;
esac
done
if [ -z "$URL" ]; then
echo "Error: --url <site_url> is required." >&2
echo "Example: bash scripts/add_site.sh --url https://example.com" >&2
exit 1
fi
ensure_user_id
TMPFILE="${WM_TMPDIR}/wm_add_site_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
_escaped_url=$(json_escape "$URL")
webmaster_user_post "/hosts" "{\"host_url\":\"$_escaped_url\"}" > "$TMPFILE"
_host_id=$(json_extract_field_raw "$(cat "$TMPFILE")" "host_id")
echo "Site added successfully."
echo "Host ID: $_host_id"
echo ""
echo "Next step: verify ownership with:"
echo " bash scripts/verify.sh --host-id \"$_host_id\" --action get --method DNS"
# Invalidate hosts cache
rm -f "$CACHE_DIR/hosts.tsv" "$CACHE_DIR/hosts.json"
echo "(hosts cache invalidated)"
#!/usr/bin/env python3
"""Yandex Webmaster — Alice (Share of Voice) efficiency extractor.
Не имеет публичного API: данные приходят в HTML через window._initData при SSR.
Авторизация — через cookie Session_id (длинноживущая, httpOnly).
Subcommands:
fetch — скачать страницу, извлечь alice.* из _initData, сохранить JSON
sov — Share-of-Voice: 12 недельных точек, TSV
competitors — топ-10 сайтов в Alice (queries.GENERAL), TSV
with-site — запросы где наш сайт присутствует (hasOwnExamples), TSV
without-site — запросы где наш сайт НЕ присутствует (noOwnExamples), TSV
summary — короткая сводка alertType + средний SoV + размеры списков
Все subcommand'ы кроме fetch читают из cache JSON; если кеша нет — делают fetch.
"""
import argparse
import json
import os
import re
import ssl
import sys
import urllib.error
import urllib.request
UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
)
# Python.org installs ship without a CA bundle. Try common system locations
# before failing back to Python defaults (which may also be empty).
_CA_CANDIDATES = [
os.environ.get("SSL_CERT_FILE", ""),
"/etc/ssl/cert.pem", # macOS, FreeBSD
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
"/etc/pki/tls/certs/ca-bundle.crt", # RHEL/CentOS
"/opt/homebrew/etc/ca-certificates/cert.pem", # Homebrew arm64
"/usr/local/etc/ca-certificates/cert.pem", # Homebrew x86_64
]
def _build_ssl_context() -> ssl.SSLContext:
for path in _CA_CANDIDATES:
if path and os.path.isfile(path):
return ssl.create_default_context(cafile=path)
return ssl.create_default_context()
ALICE_URL_TEMPLATE = (
"https://webmaster.yandex.ru/site/{host_id}/efficiency/alice/"
"?tab=GENERAL&tableType=GENERAL&onlyWithMySites=OFF"
)
def fail(msg, code=1):
print(f"Error: {msg}", file=sys.stderr)
sys.exit(code)
# ---------- HTML fetch ----------
def fetch_html(host_id: str, session_id: str) -> str:
url = ALICE_URL_TEMPLATE.format(host_id=host_id)
req = urllib.request.Request(
url,
headers={
"User-Agent": UA,
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "ru,en;q=0.9",
"Cookie": f"Session_id={session_id}",
},
)
ctx = _build_ssl_context()
try:
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
charset = resp.headers.get_content_charset() or "utf-8"
return resp.read().decode(charset, errors="replace")
except urllib.error.HTTPError as e:
fail(f"HTTP {e.code} from {url}: {e.reason}")
except urllib.error.URLError as e:
fail(f"network error: {e.reason}")
# ---------- _initData extraction ----------
def extract_init_data(html: str) -> dict:
idx = html.find("window._initData")
if idx < 0:
fail("window._initData not found in HTML — session likely expired")
eq = html.find("=", idx)
start = html.find("{", eq)
if start < 0:
fail("opening brace of _initData not found")
depth = 0
in_str = False
esc = False
end = -1
for i in range(start, len(html)):
ch = html[i]
if in_str:
if esc:
esc = False
elif ch == "\\":
esc = True
elif ch == '"':
in_str = False
else:
if ch == '"':
in_str = True
elif ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = i + 1
break
if end < 0:
fail("unbalanced braces in _initData")
raw = html[start:end]
try:
return json.loads(raw)
except json.JSONDecodeError as e:
fail(f"failed to parse _initData JSON: {e}")
def assert_authed(init_data: dict) -> None:
if not init_data.get("userIsAuth"):
fail("userIsAuth=false — Session_id cookie expired or invalid")
# ---------- Cache I/O ----------
def cache_path(cache_dir: str, host_id: str) -> str:
safe = re.sub(r"[^a-zA-Z0-9._-]", "_", host_id)
d = os.path.join(cache_dir, f"host_{safe}", "alice")
os.makedirs(d, exist_ok=True)
return os.path.join(d, "init.json")
def load_or_fetch(args, force=False) -> dict:
"""Returns alice dict, using cache when fresh."""
path = cache_path(args.cache_dir, args.host_id)
if not force and os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
pass
if not args.session_id:
fail("SESSION_ID is required for fetch (set in config/.env)")
html = fetch_html(args.host_id, args.session_id)
init = extract_init_data(html)
assert_authed(init)
alice = init.get("alice")
if not alice:
fail("init._initData.alice missing — page changed or wrong host")
with open(path, "w", encoding="utf-8") as f:
json.dump(alice, f, ensure_ascii=False)
return alice
# ---------- Output helpers ----------
def write_tsv(rows, path: str):
with open(path, "w", encoding="utf-8") as f:
for row in rows:
cleaned = ["" if c is None else str(c).replace("\t", " ").replace("\n", " ").replace("\r", " ")
for c in row]
f.write("\t".join(cleaned) + "\n")
def emit_tsv(header, rows, out_path, head=20):
write_tsv([header] + rows, out_path)
print("\t".join(header))
for r in rows[:head]:
print("\t".join("" if c is None else str(c) for c in r))
if len(rows) > head:
print(f"... ({len(rows) - head} more rows, full data in: {out_path})")
print(f"\nTotal: {len(rows)}")
print(f"TSV: {out_path}")
# ---------- Subcommands ----------
def cmd_fetch(args):
alice = load_or_fetch(args, force=True)
path = cache_path(args.cache_dir, args.host_id)
print(f"alice keys: {', '.join(alice.keys())}")
print(f"sov points: {len(alice.get('sov', []))}")
q = alice.get("queries", {}) or {}
general = q.get("GENERAL") or []
examples = q.get("EXAMPLES") or {}
has = examples.get("hasOwnExamples") or [] if isinstance(examples, dict) else []
no = examples.get("noOwnExamples") or [] if isinstance(examples, dict) else []
print(f"competitors (GENERAL): {len(general)}")
print(f"with-site (hasOwnExamples): {len(has)}")
print(f"without-site (noOwnExamples): {len(no)}")
print(f"alertType: {alice.get('alertType')}")
print(f"cached: {path}")
def cmd_sov(args):
alice = load_or_fetch(args)
sov = alice.get("sov") or []
rows = []
for p in sov:
share = p.get("sharePercent")
rows.append([
p.get("dateFrom", ""),
p.get("dateTo", ""),
f"{share:.4f}" if isinstance(share, (int, float)) else "",
f"{share * 100:.2f}%" if isinstance(share, (int, float)) else "",
])
out = os.path.join(os.path.dirname(cache_path(args.cache_dir, args.host_id)), "sov.tsv")
emit_tsv(["date_from", "date_to", "share", "share_pct"], rows, out, head=20)
def cmd_competitors(args):
alice = load_or_fetch(args)
general = ((alice.get("queries") or {}).get("GENERAL")) or []
rows = [[i + 1, item.get("url", "")] for i, item in enumerate(general)]
out = os.path.join(os.path.dirname(cache_path(args.cache_dir, args.host_id)), "competitors.tsv")
emit_tsv(["rank", "url"], rows, out, head=20)
def _flatten_examples(items):
"""Each item: {query, urls: [{url,title,host,favicon}]}.
TSV: query, rank_in_query, host, url, title."""
rows = []
for item in items or []:
q = item.get("query", "")
urls = item.get("urls") or []
for i, u in enumerate(urls, start=1):
rows.append([
q,
i,
u.get("host", ""),
u.get("url", ""),
u.get("title", ""),
])
return rows
def cmd_with_site(args):
alice = load_or_fetch(args)
ex = ((alice.get("queries") or {}).get("EXAMPLES")) or {}
items = ex.get("hasOwnExamples") or []
rows = _flatten_examples(items)
out = os.path.join(os.path.dirname(cache_path(args.cache_dir, args.host_id)), "with_site.tsv")
emit_tsv(["query", "rank", "host", "url", "title"], rows, out, head=15)
print(f"Unique queries: {len(items)}")
def cmd_without_site(args):
alice = load_or_fetch(args)
ex = ((alice.get("queries") or {}).get("EXAMPLES")) or {}
items = ex.get("noOwnExamples") or []
rows = _flatten_examples(items)
out = os.path.join(os.path.dirname(cache_path(args.cache_dir, args.host_id)), "without_site.tsv")
emit_tsv(["query", "rank", "host", "url", "title"], rows, out, head=15)
print(f"Unique queries: {len(items)}")
def cmd_summary(args):
alice = load_or_fetch(args)
sov = alice.get("sov") or []
shares = [p.get("sharePercent") for p in sov if isinstance(p.get("sharePercent"), (int, float))]
avg = sum(shares) / len(shares) if shares else 0
last = shares[-1] if shares else 0
first = shares[0] if shares else 0
q = alice.get("queries") or {}
general = q.get("GENERAL") or []
ex = q.get("EXAMPLES") or {}
has = ex.get("hasOwnExamples") or []
no = ex.get("noOwnExamples") or []
print(f"alertType: {alice.get('alertType')}")
print(f"sov points: {len(sov)}")
if sov:
print(f"sov range: {sov[0].get('dateFrom')} → {sov[-1].get('dateTo')}")
print(f"sov first: {first * 100:.2f}%")
print(f"sov last: {last * 100:.2f}%")
print(f"sov avg: {avg * 100:.2f}%")
print(f"competitors: {len(general)}")
print(f"with-site qs: {len(has)}")
print(f"without-site qs: {len(no)}")
# ---------- CLI ----------
def build_parser():
p = argparse.ArgumentParser(description="Yandex Webmaster Alice efficiency (SSR scraper)")
p.add_argument("--host-id", required=True, help="Host id, e.g. https:metallik.ru:443")
p.add_argument("--session-id", default=os.environ.get("SESSION_ID", ""),
help="Session_id cookie (default: $SESSION_ID)")
p.add_argument("--cache-dir", required=True, help="Cache directory root")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("fetch", help="force refresh + summary").set_defaults(func=cmd_fetch)
sub.add_parser("sov", help="Share-of-Voice timeline").set_defaults(func=cmd_sov)
sub.add_parser("competitors", help="Top sites in Alice").set_defaults(func=cmd_competitors)
sub.add_parser("with-site", help="queries where own site appears").set_defaults(func=cmd_with_site)
sub.add_parser("without-site", help="queries where own site is absent").set_defaults(func=cmd_without_site)
sub.add_parser("summary", help="short summary").set_defaults(func=cmd_summary)
return p
def main():
args = build_parser().parse_args()
args.func(args)
if __name__ == "__main__":
main()
#!/bin/sh
# Yandex Webmaster — Alice (Share of Voice) efficiency
#
# Усложнение: данных по Alice нет в Webmaster API v4. Они приходят только в HTML
# через window._initData при SSR. Авторизация — через cookie Session_id.
# Cм. references/ALICE_EFFICIENCY.md.
#
# Usage:
# alice.sh --host <domain> --action <action> [--no-cache]
# alice.sh --host-id <id> --action <action> [--no-cache]
#
# Actions:
# summary — короткая сводка (alertType, SoV, размеры списков). По умолчанию.
# sov — Share-of-Voice timeline (12 недель), TSV
# competitors — топ сайтов в Alice по теме, TSV
# with-site — запросы где наш сайт присутствует, TSV
# without-site — запросы где наш сайт отсутствует, TSV
# fetch — форс-обновить кеш и распечатать сводку
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
if [ -z "$SESSION_ID" ]; then
echo "Error: SESSION_ID not set in config/.env." >&2
echo "Достаньте Session_id из браузера (DevTools → Application → Cookies → yandex.ru)." >&2
echo "См. references/ALICE_EFFICIENCY.md." >&2
exit 1
fi
parse_host_params "$@"
ACTION_DEFAULT="summary"
[ -z "$ACTION" ] && ACTION="$ACTION_DEFAULT"
# Resolve host (host search needs API call → user_id)
if [ -z "$HOST_ID" ] && [ -n "$HOST_SEARCH" ]; then
ensure_user_id
fi
resolve_host
require_host
# Map action → python subcommand
case "$ACTION" in
summary|sov|competitors|fetch) PY_CMD="$ACTION" ;;
with-site) PY_CMD="with-site" ;;
without-site) PY_CMD="without-site" ;;
*)
echo "Error: unknown --action '$ACTION'" >&2
echo "Valid: summary | sov | competitors | with-site | without-site | fetch" >&2
exit 1
;;
esac
# Cache invalidation: --no-cache forces refresh
if [ -n "$NO_CACHE" ] && [ "$PY_CMD" != "fetch" ]; then
_alice_cache="$CACHE_DIR/host_$(printf '%s' "$HOST_ID" | sed 's/[^a-zA-Z0-9._-]/_/g')/alice/init.json"
rm -f "$_alice_cache"
fi
SESSION_ID="$SESSION_ID" exec python3 "$SCRIPT_DIR/alice_efficiency.py" \
--host-id "$HOST_ID" \
--cache-dir "$CACHE_DIR" \
"$PY_CMD"
#!/bin/sh
# Export all indexed pages archive
# Usage: archive_export.sh --host <domain> --action start|status [--task-id ID]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
TASK_ID=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--task-id) TASK_ID="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-start}"
TMPFILE="${WM_TMPDIR}/wm_archive_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
start)
webmaster_raw_post "/v4/user/${USER_ID}/hosts/${HOST_ID}/indexing/archive/" "{}" > "$TMPFILE"
_tid=$(json_extract_field_raw "$(cat "$TMPFILE")" "task_id")
echo "Archive export started."
echo "Task ID: $_tid"
echo "Check status: bash scripts/archive_export.sh --host ... --action status --task-id $_tid"
echo "(Generation takes 10 seconds to 3 minutes)"
;;
status)
if [ -z "$TASK_ID" ]; then
echo "Error: --task-id is required for --action status" >&2
exit 1
fi
webmaster_raw_get "/v4/user/${USER_ID}/hosts/${HOST_ID}/indexing/archive/${TASK_ID}" > "$TMPFILE"
_body=$(cat "$TMPFILE")
_state=$(json_extract_field_raw "$_body" "state")
_url=$(json_extract_field_raw "$_body" "download_url")
echo "=== Archive Export ==="
echo "Task ID: $TASK_ID"
echo "State: $_state"
if [ -n "$_url" ]; then
echo "Download: $_url"
echo "(URL valid for 24 hours)"
fi
;;
*)
echo "Error: unknown action '$ACTION'. Use: start, status" >&2
exit 1
;;
esac
#!/bin/sh
# Common functions for Yandex Webmaster API skill
# POSIX sh compatible — no bashisms
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/../config/.env"
CACHE_DIR="$SCRIPT_DIR/../cache"
WEBMASTER_API="https://api.webmaster.yandex.net"
WM_TMPDIR="${TMPDIR:-/tmp}"
mkdir -p "$WM_TMPDIR"
# --------------- Config ---------------
load_config() {
if [ -f "$CONFIG_FILE" ]; then
# shellcheck disable=SC1090
. "$CONFIG_FILE"
fi
if [ -z "$YANDEX_WEBMASTER_TOKEN" ]; then
echo "Error: YANDEX_WEBMASTER_TOKEN not found." >&2
echo "Set in config/.env or environment. See config/README.md." >&2
exit 1
fi
}
# --------------- User ID ---------------
# ensure_user_id — fetches and caches user_id
ensure_user_id() {
_eui_cache="$CACHE_DIR/user_id.txt"
if [ -f "$_eui_cache" ] && [ -s "$_eui_cache" ]; then
USER_ID=$(cat "$_eui_cache")
return 0
fi
_eui_tmp="${WM_TMPDIR}/wm_user_$$.json"
trap 'rm -f "$_eui_tmp"' EXIT
webmaster_raw_get "/v4/user" > "$_eui_tmp"
USER_ID=$(json_extract_number "$(cat "$_eui_tmp")" "user_id")
if [ -z "$USER_ID" ]; then
echo "Error: could not get user_id from API." >&2
cat "$_eui_tmp" >&2
rm -f "$_eui_tmp"
exit 1
fi
mkdir -p "$CACHE_DIR"
printf '%s' "$USER_ID" > "$_eui_cache"
rm -f "$_eui_tmp"
}
# --------------- Host resolution ---------------
# resolve_host — sets HOST_ID from --host (domain search) or --host-id (direct)
# Requires USER_ID to be set (call ensure_user_id first)
resolve_host() {
if [ -n "$HOST_ID" ]; then
return 0
fi
if [ -n "$HOST_SEARCH" ]; then
_rh_tsv="$CACHE_DIR/hosts.tsv"
if [ ! -f "$_rh_tsv" ] || [ ! -s "$_rh_tsv" ]; then
_refresh_hosts_cache
fi
_rh_match=$(grep -i "$HOST_SEARCH" "$_rh_tsv" | head -1)
if [ -z "$_rh_match" ]; then
echo "Error: no host matching '$HOST_SEARCH' in cache." >&2
echo "Run: bash scripts/hosts.sh --no-cache" >&2
exit 1
fi
HOST_ID=$(printf '%s' "$_rh_match" | cut -f1)
fi
if [ -z "$HOST_ID" ]; then
echo "Error: --host <domain> or --host-id <id> is required." >&2
exit 1
fi
}
_refresh_hosts_cache() {
_rhc_tmp="${WM_TMPDIR}/wm_hosts_$$.json"
webmaster_raw_get "/v4/user/${USER_ID}/hosts" > "$_rhc_tmp"
mkdir -p "$CACHE_DIR"
cp "$_rhc_tmp" "$CACHE_DIR/hosts.json"
# Generate TSV: host_id<TAB>url<TAB>verified<TAB>main_mirror
{
sed 's/},{/}\n{/g' "$_rhc_tmp" | while IFS= read -r _line || [ -n "$_line" ]; do
_hid=$(json_extract_field_raw "$_line" "host_id")
_url=$(json_extract_field_raw "$_line" "ascii_host_url")
_ver=$(json_extract_field_raw "$_line" "verified")
[ -z "$_hid" ] && continue
printf '%s\t%s\t%s\n' "$_hid" "$_url" "$_ver"
done
} > "$CACHE_DIR/hosts.tsv"
rm -f "$_rhc_tmp"
}
# --------------- API helpers ---------------
# webmaster_raw_get <full_path> [extra_curl_args...]
# Low-level GET with OAuth, 429 retry, error handling
webmaster_raw_get() {
_wrg_path="$1"
shift
_wrg_url="${WEBMASTER_API}${_wrg_path}"
_wrg_headers="${WM_TMPDIR}/wm_headers_$$.txt"
_wrg_body=$(curl -s -G -D "$_wrg_headers" \
-H "Authorization: OAuth $YANDEX_WEBMASTER_TOKEN" \
"$@" \
"$_wrg_url") || {
rm -f "$_wrg_headers"
echo "Error: curl failed for $_wrg_url" >&2
return 1
}
_wrg_status=$(head -1 "$_wrg_headers" | grep -o '[0-9][0-9][0-9]' | head -1)
# 429 retry
if [ "$_wrg_status" = "429" ]; then
_wrg_retry=$(grep -i 'Retry-After' "$_wrg_headers" | sed 's/[^0-9]//g' | head -1)
rm -f "$_wrg_headers"
if [ -z "${_WM_RETRY_DONE:-}" ] && [ -n "$_wrg_retry" ] && [ "$_wrg_retry" -le 60 ] 2>/dev/null; then
_wrg_jitter=$(awk 'BEGIN{srand(); printf "%d", rand()*3}')
_wrg_wait=$(( _wrg_retry + _wrg_jitter ))
echo "Rate limited. Waiting ${_wrg_wait}s..." >&2
sleep "$_wrg_wait"
_WM_RETRY_DONE=1 webmaster_raw_get "$_wrg_path" "$@"
return $?
else
echo "Error: Rate limit exceeded (429)." >&2
return 1
fi
fi
# HTTP errors
if [ -n "$_wrg_status" ] && [ "$_wrg_status" -ge 400 ] 2>/dev/null; then
rm -f "$_wrg_headers"
_wrg_err_code=$(json_extract_field_raw "$_wrg_body" "error_code")
_wrg_err_msg=$(json_extract_field_raw "$_wrg_body" "error_message")
if [ -n "$_wrg_err_code" ]; then
echo "Error: HTTP $_wrg_status — $_wrg_err_code: $_wrg_err_msg" >&2
else
echo "Error: HTTP $_wrg_status from $_wrg_url" >&2
echo "$_wrg_body" >&2
fi
return 1
fi
rm -f "$_wrg_headers"
printf '%s' "$_wrg_body"
}
# webmaster_raw_post <full_path> <json_body>
# Low-level POST with OAuth
webmaster_raw_post() {
_wrp_path="$1"
_wrp_body_data="$2"
_wrp_url="${WEBMASTER_API}${_wrp_path}"
_wrp_headers="${WM_TMPDIR}/wm_headers_$$.txt"
_wrp_resp=$(curl -s -X POST -D "$_wrp_headers" \
-H "Authorization: OAuth $YANDEX_WEBMASTER_TOKEN" \
-H "Content-Type: application/json; charset=UTF-8" \
-d "$_wrp_body_data" \
"$_wrp_url") || {
rm -f "$_wrp_headers"
echo "Error: curl failed for $_wrp_url" >&2
return 1
}
_wrp_status=$(head -1 "$_wrp_headers" | grep -o '[0-9][0-9][0-9]' | head -1)
if [ "$_wrp_status" = "429" ]; then
_wrp_retry=$(grep -i 'Retry-After' "$_wrp_headers" | sed 's/[^0-9]//g' | head -1)
rm -f "$_wrp_headers"
if [ -z "${_WM_RETRY_DONE:-}" ] && [ -n "$_wrp_retry" ] && [ "$_wrp_retry" -le 60 ] 2>/dev/null; then
_wrp_jitter=$(awk 'BEGIN{srand(); printf "%d", rand()*3}')
_wrp_wait=$(( _wrp_retry + _wrp_jitter ))
echo "Rate limited. Waiting ${_wrp_wait}s..." >&2
sleep "$_wrp_wait"
_WM_RETRY_DONE=1 webmaster_raw_post "$_wrp_path" "$_wrp_body_data"
return $?
else
echo "Error: Rate limit exceeded (429)." >&2
return 1
fi
fi
if [ -n "$_wrp_status" ] && [ "$_wrp_status" -ge 400 ] 2>/dev/null; then
rm -f "$_wrp_headers"
_wrp_err_code=$(json_extract_field_raw "$_wrp_resp" "error_code")
_wrp_err_msg=$(json_extract_field_raw "$_wrp_resp" "error_message")
if [ -n "$_wrp_err_code" ]; then
echo "Error: HTTP $_wrp_status — $_wrp_err_code: $_wrp_err_msg" >&2
else
echo "Error: HTTP $_wrp_status from $_wrp_url" >&2
echo "$_wrp_resp" >&2
fi
return 1
fi
rm -f "$_wrp_headers"
printf '%s' "$_wrp_resp"
}
# webmaster_get <host_subpath> [extra_curl_args...]
# GET /v4/user/{USER_ID}/hosts/{HOST_ID}/<subpath>
webmaster_get() {
_wg_sub="$1"
shift
webmaster_raw_get "/v4/user/${USER_ID}/hosts/${HOST_ID}${_wg_sub}" "$@"
}
# webmaster_post <host_subpath> <json_body>
# POST /v4/user/{USER_ID}/hosts/{HOST_ID}/<subpath>
webmaster_post() {
_wp_sub="$1"
_wp_body="$2"
webmaster_raw_post "/v4/user/${USER_ID}/hosts/${HOST_ID}${_wp_sub}" "$_wp_body"
}
# webmaster_user_get [extra_curl_args...]
# GET /v4/user/{USER_ID}/hosts (user-level, no specific host)
webmaster_user_get() {
_wug_sub="$1"
shift 2>/dev/null || true
webmaster_raw_get "/v4/user/${USER_ID}${_wug_sub}" "$@"
}
# webmaster_user_post <subpath> <json_body>
webmaster_user_post() {
_wup_sub="$1"
_wup_body="$2"
webmaster_raw_post "/v4/user/${USER_ID}${_wup_sub}" "$_wup_body"
}
# --------------- Cache helpers ---------------
cache_key() {
printf '%s' "$1" | cksum | awk '{print $1}'
}
cache_get() {
if [ -f "$1" ] && [ -s "$1" ]; then
cat "$1"
return 0
fi
return 1
}
cache_put() {
mkdir -p "$(dirname "$1")"
cat > "$1"
}
# cache_get_ttl <file_path> <max_age_minutes>
# Returns 0 if file exists and is newer than max_age_minutes.
# Returns 1 (miss) if file is missing, empty, or older than TTL.
cache_get_ttl() {
_cgt_file="$1"
_cgt_ttl="${2:-1440}"
if [ -f "$_cgt_file" ] && [ -s "$_cgt_file" ]; then
# find returns the file if it's OLDER than ttl → stale
_cgt_stale=$(find "$_cgt_file" -mmin +"$_cgt_ttl" 2>/dev/null)
if [ -z "$_cgt_stale" ]; then
return 0
fi
# Stale — delete and miss
rm -f "$_cgt_file"
fi
return 1
}
cache_host_dir() {
_chd_dir="$CACHE_DIR/host_$(printf '%s' "$HOST_ID" | sed 's/[^a-zA-Z0-9._-]/_/g')"
mkdir -p "$_chd_dir"
echo "$_chd_dir"
}
# --------------- JSON escape ---------------
# json_escape <string> — escapes special characters for JSON string values
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r'
}
# --------------- JSON helpers (no jq) ---------------
json_extract_field_raw() {
printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//'
}
json_extract_number() {
printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*[0-9.e+-]*" | head -1 | sed 's/.*:[[:space:]]*//'
}
json_extract_bool() {
printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*[a-z]*" | head -1 | sed 's/.*:[[:space:]]*//'
}
# json_extract_array_strings <json> <field> — extracts string array values, one per line
json_extract_array_strings() {
printf '%s' "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*\[[^]]*\]" | head -1 | grep -o '"[^"]*"' | sed 's/"//g'
}
# --------------- Output helpers ---------------
print_tsv_head() {
_pth_file="$1"
_pth_n="${2:-30}"
if [ -f "$_pth_file" ]; then
head -n "$_pth_n" "$_pth_file"
_pth_total=$(wc -l < "$_pth_file" | tr -d ' ')
if [ "$_pth_total" -gt "$_pth_n" ]; then
echo "... ($(( _pth_total - _pth_n )) more rows, full data in: $_pth_file)"
fi
fi
}
# --------------- Date helpers ---------------
# date_subtract_days <YYYY-MM-DD> <days>
# Returns date N days before the given date. POSIX-compatible.
date_subtract_days() {
_dsd_base="$1"
_dsd_days="$2"
# Try macOS date first, then GNU date
if date -v-1d +%Y-%m-%d >/dev/null 2>&1; then
date -j -f "%Y-%m-%d" "$_dsd_base" -v-"${_dsd_days}d" +%Y-%m-%d 2>/dev/null && return 0
fi
if date -d "$_dsd_base - $_dsd_days days" +%Y-%m-%d 2>/dev/null; then
return 0
fi
# Pure shell fallback: subtract days via simple month/day arithmetic
# Good enough for 90-day ranges
_dsd_y=$(echo "$_dsd_base" | cut -d- -f1)
_dsd_m=$(echo "$_dsd_base" | cut -d- -f2 | sed 's/^0//')
_dsd_d=$(echo "$_dsd_base" | cut -d- -f3 | sed 's/^0//')
_dsd_remain="$_dsd_days"
while [ "$_dsd_remain" -gt 0 ]; do
if [ "$_dsd_d" -gt "$_dsd_remain" ]; then
_dsd_d=$(( _dsd_d - _dsd_remain ))
_dsd_remain=0
else
_dsd_remain=$(( _dsd_remain - _dsd_d ))
_dsd_m=$(( _dsd_m - 1 ))
if [ "$_dsd_m" -lt 1 ]; then
_dsd_m=12
_dsd_y=$(( _dsd_y - 1 ))
fi
# Days in the new month
case "$_dsd_m" in
1|3|5|7|8|10|12) _dsd_d=31 ;;
4|6|9|11) _dsd_d=30 ;;
2) if [ $(( _dsd_y % 4 )) -eq 0 ] && { [ $(( _dsd_y % 100 )) -ne 0 ] || [ $(( _dsd_y % 400 )) -eq 0 ]; }; then _dsd_d=29; else _dsd_d=28; fi ;;
esac
fi
done
printf '%04d-%02d-%02d\n' "$_dsd_y" "$_dsd_m" "$_dsd_d"
}
# default_date_from [reference_date]
# Returns date 90 days before reference_date (default: today)
default_date_from() {
_ddf_ref="${1:-$(date +%Y-%m-%d)}"
date_subtract_days "$_ddf_ref" 90
}
# apply_default_dates — sets DATE_FROM if empty (for history scripts)
# If --date-to is set but --date-from is not: DATE_FROM = DATE_TO - 90 days
# If neither is set: DATE_FROM = today - 90 days
apply_default_dates() {
if [ -z "$DATE_FROM" ]; then
if [ -n "$DATE_TO" ]; then
DATE_FROM=$(default_date_from "$DATE_TO")
else
DATE_FROM=$(default_date_from)
fi
fi
}
# --------------- Common param parsing ---------------
# parse_host_params "$@"
# Sets: HOST_ID, HOST_SEARCH, ACTION, DATE_FROM, DATE_TO, LIMIT, OFFSET, NO_CACHE
parse_host_params() {
HOST_ID=""
HOST_SEARCH=""
ACTION=""
DATE_FROM=""
DATE_TO=""
LIMIT=""
OFFSET=""
NO_CACHE=""
while [ $# -gt 0 ]; do
case "$1" in
--host-id) HOST_ID="$2"; shift 2 ;;
--host) HOST_SEARCH="$2"; shift 2 ;;
--action) ACTION="$2"; shift 2 ;;
--date-from) DATE_FROM="$2"; shift 2 ;;
--date-to) DATE_TO="$2"; shift 2 ;;
--limit) LIMIT="$2"; shift 2 ;;
--offset) OFFSET="$2"; shift 2 ;;
--no-cache) NO_CACHE="1"; shift ;;
*) shift ;;
esac
done
}
# require_host — exits if host not resolved
require_host() {
if [ -z "$HOST_ID" ]; then
echo "Error: --host <domain> or --host-id <id> is required." >&2
exit 1
fi
}
# add_date_params — builds curl args for date_from/date_to
# Usage: eval "set -- $(add_date_params)"
add_date_params() {
_adp=""
if [ -n "$DATE_FROM" ]; then
_adp="$_adp --data-urlencode \"date_from=${DATE_FROM}T00:00:00.000+0300\""
fi
if [ -n "$DATE_TO" ]; then
_adp="$_adp --data-urlencode \"date_to=${DATE_TO}T00:00:00.000+0300\""
fi
echo "$_adp"
}
#!/bin/sh
# Site diagnostics — list all problems with severity and state
# Usage: diagnostics.sh --host <domain>
# NOT cached (always live data)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_diag_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
webmaster_get "/diagnostics" > "$TMPFILE"
echo "problem severity state last_update"
# Parse each problem entry
tr -d '\n\r' < "$TMPFILE" | sed 's/"problems"[[:space:]]*:{//;s/}[[:space:]]*$//' | \
grep -o '"[A-Z_]*"[[:space:]]*:{[^}]*}' | while IFS= read -r _entry; do
_name=$(printf '%s' "$_entry" | grep -o '^"[^"]*"' | sed 's/"//g')
_severity=$(json_extract_field_raw "$_entry" "severity")
_state=$(json_extract_field_raw "$_entry" "state")
_updated=$(json_extract_field_raw "$_entry" "last_state_update")
_date=$(printf '%s' "$_updated" | cut -c1-10)
printf '%s\t%s\t%s\t%s\n' "$_name" "$_severity" "$_state" "${_date:--}"
done
#!/bin/sh
# External links — samples and history
# Usage: external_links.sh --host <domain> --action samples|history
# [--date-from] [--date-to] [--limit N] [--offset N]
# History defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-samples}"
TMPFILE="${WM_TMPDIR}/wm_extlinks_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
samples)
_curl_args=""
if [ -n "$LIMIT" ]; then
_curl_args="--data-urlencode limit=$LIMIT"
fi
if [ -n "$OFFSET" ]; then
_curl_args="$_curl_args --data-urlencode offset=$OFFSET"
fi
# shellcheck disable=SC2086
webmaster_get "/links/external/samples" $_curl_args > "$TMPFILE"
_count=$(json_extract_number "$(cat "$TMPFILE")" "count")
echo "source_url destination_url discovery_date"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_src=$(json_extract_field_raw "$_line" "source_url")
[ -z "$_src" ] && continue
_dst=$(json_extract_field_raw "$_line" "destination_url")
_disc=$(json_extract_field_raw "$_line" "discovery_date")
printf '%s\t%s\t%s\n' "$_src" "$_dst" "${_disc:--}"
done | head -30
echo ""
echo "Total external links: ${_count:-?} (showing first 30)"
;;
history)
apply_default_dates
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/links"
_hash=$(cache_key "ext_links_history_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/links/external_history_${_hash}.tsv"
# TTL cache check (24h)
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
_curl_args="--data-urlencode indicator=LINKS_TOTAL_COUNT"
if [ -n "$DATE_FROM" ]; then
_curl_args="$_curl_args --data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/links/external/history" $_curl_args > "$TMPFILE"
{
echo "date total_links"
tr -d '\n\r' < "$TMPFILE" | grep -o '"date":"[^"]*","value":"[^"]*"' | while IFS= read -r _match; do
_date=$(printf '%s' "$_match" | sed 's/.*"date":"//;s/".*//' | cut -c1-10)
_val=$(printf '%s' "$_match" | grep -o '"value":"[^"]*"' | sed 's/"value":"//;s/"$//')
printf '%s\t%s\n' "$_date" "$_val"
done
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
;;
*)
echo "Error: unknown action '$ACTION'. Use: samples, history" >&2
exit 1
;;
esac
#!/bin/sh
# YML Feeds management — list, add, batch-add, change regions, check status
# Usage: feeds.sh --host <domain> --action list|add|batch-add|change|add-status
# [--url URL] [--type REALTY|VACANCY|GOODS|DOCTORS|CARS|SERVICES|EDUCATION|ACTIVITY]
# [--region-ids "225,213"] [--request-id ID]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
FEED_URL=""
FEED_TYPE=""
REGION_IDS=""
REQUEST_ID=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--url) FEED_URL="$2"; shift 2 ;;
--type) FEED_TYPE="$2"; shift 2 ;;
--region-ids) REGION_IDS="$2"; shift 2 ;;
--request-id) REQUEST_ID="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-list}"
TMPFILE="${WM_TMPDIR}/wm_feeds_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
list)
webmaster_get "/feeds/list" > "$TMPFILE"
_list_tmp="${WM_TMPDIR}/wm_feeds_list_$$.tsv"
{
echo "url type regions"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_furl=$(json_extract_field_raw "$_line" "url")
[ -z "$_furl" ] && continue
_ftype=$(json_extract_field_raw "$_line" "type")
_fregs=$(printf '%s' "$_line" | grep -o '"regionIds"[[:space:]]*:\[[^]]*\]' | head -1 | grep -o '[0-9]*' | tr '\n' ',' | sed 's/,$//')
printf '%s\t%s\t%s\n' "$_furl" "${_ftype:--}" "${_fregs:--}"
done
} > "$_list_tmp"
print_tsv_head "$_list_tmp" 30
rm -f "$_list_tmp"
;;
add)
if [ -z "$FEED_URL" ] || [ -z "$FEED_TYPE" ]; then
echo "Error: --url and --type are required for --action add" >&2
exit 1
fi
_escaped_furl=$(json_escape "$FEED_URL")
_json="{\"url\":\"$_escaped_furl\",\"type\":\"$FEED_TYPE\""
if [ -n "$REGION_IDS" ]; then
_json="$_json,\"regionIds\":[$REGION_IDS]"
fi
_json="$_json}"
webmaster_post "/feeds/add/start" "$_json" > "$TMPFILE"
_rid=$(json_extract_field_raw "$(cat "$TMPFILE")" "requestId")
echo "Feed upload started."
echo "Request ID: $_rid"
echo "Check status: bash scripts/feeds.sh --host ... --action add-status --request-id $_rid"
;;
batch-add)
# Reads feed definitions from stdin (JSON array)
# Example: echo '[{"url":"https://a.com/f.yml","type":"GOODS"},{"url":"https://b.com/f.yml","type":"REALTY","regionIds":[213]}]' | bash scripts/feeds.sh --host ... --action batch-add
if [ -t 0 ]; then
echo "Error: batch-add reads JSON array from stdin." >&2
echo "Example: echo '[{\"url\":\"...\",\"type\":\"GOODS\"}]' | bash scripts/feeds.sh --host ... --action batch-add" >&2
exit 1
fi
_batch_body=$(cat)
webmaster_post "/feeds/batch/add" "$_batch_body" > "$TMPFILE"
echo "url status"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_burl=$(json_extract_field_raw "$_line" "url")
[ -z "$_burl" ] && continue
_bstatus=$(json_extract_field_raw "$_line" "status")
printf '%s\t%s\n' "$_burl" "$_bstatus"
done
;;
change)
if [ -z "$FEED_URL" ] || [ -z "$REGION_IDS" ]; then
echo "Error: --url and --region-ids are required for --action change" >&2
exit 1
fi
_escaped_furl=$(json_escape "$FEED_URL")
webmaster_post "/feeds/change" "{\"url\":\"$_escaped_furl\",\"newRegionIds\":[$REGION_IDS]}" > "$TMPFILE"
echo "Feed regions updated."
;;
add-status)
if [ -z "$REQUEST_ID" ]; then
echo "Error: --request-id is required for --action add-status" >&2
exit 1
fi
webmaster_get "/feeds/add/info" --data-urlencode "requestId=$REQUEST_ID" > "$TMPFILE"
_status=$(json_extract_field_raw "$(cat "$TMPFILE")" "processStatus")
echo "Upload status: $_status"
;;
*)
echo "Error: unknown action '$ACTION'. Use: list, add, change, add-status" >&2
exit 1
;;
esac
#!/bin/sh
# Get site details and owners
# Usage: host_info.sh --host <domain> | --host-id <id>
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_host_info_$$.json"
trap 'rm -f "$TMPFILE" "${TMPFILE}_owners"' EXIT
# Get host info
webmaster_get "" > "$TMPFILE"
echo "=== Site Info ==="
_hi_url=$(json_extract_field_raw "$(cat "$TMPFILE")" "ascii_host_url")
_hi_ver=$(json_extract_bool "$(cat "$TMPFILE")" "verified")
_hi_status=$(json_extract_field_raw "$(cat "$TMPFILE")" "host_data_status")
_hi_display=$(json_extract_field_raw "$(cat "$TMPFILE")" "host_display_name")
_hi_mirror=$(json_extract_field_raw "$(cat "$TMPFILE")" "ascii_host_url")
echo "Host ID: $HOST_ID"
echo "URL: $_hi_url"
echo "Display name: ${_hi_display:--}"
echo "Verified: $_hi_ver"
echo "Data status: ${_hi_status:--}"
# Get owners
echo ""
echo "=== Owners ==="
webmaster_get "/owners" > "${TMPFILE}_owners"
echo "login verification_type verification_date"
tr -d '\n\r' < "${TMPFILE}_owners" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_login=$(json_extract_field_raw "$_line" "user_login")
_vtype=$(json_extract_field_raw "$_line" "verification_type")
_vdate=$(json_extract_field_raw "$_line" "verification_date")
[ -z "$_login" ] && continue
printf '%s\t%s\t%s\n' "$_login" "$_vtype" "${_vdate:--}"
done
#!/bin/sh
# List Yandex Webmaster sites with cache + TSV index
# Usage: hosts.sh [--no-cache] [--search <text>]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
SEARCH=""
NO_CACHE=""
while [ $# -gt 0 ]; do
case "$1" in
--no-cache) NO_CACHE="1"; shift ;;
--search) SEARCH="$2"; shift 2 ;;
*) shift ;;
esac
done
ensure_user_id
CACHE_TSV="$CACHE_DIR/hosts.tsv"
# Try cache first
if [ -z "$NO_CACHE" ] && [ -f "$CACHE_TSV" ] && [ -s "$CACHE_TSV" ]; then
if [ -n "$SEARCH" ]; then
echo "host_id url verified"
grep -i "$SEARCH" "$CACHE_TSV" || echo "(no matches for '$SEARCH')"
else
# Prepend header for print_tsv_head
_tmp_display="${WM_TMPDIR}/wm_hosts_display_$$.tsv"
{ echo "host_id url verified"; cat "$CACHE_TSV"; } > "$_tmp_display"
print_tsv_head "$_tmp_display" 30
rm -f "$_tmp_display"
fi
echo ""
echo "(cached: $CACHE_TSV)"
exit 0
fi
# Fetch from API
echo "Fetching hosts from API..." >&2
TMPFILE="${WM_TMPDIR}/wm_hosts_list_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
webmaster_user_get "/hosts" > "$TMPFILE"
# Save raw JSON
mkdir -p "$CACHE_DIR"
cp "$TMPFILE" "$CACHE_DIR/hosts.json"
# Generate TSV: host_id<TAB>url<TAB>verified
{
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_hid=$(json_extract_field_raw "$_line" "host_id")
_url=$(json_extract_field_raw "$_line" "ascii_host_url")
_ver=$(json_extract_bool "$_line" "verified")
[ -z "$_hid" ] && continue
printf '%s\t%s\t%s\n' "$_hid" "$_url" "$_ver"
done
} > "$CACHE_TSV"
# Output
echo "host_id url verified"
if [ -n "$SEARCH" ]; then
grep -i "$SEARCH" "$CACHE_TSV" || echo "(no matches for '$SEARCH')"
else
print_tsv_head "$CACHE_TSV" 30
fi
echo ""
echo "Total hosts: $(wc -l < "$CACHE_TSV" | tr -d ' ')"
echo "Cached: $CACHE_TSV"
#!/bin/sh
# Important URLs — list and history
# Usage: important_urls.sh --host <domain> --action list|history [--url URL_FOR_HISTORY]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
PAGE_URL=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--url) PAGE_URL="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-list}"
TMPFILE="${WM_TMPDIR}/wm_impurls_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
list)
webmaster_get "/important-urls" > "$TMPFILE"
echo "url indexing_status http_code searchable title changes"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_url=$(json_extract_field_raw "$_line" "url")
[ -z "$_url" ] && continue
_istatus=$(json_extract_field_raw "$_line" "status")
_icode=$(json_extract_number "$_line" "http_code")
_searchable=$(json_extract_bool "$_line" "searchable")
_title=$(json_extract_field_raw "$_line" "title")
_changes=$(json_extract_array_strings "$_line" "change_indicators" | tr '\n' ',' | sed 's/,$//')
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$_url" "${_istatus:--}" "${_icode:--}" "${_searchable:--}" "${_title:--}" "${_changes:--}"
done
;;
history)
if [ -z "$PAGE_URL" ]; then
echo "Error: --url <page_url> is required for --action history" >&2
exit 1
fi
webmaster_get "/important-urls/history" --data-urlencode "url=$PAGE_URL" > "$TMPFILE"
echo "update_date indexing_status http_code searchable title"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_udate=$(json_extract_field_raw "$_line" "update_date")
[ -z "$_udate" ] && continue
_istatus=$(json_extract_field_raw "$_line" "status")
_icode=$(json_extract_number "$_line" "http_code")
_searchable=$(json_extract_bool "$_line" "searchable")
_title=$(json_extract_field_raw "$_line" "title")
_date=$(printf '%s' "$_udate" | cut -c1-10)
printf '%s\t%s\t%s\t%s\t%s\n' "$_date" "${_istatus:--}" "${_icode:--}" "${_searchable:--}" "${_title:--}"
done
;;
*)
echo "Error: unknown action '$ACTION'. Use: list, history" >&2
exit 1
;;
esac
#!/bin/sh
# Indexing history and samples
# Usage: indexing.sh --host <domain> --action history|samples
# [--date-from] [--date-to] [--limit N] [--offset N]
# History defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-history}"
TMPFILE="${WM_TMPDIR}/wm_indexing_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
history)
apply_default_dates
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/indexing"
_hash=$(cache_key "indexing_history_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/indexing/history_${_hash}.tsv"
# TTL cache check (24h)
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
_curl_args=""
if [ -n "$DATE_FROM" ]; then
_curl_args="--data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/indexing/history" $_curl_args > "$TMPFILE"
# Flatten JSON, extract each indicator to date<TAB>value TSV, merge with awk
tr -d '\n\r' < "$TMPFILE" > "${TMPFILE}.flat"
_t2="${WM_TMPDIR}/wm_idx_2xx_$$.tsv"
_t3="${WM_TMPDIR}/wm_idx_3xx_$$.tsv"
_t4="${WM_TMPDIR}/wm_idx_4xx_$$.tsv"
_t5="${WM_TMPDIR}/wm_idx_5xx_$$.tsv"
_to="${WM_TMPDIR}/wm_idx_oth_$$.tsv"
trap 'rm -f "$TMPFILE" "${TMPFILE}.flat" "$_t2" "$_t3" "$_t4" "$_t5" "$_to"' EXIT
# Extract date<TAB>value for each indicator (one grep + sed per indicator)
_extract_indicator() {
grep -o "\"$1\"[[:space:]]*:\[[^]]*\]" "$2" | head -1 | \
grep -o '"date":"[^"]*","value":[0-9]*' | \
sed 's/"date":"//;s/","value":/\t/' | cut -c1-10,11- > "$3"
}
_extract_indicator "HTTP_2XX" "${TMPFILE}.flat" "$_t2"
_extract_indicator "HTTP_3XX" "${TMPFILE}.flat" "$_t3"
_extract_indicator "HTTP_4XX" "${TMPFILE}.flat" "$_t4"
_extract_indicator "HTTP_5XX" "${TMPFILE}.flat" "$_t5"
_extract_indicator "OTHER" "${TMPFILE}.flat" "$_to"
# Merge all indicators by date: sorted date list + awk lookup (O(n), portable)
_tdates="${WM_TMPDIR}/wm_idx_dates_$$.txt"
cut -f1 "$_t2" "$_t3" "$_t4" "$_t5" "$_to" | sort -u > "$_tdates"
{
echo "date 2xx 3xx 4xx 5xx other"
awk -F'\t' '
FILENAME == ARGV[1] { v2[$1]=$2; next }
FILENAME == ARGV[2] { v3[$1]=$2; next }
FILENAME == ARGV[3] { v4[$1]=$2; next }
FILENAME == ARGV[4] { v5[$1]=$2; next }
FILENAME == ARGV[5] { vo[$1]=$2; next }
{
d=$1
printf "%s\t%s\t%s\t%s\t%s\t%s\n", d, \
(d in v2 ? v2[d] : 0), (d in v3 ? v3[d] : 0), \
(d in v4 ? v4[d] : 0), (d in v5 ? v5[d] : 0), \
(d in vo ? vo[d] : 0)
}
' "$_t2" "$_t3" "$_t4" "$_t5" "$_to" "$_tdates"
} > "$_out_file"
rm -f "$_tdates"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
;;
samples)
_curl_args=""
if [ -n "$LIMIT" ]; then
_curl_args="--data-urlencode limit=$LIMIT"
fi
if [ -n "$OFFSET" ]; then
_curl_args="$_curl_args --data-urlencode offset=$OFFSET"
fi
# shellcheck disable=SC2086
webmaster_get "/indexing/samples" $_curl_args > "$TMPFILE"
_count=$(json_extract_number "$(cat "$TMPFILE")" "count")
echo "url status http_code access_date"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_url=$(json_extract_field_raw "$_line" "url")
[ -z "$_url" ] && continue
_status=$(json_extract_field_raw "$_line" "status")
_code=$(json_extract_number "$_line" "http_code")
_access=$(json_extract_field_raw "$_line" "access_date")
_adate=$(printf '%s' "$_access" | cut -c1-10)
printf '%s\t%s\t%s\t%s\n' "$_url" "$_status" "${_code:--}" "${_adate:--}"
done | head -30
echo ""
echo "Total URLs: ${_count:-?} (showing first 30, max 50000 via API)"
;;
*)
echo "Error: unknown action '$ACTION'. Use: history, samples" >&2
exit 1
;;
esac
#!/bin/sh
# Pages in search — history and samples
# Usage: insearch.sh --host <domain> --action history|samples
# [--date-from] [--date-to] [--limit N] [--offset N]
# History defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-history}"
TMPFILE="${WM_TMPDIR}/wm_insearch_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
history)
apply_default_dates
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/insearch"
_hash=$(cache_key "insearch_history_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/insearch/history_${_hash}.tsv"
# TTL cache check (24h)
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
_curl_args=""
if [ -n "$DATE_FROM" ]; then
_curl_args="--data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/search-urls/in-search/history" $_curl_args > "$TMPFILE"
{
echo "date pages_in_search"
tr -d '\n\r' < "$TMPFILE" | grep -o '"date":"[^"]*","value":[0-9]*' | while IFS= read -r _match; do
_date=$(printf '%s' "$_match" | sed 's/.*"date":"//;s/".*//' | cut -c1-10)
_val=$(printf '%s' "$_match" | sed 's/.*"value"://')
printf '%s\t%s\n' "$_date" "$_val"
done
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
;;
samples)
_curl_args=""
if [ -n "$LIMIT" ]; then
_curl_args="--data-urlencode limit=$LIMIT"
fi
if [ -n "$OFFSET" ]; then
_curl_args="$_curl_args --data-urlencode offset=$OFFSET"
fi
# shellcheck disable=SC2086
webmaster_get "/search-urls/in-search/samples" $_curl_args > "$TMPFILE"
_count=$(json_extract_number "$(cat "$TMPFILE")" "count")
echo "url title last_access"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_url=$(json_extract_field_raw "$_line" "url")
[ -z "$_url" ] && continue
_title=$(json_extract_field_raw "$_line" "title")
_access=$(json_extract_field_raw "$_line" "last_access")
_adate=$(printf '%s' "$_access" | cut -c1-10)
printf '%s\t%s\t%s\n' "$_url" "${_title:--}" "${_adate:--}"
done | head -30
echo ""
echo "Total in search: ${_count:-?} (showing first 30, max 50000 via API)"
;;
*)
echo "Error: unknown action '$ACTION'. Use: history, samples" >&2
exit 1
;;
esac
#!/bin/sh
# Broken internal links — samples and history
# Usage: internal_links.sh --host <domain> --action samples|history
# [--indicator SITE_ERROR|DISALLOWED_BY_USER|UNSUPPORTED_BY_ROBOT]
# [--date-from] [--date-to] [--limit N] [--offset N]
# History defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
INDICATOR=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--indicator) INDICATOR="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-samples}"
TMPFILE="${WM_TMPDIR}/wm_intlinks_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
samples)
_curl_args=""
if [ -n "$LIMIT" ]; then
_curl_args="--data-urlencode limit=$LIMIT"
fi
if [ -n "$OFFSET" ]; then
_curl_args="$_curl_args --data-urlencode offset=$OFFSET"
fi
if [ -n "$INDICATOR" ]; then
_curl_args="$_curl_args --data-urlencode indicator=$INDICATOR"
fi
# shellcheck disable=SC2086
webmaster_get "/links/internal/broken/samples" $_curl_args > "$TMPFILE"
_count=$(json_extract_number "$(cat "$TMPFILE")" "count")
echo "source_url destination_url discovery_date"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_src=$(json_extract_field_raw "$_line" "source_url")
[ -z "$_src" ] && continue
_dst=$(json_extract_field_raw "$_line" "destination_url")
_disc=$(json_extract_field_raw "$_line" "discovery_date")
printf '%s\t%s\t%s\n' "$_src" "$_dst" "${_disc:--}"
done | head -30
echo ""
echo "Total broken links: ${_count:-?} (showing first 30)"
;;
history)
apply_default_dates
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/links"
_hash=$(cache_key "int_links_history_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/links/internal_history_${_hash}.tsv"
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
_curl_args=""
if [ -n "$DATE_FROM" ]; then
_curl_args="--data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/links/internal/broken/history" $_curl_args > "$TMPFILE"
tr -d '\n\r' < "$TMPFILE" > "${TMPFILE}.flat"
_tdbu="${WM_TMPDIR}/wm_il_dbu_$$.tsv"
_tse="${WM_TMPDIR}/wm_il_se_$$.tsv"
_tubr="${WM_TMPDIR}/wm_il_ubr_$$.tsv"
trap 'rm -f "$TMPFILE" "${TMPFILE}.flat" "$_tdbu" "$_tse" "$_tubr"' EXIT
_extract_indicator() {
grep -o "\"$1\"[[:space:]]*:\[[^]]*\]" "$2" | head -1 | \
grep -o '"date":"[^"]*","value":[0-9]*' | \
sed 's/"date":"//;s/","value":/\t/' | cut -c1-10,11- > "$3"
}
_extract_indicator "SITE_ERROR" "${TMPFILE}.flat" "$_tse"
_extract_indicator "DISALLOWED_BY_USER" "${TMPFILE}.flat" "$_tdbu"
_extract_indicator "UNSUPPORTED_BY_ROBOT" "${TMPFILE}.flat" "$_tubr"
{
echo "date site_error disallowed_by_user unsupported_by_robot"
_tdates="${WM_TMPDIR}/wm_il_dates_$$.txt"
cut -f1 "$_tse" "$_tdbu" "$_tubr" | sort -u > "$_tdates"
awk -F'\t' '
FILENAME == ARGV[1] { se[$1]=$2; next }
FILENAME == ARGV[2] { dbu[$1]=$2; next }
FILENAME == ARGV[3] { ubr[$1]=$2; next }
{
d=$1
printf "%s\t%s\t%s\t%s\n", d, \
(d in se ? se[d] : 0), (d in dbu ? dbu[d] : 0), \
(d in ubr ? ubr[d] : 0)
}
' "$_tse" "$_tdbu" "$_tubr" "$_tdates"
rm -f "$_tdates"
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
;;
*)
echo "Error: unknown action '$ACTION'. Use: samples, history" >&2
exit 1
;;
esac
#!/bin/sh
# Popular search queries
# Usage: popular_queries.sh --host <domain> [--order-by TOTAL_SHOWS|TOTAL_CLICKS]
# [--device ALL|DESKTOP|MOBILE_AND_TABLET] [--date-from] [--date-to] [--limit N]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
ORDER_BY="TOTAL_SHOWS"
DEVICE="ALL"
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--order-by) ORDER_BY="$2"; shift 2 ;;
--device) DEVICE="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_popular_q_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
_curl_args="--data-urlencode order_by=$ORDER_BY"
# Compute cache path BEFORE API call
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/queries"
_hash=$(cache_key "popular_${ORDER_BY}_${DEVICE}_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/queries/popular_${_hash}.tsv"
# TTL cache check (24h)
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
_curl_args="$_curl_args --data-urlencode device_type_indicator=$DEVICE"
_curl_args="$_curl_args --data-urlencode query_indicator=TOTAL_SHOWS"
_curl_args="$_curl_args --data-urlencode query_indicator=TOTAL_CLICKS"
_curl_args="$_curl_args --data-urlencode query_indicator=AVG_SHOW_POSITION"
_curl_args="$_curl_args --data-urlencode query_indicator=AVG_CLICK_POSITION"
if [ -n "$LIMIT" ]; then
_curl_args="$_curl_args --data-urlencode limit=$LIMIT"
fi
if [ -n "$DATE_FROM" ]; then
_curl_args="$_curl_args --data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/search-queries/popular" $_curl_args > "$TMPFILE"
{
echo "query_id query_text shows clicks avg_show_pos avg_click_pos"
tr -d '\n\r' < "$TMPFILE" | sed 's/"query_id"/\n"query_id"/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_qid=$(json_extract_field_raw "$_line" "query_id")
[ -z "$_qid" ] && continue
_qt=$(json_extract_field_raw "$_line" "query_text")
_shows=$(json_extract_number "$_line" "TOTAL_SHOWS")
_clicks=$(json_extract_number "$_line" "TOTAL_CLICKS")
_avgsp=$(json_extract_number "$_line" "AVG_SHOW_POSITION")
_avgcp=$(json_extract_number "$_line" "AVG_CLICK_POSITION")
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$_qid" "$_qt" "${_shows:-0}" "${_clicks:-0}" "${_avgsp:--}" "${_avgcp:--}"
done
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
#!/bin/sh
# Search queries history (all or single query)
# Usage: queries_history.sh --host <domain> [--query-id <id>]
# [--device ALL|DESKTOP|MOBILE_AND_TABLET] [--date-from] [--date-to]
# Defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
QUERY_ID=""
DEVICE="ALL"
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--query-id) QUERY_ID="$2"; shift 2 ;;
--device) DEVICE="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
apply_default_dates
# Cache check before API call
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/queries"
_hash=$(cache_key "history_${QUERY_ID}_${DEVICE}_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/queries/history_${_hash}.tsv"
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
TMPFILE="${WM_TMPDIR}/wm_qhist_$$.json"
trap 'rm -f "$TMPFILE" "${TMPFILE}.flat"' EXIT
_curl_args="--data-urlencode query_indicator=TOTAL_SHOWS"
_curl_args="$_curl_args --data-urlencode query_indicator=TOTAL_CLICKS"
_curl_args="$_curl_args --data-urlencode query_indicator=AVG_SHOW_POSITION"
_curl_args="$_curl_args --data-urlencode device_type_indicator=$DEVICE"
if [ -n "$DATE_FROM" ]; then
_curl_args="$_curl_args --data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
if [ -n "$QUERY_ID" ]; then
# shellcheck disable=SC2086
webmaster_get "/search-queries/${QUERY_ID}/history" $_curl_args > "$TMPFILE"
else
# shellcheck disable=SC2086
webmaster_get "/search-queries/all/history" $_curl_args > "$TMPFILE"
fi
# Flatten, extract indicators to date<TAB>value TSV, merge with awk
tr -d '\n\r' < "$TMPFILE" > "${TMPFILE}.flat"
_ts="${WM_TMPDIR}/wm_qh_shows_$$.tsv"
_tc="${WM_TMPDIR}/wm_qh_clicks_$$.tsv"
_tp="${WM_TMPDIR}/wm_qh_pos_$$.tsv"
trap 'rm -f "$TMPFILE" "${TMPFILE}.flat" "$_ts" "$_tc" "$_tp"' EXIT
_extract_indicator() {
grep -o "\"$1\"[[:space:]]*:\[[^]]*\]" "$2" | head -1 | \
grep -o '"date":"[^"]*","value":[0-9.e+-]*' | \
sed 's/"date":"//;s/","value":/\t/' | cut -c1-10,11- > "$3"
}
_extract_indicator "TOTAL_SHOWS" "${TMPFILE}.flat" "$_ts"
_extract_indicator "TOTAL_CLICKS" "${TMPFILE}.flat" "$_tc"
_extract_indicator "AVG_SHOW_POSITION" "${TMPFILE}.flat" "$_tp"
{
echo "date shows clicks avg_position"
_tdates="${WM_TMPDIR}/wm_qh_dates_$$.txt"
cut -f1 "$_ts" "$_tc" "$_tp" | sort -u > "$_tdates"
awk -F'\t' '
FILENAME == ARGV[1] { shows[$1]=$2; next }
FILENAME == ARGV[2] { clicks[$1]=$2; next }
FILENAME == ARGV[3] { pos[$1]=$2; next }
{
d=$1
printf "%s\t%s\t%s\t%s\n", d, \
(d in shows ? shows[d] : 0), \
(d in clicks ? clicks[d] : 0), \
(d in pos ? pos[d] : "-")
}
' "$_ts" "$_tc" "$_tp" "$_tdates"
rm -f "$_tdates"
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
#!/bin/sh
# Advanced query analytics (POST endpoint, last 14 days)
# Usage: query_analytics.sh --host <domain> --text-indicator QUERY|URL
# [--filter-text "..."] [--filter-impressions ">100"] [--filter-clicks "<50"]
# [--filter-position "<10"] [--filter-ctr ">0.05"] [--filter-demand ">1000"]
# [--region-ids "213,2"] [--device ALL|DESKTOP|MOBILE_AND_TABLET|MOBILE|TABLET]
# [--search-location WEB_LOCATION|ALL_LOCATIONS] [--limit N] [--offset N]
# [--sort-by ASC|DESC]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
TEXT_INDICATOR="QUERY"
FILTER_TEXT=""
FILTER_IMPRESSIONS=""
FILTER_CLICKS=""
FILTER_POSITION=""
FILTER_CTR=""
FILTER_DEMAND=""
REGION_IDS=""
DEVICE_QA="ALL"
SEARCH_LOCATION="WEB_LOCATION"
SORT_BY=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--text-indicator) TEXT_INDICATOR="$2"; shift 2 ;;
--filter-text) FILTER_TEXT="$2"; shift 2 ;;
--filter-impressions) FILTER_IMPRESSIONS="$2"; shift 2 ;;
--filter-clicks) FILTER_CLICKS="$2"; shift 2 ;;
--filter-position) FILTER_POSITION="$2"; shift 2 ;;
--filter-ctr) FILTER_CTR="$2"; shift 2 ;;
--filter-demand) FILTER_DEMAND="$2"; shift 2 ;;
--region-ids) REGION_IDS="$2"; shift 2 ;;
--device) DEVICE_QA="$2"; shift 2 ;;
--search-location) SEARCH_LOCATION="$2"; shift 2 ;;
--sort-by) SORT_BY="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_qa_$$.json"
TMPFILE2="${WM_TMPDIR}/wm_qa_parsed_$$.tsv"
trap 'rm -f "$TMPFILE" "$TMPFILE2"' EXIT
# Build JSON request body
_limit="${LIMIT:-500}"
_offset="${OFFSET:-0}"
_json="{\"offset\":$_offset,\"limit\":$_limit"
_json="$_json,\"device_type_indicator\":\"$DEVICE_QA\""
_json="$_json,\"text_indicator\":\"$TEXT_INDICATOR\""
_json="$_json,\"search_location\":\"$SEARCH_LOCATION\""
# Filters
_filters=""
# Text filter (with JSON escaping)
if [ -n "$FILTER_TEXT" ]; then
_escaped_ft=$(json_escape "$FILTER_TEXT")
_filters="$_filters,\"text_filters\":[{\"text_indicator\":\"$TEXT_INDICATOR\",\"operation\":\"TEXT_CONTAINS\",\"value\":\"$_escaped_ft\"}]"
fi
# Statistic filters
_stat_filters=""
_build_stat_filter() {
_bsf_val="$1"
_bsf_field="$2"
if [ -n "$_bsf_val" ]; then
_bsf_op=""
_bsf_num=""
case "$_bsf_val" in
">="*) _bsf_op="GREATER_EQUAL"; _bsf_num=$(echo "$_bsf_val" | sed 's/^>=//') ;;
"<="*) _bsf_op="LESS_EQUAL"; _bsf_num=$(echo "$_bsf_val" | sed 's/^<=//') ;;
">"*) _bsf_op="GREATER_THAN"; _bsf_num=$(echo "$_bsf_val" | sed 's/^>//') ;;
"<"*) _bsf_op="LESS_THAN"; _bsf_num=$(echo "$_bsf_val" | sed 's/^<//') ;;
"="*) _bsf_op="EQUAL"; _bsf_num=$(echo "$_bsf_val" | sed 's/^=//') ;;
*) _bsf_op="GREATER_THAN"; _bsf_num="$_bsf_val" ;;
esac
if [ -n "$_stat_filters" ]; then
_stat_filters="$_stat_filters,"
fi
_stat_filters="${_stat_filters}{\"field\":\"$_bsf_field\",\"operation\":\"$_bsf_op\",\"value\":\"$_bsf_num\"}"
fi
}
_build_stat_filter "$FILTER_IMPRESSIONS" "IMPRESSIONS"
_build_stat_filter "$FILTER_CLICKS" "CLICKS"
_build_stat_filter "$FILTER_POSITION" "POSITION"
_build_stat_filter "$FILTER_CTR" "CTR"
_build_stat_filter "$FILTER_DEMAND" "DEMAND"
if [ -n "$_stat_filters" ]; then
_filters="$_filters,\"statistic_filters\":[$_stat_filters]"
fi
if [ -n "$_filters" ]; then
_json="$_json,\"filters\":{$(echo "$_filters" | sed 's/^,//')}"
fi
# Region IDs
if [ -n "$REGION_IDS" ]; then
_regions=$(echo "$REGION_IDS" | sed 's/,/,/g')
_json="$_json,\"region_ids\":[$_regions]"
fi
# Sort
if [ -n "$SORT_BY" ]; then
_json="$_json,\"sort_by_date\":{\"OrderDirection\":\"$SORT_BY\"}"
fi
_json="$_json}"
# Compute cache path BEFORE API call
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/queries"
_hash=$(cache_key "analytics_${TEXT_INDICATOR}_${FILTER_TEXT}_${DEVICE_QA}")
_out_file="$_host_dir/queries/analytics_${_hash}.tsv"
# TTL cache check (24h)
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
webmaster_post "/query-analytics/list" "$_json" > "$TMPFILE"
_count=$(json_extract_number "$(cat "$TMPFILE")" "count")
# Split response into one block per text_indicator, extract fields with sed
# Each block contains: "value":"<text>", then "field":"<name>","value":<num> pairs
tr -d '\n\r' < "$TMPFILE" | sed 's/"text_indicator"/\n"text_indicator"/g' > "$TMPFILE2"
echo "text impressions clicks ctr position demand" > "$_out_file"
while IFS= read -r _line || [ -n "$_line" ]; do
_text_val=$(json_extract_field_raw "$_line" "value")
[ -z "$_text_val" ] && continue
# Extract all field:value pairs inline, no subshell
_impr=$(printf '%s' "$_line" | grep -o '"field":"IMPRESSIONS"[^}]*' | head -1 | grep -o '"value":[0-9.e+-]*' | sed 's/"value"://')
_cl=$(printf '%s' "$_line" | grep -o '"field":"CLICKS"[^}]*' | head -1 | grep -o '"value":[0-9.e+-]*' | sed 's/"value"://')
_ctr=$(printf '%s' "$_line" | grep -o '"field":"CTR"[^}]*' | head -1 | grep -o '"value":[0-9.e+-]*' | sed 's/"value"://')
_pos=$(printf '%s' "$_line" | grep -o '"field":"POSITION"[^}]*' | head -1 | grep -o '"value":[0-9.e+-]*' | sed 's/"value"://')
_dem=$(printf '%s' "$_line" | grep -o '"field":"DEMAND"[^}]*' | head -1 | grep -o '"value":[0-9.e+-]*' | sed 's/"value"://')
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$_text_val" "${_impr:--}" "${_cl:--}" "${_ctr:--}" "${_pos:--}" "${_dem:--}"
done < "$TMPFILE2" >> "$_out_file"
# Output with 30-line limit
print_tsv_head "$_out_file" 30
echo ""
echo "Total results: ${_count:-?}"
echo "Cached: $_out_file"
echo "(Data covers last 14 days only)"
#!/bin/sh
# URL recrawl — submit, status, list, quota
# Usage: recrawl.sh --host <domain> --action submit|status|list|quota
# [--url URL] [--task-id ID] [--date-from] [--date-to] [--limit N] [--offset N]
# NOT cached (always live data)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
RECRAWL_URL=""
TASK_ID=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--url) RECRAWL_URL="$2"; shift 2 ;;
--task-id) TASK_ID="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-quota}"
TMPFILE="${WM_TMPDIR}/wm_recrawl_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
submit)
if [ -z "$RECRAWL_URL" ]; then
echo "Error: --url <page_url> is required for --action submit" >&2
exit 1
fi
_escaped_url=$(json_escape "$RECRAWL_URL")
webmaster_post "/recrawl/queue" "{\"url\":\"$_escaped_url\"}" > "$TMPFILE"
_body=$(cat "$TMPFILE")
_tid=$(json_extract_field_raw "$_body" "task_id")
_quota=$(json_extract_number "$_body" "quota_remainder")
echo "URL submitted for recrawl."
echo "Task ID: $_tid"
echo "Quota remaining: ${_quota:-?}"
;;
status)
if [ -z "$TASK_ID" ]; then
echo "Error: --task-id is required for --action status" >&2
exit 1
fi
webmaster_get "/recrawl/queue/${TASK_ID}" > "$TMPFILE"
_body=$(cat "$TMPFILE")
echo "=== Recrawl Task ==="
echo "Task ID: $(json_extract_field_raw "$_body" "task_id")"
echo "URL: $(json_extract_field_raw "$_body" "url")"
echo "State: $(json_extract_field_raw "$_body" "state")"
echo "Added: $(json_extract_field_raw "$_body" "added_time")"
;;
list)
_curl_args=""
if [ -n "$LIMIT" ]; then
_curl_args="--data-urlencode limit=$LIMIT"
fi
if [ -n "$OFFSET" ]; then
_curl_args="$_curl_args --data-urlencode offset=$OFFSET"
fi
if [ -n "$DATE_FROM" ]; then
_curl_args="$_curl_args --data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/recrawl/queue" $_curl_args > "$TMPFILE"
_list_tmp="${WM_TMPDIR}/wm_recrawl_list_$$.tsv"
{
echo "task_id url state added_time"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_tid=$(json_extract_field_raw "$_line" "task_id")
[ -z "$_tid" ] && continue
_url=$(json_extract_field_raw "$_line" "url")
_state=$(json_extract_field_raw "$_line" "state")
_added=$(json_extract_field_raw "$_line" "added_time")
_adate=$(printf '%s' "$_added" | cut -c1-10)
printf '%s\t%s\t%s\t%s\n' "$_tid" "$_url" "$_state" "${_adate:--}"
done
} > "$_list_tmp"
print_tsv_head "$_list_tmp" 30
rm -f "$_list_tmp"
;;
quota)
webmaster_get "/recrawl/quota" > "$TMPFILE"
_body=$(cat "$TMPFILE")
_daily=$(json_extract_number "$_body" "daily_quota")
_remainder=$(json_extract_number "$_body" "quota_remainder")
echo "=== Recrawl Quota ==="
echo "Daily quota: ${_daily:-?}"
echo "Remaining: ${_remainder:-?}"
;;
*)
echo "Error: unknown action '$ACTION'. Use: submit, status, list, quota" >&2
exit 1
;;
esac
#!/bin/sh
# Regions directory for query analytics
# Usage: regions.sh --host <domain> [--filter "москва"] [--limit N]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
FILTER=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--filter) FILTER="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_regions_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
_curl_args=""
if [ -n "$FILTER" ]; then
_curl_args="--data-urlencode filter=$FILTER"
fi
if [ -n "$LIMIT" ]; then
_curl_args="$_curl_args --data-urlencode limit=$LIMIT"
fi
# PRO endpoint
# shellcheck disable=SC2086
webmaster_raw_get "/v4/user/${USER_ID}/hosts/${HOST_ID}/pro/regions" $_curl_args > "$TMPFILE"
echo "region_id name"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_rid=$(json_extract_number "$_line" "id")
[ -z "$_rid" ] && continue
_rname=$(json_extract_field_raw "$_line" "name")
printf '%s\t%s\n' "$_rid" "$_rname"
done
#!/bin/sh
# Search events — appeared/removed from search
# Usage: search_events.sh --host <domain> --action history|samples
# [--date-from] [--date-to] [--limit N] [--offset N]
# History defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-history}"
TMPFILE="${WM_TMPDIR}/wm_sevents_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
case "$ACTION" in
history)
apply_default_dates
_host_dir=$(cache_host_dir)
mkdir -p "$_host_dir/insearch"
_hash=$(cache_key "events_history_${DATE_FROM}_${DATE_TO}")
_out_file="$_host_dir/insearch/events_${_hash}.tsv"
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
_curl_args=""
if [ -n "$DATE_FROM" ]; then
_curl_args="--data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/search-urls/events/history" $_curl_args > "$TMPFILE"
tr -d '\n\r' < "$TMPFILE" > "${TMPFILE}.flat"
_ta="${WM_TMPDIR}/wm_se_app_$$.tsv"
_tr="${WM_TMPDIR}/wm_se_rem_$$.tsv"
trap 'rm -f "$TMPFILE" "${TMPFILE}.flat" "$_ta" "$_tr"' EXIT
_extract_indicator() {
grep -o "\"$1\"[[:space:]]*:\[[^]]*\]" "$2" | head -1 | \
grep -o '"date":"[^"]*","value":[0-9]*' | \
sed 's/"date":"//;s/","value":/\t/' | cut -c1-10,11- > "$3"
}
_extract_indicator "APPEARED_IN_SEARCH" "${TMPFILE}.flat" "$_ta"
_extract_indicator "REMOVED_FROM_SEARCH" "${TMPFILE}.flat" "$_tr"
{
echo "date appeared removed"
_tdates="${WM_TMPDIR}/wm_se_dates_$$.txt"
cut -f1 "$_ta" "$_tr" | sort -u > "$_tdates"
awk -F'\t' '
FILENAME == ARGV[1] { app[$1]=$2; next }
FILENAME == ARGV[2] { rem[$1]=$2; next }
{
d=$1
printf "%s\t%s\t%s\n", d, \
(d in app ? app[d] : 0), (d in rem ? rem[d] : 0)
}
' "$_ta" "$_tr" "$_tdates"
rm -f "$_tdates"
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
;;
samples)
_curl_args=""
if [ -n "$LIMIT" ]; then
_curl_args="--data-urlencode limit=$LIMIT"
fi
if [ -n "$OFFSET" ]; then
_curl_args="$_curl_args --data-urlencode offset=$OFFSET"
fi
# shellcheck disable=SC2086
webmaster_get "/search-urls/events/samples" $_curl_args > "$TMPFILE"
_count=$(json_extract_number "$(cat "$TMPFILE")" "count")
echo "url title event event_date excluded_status"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_url=$(json_extract_field_raw "$_line" "url")
[ -z "$_url" ] && continue
_title=$(json_extract_field_raw "$_line" "title")
_event=$(json_extract_field_raw "$_line" "event")
_edate=$(json_extract_field_raw "$_line" "event_date")
_exstatus=$(json_extract_field_raw "$_line" "excluded_url_status")
_ed=$(printf '%s' "$_edate" | cut -c1-10)
printf '%s\t%s\t%s\t%s\t%s\n' "$_url" "${_title:--}" "$_event" "${_ed:--}" "${_exstatus:--}"
done | head -30
echo ""
echo "Total events: ${_count:-?} (showing first 30, max 50000 via API)"
;;
*)
echo "Error: unknown action '$ACTION'. Use: history, samples" >&2
exit 1
;;
esac
#!/bin/sh
# PRO SERP export — available dates, limits, init export, check status
# Usage: search_export.sh --host <domain> --action dates|limits|start|status
# [--dates "2025-01-01,2025-01-02"] [--paths "/,/catalog/"]
# [--region-ids "213"] [--task-id ID]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
EXPORT_DATES=""
EXPORT_PATHS=""
REGION_IDS=""
TASK_ID=""
_args=""
while [ $# -gt 0 ]; do
case "$1" in
--dates) EXPORT_DATES="$2"; shift 2 ;;
--paths) EXPORT_PATHS="$2"; shift 2 ;;
--region-ids) REGION_IDS="$2"; shift 2 ;;
--task-id) TASK_ID="$2"; shift 2 ;;
*) _args="$_args $1"; shift ;;
esac
done
# shellcheck disable=SC2086
parse_host_params $_args
ensure_user_id
resolve_host
require_host
ACTION="${ACTION:-dates}"
TMPFILE="${WM_TMPDIR}/wm_export_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
_pro_base="/v4/user/${USER_ID}/hosts/${HOST_ID}/pro"
case "$ACTION" in
dates)
webmaster_raw_get "${_pro_base}/serp/dates" > "$TMPFILE"
echo "=== Available Export Dates ==="
json_extract_array_strings "$(cat "$TMPFILE")" "dates"
;;
limits)
webmaster_raw_get "${_pro_base}/limits" > "$TMPFILE"
echo "owner feature limit used remaining active period"
tr -d '\n\r' < "$TMPFILE" | sed 's/},{/}\n{/g' | while IFS= read -r _line || [ -n "$_line" ]; do
_owner=$(json_extract_field_raw "$_line" "owner")
[ -z "$_owner" ] && continue
_feat=$(json_extract_field_raw "$_line" "feature")
_lim=$(json_extract_number "$_line" "limit")
_used=$(json_extract_number "$_line" "used")
_rem=$(json_extract_number "$_line" "remaining")
_active=$(json_extract_bool "$_line" "is_active")
_pstart=$(json_extract_field_raw "$_line" "period_start")
_pend=$(json_extract_field_raw "$_line" "period_end")
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s..%s\n' "$_owner" "$_feat" "${_lim:-0}" "${_used:-0}" "${_rem:-0}" "${_active:--}" "${_pstart:--}" "${_pend:--}"
done
;;
start)
if [ -z "$EXPORT_DATES" ] || [ -z "$EXPORT_PATHS" ]; then
echo "Error: --dates and --paths are required for --action start" >&2
echo "Example: --dates '\"2025-01-01\",\"2025-01-02\"' --paths '\"/\",\"/catalog/\"'" >&2
exit 1
fi
_json="{\"dates\":[$EXPORT_DATES],\"paths\":[$EXPORT_PATHS]"
if [ -n "$REGION_IDS" ]; then
_json="$_json,\"region_ids\":[$REGION_IDS]"
fi
_json="$_json,\"use_pro_tariff\":\"false\"}"
webmaster_raw_post "${_pro_base}/serp/queries/download/" "$_json" > "$TMPFILE"
_body=$(cat "$TMPFILE")
_tid=$(json_extract_field_raw "$_body" "task_id")
_free_used=$(json_extract_number "$_body" "free_quota_used")
_free_rem=$(json_extract_number "$_body" "free_quota_remaining")
echo "Export started."
echo "Task ID: $_tid"
echo "Free quota used: ${_free_used:-0}"
echo "Free quota remaining: ${_free_rem:-?}"
echo "Check status: bash scripts/search_export.sh --host ... --action status --task-id $_tid"
;;
status)
if [ -z "$TASK_ID" ]; then
echo "Error: --task-id is required for --action status" >&2
exit 1
fi
webmaster_raw_get "${_pro_base}/serp/queries/download/${TASK_ID}" > "$TMPFILE"
_body=$(cat "$TMPFILE")
_status=$(json_extract_field_raw "$_body" "download_status")
_url=$(json_extract_field_raw "$_body" "url")
echo "=== Export Status ==="
echo "Task ID: $TASK_ID"
echo "Status: $_status"
if [ -n "$_url" ]; then
echo "Download: $_url"
echo "(URL valid for 24 hours)"
fi
;;
*)
echo "Error: unknown action '$ACTION'. Use: dates, limits, start, status" >&2
exit 1
;;
esac
#!/bin/sh
# SQI (Site Quality Index) history
# Usage: sqi_history.sh --host <domain> [--date-from YYYY-MM-DD] [--date-to YYYY-MM-DD]
# Defaults to last 90 days if --date-from is not specified.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
apply_default_dates
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_sqi_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
_host_dir=$(cache_host_dir)
_out_file="$_host_dir/sqi_history.tsv"
# TTL cache check (24h)
if [ -z "$NO_CACHE" ] && cache_get_ttl "$_out_file" 1440; then
print_tsv_head "$_out_file" 30
echo ""
echo "(cached: $_out_file)"
exit 0
fi
# Build curl args for dates
_curl_args=""
if [ -n "$DATE_FROM" ]; then
_curl_args="$_curl_args --data-urlencode date_from=${DATE_FROM}T00:00:00.000+0300"
fi
if [ -n "$DATE_TO" ]; then
_curl_args="$_curl_args --data-urlencode date_to=${DATE_TO}T00:00:00.000+0300"
fi
# shellcheck disable=SC2086
webmaster_get "/sqi-history" $_curl_args > "$TMPFILE"
{
echo "date sqi"
tr -d '\n\r' < "$TMPFILE" | grep -o '"date":"[^"]*","value":[0-9]*' | while IFS= read -r _match; do
_date=$(printf '%s' "$_match" | sed 's/.*"date":"//;s/".*//' | cut -c1-10)
_val=$(printf '%s' "$_match" | sed 's/.*"value"://')
printf '%s\t%s\n' "$_date" "$_val"
done
} > "$_out_file"
print_tsv_head "$_out_file" 30
echo ""
echo "Cached: $_out_file"
#!/bin/sh
# Site summary: SQI, page counts, problems
# Usage: summary.sh --host <domain>
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
load_config
parse_host_params "$@"
ensure_user_id
resolve_host
require_host
TMPFILE="${WM_TMPDIR}/wm_summary_$$.json"
trap 'rm -f "$TMPFILE"' EXIT
webmaster_get "/summary" > "$TMPFILE"
_body=$(cat "$TMPFILE")
_sqi=$(json_extract_number "$_body" "sqi")
_searchable=$(json_extract_number "$_body" "searchable_pages_count")
_excluded=$(json_extract_number "$_body" "excluded_pages_count")
echo "=== Site Summary ==="
echo "SQI: ${_sqi:-0}"
echo "Searchable pages: ${_searchable:-0}"
echo "Excluded pages: ${_excluded:-0}"
echo ""
echo "=== Site Problems ==="
_fatal=$(printf '%s' "$_body" | grep -o '"FATAL"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
_critical=$(printf '%s' "$_body" | grep -o '"CRITICAL"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
_possible=$(printf '%s' "$_body" | grep -o '"POSSIBLE_PROBLEM"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
_recommend=$(printf '%s' "$_body" | grep -o '"RECOMMENDATION"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | sed 's/.*:[[:space:]]*//')
echo "FATAL: ${_fatal:-0}"
echo "CRITICAL: ${_critical:-0}"
echo "POSSIBLE_PROBLEM: ${_possible:-0}"
echo "RECOMMENDATION: ${_recommend:-0}"
if [ "${_fatal:-0}" -gt 0 ] 2>/dev/null || [ "${_critical:-0}" -gt 0 ] 2>/dev/null; then
echo ""
echo "Run 'bash scripts/diagnostics.sh --host ...' for details."
fi