Partner Catalog
WordPress-плагин: каталог поставщиков/подрядчиков — типы, специализации, регионы, UTM-метки.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/geliorn/partner-catalog/archive/refs/heads/main.zipPartner Catalog (partner-catalog)
Версия 0.2.0 · Самодостаточный WordPress-плагин: каталог подрядчиков и сервис-провайдеров (интеграторы, дистрибьюторы, хостинги, облачные платформы и т.д.) с полностью ручным наполнением карточек через админку. Никакого коллектора/парсинга/внешнего API — в этом ключевое архитектурное отличие от llm-agent-github-repos. Не зависит от ACF/CPT UI — переносим на любой сайт копированием папки.
Проектный план и обоснование решений (референсы startpack.ru/workspace.ru, разбор полей, макеты) — docs/partner-catalog-plan.md в корне репозитория. Этот файл — справочник по тому, что реально реализовано в коде.
1. Структура данных
Тип записи (class-pc-cpt.php)
contractor(Подрядчики) — публичный, с архивом (/contractors/),show_in_rest. Поддержка:title,editor(полное описание компании черезpost_content),thumbnail(логотип),revisions.
Таксономии
contractor_type— тип сервиса/партнёра. Открытый список терминов, множественный выбор. Это ключевой механизм универсальности плагина: чтобы добавить новый тип сервиса (хостинг, облачная платформа, что угодно ещё) — не нужно трогать код, достаточно завести новый термин через админку. Компания может одновременно быть, например, и интегратором, и дистрибьютором.contractor_specialization— специализация/платформы/технологии подрядчика (1С, Bitrix24, AWS, MCP/AI-агенты и т.д.). Множественный выбор, растущий тег-подобный список.contractor_region— регион/город присутствия. Множественный выбор (компания может работать в нескольких регионах). В отличие от каталога GitHub-репозиториев (где регион осмысленно убрали как неприменимый), здесь это реальный, часто используемый фильтр — подтверждено обоими изученными референсами.
Поля (register_post_meta, show_in_rest: true — Block Bindings в паттернах работают сразу)
Полный список — PC_Meta_Fields::meta_keys():
| Meta-ключ | Назначение |
|---|---|
_pc_short_description |
Короткое описание (1–2 предложения) для карточки в архиве — отдельное поле, не excerpt, чтобы не путать с SEO-описанием |
_pc_website |
Сайт партнёра — никогда не выводится напрямую в разметку, см. раздел 4 |
_pc_phone, _pc_email, _pc_telegram, _pc_vk |
Контакты. Фиксированный набор полей, не универсальный репитер — см. «Осознанные упрощения» ниже |
_pc_price_type, _pc_price_value |
Универсальное поле цены — см. раздел 2 |
_pc_features |
Особенности сервиса, тезисно — каждая мысль с новой строки, на выходе немаркированный список (блок partner-catalog/meta-list, раздел 3) |
_pc_partnership |
Партнёрства/сертификации, каждая с новой строки — на выходе строка бейджей (тот же блок, стиль badges) |
_pc_founded_year |
Год основания |
_pc_team_size |
Размер команды (свободный текст, например «10–50 человек») |
_pc_clients |
Ключевые клиенты, каждый с новой строки — тот же список-блок |
_pc_utm_override |
Ручные UTM-параметры для конкретной записи — см. раздел 4 |
Полное описание (post_content) — обычный Gutenberg-контент, редактируется в стандартном редакторе записи, не отдельное meta-поле. Так работает core/post-content в single-паттерне.
2. Цена — универсальное поле (_pc_price_type + _pc_price_value)
Разные типы сервисов озвучивают цену по-разному (интегратор — «от», хостинг — тариф/подписка, разработка под ключ — фиксированная сумма, консалтинг — почасовая ставка). Поэтому цена — это тип (select) + значение (текст), а не один свободный текст:
Тип (_pc_price_type) |
Как выводится |
|---|---|
from |
«от {value} ₽» |
range |
«{value} ₽» (значение уже вида «150 000 – 500 000») |
fixed |
«{value} ₽ за проект» |
hourly |
«{value} ₽/час» |
on_request |
«Цена по запросу» (значение не нужно) |
В meta-боксе под полем — подсказка с примерами по каждому типу. Единицы измерения вроде «/мес» дописываются прямо в значении — форматтер их не навязывает (см. blocks/price/render.php).
Форматирование — не core/post-meta binding (он не умеет условной логики), а собственный маленький динамический блок partner-catalog/price (раздел 3).
3. Динамические блоки (class-pc-blocks.php, blocks/)
core/post-meta binding умеет только отдать значение поля как есть — не может превратить многострочный текст в список или применить условие по типу цены. Плюс два блока, заменяющие сторонние плагины (раздел 3а). Отсюда четыре маленьких серверных блока:
| Блок | Назначение |
|---|---|
partner-catalog/meta-list |
Читает multiline meta-поле (_pc_features, _pc_partnership, _pc_clients), разбивает по строкам, рендерит <ul><li>. Атрибут style: bullet (обычный список) или badges (строка пилюль без маркеров — для партнёрств) |
partner-catalog/price |
Форматирует _pc_price_type + _pc_price_value в готовую строку, см. раздел 2 |
partner-catalog/rating |
Свой рейтинговый виджет — см. раздел 3а |
partner-catalog/related-posts |
Свой список похожих подрядчиков — см. раздел 3а |
Все четыре — без сборки (JS через wp.blocks.registerBlockType, не собранный React/JSX), рендер — чистый PHP (render.php), поэтому корректно работают внутри Query Loop (пересчитываются на каждый элемент цикла, как core/post-title) — в отличие от шорткода, который резолвится один раз на всю страницу (см. известную находку про [ratings] в README llm-agent-github-repos, раздел 8б — та же причина, тот же класс проблемы, из-за которой рейтинг и похожие подрядчики тоже сделаны блоками, а не шорткодами).
3а. Свой рейтинг и «Похожие подрядчики» (class-pc-ratings.php, class-pc-related.php) — с версии 0.2.0
Решение 2026-09-04 (одновременно с тем же изменением в llm-agent-github-repos): WP-PostRatings и Contextual Related Posts убраны из зависимостей — обе функции по объёму оказались достаточно маленькими, чтобы держать свой код было разумнее, чем тянуть два сторонних плагина на каждый из 20 сайтов сети. Filter Everything остался сторонним (раздел 5) — там объём для самостоятельной переделки несоразмерный.
- Рейтинг. Два postmeta-счётчика (
_pc_rating_total,_pc_rating_count), инкремент атомарнымUPDATE ... SET meta_value = meta_value + N(не read-then-write — иначе одновременные голоса теряются), голосование кликом по сердечку через AJAX (wp_ajax_pc_rate), один голос с браузера через cookiepc_voted_{id}— без другой защиты от накрутки, тот же уровень простоты, что у счётчика кликов (раздел 4.2). - Похожие подрядчики.
get_posts()сtax_query(relation: OR) по общим терминамcontractor_typeИЛИcontractor_specialization— точнее, чем generic keyword-matching CRP, раз у записи уже есть две структурные оси похожести. - Код не переиспользуется между
partner-catalogиllm-agent-github-repos(дублирован) — оба плагина обязаны оставаться независимо переносимыми по отдельности.
4. Аналитика и UTM-метки
Плагин не только показывает карточки, но и считает переходы/клики по каждой и размечает исходящие ссылки UTM-метками. Без защиты от фрода/ботов — простой внутренний дашборд, не CPA-платформа.
4.1. UTM — авто или вручную, с приоритетом override (class-pc-links.php::build_final_url())
- Авто (по умолчанию). Настройки плагина задают шаблон:
utm_source,utm_medium,utm_campaign/utm_contentс токенами{slug}и{contractor_type}. - Вручную (опционально, на уровне записи). Поле «Ручные UTM-параметры» (
_pc_utm_override) в карточке подрядчика — строка видаutm_source=vk&utm_medium=cpc. Если заполнено — полностью заменяет автогенерацию для этой записи. - Реальный URL партнёра (
_pc_website) никогда не печатается в разметке напрямую — только через генератор.
4.2. Счётчик переходов/кликов (class-pc-stats.php)
Своя таблица {prefix}pc_stats (contractor_id, event_type — view/click, stat_date, count; уникальный ключ на все три, инкремент через INSERT … ON DUPLICATE KEY UPDATE):
view— на каждый реальный показsingle-contractor(template_redirect, без дедупликации по IP/сессии — намеренно просто).click— все ссылки «на сайт партнёра» в паттернах ведут не на реальный сайт напрямую, а на собственный редирект-эндпоинт/out/{id}/(class-pc-links.php). Обработчик: проверка записи → инкремент клика → подстановка UTM (раздел 4.1) →wp_redirect()(302). Один эндпоинт закрывает обе задачи.
Кнопка на сайте партнёра биндится не на реальное meta-поле, а на виртуальный _pc_outbound_url — оно не хранится в БД, значение подставляется на лету через short-circuit фильтра get_post_metadata (нужно, чтобы Block Bindings внутри Query Loop могли получить готовый URL редиректа для каждой карточки отдельно — то же ограничение шорткодов внутри цикла, что и в разделе 3).
4.3. Дашборд в админке (class-pc-admin.php, меню «Подрядчики → Аналитика»)
KPI (переходы, клики, конверсия %), график по дням (простой canvas без внешних библиотек — сознательно, чтобы не тянуть CDN-зависимость в wp-admin, см. assets/js/pc-admin-dashboard.js), таблица по подрядчикам, фильтры по типу/специализации/региону и по конкретной записи, диапазон дат.
4.4. Цели в Яндекс.Метрику (assets/js/pc-analytics.js)
ID счётчика — отдельное поле в настройках (не предполагается, что счётчик уже где-то программно доступен плагину). Две цели, обе no-op, если ym не определён или ID не задан:
partner_profile_view— при загрузке страницы подрядчика.partner_click— по клику на ссылку с классомpc-outbound-link(только реальная кнопка «Перейти на сайт», не социальные ссылки — см. комментарий вclass-pc-patterns.php::bound_button()).
5. Паттерны (class-pc-patterns.php)
Реализация макетов из docs/mockups/partner-catalog-pattern-*.html — не копия паттернов llm-agent-github-repos: там репозиторий со звёздами/языком/лицензией, здесь компания с контактами/специализацией/ценой — разный состав данных, разная композиция.
| Паттерн | Что это |
|---|---|
partner-catalog/card |
Одна карточка подрядчика — содержимое Post Template внутри Query Loop. Единственный источник разметки карточки. |
partner-catalog/archive-grid |
Сетка карточек для архива каталога/типа/специализации/региона, inherit:true + пагинация. Боковой AJAX-фильтр (Filter Everything) — настраивается отдельно в самой админке Filter Everything, тем же способом, что описан в README llm-agent-github-repos (раздел 8в) — в код паттерна не встроен. |
partner-catalog/single-layout |
Шапка (лого, тип/специализация/регион, партнёрства, CTA на сайт) → рейтинг → статистика (год основания/команда/цена) → описание + особенности + клиенты → боковая колонка контактов → похожие подрядчики. |
Гейтинг по интеграциям — блок рейтинга (partner-catalog/rating) и «Похожие подрядчики» (partner-catalog/related-posts) вставляются только если соответствующий тумблер в настройках включён (PC_Settings::integration_enabled()), проверяется на PHP-стороне в момент регистрации паттерна — тот же принцип, что в GitHub-плагине. Тумблеры больше не про «установлен ли сторонний плагин» (см. раздел 3а) — просто «включена ли фича».
Патterns — только вставляемые заготовки. Как и в GitHub-плагине: привязка паттернов к реальным шаблонам сайта (архив, single) — отдельный ручной шаг в Site Editor, не выполняется автоматически при регистрации.
6. Настройки (class-pc-admin.php, class-pc-settings.php)
Пункт меню «Подрядчики» (pc-main) + подпункты:
- Настройки — UTM-шаблон (авто-режим), ID счётчика Яндекс.Метрики, SEO-плагин, интеграции (рейтинг / похожие подрядчики — свой код, раздел 3а; Filter Everything — флаг-документация, сторонний плагин).
- Аналитика — дашборд, раздел 4.3.
Плюс штатные экраны CPT/таксономий: «Все подрядчики», «Тип сервиса», «Специализация», «Регион».
7. Осознанные упрощения / отличия от плана
- Контакты — фиксированные поля (телефон/email/Telegram/VK), не универсальный репитер. В остальном стеке (включая
llm-agent-github-repos) репитеров тоже нет ни одного — решено не вводить этот паттерн ради простоты ручного ввода. Если понадобится больше каналов связи — расширить список полей, не превращать в репитер без явной необходимости. - Функция «Сравнить» (2 колонки) — не реализована в этой версии. План (
docs/partner-catalog-plan.md, раздел 8) явно относит её к «после MVP»: выбор чекбоксом на карточке → localStorage → отдельная страница сравнения по ID из query-параметров. Макет паттерна уже готов (docs/mockups/partner-catalog-pattern-compare.html), код — нет. - Матрица «платформа × конкретная услуга» (как на startpack.ru — для каждого вендора отдельный список услуг) — сознательно не реализована, отмечена в плане как возможное расширение, не нужное для ручного заполнения на старте.
8. Известные ограничения
- Счётчики
view/clickне защищены от повторных заходов/ботов — если это станет проблемой (например, разброс конверсии из-за самих поисковых роботов), потребуется отдельная доработка (исключение известных User-Agent, троттлинг по IP/сессии). - UTM-override (
_pc_utm_override) ожидает формат query-строки (key=value&key2=value2) — при опечатке в форматеparse_str()либо ничего не распарсит, либо распарсит не то, что имелось в виду; валидации формата в meta-боксе нет. - Дашборд аналитики не пагинирует таблицу «по подрядчикам» — при большом количестве записей и активности стоит добавить лимит/постраничность (сейчас жёстко ограничено 30 строками через
LIMIT, без навигации по остальным).