Prywatne wydarzenia, których nie wolno reklamować
Klient dzwoni: „Robimy zamknięty event dla wybranych klientów. Zaproszenia idą przez maila. Ale strona wydarzenia musi być dostępna, żeby ludzie mogli się zapisać. Tylko nie chcemy, żeby ktoś przypadkiem trafił na to z bloga albo z sekcji «nasze wydarzenia» na stronie głównej”.
Czyli: wpis w WordPress ma się nie pokazywać nigdzie, gdzie nie chcemy go pokazywać, ale działać pod bezpośrednim URL-em. Nie da się tego zrobić samym statusem „prywatny” wp-admin, bo tam prywatny znaczy „widoczny tylko dla zalogowanego admina” – a nam chodzi o coś innego. Ktoś z linkiem ma wejść bez logowania.
Rozwiązanie sprowadza się do jednego pola ACF i kilku filtrów zapytań w motywie.
ACF: pole „ukryj z listingów”
Do custom post type (u mnie były to wydarzenia – CPT o nazwie „events”) dodaję pole ACF typu true/false. Nazywam je „hide_from_listings” i umieszczam w prawej kolumnie edytora (sidebar), żeby było widoczne od razu przy edycji.
Konfiguracja pola w JSON-ie ACF (do wgrania jako group w acf-json/):
{
"key": "group_hide_from_listings",
"title": "Widoczność wpisu",
"fields": [
{
"key": "field_hide_from_listings",
"label": "Ukryj z listingów i pickerów",
"name": "hide_from_listings",
"type": "true_false",
"instructions": "Wpis pozostaje dostępny pod bezpośrednim URL, ale nie pokaże się w listingach ani pickerach.",
"ui": 1,
"ui_on_text": "TAK",
"ui_off_text": "NIE"
}
],
"location": [[{
"param": "post_type",
"operator": "==",
"value": "events"
}]],
"position": "side"
}
Marketing dostaje przełącznik. Zaznacza TAK – wpis staje się „prywatny”. Zapisuje – gotowe. Adres URL wpisu dalej działa, ale znika z listingów. Cała reszta konfiguracji się nie zmienia.
Funkcja pomocnicza – jedno miejsce do edycji
Zanim wpiszemy filtr do pętli głównej, wyciągnę tablicę meta_query do osobnej funkcji pomocniczej. Bo ta sama tablica pojawi się za chwilę w kilku miejscach – w pre_get_posts i w każdym pickerze ACF. Jak w przyszłości zmieni się nazwa pola ACF, poprawiam jedno miejsce zamiast szukać po całym motywie.
/**
* Zwraca meta_query wykluczajaca wpisy z hide_from_listings = 1.
* Uzywana w pre_get_posts i w blokach ACF z pickerami.
*/
function get_hide_from_listings_meta_query() {
return array(
'relation' => 'OR',
array(
'key' => 'hide_from_listings',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'hide_from_listings',
'value' => '1',
'compare' => '!=',
),
);
}
Filtr na pętli głównej: /nowosci/, /blog/ itd.
WordPress w większości motywów renderuje listingi przez główną pętlę – domyślne WP_Query obsługiwane przez template hierarchy (index.php, home.php, archive.php). Wpięcie się w to działa przez akcję pre_get_posts.
Dodaję do functions.php motywu:
function moje_wyklucz_ukryte_z_petli( $query ) {
if ( is_admin() || !$query->is_main_query() ) {
return;
}
// Strony gdzie renderujemy listingi - blog, archiwum autora/tagu,
// wyszukiwarka, archiwum CPT events (domyslny listing /events/)
$czy_listing = $query->is_home()
|| $query->is_author()
|| $query->is_tag()
|| $query->is_search()
|| $query->is_post_type_archive( 'events' );
if ( $czy_listing ) {
// Zachowaj ewentualne istniejace meta_query (od innych wtyczek)
// i dolacz nasze jako kolejny warunek
$meta_query = $query->get( 'meta_query' ) ?: array();
$meta_query[] = get_hide_from_listings_meta_query();
$query->set( 'meta_query', $meta_query );
}
}
add_action( 'pre_get_posts', 'moje_wyklucz_ukryte_z_petli' );
Dwa niuanse warto zaznaczyć. Po pierwsze – is_post_type_archive(’events’). Jeśli custom post type ma has_archive => true, WordPress automatycznie generuje listing pod /events/. Bez tego warunku ukryte wydarzenia dalej by się tam pokazywały mimo wszystkich innych filtrów.
Po drugie – pobranie istniejącego meta_query przez $query->get(’meta_query’) ?: array() i dołączenie naszego zamiast overwrite. Jeśli inna wtyczka (WooCommerce, MemberPress, cokolwiek) też dorzuca meta_query na tej samej pętli, nasz filter dopisuje się obok zamiast nadpisywać. Może się nigdy nie przydać, ale kosztuje jedną linijkę więcej.
Dlaczego relation OR z NOT EXISTS
Ten fragment jest kluczowy i nie oczywisty na pierwszy rzut oka. Chcę wykluczyć wpisy oznaczone jako ukryte – ale WIĘKSZOŚĆ wpisów nie ma w ogóle tego pola meta (bo je dodałam tylko dla events, a w listingu są też zwykłe post, case-studies itd.).
Gdybym napisała prostsze zapytanie typu „hide_from_listings != 1”, WordPress by wykluczył wpisy oznaczone jako 1, ale też pominął wszystkie te, które w ogóle nie mają tego pola meta (bo w SQL NULL nie jest równe „1”, ale też nie jest różne od „1” w klasyczny sposób).
Rozwiązanie: OR z dwoma warunkami. Weź wpis, jeśli nie ma pola meta (NOT EXISTS) LUB ma pole meta ale z wartością inną niż 1 (!=). Pierwsze łapie masę wpisów bez tego pola, drugie łapie wpisy oznaczone świadomie jako publiczne. Wykluczone zostają tylko te, które mają wartość dokładnie „1”.
Filtr na pickerach ACF w motywie
Motyw ma bloki typu „wybrane wpisy” z konfiguracją ACF – na stronie głównej slidery, w footerze polecane wpisy, na stronach tematycznych karuzele. Wszystkie one używają WP_Query albo get_posts, ale nie przechodzą przez pre_get_posts (bo to nie jest główna pętla).
Każdy z tych bloków trzeba tak samo obłożyć meta_query. Dzięki funkcji pomocniczej wywołanie w bloku ACF sprowadza się do jednej linijki:
// W szablonie bloku posts-picker.php albo events-picker.php:
$picked_posts = get_posts(array(
'post_type' => 'events',
'posts_per_page' => 4,
'orderby' => 'date',
'order' => 'DESC',
'post_status' => 'publish',
'meta_query' => array( get_hide_from_listings_meta_query() ),
));
Nie da się tego zrobić globalnie jednym filtrem – bo pickery różnie budują zapytania i różnie renderują. Ale wywołanie funkcji pomocniczej jest krótkie i czytelne. U mnie musiałam ją dodać w trzech miejscach: dwóch blokach ACF z pickerami i głównej pętli listingu blog.
Czego to NIE robi
Uczciwie: to rozwiązanie NIE ustawia wpisu jako noindex dla Google. Wpis dalej ma URL, dalej jest w sitemap XML wtyczki SEO (u mnie Yoast), dalej może się pojawić w wynikach wyszukiwania Google gdy ktoś wpisze frazę pasującą do treści.
Klient chciał TYLKO: nie pokazywać w listingach na stronie. Google indexing to osobna sprawa. Jeśli w danym przypadku dodatkowo trzeba by zablokować Google – dorobiłabym filtr do Yoast, żeby oznaczał te wpisy jako noindex. Ale to inna decyzja biznesowa i inna warstwa.
Podsumowanie
Trzy klocki, cały workflow:
- Pole ACF true/false w prawej kolumnie edytora – marketing sam ustawia widoczność
- Filtr pre_get_posts dla głównych pętli (blog, tag, autor, search, archiwum CPT)
- Meta_query w każdym pickerze ACF w motywie – inline w bloku PHP
Cały mechanizm oparty na jednym meta polu, filtrach które nie ruszają post_status i nie generują 404. Bezpośredni URL działa bez zmian. Marketing dostaje kontrolę bez ryzyka zepsucia strony.



