GitHub Repo Collector
WordPress-плагин: сборщик подборок GitHub-репозиториев с карточками, README (GitHub-HTML) и переводом Yandex.
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/llm-agent-github-repos/archive/refs/heads/main.zipGitHub Repo Collector (llm-agent-github-repos)
Версия 0.3.0 · Самодостаточный WordPress-плагин: собирает подборки GitHub-репозиториев по настраиваемым правилам (топики, звёзды, свежесть) и публикует их на сайте карточками. Не зависит от ACF/CPT UI и вообще от каких-либо других плагинов — переносим на любой сайт копированием папки.
Проектный план и обоснование решений — docs/github-repo-parser-plan.md в корне репозитория. Этот файл — справочник по тому, что реально реализовано в коде.
1. Структура данных
Типы записей (регистрируются в коде, class-lagr-cpt.php)
github_repo— собранные репозитории. Публичный, с архивом,show_in_rest. Поддержка:title,editor(используется под README/LLM-текст черезpost_content),thumbnail,revisions.github_repo_rule— правила сбора. Непубличный (только в админке), без REST.
Таксономии
repo_topic— своя таксономия. Всегда ровно один термин на запись — тот топик, что задан в правиле сбора (не путать с полным набором родных топиков репозитория на GitHub).repo_language— своя, один термин: основной язык репозитория (languageиз Repos API).repo_license— своя, один термин: SPDX id лицензии.- Остальные топики репозитория (всё, что в GitHub-топиках репозитория, но не совпало с топиком правила) + теги от LLM (если включена генерация) — уходят в настраиваемую таксономию, по умолчанию
post_tag(общий словарь с блогом). См. настройку «Топики репозитория» → «Куда девать остальные топики». Значения:post_tag(по умолчанию),repo_topic(слить всё в одну, старое поведение),none(не добавлять), либо любая другая зарегистрированная публичная таксономия сайта. Связь выбранной таксономии сgithub_repoрегистрируется автоматически наinit(LAGR_CPT::link_extra_topics_taxonomy()), не требует ручной настройки темы или разового скрипта.
Важно про /tag/x/ и /category/x/ (встроенные таксономии). Одной привязки таксономии недостаточно, чтобы репозитории реально показывались на архиве тега — WordPress по умолчанию ограничивает запрос архива встроенной таксономии post_type=post, даже когда к ней подключены другие типы записей (в отличие от is_tax()-архивов вроде нашего repo_topic, где WP сам расширяет post_type). Без дополнительного кода /tag/ai/ показывал бы 0 репозиториев, хотя термины и счётчики были бы верными. Исправлено хуком pre_get_posts — LAGR_CPT::include_shared_taxonomies_in_archives() — расширяет post_type на архиве встроенной таксономии до всех реально зарегистрированных для неё типов.
Также нужна разовая настройка Relevanssi, если используется как поисковый движок (см. integration_relevanssi): по умолчанию он индексирует только post/page — github_repo нужно добавить в опцию relevanssi_index_post_types и пересобрать индекс (админка: Relevanssi → Индексация → «Построить индекс», либо разово relevanssi_build_index()). Без этого поиск по сайту не находит ни одного репозитория и не показывает при этом никакой ошибки — просто пустая выдача.
Поля (register_post_meta, show_in_rest: true — поэтому Block Bindings в шаблонах работают сразу, без дополнительного mu-plugin)
Полный список — LAGR_Meta_Fields::repo_meta_keys():
| Meta-ключ | Источник |
|---|---|
_gh_full_name |
Repos/Search API, ключ дедупа |
_gh_stars, _gh_forks, _gh_watchers, _gh_open_issues |
Repos API |
_gh_language, _gh_license, _gh_homepage |
Repos API |
_gh_created_at, _gh_pushed_at, _gh_updated_at |
Repos API |
_gh_default_branch, _gh_size_kb, _gh_owner_login, _gh_owner_type, _gh_owner_avatar |
Repos API |
short_description |
description репозитория (обрезано до ~200) либо первая строка README, либо LLM |
_gh_score |
stars + forks × score_forks_weight (настройка) |
_gh_rule_id |
ID правила, которое собрало запись |
_gh_needs_review |
1, если данные источника изменились после первого сбора — не перезаписываем молча |
_gh_source_description |
«снимок» description на момент сбора — с чем сравнивать при повторном проходе |
_gh_generated_by_llm |
1, если short/full description сгенерированы ИИ, а не взяты как есть |
_gh_contributors_count, _gh_latest_release_tag, _gh_latest_release_at, _gh_languages_json, _gh_top_files_json |
опциональные поля — см. раздел 4 |
Полное описание (README, сконвертированный из Markdown, либо текст от LLM) пишется не в отдельное поле, а в нативный post_content — так работает стандартный блок core/post-content и автоматически подключается Contextual Related Posts (у него хук на the_content).
2. Правила сбора (github_repo_rule)
Все поля из LAGR_Meta_Fields::rule_meta_keys(), редактируются на экране правила (class-lagr-meta-fields.php::render_rule_box):
| Поле | Meta-ключ | Дефолт |
|---|---|---|
| GitHub-топики (через запятую, комбинируются через И) | _lagr_topics |
— |
| Звёзды: оператор | _lagr_stars_operator |
> |
| Звёзды: число | _lagr_stars_value |
— |
| Свежесть, дней (0 = без ограничения) | _lagr_freshness_days |
— |
| Исключать форки / архивные / требовать описание | _lagr_exclude_forks, _lagr_exclude_archived, _lagr_require_description |
вкл |
| Мин. размер, КБ | _lagr_min_size_kb |
— |
Сортировка (updated/stars/forks) |
_lagr_sort_by |
updated |
| Лимит за прогон (макс. 100 — предел Search API) | _lagr_limit |
— |
| Сырой GitHub-запрос (продвинутый режим) | _lagr_raw_query |
пусто |
| Куда публиковать (post type) | _lagr_target_post_type |
github_repo |
| Режим публикации | _lagr_publish_mode |
draft |
| Интервал запуска, минут (минимум 10 — см. раздел 5) | _lagr_interval_minutes |
1440 (24ч) |
| Включено | _lagr_enabled |
— |
Если _lagr_raw_query заполнено — используется как есть вместо сборки из топиков/звёзд/свежести (LAGR_Query_Builder::build_from_rule()). GitHub-запрос поддерживает и квалификатор размера (size:>=N), добавляется в запрос автоматически.
Валидация при сохранении. При каждом сохранении правила (LAGR_Meta_Fields::save_rule_meta) плагин реально выполняет тестовый запрос к GitHub (LAGR_GitHub_Client::test_search()) и пишет результат («нашлось N репозиториев» либо текст ошибки) в _lagr_last_test_result — показывается прямо на экране редактирования.
Служебные, не редактируются вручную: _lagr_last_run_at, _lagr_running_since, _lagr_last_run_summary, _lagr_last_test_result — пишутся только кодом коллектора/планировщика.
Стартовые правила (class-lagr-rule-defaults.php)
При активации плагина создаётся ~41 правило — весь набор топиков из refs/git-post-bot (build_presets()) и refs/git-digest-bot (digest_topics), плюс agent-skills. Включены по умолчанию только три: «AI-агенты» (ai-agent, >50), «Agent Skills» (agent-skills, >10), «MCP» (mcp, >30). Остальные — данные, не код, можно включать/редактировать/удалять как угодно.
3. Сбор данных (class-lagr-collector.php)
LAGR_Collector::run_rule( $rule_id ):
- Проверка лока правила (
_lagr_running_since, протухает через 30 минут — см. раздел 5). - Проверка бюджета GitHub API — если исчерпан на текущий час, правило пропускается целиком.
- Сборка/использование GitHub-запроса, вызов
/search/repositories(sort/order/per_pageиз настроек правила). - Фильтр
require_description(форки/архив/размер/свежесть уже зашиты в сам запрос через квалификаторыfork:/archived:/size:/pushed:). - Дедуп по
_gh_full_name(WP_Queryпо мете) — если уже существует, при изменении звёзд/description ставится_gh_needs_review = 1, поля не трогаются молча (та же non-destructive политика, что и в основном ТЗ каталога). - Для новых кандидатов: README (
/repos/{full}/readme), опциональные поля (раздел 4), скоринг, генерация описаний (раздел 6),wp_insert_post(черновик или сразу публикация — по настройке правила), таксономии (раздел 1), SEO meta description (LAGR_SEO::apply_meta_description()— раздел 8а). - Итоговая сводка — в
_lagr_last_run_summaryи в журнал (LAGR_Log).
Markdown → HTML fallback (когда LLM выключен) — минимальный конвертер без зависимостей (markdown_to_html()): заголовки, **bold**, ссылки, вырезает бейджи/картинки-шапки и типовые raw-HTML обёртки логотипов (<picture>, <div align="center">, <img>). Не идеален для экзотичной разметки — известное ограничение, не проблема для основного текста.
4. Опциональные поля
Выключены по умолчанию (настройки плагина), запрашиваются только для отфильтрованных кандидатов, не для сырых результатов поиска — экономия бюджета GitHub API:
| Настройка | Метод клиента | Meta-поле |
|---|---|---|
| Контрибьюторы | get_contributors_count() (через заголовок Link, per_page=1) |
_gh_contributors_count |
| Релизы | get_latest_release() |
_gh_latest_release_tag, _gh_latest_release_at |
| Языки (%) | get_languages() |
_gh_languages_json |
| Верхнеуровневые файлы | get_top_level_contents() |
_gh_top_files_json |
4а. Язык и лицензия — отдельные переключатели, не «опциональные поля»
_gh_language/_gh_license — другая история: это бесплатные данные, приходят вместе с базовым ответом Repos API (без доп. запроса) и сохраняются в meta всегда, независимо ни от каких настроек. Отдельными настройками (collect_language, collect_license, обе выключены по умолчанию) управляется только:
- присвоение таксономии
repo_language/repo_licenseпри сборе (class-lagr-collector.php::create_repo_post()); - показ языка/лицензии в паттернах — карточка (
card_markup()) и single-страница (single_repo_layout_markup()), через хелперыLAGR_Patterns::language_term_block()/license_term_block().
Выключены по умолчанию осознанно: основной контент сети — Agent Skills и MCP-серверы, для них язык/лицензия обычно не главный признак каталогизации (в отличие от общего каталога кода). Для другой тематики сайта сети — включить оба чекбокса в настройках, взять данные из уже сохранённого _gh_language/_gh_license не получится задним числом для уже собранных записей (таксономия не была присвоена) — понадобится либо ре-собрать, либо разовый скрипт wp_set_object_terms() по уже сохранённой meta.
Боковой фильтр Filter Everything (раздел 8в) изначально включал фильтры «Язык»/«Лицензия» — убраны из набора вручную 2026-09-04 вслед за выключением этих настроек, чтобы не оставлять на архиве рабочий фильтр по несуществующим в разметке полям.
5. Расписание (class-lagr-scheduler.php, cron.php)
Три независимых способа запустить один и тот же тик — решение принято после разбора refs/wp-automatic (устойчивый cron даже на старом PHP, потому что не зависит от WP-CLI):
- WP-Cron, кастомный интервал 10 минут (
lagr_10_minutes, регистрируется черезcron_schedules). Работает из коробки, зависит от посещаемости сайта. - HTTP-эндпоинт с секретом —
?github_repos_cron=СЕКРЕТна главный URL сайта (LAGR_Scheduler::maybe_handle_http_trigger(), хукinit). Секрет — 32 случайных hex-символа (LAGR_Settings::regenerate_secret()), не придумывается руками (в отличие от wp-automatic, где дефолтный секрет — предсказуемое словоcron). Сравнение черезhash_equals(). Свой троттлинг — не чаще раза в минуту даже с верным секретом. - Прямой PHP CLI,
cron.phpв корне плагина — подключаетwp-load.phpнапрямую (../../../wp-load.php), в обход WP-CLI. При вызове из HTTP (не из CLI) тоже требует секрет.
Все три ведут в LAGR_Scheduler::run_tick() — общая логика: взять включённые правила, сравнить _lagr_interval_minutes (минимум 10 — тик физически не может сработать чаще собственной частоты) с _lagr_last_run_at, запустить созревшие.
Блокировки:
- Лок правила (
_lagr_running_since) — таймаут 30 минут (три тика), не 10, чтобы не словить гонку ровно на границе интервала. - Лок самого тика — транзиент
lagr_tick_running, TTL 9 минут (с запасом внутри 10-минутного окна).
Бюджет GitHub API (class-lagr-github-client.php) — опция _gh_api_budget ({hour, used}), сбрасывается автоматически при смене часа. Порог — настройка gh_budget_threshold (дефолт 4500 из 5000, запас на ручные операции).
Готовые команды для внешнего крона показываются прямо в настройках плагина (LAGR_Scheduler::get_trigger_commands()) — curl / wget / прямой php cron.php, ничего не нужно вычислять руками.
6. Генерация описаний (class-lagr-llm-client.php)
Выключено по умолчанию (llm_enabled = false) — тогда short_description = обрезанное description репозитория (либо первая строка README), full_description/post_content = README как есть через Markdown-конвертер.
Если включено — один вызов на репозиторий, промпт просит вернуть JSON {short_description, full_description, tags} (см. LAGR_LLM_Client::SYSTEM_PROMPT). Разбор ответа терпим к обёртке в ```json (модели иногда так делают вопреки инструкции).
4 провайдера, все — один HTTP-клиент с разными пресетами (LAGR_Settings::llm_provider_config()), потому что все четыре — OpenAI-совместимый chat/completions:
| Провайдер | Endpoint | Авторизация |
|---|---|---|
| Yandex Foundation Models | ai.api.cloud.yandex.net/v1/chat/completions |
Api-Key + x-folder-id |
| DeepSeek напрямую | api.deepseek.com/v1/chat/completions |
Bearer |
| Timeweb (TWC AI API) | agent.timeweb.cloud/.../agents/{agent_access_id}/v1/chat/completions |
Bearer + x-proxy-source; поле model игнорируется — модель настроена на стороне агента в панели Timeweb |
| OpenRouter | openrouter.ai/api/v1/chat/completions |
Bearer |
7. Скоринг
score = stars + forks × score_forks_weight (по умолчанию вес 3)
Используется только для сортировки очереди модерации по важности — не влияет на сам факт публикации (это решает _lagr_publish_mode).
8. Админка (class-lagr-admin.php)
Пункт меню «GitHub-репозитории» (lagr-main) + подпункты, зарегистрированные плагином:
- Очередь модерации (стартовая страница) — таблица правил (статус, интервал, последний запуск, кнопка «Запустить сейчас»), очередь черновиков по score с кнопкой «Опубликовать», список записей с флагом «требует проверки».
- Настройки — GitHub-токен, бюджет запросов/час, опциональные поля (раздел 4), язык и лицензия репозитория (раздел 4а), топики репозитория (куда девать «остальные» — раздел 1), интеграции с другими плагинами (раздел 8а), вес скоринга, генерация LLM (раздел 6), секрет и команды cron (раздел 5).
- Журнал — последние ~200 записей лога (
LAGR_Log), человекочитаемый список того, что и когда собрал планировщик.
Плюс штатные экраны CPT: Правила сбора (список/редактирование github_repo_rule), GitHub-репозитории (список/редактирование github_repo).
8а. Интеграции с другими плагинами (class-lagr-seo.php, LAGR_Settings::integration_enabled())
Не каждый из 20 сайтов сети обязательно будет держать одинаковый набор сторонних плагинов — секция настроек «Интеграции» даёт переключить это без правки кода:
| Настройка | Плагин | Эффект |
|---|---|---|
seo_plugin (yoast/rankmath/none) |
Yoast SEO или Rank Math | Подключено к коду. При создании записи LAGR_SEO::apply_meta_description() пишет сгенерированное краткое описание (первые ~30 слов, без тегов) напрямую в meta-поле выбранного плагина: _yoast_wpseo_metadesc для Yoast, rank_math_description для Rank Math. none — ничего не пишет. |
integration_ratings |
— (свой код, см. раздел 8г) | Подключено. С 0.3.0 больше не про сторонний плагин — гейтит вставку собственного блока lagr/rating в карточку и single-страницу (LAGR_Patterns::ratings_block()). Выключено — рейтинг просто не попадает в разметку паттерна при регистрации. |
integration_related_posts |
— (свой код, см. раздел 8г) | Подключено. С 0.3.0 больше не про сторонний плагин — гейтит вставку собственного блока lagr/related-posts на single-странице (LAGR_Patterns::related_posts_block()). |
integration_filter_everything |
Filter Everything | НЕ подключено, всё ещё вручную. Боковая колонка с [fe_widget] вшита напрямую во все 4 архивных шаблона в install-templates.php, без проверки этого флага. На сайте без Filter Everything это даст пустую колонку с нерабочим шорткодом; при переносе на другой сайт сети — либо ставить и настраивать Filter Everything так же (раздел 8в), либо убрать колонку из шаблона. |
integration_external_links |
WP External Links | Флаг-документация, не читается кодом: подтверждает, что внешние ссылки в контенте не нужно вручную помечать target=_blank/rel=nofollow — это уже делает сторонний плагин автоматически, независимо от значения флага. |
integration_relevanssi |
Relevanssi | Флаг-документация, не читается кодом: Relevanssi сам перехватывает поиск WordPress через собственные хуки, как только активен и настроен (раздел про /tag/x/ и поиск в разделе 1) — переключатель ничего не включает и не выключает программно. |
LAGR_Settings::integration_enabled( 'ratings' ) — обёртка над get( 'integration_ratings' ), используется в class-lagr-patterns.php.
Реально читаются кодом на 2026-09-04: seo_plugin, integration_ratings, integration_related_posts — все три меняют фактическую разметку паттернов. integration_filter_everything/integration_external_links/integration_relevanssi — флаги-заготовки/документация, ничего не переключают программно (см. пояснения в таблице выше).
8б. Паттерны (class-lagr-patterns.php)
Реализация макетов из docs/mockups/github-repo-pattern-*.html в виде настоящих блочных паттернов (register_block_pattern()), доступных в инсертере редактора под категорией «GitHub Repo Collector». Как и договаривались — это только вставляемые заготовки: после вставки в шаблон/страницу через редактор паттерн превращается в обычные, полностью редактируемые блоки. Сама привязка паттернов к реальным шаблонам сайта (архив, single, главная) — отдельный, осознанный шаг, не выполняется автоматически при регистрации.
| Паттерн | Что это |
|---|---|
lagr/repo-card |
Одна карточка репозитория — содержимое Post Template внутри core/query (Query Loop). Единственный источник разметки карточки — переиспользуется в остальных паттернах ниже, без копипасты. |
lagr/archive-grid |
Сетка карточек для архива темы/языка/лицензии — core/query с inherit:true (наследует запрос текущего архива) + пагинация + состояние «ничего не найдено». |
lagr/homepage-tiles-top |
Плитка на главную, 6 карточек, сортировка по _gh_score (см. ниже про query_loop_block_query_vars). |
lagr/homepage-tiles-new |
Плитка на главную, 6 карточек, сортировка по дате. |
lagr/tag-and-topic-cloud |
Два core/tag-cloud рядом — repo_topic и post_tag. |
lagr/hero-search |
core/search + быстрые ссылки на топ-6 самых крупных тем (реальные термины repo_topic, не статичный текст). |
lagr/single-repo-layout |
Полная разметка одиночной записи — шапка, рейтинг, статистика, core/post-content + боковая колонка, похожие записи. |
Сортировка по score в Query Loop. Штатный инспектор блока «Цикл запроса» не умеет сортировать по произвольному meta-полю — только по фиксированному набору (дата/заголовок/случайно). Решение: в JSON-атрибут query паттерна homepage-tiles-top добавлен собственный ключ lagrOrderBy: "score" (WP не отбрасывает незнакомые ключи в объекте query, просто их игнорирует при стандартной сборке $query_args) — и хук query_loop_block_query_vars в LAGR_Patterns::filter_score_order() перехватывает именно эту комбинацию и подставляет meta_key=_gh_score, orderby=meta_value_num.
Гейтинг по интеграциям. Блок рейтинга (wp-postratings/ratings — почему не шорткод, см. ниже) и блок «Похожие репозитории» ([crp], Contextual Related Posts) вставляются в паттерн только если соответствующий тумблер в настройках включён (LAGR_Settings::integration_enabled()) — проверяется в момент регистрации паттерна на PHP-стороне, а не в браузере.
Важная находка при реализации: _gh_* meta и Block Bindings
Все meta-поля репозитория названы с ведущим подчёркиванием (_gh_stars и т.д.) — стандартная WP-конвенция, чтобы поле не показывалось в дефолтном метабоксе «Произвольные поля» (у нас свои метабоксы). Но у этой конвенции есть побочный эффект: is_protected_meta() считает любое поле с _ защищённым, а источник core/post-meta в Block Bindings явно отказывается отдавать защищённые поля — независимо от show_in_rest. Без учёта этого все привязки на паттернах молча показывали бы placeholder вместо реальных данных (проверено вживую: до фикса карточка показывала «0» звёзд и слово «owner» вместо реального ника, хотя в БД лежали верные значения).
Исправлено точечным фильтром в LAGR_Meta_Fields::unprotect_for_bindings() (хук is_protected_meta) — снимает защиту только с полей из repo_meta_keys(), ничего не меняет для остальных _-полей сайта.
Паттерны вставлены в реальные шаблоны сайта
Шесть шаблонов установлены как записи wp_template (тема twentytwentyfive) через разовый скрипт (не входит в сам плагин — миграция выполнена вручную при внедрении):
| Шаблон | Содержимое |
|---|---|
archive-github_repo |
core/query-title + core/term-description + lagr/archive-grid |
taxonomy-repo_topic / taxonomy-repo_language / taxonomy-repo_license |
то же самое — один и тот же archive-grid работает для всех, т.к. Query Loop там с inherit:true |
single-github_repo |
lagr/single-repo-layout |
front-page |
lagr/hero-search → lagr/homepage-tiles-top → lagr/tag-and-topic-cloud → lagr/homepage-tiles-new → блок «Из блога» (обычный core/query по post) |
front-page.html перекрывает главную независимо от настройки «Показывать на главной странице» в Настройках → Чтение — is_front_page() истинен что при «последних записях», что при «статической странице», поэтому этот шаблон сработал без изменения Reading Settings; блог как был на /-корне в виде home.html, так и остался бы там, если бы не было front-page.html (теперь достаточно приоритетен и используется вместо него для главной).
Важное ограничение: контент шаблона — это одноразовый снимок, не живая ссылка на паттерн. Once вставлен в wp_template, PHP-код паттерна больше не перевызывается для этого шаблона — правки в class-lagr-patterns.php (новое поле в карточке, другой текст) НЕ появятся на сайте, пока шаблон не переустановить заново (пересобрать контент через WP_Block_Patterns_Registry и обновить wp_template пост). Это ровно то же поведение, что и при вставке паттерна вручную через редактор — паттерн становится обычным контентом. На это напоролись при реализации: попытка показать «популярные темы» на главной через get_terms() в PHP на момент регистрации паттерна заморозила список на состоянии базы в момент установки шаблона — почему в итоге это заменено на настоящий динамический блок core/tag-cloud (см. код hero_search_markup()) вместо пре-вычисленной строки.
Баг рейтинга внутри Query Loop: [ratings] показывал один и тот же ID на всех карточках
При первой живой проверке плитки на главной (после публикации реальных данных) все карточки показывали один и тот же ID рейтинга — причём чужой, совпадающий с ID первой записи блога, а не текущего репозитория. Причина — в самом устройстве core/shortcode: этот блок НЕ выполняет [ratings] сразу (render_block_core_shortcode() в ядре WP лишь оборачивает текст в wpautop()), реальное разворачивание шорткода происходит позже, одним проходом do_shortcode() по уже собранному HTML всей страницы — то есть уже ПОСЛЕ того, как core/post-template завершил перебор карточек и вызвал wp_reset_postdata(). К этому моменту глобальный $post/$id (на них опирается легаси-функция the_ratings()) указывает на «внешний» пост страницы, один и тот же для всех вхождений [ratings] разом.
Исправлено переходом с шорткода на собственный нативный блок плагина — wp-postratings/ratings (WP-PostRatings его регистрирует наравне с шорткодом, class-wp-postratings-blocks.php). Блок — полноценный динамический блок ядра Gutenberg, рендерится синхронно в момент прохода core/post-template по каждому элементу — так же, как core/post-title/core/post-terms, поэтому корректно видит текущий пост цикла. Проверено вживую: 12 карточек на главной — 12 разных, верных ID.
Ещё две правки, потребовавшиеся ради паттернов
_gh_url— новое meta-поле (https://github.com/{full_name}), нужно было для привязкиurlкнопки «GitHub →» (Block Bindings не умеет конкатенировать строки, только отдавать готовое значение поля). Для 100 уже собранных до этой правки записей потребовался разовый бэкфилл (без обращений к GitHub API — вычисляется из уже сохранённого_gh_full_name).post_date/post_modifiedтеперь берутся изcreated_at/pushed_atрепозитория, а не из момента сбора — иначеcore/post-dateв паттернах показывал бы дату импорта, а не реальную свежесть репозитория на GitHub.
8в. Filter Everything — боковой фильтр архива
Настроен через админку плагина Filter Everything (не через наш плагин) — «Набор фильтров» (CPT filter-set, пост #409):
| Фильтр | Критерий | Вид |
|---|---|---|
| Теги | таксономия post_tag |
чекбоксы |
| Звёзды GitHub | meta _gh_stars, тип «Числа» |
числовой диапазон (слайдер) |
«Язык» и «Лицензия» были в наборе изначально, убраны 2026-09-04 вслед за выключением collect_language/collect_license (раздел 4а) — иначе фильтр показывал бы рабочие чекбоксы для полей, которых больше нет ни на карточках, ни на single-странице.
Локация: «Common WordPress pages» → «All archive pages for this Post Type» на типе записи github_repo — один набор фильтров автоматически покрывает архив CPT (/repos/) и все три его таксономии (/repo-topic/x/, /repo-language/x/, /repo-license/x/), без необходимости создавать 4 разных набора.
Вставлен в шаблоны шорткодом [fe_widget] внутри core/columns (22% сайдбар / 78% сетка) — см. install-templates.php. Проверено вживую: клик по чекбоксу реально фильтрует список через AJAX (не просто рисует чекбокс).
Известное ограничение: это НЕ управляется тумблером integration_filter_everything из настроек плагина (см. раздел 8а) — шорткод вшит в шаблон напрямую. На сайте сети без Filter Everything колонка отрисуется, но фильтр не заработает.
При редактировании набора фильтров через админку Filter Everything — осторожно с кнопками «Удалить». На экране их две с одинаковым текстом: (1) ссылка в панели «Опубликовать» отправляет в корзину весь набор фильтров; (2) кнопка внутри развёрнутой строки конкретного фильтра (button.wpc-button-link-delete) удаляет только её, но требует подтверждения второй кнопкой «Удалить, я уверен» — и после такого удаления обязательно нажать «Обновить» всей формы, иначе фронтенд-виджет [fe_widget] рендерится пустым (изменения на бэкенде уже применились, но публичный вывод не пересобирается без полного сохранения).
8г. Свой рейтинг и «Похожие репозитории» (class-lagr-ratings.php, class-lagr-related.php) — с версии 0.3.0
Решение 2026-09-04: WP-PostRatings и Contextual Related Posts убраны из зависимостей плагина — не потому что были плохи, а потому что для этих двух функций объём собственной реализации оказался маленьким, а контроль/переносимость на 20 сайтов сети — выше. Filter Everything и Relevanssi (раздел 8в и раздел 1) остались сторонними — там объём для самостоятельной переделки уже несоразмерный.
Рейтинг (lagr/rating, класс LAGR_Ratings). Хранение — два postmeta-счётчика: _lagr_rating_total (сумма голосов), _lagr_rating_count (число голосов), среднее считается на лету. Инкремент — не get_post_meta() + update_post_meta() (это read-then-write: два одновременных голоса могут прочитать одно и то же стартовое значение, один голос потеряется), а прямой UPDATE wp_postmeta SET meta_value = meta_value + N WHERE post_id=... AND meta_key=... — атомарно на уровне строки в InnoDB. Голосование — клик по сердечку на фронтенде (blocks/rating/view.js, автоподключается через viewScript в block.json, без ручного wp_enqueue_script()) отправляет AJAX (wp_ajax_lagr_rate/wp_ajax_nopriv_lagr_rate) с nonce; один голос с браузера — через cookie lagr_voted_{post_id}, без другой защиты от накрутки (сознательно просто, как и счётчик кликов в partner-catalog).
Похожие репозитории (lagr/related-posts, класс LAGR_Related). Обычный get_posts() с tax_query по repo_topic (тому самому курируемому термину, всегда один на запись — более точный сигнал «похожести», чем generic keyword-matching CRP), отсортировано по _gh_stars. Оба блока — динамические (render.php, без сохранённой разметки) по той же причине, что и wp-postratings/ratings раньше (раздел «Баг рейтинга внутри Query Loop» ниже) — рендерятся синхронно за каждый элемент цикла, корректно работают внутри Query Loop.
Русская плюрализация («1 оценка» / «3 оценки» / «5 оценок») — своя функция ru_plural() (классические 3 формы), потому что штатная _n() в WordPress поддерживает только 2 формы (единственное/множественное) и для русского языка даёт неверный текст на «оценка/оценки/оценок».
Обычные записи блога (post) продолжают использовать WP-PostRatings как раньше — этот плагин теперь просто не установлен как зависимость llm-agent-github-repos/partner-catalog, но с сайта не убирался и не трогался.
9. WP-CLI (class-lagr-cli.php)
Регистрируется только если запущено под WP-CLI (defined('WP_CLI') && WP_CLI), не обязательно для работы плагина — альтернатива для сайтов, где есть SSH:
wp github-repos tick # прогнать тик (то же, что cron)
wp github-repos run --rule=<id> # запустить конкретное правило прямо сейчас, игнорируя интервал
wp github-repos list-rules # таблица всех правил: ID, включено, интервал, последний запуск
Важно для этого сервера (Beget): команда wp — shell-обёртка с зашитым путём на PHP 7.4, не сам .phar. Для действий, чувствительных к версии PHP (например, установка стороннего плагина с Requires PHP выше 7.4), вызывать .phar напрямую: php8.3 /usr/local/bin/wp-cli.phar ... — WP_CLI_PHP=... wp ... не всегда это перекрывает (проверено эмпирически). См. docs/stack-decisions.md.
10. Известные ограничения / открытые вопросы
- Markdown→HTML fallback (без LLM) не разбирает списки/таблицы/блоки кода — только заголовки, bold, ссылки, параграфы. Осознанный минимализм, не баг.
- Пагинация GitHub Search API за один запрос ограничена 100 результатами — если понадобится собирать больше 100 за прогон одного правила, нужна доработка (не в MVP).
- «Топ по популярности» через реальные просмотры/лайки — WP Post Views Counter сейчас не используется (решили отказаться от счётчика просмотров), WP-PostRatings даёт рейтинг, но не количество посещений.