Compare commits
7 Commits
v1.0.0
...
d0a7b8eb9d
| Author | SHA1 | Date | |
|---|---|---|---|
| d0a7b8eb9d | |||
| d36408a4a6 | |||
| c002b08275 | |||
| 82d2345d12 | |||
| 949fb19e95 | |||
| 19409ac062 | |||
| 4c6173619b |
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
All notable changes to wp-multi-city. Format follows [Keep a Changelog](https://keepachangelog.com/).
|
All notable changes to wp-multi-city. Format follows [Keep a Changelog](https://keepachangelog.com/).
|
||||||
|
|
||||||
|
## [1.0.1] — 2026-05-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Процедурный wrapper `wpmc_main_town_slug()` (документирован в README, отсутствовал в коде).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- WP 6.7+ notice `_load_textdomain_just_in_time`: убраны `__()` из labels CPT/таксономии/ACF field group.
|
||||||
|
- ACF `acf_get_value` "called too early": `did_action('acf/init')` guard в 5 Helpers методах.
|
||||||
|
|
||||||
## [1.0.0] — 2026-05-17
|
## [1.0.0] — 2026-05-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ final class FieldGroup
|
|||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'key' => self::REPEATER_KEY,
|
'key' => self::REPEATER_KEY,
|
||||||
'label' => __('Подстановки по городам', 'wp-multi-city'),
|
'label' => 'Подстановки по городам',
|
||||||
'name' => 'list_shortcode',
|
'name' => 'list_shortcode',
|
||||||
'type' => 'repeater',
|
'type' => 'repeater',
|
||||||
'layout' => 'table',
|
'layout' => 'table',
|
||||||
'button_label' => __('Добавить город', 'wp-multi-city'),
|
'button_label' => 'Добавить город',
|
||||||
'sub_fields' => [
|
'sub_fields' => [
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_list_shortcode_town',
|
'key' => 'field_wpmc_list_shortcode_town',
|
||||||
'label' => __('Город', 'wp-multi-city'),
|
'label' => 'Город',
|
||||||
'name' => 'town',
|
'name' => 'town',
|
||||||
'type' => 'taxonomy',
|
'type' => 'taxonomy',
|
||||||
'taxonomy' => 'town',
|
'taxonomy' => 'town',
|
||||||
@@ -45,7 +45,7 @@ final class FieldGroup
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_list_shortcode_text',
|
'key' => 'field_wpmc_list_shortcode_text',
|
||||||
'label' => __('Значение', 'wp-multi-city'),
|
'label' => 'Значение',
|
||||||
'name' => 'text',
|
'name' => 'text',
|
||||||
'type' => 'textarea',
|
'type' => 'textarea',
|
||||||
'rows' => 4,
|
'rows' => 4,
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ final class Helpers
|
|||||||
private static array $page_unic_cache = [];
|
private static array $page_unic_cache = [];
|
||||||
private static int $bypass_depth = 0;
|
private static int $bypass_depth = 0;
|
||||||
|
|
||||||
|
private static function acf_ready(): bool
|
||||||
|
{
|
||||||
|
return \did_action('acf/init') > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static function reset_cache(): void
|
public static function reset_cache(): void
|
||||||
{
|
{
|
||||||
self::$current_town_memo = null;
|
self::$current_town_memo = null;
|
||||||
@@ -80,6 +85,9 @@ final class Helpers
|
|||||||
|
|
||||||
public static function alt_get_field(string $selector, $post_id = false, bool $format_value = true)
|
public static function alt_get_field(string $selector, $post_id = false, bool $format_value = true)
|
||||||
{
|
{
|
||||||
|
if (!self::acf_ready()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$post_id = \apply_filters('wpmc_alt_get_field_post_id', $post_id, $selector);
|
$post_id = \apply_filters('wpmc_alt_get_field_post_id', $post_id, $selector);
|
||||||
if (!\function_exists('get_field')) {
|
if (!\function_exists('get_field')) {
|
||||||
return null;
|
return null;
|
||||||
@@ -106,6 +114,9 @@ final class Helpers
|
|||||||
|
|
||||||
public static function index_town_alt_page_id(): ?int
|
public static function index_town_alt_page_id(): ?int
|
||||||
{
|
{
|
||||||
|
if (!self::acf_ready()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!\function_exists('get_field')) {
|
if (!\function_exists('get_field')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -116,6 +127,9 @@ final class Helpers
|
|||||||
|
|
||||||
public static function main_town_slug(): ?string
|
public static function main_town_slug(): ?string
|
||||||
{
|
{
|
||||||
|
if (!self::acf_ready()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!\function_exists('get_field')) {
|
if (!\function_exists('get_field')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -132,6 +146,9 @@ final class Helpers
|
|||||||
if (array_key_exists($key, self::$page_unic_cache)) {
|
if (array_key_exists($key, self::$page_unic_cache)) {
|
||||||
return self::$page_unic_cache[$key];
|
return self::$page_unic_cache[$key];
|
||||||
}
|
}
|
||||||
|
if (!self::acf_ready()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$term = self::get_term_by_slug($town_slug);
|
$term = self::get_term_by_slug($town_slug);
|
||||||
if ($term === null) {
|
if ($term === null) {
|
||||||
return self::$page_unic_cache[$key] = null;
|
return self::$page_unic_cache[$key] = null;
|
||||||
@@ -161,6 +178,9 @@ final class Helpers
|
|||||||
if ($path === null) {
|
if ($path === null) {
|
||||||
$path = $_SERVER['REQUEST_URI'] ?? '/';
|
$path = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
}
|
}
|
||||||
|
if (!self::acf_ready()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!\function_exists('get_field')) {
|
if (!\function_exists('get_field')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class OptionsPage
|
|||||||
'fields' => [
|
'fields' => [
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_main_town_slug',
|
'key' => 'field_wpmc_main_town_slug',
|
||||||
'label' => __('Главный город (без префикса URL)', 'wp-multi-city'),
|
'label' => 'Главный город (без префикса URL)',
|
||||||
'name' => 'main_town_slug',
|
'name' => 'main_town_slug',
|
||||||
'type' => 'taxonomy',
|
'type' => 'taxonomy',
|
||||||
'taxonomy' => 'town',
|
'taxonomy' => 'town',
|
||||||
@@ -47,7 +47,7 @@ final class OptionsPage
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_index_town_alt',
|
'key' => 'field_wpmc_index_town_alt',
|
||||||
'label' => __('Главная для других городов', 'wp-multi-city'),
|
'label' => 'Главная для других городов',
|
||||||
'name' => 'index_town_alt',
|
'name' => 'index_town_alt',
|
||||||
'type' => 'post_object',
|
'type' => 'post_object',
|
||||||
'post_type' => ['page'],
|
'post_type' => ['page'],
|
||||||
@@ -57,15 +57,15 @@ final class OptionsPage
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_page_unic',
|
'key' => 'field_wpmc_page_unic',
|
||||||
'label' => __('Клоны страниц', 'wp-multi-city'),
|
'label' => 'Клоны страниц',
|
||||||
'name' => 'page_unic',
|
'name' => 'page_unic',
|
||||||
'type' => 'repeater',
|
'type' => 'repeater',
|
||||||
'layout' => 'table',
|
'layout' => 'table',
|
||||||
'button_label' => __('Добавить клон', 'wp-multi-city'),
|
'button_label' => 'Добавить клон',
|
||||||
'sub_fields' => [
|
'sub_fields' => [
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_page_unic_page_old',
|
'key' => 'field_wpmc_page_unic_page_old',
|
||||||
'label' => __('Оригинал', 'wp-multi-city'),
|
'label' => 'Оригинал',
|
||||||
'name' => 'page_old',
|
'name' => 'page_old',
|
||||||
'type' => 'post_object',
|
'type' => 'post_object',
|
||||||
'post_type' => ['page'],
|
'post_type' => ['page'],
|
||||||
@@ -75,7 +75,7 @@ final class OptionsPage
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_page_unic_page_new',
|
'key' => 'field_wpmc_page_unic_page_new',
|
||||||
'label' => __('Клон', 'wp-multi-city'),
|
'label' => 'Клон',
|
||||||
'name' => 'page_new',
|
'name' => 'page_new',
|
||||||
'type' => 'post_object',
|
'type' => 'post_object',
|
||||||
'post_type' => ['page'],
|
'post_type' => ['page'],
|
||||||
@@ -85,7 +85,7 @@ final class OptionsPage
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_page_unic_town',
|
'key' => 'field_wpmc_page_unic_town',
|
||||||
'label' => __('Город', 'wp-multi-city'),
|
'label' => 'Город',
|
||||||
'name' => 'town',
|
'name' => 'town',
|
||||||
'type' => 'taxonomy',
|
'type' => 'taxonomy',
|
||||||
'taxonomy' => 'town',
|
'taxonomy' => 'town',
|
||||||
@@ -99,18 +99,18 @@ final class OptionsPage
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_page_no_rep',
|
'key' => 'field_wpmc_page_no_rep',
|
||||||
'label' => __('Исключения из роутинга', 'wp-multi-city'),
|
'label' => 'Исключения из роутинга',
|
||||||
'name' => 'page_no_rep',
|
'name' => 'page_no_rep',
|
||||||
'type' => 'repeater',
|
'type' => 'repeater',
|
||||||
'layout' => 'table',
|
'layout' => 'table',
|
||||||
'button_label' => __('Добавить исключение', 'wp-multi-city'),
|
'button_label' => 'Добавить исключение',
|
||||||
'sub_fields' => [
|
'sub_fields' => [
|
||||||
[
|
[
|
||||||
'key' => 'field_wpmc_page_no_rep_link',
|
'key' => 'field_wpmc_page_no_rep_link',
|
||||||
'label' => __('Путь', 'wp-multi-city'),
|
'label' => 'Путь',
|
||||||
'name' => 'link',
|
'name' => 'link',
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'instructions' => __('Префикс URL, например /privacy/', 'wp-multi-city'),
|
'instructions' => 'Префикс URL, например /privacy/',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ final class PostType
|
|||||||
{
|
{
|
||||||
register_post_type(self::SLUG, [
|
register_post_type(self::SLUG, [
|
||||||
'labels' => [
|
'labels' => [
|
||||||
'name' => __('Шорткоды', 'wp-multi-city'),
|
'name' => 'Шорткоды',
|
||||||
'singular_name' => __('Шорткод', 'wp-multi-city'),
|
'singular_name' => 'Шорткод',
|
||||||
'menu_name' => __('Шорткоды', 'wp-multi-city'),
|
'menu_name' => 'Шорткоды',
|
||||||
'add_new' => __('Добавить', 'wp-multi-city'),
|
'add_new' => 'Добавить',
|
||||||
'add_new_item' => __('Добавить шорткод', 'wp-multi-city'),
|
'add_new_item' => 'Добавить шорткод',
|
||||||
'edit_item' => __('Редактировать', 'wp-multi-city'),
|
'edit_item' => 'Редактировать',
|
||||||
'new_item' => __('Новый', 'wp-multi-city'),
|
'new_item' => 'Новый',
|
||||||
'view_item' => __('Смотреть', 'wp-multi-city'),
|
'view_item' => 'Смотреть',
|
||||||
'search_items' => __('Поиск', 'wp-multi-city'),
|
'search_items' => 'Поиск',
|
||||||
'not_found' => __('Не найдено', 'wp-multi-city'),
|
'not_found' => 'Не найдено',
|
||||||
],
|
],
|
||||||
'public' => false,
|
'public' => false,
|
||||||
'exclude_from_search' => true,
|
'exclude_from_search' => true,
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ final class Taxonomy
|
|||||||
{
|
{
|
||||||
register_taxonomy(self::SLUG, [PostType::SLUG], [
|
register_taxonomy(self::SLUG, [PostType::SLUG], [
|
||||||
'labels' => [
|
'labels' => [
|
||||||
'name' => __('Города', 'wp-multi-city'),
|
'name' => 'Города',
|
||||||
'singular_name' => __('Город', 'wp-multi-city'),
|
'singular_name' => 'Город',
|
||||||
'menu_name' => __('Города', 'wp-multi-city'),
|
'menu_name' => 'Города',
|
||||||
'add_new_item' => __('Добавить', 'wp-multi-city'),
|
'add_new_item' => 'Добавить',
|
||||||
],
|
],
|
||||||
'public' => false,
|
'public' => false,
|
||||||
'publicly_queryable' => false,
|
'publicly_queryable' => false,
|
||||||
|
|||||||
@@ -30,3 +30,10 @@ if (!function_exists('wpmc_remove_domain_link')) {
|
|||||||
return Helpers::remove_domain_link($url);
|
return Helpers::remove_domain_link($url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('wpmc_main_town_slug')) {
|
||||||
|
function wpmc_main_town_slug(): ?string
|
||||||
|
{
|
||||||
|
return Helpers::main_town_slug();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-1
@@ -4,7 +4,7 @@ Tags: multisite, multi-city, taxonomy, acf, shortcode
|
|||||||
Requires at least: 6.0
|
Requires at least: 6.0
|
||||||
Tested up to: 6.9
|
Tested up to: 6.9
|
||||||
Requires PHP: 8.0
|
Requires PHP: 8.0
|
||||||
Stable tag: 1.0.0
|
Stable tag: 1.0.1
|
||||||
License: GPLv2 or later
|
License: GPLv2 or later
|
||||||
|
|
||||||
Мультигородская подсистема: одна WP-инсталляция обслуживает несколько городов по схеме /{city}/{path}/ с подстановками контента и URL.
|
Мультигородская подсистема: одна WP-инсталляция обслуживает несколько городов по схеме /{city}/{path}/ с подстановками контента и URL.
|
||||||
@@ -23,6 +23,11 @@ License: GPLv2 or later
|
|||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 1.0.1 =
|
||||||
|
* Add: процедурный wrapper `wpmc_main_town_slug()` (был задокументирован в README, но отсутствовал в коде).
|
||||||
|
* Fix: WP 6.7+ notice `_load_textdomain_just_in_time` — убраны `__()` обёртки из labels CPT/таксономии/ACF field group (плагин не поставляется с переводами).
|
||||||
|
* Fix: ACF `acf_get_value` "called too early" notice — добавлен `did_action('acf/init')` guard в `Helpers::main_town_slug()`, `index_town_alt_page_id`, `page_unic_swap`, `is_routable_path`, `alt_get_field`. До инициализации ACF возвращают безопасные defaults (null/true).
|
||||||
|
|
||||||
= 1.0.0 =
|
= 1.0.0 =
|
||||||
* Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4).
|
* Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4).
|
||||||
* taxonomy town + CPT shortcode-town + ACF list_shortcode repeater.
|
* taxonomy town + CPT shortcode-town + ACF list_shortcode repeater.
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_alt_get_field_applies_do_shortcode_to_string_values(): void
|
public function test_alt_get_field_applies_do_shortcode_to_string_values(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
// expect() must come before when('function_exists') so Brain Monkey's
|
// expect() must come before when('function_exists') so Brain Monkey's
|
||||||
// FunctionStub::__construct can eval-define get_field() before the shim
|
// FunctionStub::__construct can eval-define get_field() before the shim
|
||||||
// fakes function_exists('get_field') === true.
|
// fakes function_exists('get_field') === true.
|
||||||
@@ -153,6 +154,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_alt_get_field_returns_arrays_as_is(): void
|
public function test_alt_get_field_returns_arrays_as_is(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
// expect() before when('function_exists') to avoid FunctionStub seeing
|
// expect() before when('function_exists') to avoid FunctionStub seeing
|
||||||
// the shim and skipping the eval-declaration of get_field.
|
// the shim and skipping the eval-declaration of get_field.
|
||||||
// do_shortcode is intentionally NOT registered: if the implementation
|
// do_shortcode is intentionally NOT registered: if the implementation
|
||||||
@@ -167,6 +169,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_alt_get_field_post_id_filter_overrides_caller_id(): void
|
public function test_alt_get_field_post_id_filter_overrides_caller_id(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('title', 99, true)
|
->with('title', 99, true)
|
||||||
@@ -207,13 +210,27 @@ final class HelpersTest extends TestCase
|
|||||||
self::assertTrue(function_exists('wpmc_get_term_by_slug'));
|
self::assertTrue(function_exists('wpmc_get_term_by_slug'));
|
||||||
self::assertTrue(function_exists('wpmc_alt_get_field'));
|
self::assertTrue(function_exists('wpmc_alt_get_field'));
|
||||||
self::assertTrue(function_exists('wpmc_remove_domain_link'));
|
self::assertTrue(function_exists('wpmc_remove_domain_link'));
|
||||||
|
self::assertTrue(function_exists('wpmc_main_town_slug'));
|
||||||
|
|
||||||
// remove_domain_link is pure (no WP function dependencies) — exercise the proxy.
|
// remove_domain_link is pure (no WP function dependencies) — exercise the proxy.
|
||||||
self::assertSame('/foo/', wpmc_remove_domain_link('https://site.com/foo/'));
|
self::assertSame('/foo/', wpmc_remove_domain_link('https://site.com/foo/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_wpmc_main_town_slug_delegates_to_helpers(): void
|
||||||
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
|
->once()
|
||||||
|
->with('main_town_slug', 'option')
|
||||||
|
->andReturn('spb');
|
||||||
|
|
||||||
|
require_once dirname(__DIR__) . '/includes/wpmc-functions.php';
|
||||||
|
self::assertSame('spb', wpmc_main_town_slug());
|
||||||
|
}
|
||||||
|
|
||||||
public function test_main_town_slug_reads_option_via_acf(): void
|
public function test_main_town_slug_reads_option_via_acf(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('main_town_slug', 'option')
|
->with('main_town_slug', 'option')
|
||||||
@@ -224,6 +241,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_main_town_slug_returns_null_when_option_empty(): void
|
public function test_main_town_slug_returns_null_when_option_empty(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('main_town_slug', 'option')
|
->with('main_town_slug', 'option')
|
||||||
@@ -234,6 +252,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_is_routable_path_returns_true_when_no_exclusions(): void
|
public function test_is_routable_path_returns_true_when_no_exclusions(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('page_no_rep', 'option')
|
->with('page_no_rep', 'option')
|
||||||
@@ -244,6 +263,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_is_routable_path_returns_false_when_path_matches_exclusion(): void
|
public function test_is_routable_path_returns_false_when_path_matches_exclusion(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('page_no_rep', 'option')
|
->with('page_no_rep', 'option')
|
||||||
@@ -257,6 +277,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_is_routable_path_uses_request_uri_when_path_null(): void
|
public function test_is_routable_path_uses_request_uri_when_path_null(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
$_SERVER['REQUEST_URI'] = '/terms/';
|
$_SERVER['REQUEST_URI'] = '/terms/';
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
@@ -268,6 +289,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_page_unic_swap_returns_clone_id_when_row_matches(): void
|
public function test_page_unic_swap_returns_clone_id_when_row_matches(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
$term = new \WP_Term();
|
$term = new \WP_Term();
|
||||||
$term->term_id = 11;
|
$term->term_id = 11;
|
||||||
$term->slug = 'tyumen';
|
$term->slug = 'tyumen';
|
||||||
@@ -285,6 +307,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_page_unic_swap_returns_null_when_no_row_matches(): void
|
public function test_page_unic_swap_returns_null_when_no_row_matches(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
$term = new \WP_Term();
|
$term = new \WP_Term();
|
||||||
$term->term_id = 11;
|
$term->term_id = 11;
|
||||||
$term->slug = 'tyumen';
|
$term->slug = 'tyumen';
|
||||||
@@ -298,6 +321,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_page_unic_swap_caches_within_request(): void
|
public function test_page_unic_swap_caches_within_request(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
$term = new \WP_Term();
|
$term = new \WP_Term();
|
||||||
$term->term_id = 11;
|
$term->term_id = 11;
|
||||||
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
||||||
@@ -314,6 +338,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_index_town_alt_page_id_returns_int_when_option_set(): void
|
public function test_index_town_alt_page_id_returns_int_when_option_set(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('index_town_alt', 'option')
|
->with('index_town_alt', 'option')
|
||||||
@@ -324,6 +349,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_index_town_alt_page_id_returns_null_when_zero(): void
|
public function test_index_town_alt_page_id_returns_null_when_zero(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_field')
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
->once()
|
->once()
|
||||||
->with('index_town_alt', 'option')
|
->with('index_town_alt', 'option')
|
||||||
@@ -334,6 +360,7 @@ final class HelpersTest extends TestCase
|
|||||||
|
|
||||||
public function test_reset_cache_clears_page_unic_cache(): void
|
public function test_reset_cache_clears_page_unic_cache(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
$term = new \WP_Term();
|
$term = new \WP_Term();
|
||||||
$term->term_id = 11;
|
$term->term_id = 11;
|
||||||
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
\Brain\Monkey\Functions\when('get_term_by')->justReturn($term);
|
||||||
@@ -348,6 +375,48 @@ final class HelpersTest extends TestCase
|
|||||||
self::assertTrue(true);
|
self::assertTrue(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_main_town_slug_returns_null_before_acf_init(): void
|
||||||
|
{
|
||||||
|
\Brain\Monkey\Functions\expect('did_action')
|
||||||
|
->with('acf/init')
|
||||||
|
->andReturn(0);
|
||||||
|
// get_field should NOT be called — no expectation means Brain Monkey throws if it is.
|
||||||
|
|
||||||
|
self::assertNull(Helpers::main_town_slug());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_main_town_slug_calls_get_field_after_acf_init(): void
|
||||||
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
|
->once()
|
||||||
|
->with('main_town_slug', 'option')
|
||||||
|
->andReturn('spb');
|
||||||
|
|
||||||
|
self::assertSame('spb', Helpers::main_town_slug());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_is_routable_path_returns_true_before_acf_init(): void
|
||||||
|
{
|
||||||
|
\Brain\Monkey\Functions\expect('did_action')
|
||||||
|
->with('acf/init')
|
||||||
|
->andReturn(0);
|
||||||
|
// get_field should NOT be called.
|
||||||
|
|
||||||
|
self::assertTrue(Helpers::is_routable_path('/some/path/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_is_routable_path_checks_exclusions_after_acf_init(): void
|
||||||
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
|
\Brain\Monkey\Functions\expect('get_field')
|
||||||
|
->once()
|
||||||
|
->with('page_no_rep', 'option')
|
||||||
|
->andReturn([['link' => '/excluded/']]);
|
||||||
|
|
||||||
|
self::assertFalse(Helpers::is_routable_path('/excluded/page/'));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_is_bypassed_returns_false_by_default(): void
|
public function test_is_bypassed_returns_false_by_default(): void
|
||||||
{
|
{
|
||||||
self::assertFalse(Helpers::is_bypassed());
|
self::assertFalse(Helpers::is_bypassed());
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ final class LinkFilterTest extends TestCase
|
|||||||
{
|
{
|
||||||
$_SERVER['REQUEST_URI'] = '/' . $town_slug . '/';
|
$_SERVER['REQUEST_URI'] = '/' . $town_slug . '/';
|
||||||
$_SERVER['HTTP_HOST'] = $host;
|
$_SERVER['HTTP_HOST'] = $host;
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => $town_slug]);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => $town_slug]);
|
||||||
\Brain\Monkey\Functions\when('get_field')->alias(function ($name) use ($main) {
|
\Brain\Monkey\Functions\when('get_field')->alias(function ($name) use ($main) {
|
||||||
if ($name === 'main_town_slug') return $main;
|
if ($name === 'main_town_slug') return $main;
|
||||||
@@ -62,6 +63,7 @@ final class LinkFilterTest extends TestCase
|
|||||||
{
|
{
|
||||||
$_SERVER['REQUEST_URI'] = '/random/';
|
$_SERVER['REQUEST_URI'] = '/random/';
|
||||||
$_SERVER['HTTP_HOST'] = 'site.com';
|
$_SERVER['HTTP_HOST'] = 'site.com';
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([10 => 'spb']);
|
||||||
\Brain\Monkey\Functions\when('get_field')->justReturn(false);
|
\Brain\Monkey\Functions\when('get_field')->justReturn(false);
|
||||||
|
|
||||||
@@ -129,6 +131,7 @@ final class LinkFilterTest extends TestCase
|
|||||||
{
|
{
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||||||
$_SERVER['HTTP_HOST'] = 'site.com';
|
$_SERVER['HTTP_HOST'] = 'site.com';
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||||||
\Brain\Monkey\Functions\when('get_field')->alias(function ($name) {
|
\Brain\Monkey\Functions\when('get_field')->alias(function ($name) {
|
||||||
if ($name === 'main_town_slug') return 'spb';
|
if ($name === 'main_town_slug') return 'spb';
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ final class RedirectTest extends TestCase
|
|||||||
|
|
||||||
public function test_handle_skips_when_current_town_equals_main(): void
|
public function test_handle_skips_when_current_town_equals_main(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
|
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/spb/about/';
|
$_SERVER['REQUEST_URI'] = '/spb/about/';
|
||||||
@@ -36,6 +37,7 @@ final class RedirectTest extends TestCase
|
|||||||
|
|
||||||
public function test_handle_skips_when_post_not_set(): void
|
public function test_handle_skips_when_post_not_set(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
|
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/about/';
|
$_SERVER['REQUEST_URI'] = '/about/';
|
||||||
@@ -53,6 +55,7 @@ final class RedirectTest extends TestCase
|
|||||||
|
|
||||||
public function test_handle_skips_when_url_already_has_town_prefix(): void
|
public function test_handle_skips_when_url_already_has_town_prefix(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
|
\Brain\Monkey\Functions\expect('wp_safe_redirect')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/about/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/about/';
|
||||||
@@ -70,6 +73,7 @@ final class RedirectTest extends TestCase
|
|||||||
|
|
||||||
public function test_handle_redirects_when_post_is_page_unic_clone_without_prefix(): void
|
public function test_handle_redirects_when_post_is_page_unic_clone_without_prefix(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/about/';
|
$_SERVER['REQUEST_URI'] = '/about/';
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_skips_when_current_town_is_null(): void
|
public function test_route_skips_when_current_town_is_null(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('url_to_postid')->never();
|
\Brain\Monkey\Functions\expect('url_to_postid')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/something-not-a-city/';
|
$_SERVER['REQUEST_URI'] = '/something-not-a-city/';
|
||||||
@@ -61,6 +62,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_skips_when_current_town_equals_main_town(): void
|
public function test_route_skips_when_current_town_equals_main_town(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('url_to_postid')->never();
|
\Brain\Monkey\Functions\expect('url_to_postid')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/spb/';
|
$_SERVER['REQUEST_URI'] = '/spb/';
|
||||||
@@ -77,6 +79,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_skips_when_path_not_routable(): void
|
public function test_route_skips_when_path_not_routable(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('url_to_postid')->never();
|
\Brain\Monkey\Functions\expect('url_to_postid')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/privacy/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/privacy/';
|
||||||
@@ -94,6 +97,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_uses_main_page_alt_term_meta_for_root_url(): void
|
public function test_route_uses_main_page_alt_term_meta_for_root_url(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||||||
@@ -123,6 +127,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_falls_back_to_index_town_alt_option(): void
|
public function test_route_falls_back_to_index_town_alt_option(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||||||
@@ -148,6 +153,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_does_not_install_query_when_neither_main_nor_fallback_set(): void
|
public function test_route_does_not_install_query_when_neither_main_nor_fallback_set(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_post')->never();
|
\Brain\Monkey\Functions\expect('get_post')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/';
|
||||||
@@ -169,6 +175,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_resolves_internal_page_via_url_to_postid(): void
|
public function test_route_resolves_internal_page_via_url_to_postid(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/about/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/about/';
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||||||
@@ -193,6 +200,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_applies_page_unic_swap_when_clone_exists(): void
|
public function test_route_applies_page_unic_swap_when_clone_exists(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/about/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/about/';
|
||||||
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
\Brain\Monkey\Functions\when('get_terms')->justReturn([11 => 'tyumen']);
|
||||||
@@ -220,6 +228,7 @@ final class RouterTest extends TestCase
|
|||||||
|
|
||||||
public function test_route_does_not_install_query_when_url_to_postid_returns_zero(): void
|
public function test_route_does_not_install_query_when_url_to_postid_returns_zero(): void
|
||||||
{
|
{
|
||||||
|
\Brain\Monkey\Functions\stubs(['did_action' => 1]);
|
||||||
\Brain\Monkey\Functions\expect('get_post')->never();
|
\Brain\Monkey\Functions\expect('get_post')->never();
|
||||||
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
\Brain\Monkey\Functions\when('is_admin')->justReturn(false);
|
||||||
$_SERVER['REQUEST_URI'] = '/tyumen/unknown/';
|
$_SERVER['REQUEST_URI'] = '/tyumen/unknown/';
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Plugin Name: WP Multi City
|
* Plugin Name: WP Multi City
|
||||||
* Description: Мультигородская подсистема: taxonomy town, шорткоды для подстановки по городу, роутер /{city}/{path}/, page-clones по городам.
|
* Description: Мультигородская подсистема: taxonomy town, шорткоды для подстановки по городу, роутер /{city}/{path}/, page-clones по городам.
|
||||||
* Version: 1.0.0
|
* Version: 1.0.1
|
||||||
* Author: Vladimir Bryzgalov
|
* Author: Vladimir Bryzgalov
|
||||||
* Requires PHP: 8.0
|
* Requires PHP: 8.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
@@ -18,7 +18,7 @@ if (!defined('ABSPATH')) exit;
|
|||||||
defined('WPMC_PLUGIN_FILE') || define('WPMC_PLUGIN_FILE', __FILE__);
|
defined('WPMC_PLUGIN_FILE') || define('WPMC_PLUGIN_FILE', __FILE__);
|
||||||
defined('WPMC_PLUGIN_DIR') || define('WPMC_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
defined('WPMC_PLUGIN_DIR') || define('WPMC_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
defined('WPMC_PLUGIN_URL') || define('WPMC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
defined('WPMC_PLUGIN_URL') || define('WPMC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
defined('WPMC_VERSION') || define('WPMC_VERSION', '1.0.0');
|
defined('WPMC_VERSION') || define('WPMC_VERSION', '1.0.1');
|
||||||
|
|
||||||
require_once WPMC_PLUGIN_DIR . 'includes/class-plugin.php';
|
require_once WPMC_PLUGIN_DIR . 'includes/class-plugin.php';
|
||||||
\WPMultiCity\Plugin::register_autoloader();
|
\WPMultiCity\Plugin::register_autoloader();
|
||||||
|
|||||||
+4
-4
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "WP Multi City",
|
"name": "WP Multi City",
|
||||||
"slug": "wp-multi-city",
|
"slug": "wp-multi-city",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"download_url": "https://git.netranking.ru/bryzgalov/wp-multi-city/releases/download/v1.0.0/wp-multi-city-1.0.0.zip",
|
"download_url": "https://git.netranking.ru/bryzgalov/wp-multi-city/releases/download/v1.0.1/wp-multi-city-1.0.1.zip",
|
||||||
"homepage": "https://git.netranking.ru/bryzgalov/wp-multi-city",
|
"homepage": "https://git.netranking.ru/bryzgalov/wp-multi-city",
|
||||||
"requires": "6.0",
|
"requires": "6.0",
|
||||||
"tested": "6.9",
|
"tested": "6.9",
|
||||||
"requires_php": "8.0",
|
"requires_php": "8.0",
|
||||||
"last_updated": "2026-05-17 00:00:00",
|
"last_updated": "2026-05-17 12:02:11",
|
||||||
"author": "Vladimir Bryzgalov",
|
"author": "Vladimir Bryzgalov",
|
||||||
"sections": {
|
"sections": {
|
||||||
"description": "Мультигородская подсистема: taxonomy town, [shortcode_dmr] для подстановок, URL-роутер /{city}/{path}/, ACF-driven page clones.",
|
"description": "Мультигородская подсистема: taxonomy town, [shortcode_dmr] для подстановок, URL-роутер /{city}/{path}/, ACF-driven page clones.",
|
||||||
"changelog": "<h4>1.0.0</h4><ul><li>Первый релиз. MVP-ядро S1–S7.</li></ul>"
|
"changelog": "<h4>1.0.1</h4><ul><li>Add: процедурный wrapper `wpmc_main_town_slug()` (был задокументирован в README, но отсутствовал в коде).</li><li>Fix: WP 6.7+ notice `_load_textdomain_just_in_time` — убраны `__()` обёртки из labels CPT/таксономии/ACF field group (плагин не поставляется с переводами).</li><li>Fix: ACF `acf_get_value` \"called too early\" notice — добавлен `did_action('acf/init')` guard в `Helpers::main_town_slug()`, `index_town_alt_page_id`, `page_unic_swap`, `is_routable_path`, `alt_get_field`. До инициализации ACF возвращают безопасные defaults (null/true).</li></ul><h4>1.0.0</h4><ul><li>Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4).</li><li>taxonomy town + CPT shortcode-town + ACF list_shortcode repeater.</li><li>[shortcode_dmr id=N] для подстановок по городу.</li><li>URL-роутер /{city}/{path}/ с поддержкой page_unic / main_page_alt / page_no_rep.</li><li>Глобальные фильтры the_title, home_url, post_link, acf/load_value, rest_url.</li><li>Self-hosted updates через Gitea + plugin-update-checker v5.6.</li></ul>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user