WordPress udostępnia możliwość zbudowania szablonu, który zamiast podstawowej treści strony wyświetli dane innej strony lub stron, wpisów czy metadanych. Wystarczy wskazać w argumentach dla obiektu klasy wp_query interesujące nas przestrzenie, wykonać pętle i sukcesywnie wyświetlać interesujące nas informacje.
// WP_Query arguments $args = array( 'page_id' => '1,2,7,9', 'post_type' => array( 'page' ), 'post_status' => array( 'publish' ), 'nopaging' => true, 'order' => 'ASC', 'orderby' => 'menu_order', ); // The Query $query = new WP_Query( $args );
W powyższym przykładzie pobieramy strony o numerach ID: 1, 2, 7 i 9; typ postu: page; opublikowane; bez paginacji (wszystko na jednej stronie); posortowane rosnąco wg pola menu_order.
Przykład zastosowania
Robiłem ostatnio dla klienta stronę kontaktu, gdzie były dane kilku placówek i działów przedsiębiorstwa. Strona zbudowana na bazie Bootstrapa (zdjęcie poniżej), z możliwością edycji danych. Wiadomo, że edytor WordPressa działa jak chce, więc założyłem, że po pierwszej próbie edycji przez pracowników, cały układ pójdzie w rozsypkę. Aby do tego nie dopuścić dane musiałyby zostać odseparowane od prezentacji – najlepiej za pomocą formularzy ACF.
Nie należy spodziewać się, że nasz klient porusza się w HTML jak ryba w wodzie i trzeba założyć, że szybciej wszystko zepsuje niż zrobi sam poprawki.
Można natomiast przyjąć, że bez problemu wypełni kilka prostych pól formularza – wystarczy mu je dobrze opisać. Dlatego przyda nam się wtyczka Advanced Custom Fields for WordPress Developers, w której za darmo wykonamy potrzebne nam tabelki i przypiszemy do stron z danymi działów i placówek handlowych.

Na pierwszym zdjęciu da się zauważyć, że mamy dwa typy danych. Pierwszy to adres, a drugi to kolumny z danymi pracowników działu. Zatem i nasze pola niestandardowe musimy umieścić w dwóch sekcjach: adres i kontakty.
Kontakty
Tutaj zastosowałem typ "Pole powtarzalne" czyli wielowymiarową tablicę z takimi polami jak:
- tytul – tekst
- telefon – tekst
- komorka – tekst
- email – E‑mail
- opis – edytor WYSIWIG
- zdjecie – obraz
Pracowników może być wielu, dlatego tablica sprawdzi się tutaj idealnie. "tytul" to pole na imię i nazwisko pracownika. Celowo rozdzieliłem "telefon" i "komorka". Często pracownicy korzystają z jednego telefonu stacjonarnego w biurze, ale komórkę każdy ma dziś raczej swoją. Pole "email" w ACF ma swoją reprezentację (input[type="email"]). Szkoda, że nie pomyślano o implementacji pola "tel". Do opisu pracownika celowo użyłem edytora WYSIWIG, a nie obszaru tekstowego z uwagi na wygodę późniejszych obsługując. Projekt zakłada, że jeżeli pracownik zgodzi się na użycie swojego zdjęcia, to szablon je wyświetli. Do tego użyłem pola typu "obraz", które w tym wypadku zwraca ID zdjęcia.
Adres
Dla adresu wystarczy zestaw pól czyli "Grupa":
- adres – obszar tekstowy
- godziny_otwarcia_pn_-_pt – tekst
- godziny_otwarcia_sobota – tekst
- telefon – tekst
- komorka – tekst
- email – E‑mail
"adres" to obszar tekstowy z opcją dodawania znacznika <br> na końcu linii. Tym razem nie użyłem WYSIWIG, bo uznałem że byłby to przerost formy nad treścią. W tym polu ma być ulica, kod pocztowy itd. Nie ma sensu uruchamiać edytora. Godziny otwarcia zostały rozbite na dni powszednie i sobotę. Jest to zwykłe pole tekstowe. Pola do edycji daty i czasu nie mają tutaj zastosowania. Pozostałe pola jak wyżej.
W sekcji ustawień trzeba też ograniczyć występowanie pól tylko do podstron strony (rodzica) o nazwie "Kontakt". Byłoby nonsensem i marnowaniem zasobów gdyby te pola wyświetlały się wszędzie, na każdej stronie edycji wpisów czy stron.
Podział na sekcje (działy firmy)
W przypadku mojego klienta występuje pięć sekcji: trzy biura i dwa zespołu obsługi. Można by to wszytko wrzucić do jednego worka, stworzyć duży formularz oparty na dużym polu powtarzalnym i z głowy. To jednak jest wygodne i czytelne tylko dla biegłych programistów lub edytorów stron. Ja wychodzę z założenia, że im czytelniej tym klient bardziej zadowolony. Dlatego zastosowałem tutaj układ "Rodzic + dzieci". Rodzicem jest strona o nazwie "Kontakt", a dziećmi kilka podstron kontaktu – każda nazwana jak nazwa sekcji: CENTRALNE BIURO HANDLOWE, BIURO SPRZEDAŻY – GDYNIA, BIURO ZARZĄDU, OBSŁUGA INWESTYCJI i OBSŁUGA PROJEKTÓW. Jest to jasny podział, od razu wiadomo co gdzie jest. Dodatkowo, korzystając z atrybutu strony "kolejność" (menu_order) ustalamy porządek wyświetlania bloków. Najwyższy ma liczbę 1, najniższy 5.
Po wypełnieniu pól mamy teoretycznie stworzoną sekcję. Gdy podamy wszystkie dane system powinien je wyświetlić jak na zdjęciu nr 1. Na pewno nie! Przede wszystkim WordPress domyślnie wyświetli zawartość pola "post_content", czyli to co mamy w edytorze gdy wejdziemy do edycji strony. A tam jest pusto.
Szablony stron
WordPress wyświetla treści dobierając dynamicznie szablony wg typu wpisu.

Szablony dedykowane
Dla typu postu "page" WordPress podstawi szablon page.php. Jest to domyślny szablon jeżeli WordPress uzna, że ma do czynienia ze stroną, a nie na przykład z wpisem bloga czy tagiem. Jeżeli go nie znajdzie, to spróbuje podstawić singular.php, a w przypadku niepowodzenia index.php. Szablon musi zawierać plik index.php, więc nie musimy się obawiać, że nic się pokaże. Idąc w lewą stronę diagramu widzimy, że są opcje page-$id.php i page-$slug.php. Pierwszy to połączenie typu "page" i ID wpisu. Drugi łączy typ i tzw "slug" czyli unikalny, własny link każdego typu postu. Wygodniej korzystać z drugiej opcji. Szczególnie gdy tworzymy uniwersalne rozwiązania, a nigdy nie wiadomo jaki numer ID dostanie konkretna strona. Za to możemy założyć, że strona "Kontakt" będzie miała slug "kontakt".
Szablony uniwersalne
Istnieje też możliwość stworzenie własnego szablonu, pod dowolną nazwą i podstawienia go do dowolnej strony (opcja nie dotyczy wpisów bloga). Wystarczy stworzyć plik o dowolnej nazwie, np "szablon-strony.php" i umieścić go w głównym katalogu naszej skórki. Żeby szablon był dostępny w atrybutach strony musi zawierać prosty nagłówek:
<?php /* Template name: Partner */ get_header(); ?>
W trzeciej linii zapisujemy nazwę szablonu, która będzie widoczna w polu wyboru. Tyle wystarczy, ale jak chcesz poznać inne opcje to zajrzyj na stronę Page Templates
Dużą zaletą szablonu uniwersalnego jest to, że możemy go zastosować do wielu stron różnych typów. Dobrym przykładem tu może być szablon w którym strona wyświetlana jest na całej szerokości i drugi gdzie mamy podział na kolumny. Od nas tylko zależy jak wyświetlimy naszą treść.
Jednak w przypadku naszego zadania zdecydowałem się na utworzenie szablonu dedykowanego. Dlaczego? Dlatego, że układ strony jest wyjątkowy i raczej nie powtórzy się na innych stronach. Zatem tworzymy stronę "page-kontakt.php".
Zawartość "page-kontakt.php"
<?php /** * The template for displaying kontakt page * * This is the template that displays all pages by default. * Please note that this is the WordPress construct of pages * and that other 'pages' on your WordPress site may use a * different template. * * @link https://developer.wordpress.org/themes/basics/template-hierarchy/ * * @package skb */ get_header(); ?> <section> <div class="container-xl"> <div class="row justify-content-center"> <div class="col-xl-12"> <?php // WP_Query arguments $args = array( 'post_parent' => get_the_ID(), 'post_type' => array( 'page' ), 'post_status' => array( 'publish' ), 'order' => 'ASC', 'orderby' => 'menu_order', ); // The Query $query = new WP_Query( $args ); // The Loop if ( $query->have_posts() ) { while ( $query->have_posts() ) { $query->the_post(); ?> <div class="container-xl mb-4"> <div class="row justify-content-center"> <?php echo do_shortcode('[skb_kontakt post_id="' . get_the_ID() . '"]'); ?> </div> </div> <?php } } else { // no posts found } // Restore original Post Data wp_reset_postdata(); ?> </div> </div> </div> </section> <?php get_footer();
Zwróć uwagę na zaznaczone linie (26−30 i 46). W linii 26 przypisujemy wartość ID strony "kontakt" do klucza "post_parent". Sortujemy strony wg opcji "menu_order". Tym sposbem otrzymujemy zwartość podstron przypisanych do kontaktu. W dalszym ciągu wykonuje się pętla, o ile warunki pętli są spełnione. W linii 46 wykonuje się tzw "shortcode" czyli skrót. To nic innego jak funkcja którą można wpisać bezpośrednio do edytora lub użyć w kodzie. Wykonujemy zatem skrót "skb_kontakt" z parametrem "post_id" równym ID postu z pętli.
Oczywiście zamiast skrótu można by wpisać jego zawartość do pętli. Skróty mają jednak tę zaletę, że ich kod można używać wielokrotnie w wielu miejscach witryny i do wielu celów – co pokażę w dalszej części wpisu.
Zawartość "skb_kontakt"
<?php function skb_sc_kontakt($atts) { $atts = shortcode_atts( array( 'post_id' => 0, ), $atts, 'skb_sc_kontakt' ); ob_start(); extract($atts); ?> <div class="col my-3"> <?php if (strlen($tytul = get_the_title($post_id))) { ?> <header> <h2><?php echo $tytul; ?></h2> </header> <?php } $teledane = [ [get_field('adres_telefon', $post_id), "tel", "fas fa-phone-alt"], [get_field('adres_komorka', $post_id), "tel", "fas fa-mobile-alt"], [get_field('adres_email', $post_id), "mailto", "far fa-envelope"] ]; $kontaktArr = []; foreach ($teledane as $td) { if (strlen($td[0])) { $kontaktArr[] = sprintf('<i class="%s"></i> <a href="%s:%s">%s</a>', $td[2], $td[1], $td[0], $td[0]); } } $kontakt = implode("<br>", $kontaktArr); $godzinyArr = [ get_field('adres_godziny_otwarcia_pn_-_pt', $post_id), get_field('adres_godziny_otwarcia_sobota', $post_id) ]; ?> <?php if (strlen($adres = get_field('adres_adres' , $post_id ))): ?> <p class="text-center"><?php echo $adres; ?></p> <?php endif; ?> <?php if (strlen($kontakt)): ?> <p class="text-center"><?php echo $kontakt; ?></p> <?php endif; ?> <?php if (strlen($godzinyArr[0])): ?> <p class="text-center"><strong>Godziny otwarcia:</strong><br />pn - pt: <?php echo $godzinyArr[0]; ?><br /> sobota: <?php echo $godzinyArr[1]; ?></p> <?php endif; ?> <?php $kontakty = get_field('kontakty'); if (is_array($kontakty)): ?> <div class="row justify-content-around"> <?php foreach ($kontakty as $kontakt): $teledane = [ [$kontakt["telefon"], "tel", "fas fa-phone-alt"], [$kontakt["komorka"], "tel", "fas fa-mobile-alt"], [$kontakt["email"], "mailto", "far fa-envelope"] ]; $kontaktArr = []; foreach ($teledane as $td) { if (strlen($td[0])) { $kontaktArr[] = sprintf('<i class="%s"></i> <a href="%s:%s">%s</a>', $td[2], $td[1], $td[0], $td[0]); } } $namiar = implode("<br>", $kontaktArr); ?> <div class="col-md-6 col-lg-4 text-center my-4"> <?php if ($kontakt["zdjecie"]): ?> <img class="rounded-circle img-fluid mb-4 px-4" src="<?php echo wp_get_attachment_image_url($kontakt["zdjecie"], 'medium'); ?>"> <?php endif; ?> <p class="text-center"><strong><?php echo $kontakt["tytul"]; ?></strong></p> <p class="text-center"><?php echo $namiar; ?></p> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> <?php $output_string = ob_get_contents(); ob_end_clean(); return $output_string; } add_shortcode('skb_kontakt', 'skb_sc_kontakt'); ?>
Nie jest to artykuł o tworzeniu shortcodów, więc skupię się tylko na tym co istotne dla działania tej metody. Żeby wyświetlić wynik skrótu w wybranym przez nas miejscu lub przypisać go do zmiennej funkcja musi zwrócić wartość (linia 109). Żeby uniknąć przypisywania kodu html do zwracanej zmiennej stosujemy buforowanie ob_start() (linia 11). Wszystko od tej pory znajduje się w buforze, którego zawartość w linii 107 przypisujemy do zmiennej $output_string, a w linii 108 czyścimy bufor.
Od linii 14 do 66 obsługiwany jest adres, a później namiary na pracowników. Podstawową funkcją jest tu get_field(). Jest to funkcja stworzona przez ACF. Pobiera parametr nazwy pola i opcjonalnie ID postu. W naszym przypadku jest to zbędne, bo shortcode wykonywany jest pętli wp_query, a to powoduje, że aktualnym ID jest numer iterowanego postu.
W linii 25 ustalamy $teledane placówki, czyli numer telefonu, komórki i adres email. Tworzymy tablicę wielowymiarową zawierającą pole, jego typ i ikonę Fontawesome. Potem tworzymy kolejną tablice, która będzie zawierała sformatowany kod na podstawie teledanych. Pola, które nie zawierają danych będą pominięte (warunek z linii 36). Na koniec do zmiennej $kontakt przypisujemy kod połączony dzięki funkcji implode(). Jako łącznik stosujemy znacznik łamania linii <br />.
Inaczej jest w przypadku godzin otwarcia. Nie usuwamy pustych pól. Jeżeli firma nie pracuje w sobotę, to należy to wyraźnie zaznaczyć pisząc, że jest nieczynne.
Od linii 50 do 61 mamy prezentację danych warunkowaną funkcją strlen(). Jeżeli długość zmiennej jest większa od zera lub nie ma wartości false, to spełniony jest warunek prezentacji. W przypadku godzin otwarcia badane jest pole dni powszednich. Dla potrzeb klienta to wystarczy.
Następnie w linii 65 sprawdzamy czy mamy jakieś dane kontaktowe. Jeżeli tak, to w linii 68 dodajemy wiersz w Bootstrapie, opracowujemy dane do wyświetlenia, a w linii 78 rozpoczynamy pętlę. Zwróć uwagę na warunek z linii 90. Jeżeli wartość pola "zdjecie" zawiera ID zdjęcia, to nad kontaktami wyświetlone zostanie zdjęcie pracowanika za pomocą funkcji wp_get_attachment_image_url(). Jej drugi parament oznacza, że zostanie podstawiony wariant "medium", a w tym przypadku to zdjęcie o rozmiarach 300px x 300px.
W linii 112 rejestrowany jest nasz skrót. Pierwszy parametr to użyta nazwa, a drugi, to funkcja zwrotna, którą właśnie opisałem.
Blokowanie podstron
Każdą podstronę domyślnie powinno dać się wyświetlić. Mi zależy na tym, by była tylko strona "kontakt", a pokrewne kierowały do rodzica. Dlaczego? By nie dublować treści i przy okazji wskazać najważniejszą treść. W tym celu musimy przechwycić informację, że WordPress ma do wyświetlenie potomka i przekierować adres na rodzica dzięki funkcji wp_redirect(). W pliku nagłówkowym "header.php" na samym początku dodajemy warunek:
if (($parent = wp_get_post_parent_id(get_the_ID())) > 0) { switch ($parent) { case "8": wp_redirect(get_permalink($parent), '301'); break; } }
Do zmiennej $parent przypisujemy wynik funkcji wp_get_post_parent_id(), która zwraca numer ID nadrzędnej strony. Jeżeli wynikiem jest 0, to znaczy, że strona jest na najwyższym poziomie. Jeżeli jest inaczej, to mamy do czynienia ze stroną potomną. W moim przypadku "Kontakt" ma ID równe 8. Jeżeli $parent ma wartość 8, to następuje przekierowanie na stronę rodzica. Adres strony mamy dzięki get_permalink(), a dodatkowo przekierowanie otrzymuje kod odpowiedzi HTTP 301 – czyli przeniesiony permanentnie.
Widoczność w wyszukiwarce witryny
Możliwość łatwego przeszukania zasobów witryny opartej na WordPressie jest dużą zaletą systemu. Funkcja szukająca sprawdza tytuł strony, jej treść z pola edytora i zajawki (post_excerpt). Nie sprawdza jednak metadanych, więc żadna z naszych właśnie stworzonych nie zostanie odnaleziona. Należy zatem w którymś z w/w pól umieścić dane, tak by system znalazł kontakt. Z pomocą przychodzi akcja ACF "acf/save_post". Przechwytywane jest zdarzenie zapisu postu z numerem ID. W tym przypadku uznałem, że kod akcji należy umieścić w wymaganym pluginie, tzw. "mu-plugins". Niezależnie od skóry z jakiej korzystamy, ta akcja zawsze się wykona.
add_action('acf/save_post', 'wdax_acf_save_post'); function wdax_acf_save_post( $post_id ) { $parent_id = wp_get_post_parent_id($post_id); if($parent_id > 0) { if ($parent_id == 8) { wdax_generate_kontakt_page($post_id, $parent_id); } } }
Jeżeli podczas zapisu system stwierdzi, że ma do czynienia z podstroną kontaktu, wykona funkcję wdax_generate_kontakt_page(). Rozbiłem kod na więcej funkcji dla lepszej czytelności.
function wdax_generate_kontakt_page($post_id, $parent_id=0) { if ($parent_id == 0) return false; $args = [ "post_status" => ['publish'], "parent" => $parent_id, "sort_column" => 'menu_order', "sort_order" => 'ASC,' ]; $pages = get_pages($args); $html = ''; if (is_array($pages)) { foreach ($pages as $page) { $html .= do_shortcode('[skb_kontakt post_id="' . $page->ID . '"]'); } } $postArr = [ "ID" => $parent_id, 'post_content' => strip_tags($html), ]; return wp_update_post($postArr); }
Zadaniem funkcji jest przelanie treści podstron do edytora rodzica. W linii 27 zastosowałem funkcję get_pages() zamiast pętli wp_query. Dostajemy tablicę obiektów stron. Jeżeli zmienna $pages jest tablicą, to wykonujemy pętlę foreach i do wcześniej utworzonej zmiennej $html dopisujemy zawartość skrótu "skb_kontakt" dla strony o ID wskazanym w obiekcie $page.
Znając ID rodzica aktualizujemy wartość jego pola "post_content" przypisując mu treść zmiennej $html. Dodatkowo usunąłem wszystkie znaczniki HTML, żeby odchudzić wielość danych ze zbędnych treści. My i tak nie będziemy pokazywać tych informacji na zewnątrz, więc warto zadbać o czas ładowania strony i nie dodawać niepotrzebnych bajtów.
Funkcja zwraca wynik wykonania wp_update_post(). Jeżeli wszystko się udało, to wynikiem będzie liczba 8. Dzięki temu za każdym razem gdy dokonamy zmian na podstronach zaktualizowana też będzie strona rodzica "Kontakt".
Podsumowanie
Najważniejsza jest wygoda klienta. Można oczywiście tak wszystko zagmatwać, że tylko my będziemy mogli dokonać zmian, ale prędzej czy później klient z nas zrezygnuje. Dlatego lepiej tworzyć systemy przejrzyste, czytelne. Tutaj zrobiliśmy coś efektownego i skrojonego na miarę. Proste formularze, dedykowany szablon i WordPress poinformowany jak się ma zachowywać. W dodatku wszystko łatwe do przeniesienia na inną witrynę i do dalszej rozbudowy. Mam nadzieję, że ten przykład na coś ci się przyda 🙂