Как выводить записи на главную страницу в Kadence: карточки, мета, просмотры

Разбираем вывод записей циклом Kadence: карточки архива на главной, мета с разделителями, просмотры Post Views Counter и проверку кэша.

Карточки записей на главной легко собрать блоком «Сетка записей», но тогда оформление блока и архива живёт в двух местах. Если задача — получить на статической главной те же карточки, что в блоге, надёжнее запустить цикл темы и отдать каждую запись штатному шаблону Kadence.

Быстрый старт

Если механика вам знакома, всё собирается по этому списку. Разборы каждого шага — ниже.

  1. В Настройщике задайте стиль содержимого, число колонок и разделитель меты: Внешний вид → Настроить → Макет Записей и Страниц → Макет архива.
  2. В Post Views Counter отметьте нужные типы записей, выберите режим REST API, а на вкладке отображения поставьте расположение Вручную и пустую подпись.
  3. Поставьте пять сниппетов из статьи: шорткод с циклом темы, связующие правила CSS, вывод просмотров в мету, сброс кэша чтения и обновление чисел при кэше страниц.
  4. На главной добавьте блок «Шорткод» с [site_posts count="3" columns="3"], а прежний блок «Сетка записей» удалите.
  5. Откройте главную в приватном окне и сверьте карточки с архивом: высота, кегль заголовка, фон, положение «Читать далее» и растущее число просмотров.

Проверено на WordPress 7.0.3, теме Kadence 1.5.2 с дочерней темой, плагинах Post Views Counter 1.7.14 и Redis Object Cache 2.8.0. На других версиях идентификаторы настроек и имена хуков стоит перепроверить — как именно, показано дальше.

Два способа вывести записи

У Kadence Blocks есть готовая «Сетка записей». Её оформление хранится в атрибутах самого блока. Это удобно для самостоятельной секции, но изменение параметров в «Внешний вид → Настроить → Макет записей и страниц → Макет архива» не обязано повториться в блоке.

Цикл темы устроен иначе: запрос выбирает записи, а template-parts/content/entry.php строит каждую карточку. На проверенном сайте этот файл выводит <li class="entry-list-item">, затем <article class="entry content-bg loop-entry …"> и вызывает хуки миниатюры и содержимого. Поэтому архив и секция на главной используют одну разметку и одну точку настройки.

Отсюда простое правило выбора. Если секция на главной живёт своей жизнью и повторять архив не должна, штатный блок проще: он настраивается мышкой и не требует ни строки кода. Цикл темы оправдан там, где карточки обязаны быть одинаковыми везде, — тогда одна правка в настройках расходится по всему сайту сразу, а не по одному месту из трёх.

Шорткод с циклом Kadence

Сразу о том, куда класть код. Подойдёт любой из трёх вариантов: менеджер сниппетов, functions.php дочерней темы или собственный mu-plugin. В дочерней теме код переживёт обновления родительской, в менеджере — виден в одном списке и отключается по одному. Чего делать не стоит — складывать все куски в один сниппет: когда что-то сломается, отключать придётся всё сразу. Держите одну задачу на сниппет и давайте им говорящие имена.

Первый сниппет регистрирует [site_posts]. Параметры: count — число карточек, columns — от одной до четырёх колонок, post_type — существующий тип записи. Глобальный флаг нужен только на время цикла: по нему следующий сниппет понимает, что карточки выведены на статической странице.

<?php
/**
 * Шорткод [site_posts count="6" columns="3" post_type="post"].
 * Разметку карточки строит сама тема Kadence.
 */
function site_posts_shortcode( $atts ) {
	$atts = shortcode_atts(
		array(
			'count'     => 6,
			'columns'   => 3,
			'post_type' => 'post',
		),
		$atts,
		'site_posts'
	);

	$count     = max( 1, min( 24, absint( $atts['count'] ) ) );
	$columns   = max( 1, min( 4, absint( $atts['columns'] ) ) );
	$post_type = sanitize_key( $atts['post_type'] );

	if ( ! post_type_exists( $post_type ) ) {
		$post_type = 'post';
	}

	$query = new WP_Query(
		array(
			'post_type'           => $post_type,
			'post_status'         => 'publish',
			'posts_per_page'      => $count,
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true,
		)
	);

	if ( ! $query->have_posts() ) {
		wp_reset_postdata();
		return '';
	}

	$previous_flag = $GLOBALS['site_theme_loop_shortcode'] ?? false;
	$GLOBALS['site_theme_loop_shortcode'] = true;

	ob_start();
	printf(
		'<ul class="content-wrap kadence-posts-list grid-cols post-archive grid-sm-col-2 grid-lg-col-%1$d item-image-style-above">',
		$columns
	);

	while ( $query->have_posts() ) {
		$query->the_post();
		get_template_part( 'template-parts/content/entry' );
	}

	echo '</ul>';
	wp_reset_postdata();
	$GLOBALS['site_theme_loop_shortcode'] = $previous_flag;

	return ob_get_clean();
}
add_shortcode( 'site_posts', 'site_posts_shortcode' );

Классы content-wrap, kadence-posts-list, grid-cols, grid-sm-col-2 и grid-lg-col-N подключают сетку и адаптив темы. На проверенном сайте рабочая секция на главной использует тот же шаблон: у карточки заголовок h3, а в архиве — h2. Это корректная иерархия: на главной карточки находятся под заголовком секции второго уровня.

Осталось поставить шорткод на страницу. В редакторе главной добавьте блок «Шорткод» и впишите [site_posts count="3" columns="3"]. Блок «Сетка записей», если он там стоял, удалите — иначе на странице окажутся две секции с разным оформлением. Секцию удобно обернуть обычным рядом Kadence: отступы и фон тогда задаются настройками ряда, а не CSS.

Связующие правила между архивом и статической страницей

Дальше по тексту я называю мостиком несколько строк CSS, которые связывают оформление архива с карточками на обычной странице. Нужны они вот почему: часть динамического CSS Kadence ориентирована на контекст архива и на h2.entry-title. На статической странице селектор темы может оказаться сильнее короткого правила. Поэтому мостик привязан к контейнеру шорткода и использует не меньше трёх классов.

/* Фон карточки на странице берём из той же палитры, что и в архиве. */
.site-main .kadence-posts-list .loop-entry.content-bg {
	background: var(--global-palette8);
}

/* h3 на главной повторяет измеренный заголовок карточки архива. */
.site-main .kadence-posts-list .loop-entry h3.entry-title {
	font-size: 24px;
	line-height: 1.3;
}

@media (max-width: 1024px) {
	.site-main .kadence-posts-list .loop-entry h3.entry-title {
		font-size: 22px;
	}
}

@media (max-width: 767px) {
	.site-main .kadence-posts-list .loop-entry h3.entry-title {
		font-size: 21px;
	}
}

На живом архиве getComputedStyle() показал для заголовка 24 px и межстрочный интервал 31,2 px, для меты — 13,6 px и 21,76 px. В действующем CSS темы также найдены адаптивные значения заголовка 22 и 21 px. Здесь !important не требуется. Если в следующей версии Kadence статическая страница начнёт получать те же правила автоматически, мостик можно удалить после сравнения вычисленных стилей.

Что задаётся в настройках

ЗадачаГде настраиватьЧто проверено
Фон и отступы карточкиМакет Записей и Страниц → Макет архива → Стиль содержимогоpost_archive_content_style, выбран блочный стиль
Число колонокМакет Записей и Страниц → Макет архива → Колонок архива записейpost_archive_columns, выбрано 3
Состав и разделитель метыМакет архива → Элементы отдельных записей → Метаpost_archive_elements, показана дата, выбран vline
Кегль заголовкаМакет архива → вкладка «Дизайн» → Шрифт названия отдельной записиpost_archive_item_title_font, 24 px
Длина автоотрывкаМакет архива → Элементы отдельных записей → Отрывок20 слов
Ссылка продолженияМакет архива → Элементы отдельных записей → Читать далееЭлемент включён

Три строки из этой таблицы обязательны, если хотите одинаковые карточки в архиве и на главной: стиль содержимого, число колонок и разделитель меты. Остальное — вкусовое: кегль заголовка, длина автоотрывка и подпись ссылки продолжения меняются под макет и на механику не влияют.

Параметр количества слов применяется к автоматически собранному отрывку. Если поле «Отрывок» заполнено вручную, WordPress отдаёт готовый текст, и ограничение темы его не сокращает. Проверка простая: временно задайте автоотрывку пять слов на тестовом стенде, обновите архив и сравните запись с ручным отрывком и запись без него. На рабочем сайте ради такой проверки настройку менять не нужно.

Как Post Views Counter считает просмотры

В Post Views Counter 1.7.14 сначала откройте «Подсчёт» и проверьте четыре группы параметров.

  1. Типы записей. Счётчик работает только для отмеченных типов. На проверенном сайте отмечены «Записи», а страницы, кейсы и товары не отмечены.
  2. Режим работы. Доступны PHP, JavaScript и REST API; «Быстрый AJAX» помечен PRO. Сейчас выбран PHP. Если полный HTML страницы отдаёт серверный кэш, PHP-код WordPress при повторном запросе может не запускаться, поэтому для такого сайта выбирают JavaScript или REST API. REST удобнее диагностировать отдельным маршрутом.
  3. Исключения. На сайте исключены поисковые роботы и вошедшие пользователи. Поэтому просмотр администратора не увеличивает число; проверку проводят в приватном окне.
  4. Интервал. Сейчас установлен 24-часовой интервал. Повторный визит того же браузера в этот период не считается новым просмотром.

На вкладке «Отображение» поле Views Label пустое, включена иконка, а расположение установлено в Manual only. Так плагин не вставляет счётчик автоматически и не создаёт дубль. На текущем сайте опция «Этикетка» включена, но пустое поле даёт пустой span; для новой настройки его можно отключить и оставить только иконку.

У плагина есть два публичных REST-маршрута, и это пригодится для диагностики. Чтение: /wp-json/post-views-counter/get-post-views/ID — проверено запросом без cookies, отвечает 200 и отдаёт голое число. Одна тонкость: если передать несколько идентификаторов через запятую, маршрут вернёт не список, а их сумму, так что для набора карточек запрашивать придётся поштучно. Запись: /wp-json/post-views-counter/view-post/ID принимает POST — именно им браузер регистрирует просмотр в режиме REST API. Дёргать его руками стоит только на тестовой записи: он увеличивает счётчик.

Просмотры внутри меты Kadence

В entry_loop_meta.php Kadence сначала открывает <div class="entry-meta …">, затем вызывает kadence_before_loop_entry_meta и только после этого печатает дату и остальные элементы. Поэтому счётчик надо вставлять этим хуком, а не рядом с контейнером.

<?php
/**
 * Добавляет просмотры первым прямым потомком .entry-meta.
 * Ноль пропускаем по числовому API, а HTML получаем только для вывода.
 */
function site_add_views_to_loop_meta() {
	$inside_shortcode = ! empty( $GLOBALS['site_theme_loop_shortcode'] );

	if ( ! is_home() && ! is_archive() && ! is_search() && ! $inside_shortcode ) {
		return;
	}

	if ( ! function_exists( 'pvc_get_post_views' ) || ! function_exists( 'pvc_post_views' ) ) {
		return;
	}

	$post_id = get_the_ID();
	$views   = (int) pvc_get_post_views( $post_id );

	if ( $views < 1 ) {
		return;
	}

	echo '<span class="post-views-loop">';
	echo wp_kses_post( pvc_post_views( $post_id, false ) );
	echo '</span>';
}
add_action( 'kadence_before_loop_entry_meta', 'site_add_views_to_loop_meta' );

Обратите внимание на порядок вызовов в сниппете. У плагина две похожие функции с разным результатом: pvc_get_post_views( $id ) возвращает целое число, а pvc_post_views( $id, false ) — готовую разметку с контейнером, иконкой и числом. Эта разметка непустая даже при нуле просмотров, поэтому привычная проверка if ( $views ) после второй функции ноль не отсекает и печатает «0» с иконкой. Сначала спрашиваем число, и только если оно больше нуля — забираем разметку.

Настройка разделителя добавляет контейнеру класс entry-meta-divider-vline. В CSS Kadence на сайте найдены три правила:

.entry-meta.entry-meta-divider-vline > ::after { content: "|"; }
.entry-meta.entry-meta-divider-slash > ::after { content: "/"; }
.entry-meta.entry-meta-divider-dash > ::after { content: "-"; }

Счётчик становится прямым потомком .entry-meta, поэтому получает тот же разделитель автоматически. На главной вычисленное значение ::after у .post-views-loop равно "|". Отдельную черту добавлять не требуется.

Точки, двоеточия и расстояние у иконки

Post Views Counter использует .post-views-icon::after и .post-views-label::after. В зависимости от версии стилей рядом с числом может появиться точка или двоеточие. Кроме того, правило плагина .post-views.entry-meta > span имеет два класса и тег и на текущем сайте содержит !important. Одиночный селектор иконки не сможет вернуть отступ.

/* Псевдоэлементы плагина не должны добавлять знаки рядом со счётчиком.
   Здесь !important не нужен: своего display у этих псевдоэлементов плагин не задаёт. */
.post-views-icon::after,
.post-views-label::after {
	display: none;
}

/* А здесь нужен, и это проверяется чтением стилей плагина:
   frontend.css задаёт .post-views.entry-meta > span { margin-right: 0 !important }. */
.post-views.entry-meta > span.post-views-icon {
	margin-right: 5px !important;
}

Один псевдоэлемент нельзя одновременно использовать для двух независимых значков. Если собственная иконка времени чтения занимает ::before, не назначайте туда же ещё один декоративный элемент. Для отдельного разделителя в такой кастомной конструкции используйте границу элемента, а псевдоэлемент оставьте иконке. Штатный разделитель Kadence выше продолжает работать через ::after прямого потомка.

Объектный кэш: число в таблице есть, а на странице старое

Функция pvc_get_post_views() строит SQL-запрос к wp_post_views, вычисляет его MD5-хэш и сохраняет результат в группе объектного кэша pvc-get_post_views. В версии 1.7.14 срок по умолчанию — 300 секунд. Код записи обновляет таблицу или рабочую группу pvc, но не очищает группу готовых результатов чтения. При постоянном Redis это даёт понятный симптом: в таблице уже новое число, а функция некоторое время возвращает старое.

Диагностика состоит из двух чтений: сравните SELECT SUM(count) … FROM wp_post_views для нужного ID с результатом pvc_get_post_views( ID ). Если таблица обновилась, а функция отстаёт, логика записи работает и проблема находится в кэше чтения. В интерфейсе проверенной версии «Поддержка объектного кэша» помечена PRO.

Расхождение не вечное. В pvc_get_post_views() срок жизни записи задаётся строкой $expire = absint( apply_filters( 'pvc_object_cache_expire', 300 ) ); — по умолчанию пять минут, после чего число обновится само. Поэтому если счётчик не критичен к секундам, можно ничего не делать: витрина отстанет максимум на пять минут.

Если ждать не хочется, у плагина есть штатный фильтр — он меняет срок без единой строки работы с кэшем и без сброса группы:

<?php
/**
 * Сокращает срок жизни кэша готового числа просмотров.
 * Ноль здесь не подходит: для wp_cache_add() это «хранить бессрочно».
 */
add_filter( 'pvc_object_cache_expire', function () {
	return 30;
} );

Что выбрать, зависит от нагрузки. Фильтр проще и работает в любом режиме счётчика, но заставляет чаще ходить в базу. Сброс группы точнее: он срабатывает ровно тогда, когда число действительно изменилось, — но только в режимах, где просмотр регистрируется REST-запросом.

<?php
/**
 * После успешного REST-запроса сбрасывает только группу чтения PVC.
 * Для режима PHP этот фильтр не запускается.
 */
function site_flush_pvc_read_cache_after_rest( $response, $server, $request ) {
	$route = $request->get_route();

	if ( 0 !== strpos( $route, '/post-views-counter/view-post' ) ) {
		return $response;
	}

	if ( function_exists( 'wp_cache_supports' )
		&& wp_cache_supports( 'flush_group' )
		&& function_exists( 'wp_cache_flush_group' )
	) {
		wp_cache_flush_group( 'pvc-get_post_views' );
	}

	return $response;
}
add_filter( 'rest_post_dispatch', 'site_flush_pvc_read_cache_after_rest', 10, 3 );

Сниппет намеренно ничего не делает в режиме PHP: там нет REST-запроса к view-post. Если хостинг не поддерживает очистку отдельной группы, код также спокойно возвращает исходный ответ и не сбрасывает весь объектный кэш сайта.

Кэш страниц: другой слой и другая проблема

Предыдущий раздел был про объектный кэш — тот, что держит значения внутри PHP. Кэш страниц устроен иначе и лечится иначе, а перепутать их легко, потому что симптом похож: число на витрине не меняется.

Разница вот в чём. При кэше страниц посетителю отдаётся заранее собранный HTML, и число просмотров запечено в него на момент сборки. PHP при таком запросе вообще не выполняется. Значит ни хук вывода меты, ни сброс группы объектного кэша тут не работают — им попросту негде запуститься. Счётчик при этом может исправно расти: в режиме REST API просмотр регистрирует браузер отдельным запросом, база обновляется, а страница показывает старое значение до пересборки кэша.

Как различить два случая. Запросите страницу без cookies и посмотрите заголовки ответа: если есть x-cache, x-cached-by или age, отвечает кэш страниц. Если заголовков нет, а число всё равно отстаёт — виноват объектный кэш, и лечится он способами из предыдущего раздела.

У плагина есть штатный ответ именно на этот слой — параметр «Динамическая загрузка» на вкладке отображения. В коде он переключает класс контейнера:

$class .= $options['dynamic_loading'] === true ? ' load-dynamic' : ' load-static';

При load-dynamic число не печатается в HTML, а подгружается запросом из браузера, поэтому кэш страниц ему не мешает; под это в стилях плагина заготовлены состояния loading, loaded и load-error. Одна оговорка, из-за которой я и вынес это отдельно: в бесплатной версии переключатель заблокирован и помечен PRO. Проверяется за секунду — откройте вкладку отображения и посмотрите, активен ли чекбокс.

Что делать на бесплатной версии, по возрастанию усилий. Первое — ничего: для многих проектов отставание счётчика на время жизни кэша некритично, число не отчётность. Второе — исключить нужные страницы из кэша средствами кэширующего плагина, но тогда вы теряете кэш ровно там, где он полезнее всего. Третий путь работает и с кэшем, и без него, и разбирается ниже.

Идея простая: не печатать число в HTML намертво, а дать браузеру забрать его отдельным запросом. Кэш страниц такие запросы не трогает — в правилах доставки адреса wp-json исключены явно, да и кэш отдаётся только для адресов без строки запроса, а наш идёт с параметром. Маршрут плагина для этого не годится: на список идентификаторов он возвращает их сумму. Значит нужен свой, отдающий карту «идентификатор → число» одним запросом на всю страницу.

<?php
/**
 * Свежие просмотры на страницах, отданных из кэша.
 * Маршрут отдаёт карту «идентификатор → число» одним запросом.
 * Выключается: add_filter( 'site_views_refresh_enabled', '__return_false' );
 */
add_action( 'rest_api_init', function () {
	register_rest_route( 'site/v1', '/views', array(
		'methods'             => 'GET',
		'permission_callback' => '__return_true',
		'callback'            => function ( $request ) {
			if ( ! function_exists( 'pvc_get_post_views' ) ) {
				return rest_ensure_response( array() );
			}

			$ids = array_slice(
				array_unique( array_filter( array_map( 'absint', explode( ',', (string) $request->get_param( 'ids' ) ) ) ) ),
				0,
				50
			);

			// Оставляем только существующие опубликованные записи — одним запросом.
			if ( $ids ) {
				$ids = get_posts( array(
					'post__in'            => $ids,
					'post_type'           => 'any',
					'post_status'         => 'publish',
					'posts_per_page'      => count( $ids ),
					'fields'              => 'ids',
					'ignore_sticky_posts' => true,
					'no_found_rows'       => true,
				) );
			}

			$out = array();
			foreach ( $ids as $id ) {
				$out[ $id ] = (int) pvc_get_post_views( $id );
			}

			return rest_ensure_response( $out );
		},
	) );
} );

Вторая половина — скрипт в подвале. Он находит счётчики, достаёт идентификатор записи из класса post-NNN у контейнера плагина, делает один запрос на все карточки сразу и подставляет свежие числа.

<?php
add_action( 'wp_footer', function () {
	if ( is_admin() || ! apply_filters( 'site_views_refresh_enabled', true ) ) {
		return;
	}

	$js = '(function(){'
		. 'var n=document.querySelectorAll(".post-views");'
		. 'if(!n.length){return;}'
		. 'var map={},ids=[];'
		. 'Array.prototype.forEach.call(n,function(el){'
		. 'var m=(el.className||"").match(/(?:^|\s)post-(\d+)(?:\s|$)/);'
		. 'if(!m){return;}'
		. 'var c=el.querySelector(".post-views-count");'
		. 'if(!c){return;}'
		. 'if(!map[m[1]]){map[m[1]]=[];ids.push(m[1]);}'
		. 'map[m[1]].push(c);'
		. '});'
		. 'if(!ids.length){return;}'
		. 'fetch(' . wp_json_encode( rest_url( 'site/v1/views' ) ) . '+"?ids="+ids.join(","),{credentials:"omit"})'
		. '.then(function(r){return r.ok?r.json():null;})'
		. '.then(function(d){if(!d){return;}'
		. 'Object.keys(d).forEach(function(id){'
		. '(map[id]||[]).forEach(function(el){'
		. 'if(String(d[id])!==el.textContent.trim()){el.textContent=d[id];}'
		. '});});})'
		. '.catch(function(){});'
		. '})();';

	echo '<script id="site-views-refresh">' . $js . '</script>';
} );

Проверял так. Включил кэш страниц, прогрел архив, зафиксировал в отданном HTML число 12 у записи. Затем анонимный посетитель открыл саму запись — счётчик в базе стал 13. Повторный запрос архива вернул тот же HTML байт в байт, с прежним числом 12: страница пришла из кэша. При этом маршрут отдавал уже 13, а скрипт присутствовал в закэшированном HTML.

Финальная проверка: взял ровно тот HTML, что получает аноним, подменил в нём все девять чисел на заведомо неверные и выполнил скрипт из подвала. Все девять счётчиков вернулись к актуальным значениям, ни одного подменённого не осталось. Это и есть ответ на вопрос, работает ли связка: страница приходит из кэша со старым числом, а посетитель видит новое сразу после загрузки.

Три предостережения. Если на момент сборки кэша у записи было ноль просмотров, счётчик не выводится вовсе, и подставлять скрипту некуда — число появится после пересборки страницы. Если в оптимизаторе включена отложенная загрузка JavaScript, инлайн-скрипт может не выполниться до первого касания экрана — добавьте его в исключения. И проверьте, что REST API не отключён плагином оптимизации: такой переключатель есть, например, у Perfmatters, и он ломает подход целиком.

И проверьте, включён ли у вас кэш страниц вообще. На многих проектах на время работ его выключают и забывают: тогда всё описанное здесь просто не проявляется — ровно до дня запуска.

Равная высота карточек и «Читать далее» на одном уровне

Здесь легко потратить вечер на лишний код. Равную высоту карточек в ряду тема держит сама: ячейка списка объявлена как display: flex; flex-direction: column, а article внутри получает flex: 1 1 0%. Проверяется одним замером — выключите свой CSS и сравните высоты соседних карточек через getBoundingClientRect(). Они совпадут. Правила вида height: 100% у ячейки и у статьи в такой ситуации ничего не делают.

А вот ссылку «Читать далее» тема по нижнему краю не выравнивает, и штатной настройки под это нет. В макете архива есть «Вертикальное выравнивание содержимого», но варианты там только «Сверху» и «По центру»; у собственных типов записей этой настройки может не быть вовсе. В разделе элементов у «Читать далее» доступны лишь включение и подпись. Это тот случай, когда CSS оправдан, — и он же единственный в статье, где приходится описывать раскладку руками.

/* Карточка — flex-колонка, чтобы подвал можно было прижать вниз. */
.kadence-posts-list article.loop-entry {
	display: flex;
	flex-direction: column;
}

/* flex: 1 1 auto, а не height: 100%. Если у карточки появится миниатюра,
   height: 100% растянет текстовую часть на высоту всей карточки,
   и «Читать далее» выедет за границу. */
.kadence-posts-list article.loop-entry .entry-content-wrap {
	display: flex;
	flex: 1 1 auto;
	flex-direction: column;
	gap: 16px;
}

/* Прижимаем именно .entry-footer: он прямой потомок flex-контейнера.
   На .more-link-wrap margin-top: auto не подействует — она лежит
   внутри .entry-actions, на два уровня глубже, flex-контекста там нет. */
.kadence-posts-list article.loop-entry .entry-footer {
	margin-top: auto;
	padding-top: 16px;
}

/* Во flex-колонке отступы соседей складываются, а не схлопываются:
   расстояния держит gap, margin обнулён и у детей, и внутри них. */
.kadence-posts-list article.loop-entry .entry-header,
.kadence-posts-list article.loop-entry .entry-taxonomies,
.kadence-posts-list article.loop-entry .entry-summary,
.kadence-posts-list article.loop-entry .entry-summary > p,
.kadence-posts-list article.loop-entry .entry-meta,
.kadence-posts-list article.loop-entry .entry-title,
.kadence-posts-list article.loop-entry .entry-actions,
.kadence-posts-list article.loop-entry .more-link-wrap {
	margin: 0;
}

/* Рубрика, заголовок и мета — одна группа, ей зазор поменьше. */
.kadence-posts-list article.loop-entry .entry-header {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

Две вещи в этом коде принципиальны. Первая: flex: 1 1 auto вместо height: 100% — «займи то, что осталось», а не «стань ростом с родителя». Если над текстом появится миниатюра, второй вариант сделает содержимое выше контейнера и ссылка обрежется. Вторая: во flex-колонке отступы соседей не схлопываются, а складываются, поэтому расстояния задаёт gap, а margin обнуляется и у детей, и внутри них — свой отступ есть и у .entry-meta, и у абзаца внутри .entry-summary.

Разница высот у самих карточек остаётся задачей редактора, а не разработчика: её создаёт разная длина отрывка, а ограничение по словам, как показано выше, на ручные отрывки не действует. Рабочее правило — держать поле «Отрывок» в пределах 130–150 символов у всех записей.

Как проверить результат

  • Сравните getComputedStyle() заголовка, меты и фона в архиве и на главной, а не только значения в настройщике.
  • Проверьте высоту карточек одного ряда и нижнюю координату ссылок «Читать далее» — чем это выравнивается, разобрано в разделе о равной высоте выше. Ручные отрывки при этом должны быть сопоставимой длины.
  • Откройте запись в приватном окне: администраторы исключены из подсчёта. Учитывайте 24-часовой интервал.
  • При REST API запросите /wp-json/post-views-counter/get-post-views/ID без авторизации. POST к view-post проверяйте только на тестовой записи: он увеличивает счётчик.
  • Для мобильной проверки задайте внешнему iframe ширину 390 px и после загрузки убедитесь, что iframe.contentWindow.innerWidth действительно равно 390. Затем сравните document.documentElement.scrollWidth и clientWidth.

Последняя проверка защищает от ложного результата: CSS-ширина iframe и фактическая ширина его окна могут различаться. Горизонтальной прокрутки нет, когда scrollWidth <= clientWidth.

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

// Геометрия карточек: высота и положение «Читать далее» внутри карточки.
// Внутри одного ряда высоты обязаны совпадать, координаты подвала — тоже.
copy([...document.querySelectorAll('.kadence-posts-list .entry-list-item')].map((li, i) => {
	const art  = li.querySelector('article');
	const foot = li.querySelector('.entry-footer');
	const R    = el => el.getBoundingClientRect();
	return {
		карточка: i,
		высота:   Math.round(R(art).height),
		подвал:   Math.round(R(foot).top - R(art).top)
	};
}));

Второй выводит оформление одной карточки. Запустите его сначала на архиве, потом на главной и сравните пять значений: уровень заголовка ожидаемо разойдётся (в архиве h2, на главной h3), а кегль, межстрочный, фон и разделитель обязаны совпасть. Расхождение кегля или фона означает, что связующие правила не долетели, — проверяйте вес селектора.

// Сверка оформления архива и главной. Выполнить на обеих страницах и сравнить.
(() => {
	const art   = document.querySelector('.kadence-posts-list article.loop-entry');
	const title = art.querySelector('.entry-title');
	const meta  = art.querySelector('.entry-meta');
	const cs    = el => getComputedStyle(el);
	return {
		уровеньЗаголовка: title.tagName,
		кегль:            cs(title).fontSize,
		межстрочный:      cs(title).lineHeight,
		фонКарточки:      cs(art).backgroundColor,
		разделительМеты:  getComputedStyle(meta.firstElementChild, '::after').content
	};
})();

Итоговая схема

В настройках Kadence остаются стиль карточки, число колонок, состав меты, разделитель, типографика, отрывок и «Читать далее». Код решает только то, чего нет в настройщике: запускает цикл темы на статической странице, связывает контекст страницы с архивным оформлением, вставляет просмотры внутрь меты, при REST-режиме очищает кэш готового числа и обновляет счётчики на страницах, отданных из кэша. Итого пять небольших сниппетов, каждый со своей задачей и каждый отключаемый по отдельности.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *