Strona na bazie podstron

WordPress udostęp­nia możli­wość zbudo­wa­nia szablo­nu, który zamiast podsta­wo­wej treści strony wyświe­tli dane innej strony lub stron, wpisów czy metada­nych. Wystar­czy wskazać w argumen­tach dla obiek­tu klasy wp_​query intere­su­ją­ce nas przestrze­nie, wykonać pętle i sukce­syw­nie wyświe­tlać intere­su­ją­ce nas infor­ma­cje.

// 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ła­dzie pobie­ra­my strony o numerach ID: 1, 2, 7 i 9; typ postu: page; opubli­ko­wa­ne; bez pagina­cji (wszyst­ko na jednej stronie); posor­to­wa­ne rosną­co wg pola menu_​order.

Przykład zastosowania

Robiłem ostat­nio dla klien­ta stronę kontak­tu, gdzie były dane kilku placó­wek i działów przed­się­bior­stwa. Strona zbudo­wa­na na bazie Bootstra­pa (zdjęcie poniżej),  z możli­wo­ścią edycji danych. Wiado­mo, że edytor WordPres­sa działa jak chce, więc założy­łem, że po pierw­szej próbie edycji przez pracow­ni­ków, cały układ pójdzie w rozsyp­kę. Aby do tego nie dopuścić dane musia­ły­by zostać odsepa­ro­wa­ne od prezen­ta­cji – najle­piej za pomocą formu­la­rzy ACF.

Nie należy spodzie­wać się, że nasz klient porusza się w HTML jak ryba w wodzie i trzeba założyć, że szybciej wszyst­ko zepsu­je niż zrobi sam popraw­ki.

Można natomiast przyjąć, że bez proble­mu wypeł­ni kilka prostych pól formu­la­rza – wystar­czy mu je dobrze opisać. Dlate­go przyda nam się wtycz­ka Advan­ced Custom Fields for WordPress Develo­pers, w której za darmo wykona­my potrzeb­ne nam tabel­ki i przypi­sze­my do stron z danymi działów i placó­wek handlo­wych.

Kliknij by powięk­szyć

Na pierw­szym zdjęciu da się zauwa­żyć, że mamy dwa typy danych. Pierw­szy to adres, a drugi to kolum­ny z danymi pracow­ni­ków działu. Zatem i nasze pola niestan­dar­do­we musimy umieścić w dwóch sekcjach: adres i kontak­ty.

Kontakty

Tutaj zasto­so­wa­łem typ "Pole powta­rzal­ne" czyli wielo­wy­mia­ro­wą tabli­cę z takimi polami jak:

  • tytultekst
  • telefon – tekst
  • komor­ka – tekst
  • emailE‑mail
  • opisedytor WYSIWIG
  • zdjecieobraz

Pracow­ni­ków może być wielu, dlate­go tabli­ca spraw­dzi się tutaj ideal­nie. "tytul" to pole na imię i nazwi­sko pracow­ni­ka. Celowo rozdzie­li­łem "telefon" i "komor­ka". Często pracow­ni­cy korzy­sta­ją z jedne­go telefo­nu stacjo­nar­ne­go w biurze, ale komór­kę każdy ma dziś raczej swoją. Pole "email" w ACF ma swoją repre­zen­ta­cję (input[type="email"]). Szkoda, że nie pomyśla­no o imple­men­ta­cji pola "tel". Do opisu pracow­ni­ka celowo użyłem edyto­ra WYSIWIG, a nie obsza­ru teksto­we­go z uwagi na wygodę później­szych obsłu­gu­jąc. Projekt zakła­da, że jeżeli pracow­nik zgodzi się na użycie swoje­go zdjęcia, to szablon je wyświe­tli. Do tego użyłem pola typu "obraz", które w tym wypad­ku zwraca ID zdjęcia.

Adres

Dla adresu wystar­czy zestaw pól czyli "Grupa":

  • adres – obszar teksto­wy
  • godzi­ny­_o­twar­cia­_pn_-_pt – tekst
  • godziny_​otwarcia_​sobota – tekst
  • telefon – tekst
  • komor­ka – tekst
  • emailE‑mail

"adres" to obszar teksto­wy z opcją dodawa­nia znacz­ni­ka <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 poczto­wy itd. Nie ma sensu urucha­miać edyto­ra. Godzi­ny otwar­cia zosta­ły rozbi­te na dni powsze­dnie i sobotę. Jest to zwykłe pole teksto­we. Pola do edycji daty i czasu nie mają tutaj zasto­so­wa­nia. Pozosta­łe pola jak wyżej.

W sekcji ustawień trzeba też ograni­czyć wystę­po­wa­nie pól tylko do podstron strony (rodzi­ca) o nazwie "Kontakt". Byłoby nonsen­sem i marno­wa­niem zasobów gdyby te pola wyświe­tla­ły się wszędzie, na każdej stronie edycji wpisów czy stron.

Podział na sekcje (działy firmy)

W przypad­ku mojego klien­ta wystę­pu­je pięć sekcji: trzy biura i dwa zespo­łu obsłu­gi. Można by to wszyt­ko wrzucić do jedne­go worka, stworzyć duży formu­larz oparty na dużym polu powta­rzal­nym i z głowy. To jednak jest wygod­ne i czytel­ne tylko dla biegłych progra­mi­stów lub edyto­rów stron. Ja wycho­dzę z założe­nia, że im czytel­niej tym klient bardziej zadowo­lo­ny. Dlate­go zasto­so­wa­łem tutaj układ "Rodzic + dzieci". Rodzi­cem jest strona o nazwie "Kontakt", a dzieć­mi kilka podstron kontak­tu – każda nazwa­na 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 wiado­mo co gdzie jest. Dodat­ko­wo, korzy­sta­jąc z atrybu­tu strony "kolej­ność" (menu_​order) ustala­my porzą­dek wyświe­tla­nia bloków. Najwyż­szy ma liczbę 1, najniż­szy 5.

Po wypeł­nie­niu pól mamy teore­tycz­nie stworzo­ną sekcję. Gdy podamy wszyst­kie dane system powinien je wyświe­tlić jak na zdjęciu nr 1. Na pewno nie! Przede wszyst­kim WordPress domyśl­nie wyświe­tli zawar­tość pola "post_​content", czyli to co mamy w edyto­rze gdy wejdzie­my do edycji strony. A tam jest pusto.

Szablony stron

WordPress wyświe­tla treści dobie­ra­jąc dynamicz­nie szablo­ny wg typu wpisu.

Powyż­szy diagram pokazu­je, które pliki szablo­nów są wywoły­wa­ne w celu wygene­ro­wa­nia strony WordPress na podsta­wie hierar­chii szablo­nów WordPress.

Szablony dedykowane

Dla typu postu "page" WordPress podsta­wi szablon page.php. Jest to domyśl­ny szablon jeżeli WordPress uzna, że ma do czynie­nia ze stroną, a nie na przykład z wpisem bloga czy tagiem. Jeżeli go nie znajdzie, to spróbu­je podsta­wić singular.php, a w przypad­ku niepo­wo­dze­nia index.php. Szablon musi zawie­rać plik index.php, więc nie musimy się obawiać, że nic się pokaże. Idąc w lewą stronę diagra­mu widzi­my, że są opcje page-$id.php i page-$slug.php. Pierw­szy to połącze­nie typu "page" i ID wpisu. Drugi łączy typ i tzw "slug" czyli unikal­ny, własny link każde­go typu postu. Wygod­niej korzy­stać z drugiej opcji. Szcze­gól­nie gdy tworzy­my uniwer­sal­ne rozwią­za­nia, a nigdy nie wiado­mo jaki numer ID dosta­nie konkret­na strona. Za to możemy założyć, że strona "Kontakt" będzie miała slug "kontakt".

Szablony uniwersalne

Istnie­je też możli­wość stworze­nie własne­go szablo­nu, pod dowol­ną nazwą i podsta­wie­nia go do dowol­nej strony (opcja nie dotyczy wpisów bloga). Wystar­czy stworzyć plik o dowol­nej nazwie, np "szablon-strony.php" i umieścić go w głównym katalo­gu naszej skórki. Żeby szablon był dostęp­ny w atrybu­tach strony musi zawie­rać prosty nagłó­wek:

<?php
/*
Template name: Partner

*/

get_header();
?>

W trzeciej linii zapisu­je­my nazwę szablo­nu, która będzie widocz­na w polu wyboru. Tyle wystar­czy, ale jak chcesz poznać inne opcje to zajrzyj na stronę Page Templa­tes

Dużą zaletą szablo­nu uniwer­sal­ne­go jest to, że możemy go zasto­so­wać do wielu stron różnych typów. Dobrym przykła­dem tu może być szablon w którym strona wyświe­tla­na jest na całej szero­ko­ści i drugi gdzie mamy podział na kolum­ny. Od nas tylko zależy jak wyświe­tli­my naszą treść.

Jednak w przypad­ku nasze­go zadania zdecy­do­wa­łem się na utworze­nie szablo­nu dedyko­wa­ne­go. Dlacze­go? Dlate­go, że układ strony jest wyjąt­ko­wy i raczej nie powtó­rzy się na innych stronach. Zatem tworzy­my 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 zazna­czo­ne linie (26−30 i 46). W linii 26 przypi­su­je­my wartość ID strony "kontakt" do klucza "post_​parent". Sortu­je­my strony wg opcji "menu_​order". Tym sposbem otrzy­mu­je­my zwartość podstron przypi­sa­nych do kontak­tu. W dalszym ciągu wykonu­je się pętla, o ile warun­ki pętli są spełnio­ne. W linii 46 wykonu­je się tzw "short­co­de" czyli skrót. To nic innego jak funkcja którą można wpisać bezpo­śred­nio do edyto­ra lub użyć w kodzie. Wykonu­je­my zatem skrót "skb_​kontakt" z parame­trem "post_​id" równym ID postu z pętli.

Oczywi­ście zamiast skrótu można by wpisać jego zawar­tość do pętli. Skróty mają jednak tę zaletę, że ich kod można używać wielo­krot­nie w wielu miejscach witry­ny 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>&nbsp;<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>&nbsp;<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 tworze­niu short­co­dów, więc skupię się tylko na tym co istot­ne dla działa­nia tej metody. Żeby wyświe­tlić wynik skrótu w wybra­nym przez nas miejscu lub przypi­sać go do zmien­nej funkcja musi zwrócić wartość (linia 109). Żeby uniknąć przypi­sy­wa­nia kodu html do zwraca­nej zmien­nej stosu­je­my buforo­wa­nie ob_​start() (linia 11). Wszyst­ko od tej pory znajdu­je się w buforze, które­go zawar­tość w linii 107 przypi­su­je­my do zmien­nej $output_​string, a w linii 108 czyści­my bufor.

Od linii 14 do 66 obsłu­gi­wa­ny jest adres, a później namia­ry na pracow­ni­ków. Podsta­wo­wą funkcją jest tu get_​field(). Jest to funkcja stworzo­na przez ACF. Pobie­ra parametr nazwy pola i opcjo­nal­nie ID postu. W naszym przypad­ku jest to zbędne, bo short­co­de wykony­wa­ny jest pętli wp_​query, a to powodu­je, że aktual­nym ID jest numer itero­wa­ne­go postu.

W linii 25 ustala­my $teleda­ne placów­ki, czyli numer telefo­nu, komór­ki i adres email. Tworzy­my tabli­cę wielo­wy­mia­ro­wą zawie­ra­ją­cą pole, jego typ i ikonę  Fonta­we­so­me. Potem tworzy­my kolej­ną tabli­ce, która będzie zawie­ra­ła sforma­to­wa­ny kod na podsta­wie teleda­nych. Pola, które nie zawie­ra­ją danych będą pominię­te (warunek z linii 36). Na koniec do zmien­nej $kontakt przypi­su­je­my kod połączo­ny dzięki funkcji implo­de(). Jako łącznik stosu­je­my znacz­nik łamania linii <br /​>.

Inaczej jest w przypad­ku godzin otwar­cia. Nie usuwa­my pustych pól. Jeżeli firma nie pracu­je w sobotę, to należy to wyraź­nie zazna­czyć pisząc, że jest nieczyn­ne.

Od linii 50 do 61 mamy prezen­ta­cję danych warun­ko­wa­ną funkcją strlen(). Jeżeli długość zmien­nej jest większa od zera lub nie ma warto­ści false, to spełnio­ny jest warunek prezen­ta­cji. W przypad­ku godzin otwar­cia badane jest pole dni powsze­dnich. Dla potrzeb klien­ta to wystar­czy.

Następ­nie w linii 65 spraw­dza­my czy mamy jakieś dane kontak­to­we. Jeżeli tak, to w linii 68 dodaje­my wiersz w Bootstra­pie, opraco­wu­je­my dane do wyświe­tle­nia, a w linii 78 rozpo­czy­na­my pętlę. Zwróć uwagę na warunek z linii 90. Jeżeli wartość pola "zdjecie" zawie­ra ID zdjęcia, to nad kontak­ta­mi wyświe­tlo­ne zosta­nie zdjęcie praco­wa­ni­ka za pomocą funkcji wp_​get_​attachment_​image_​url(). Jej drugi parament oznacza, że zosta­nie podsta­wio­ny wariant "medium", a w tym przypad­ku to zdjęcie o rozmia­rach 300px x 300px.

W linii 112 rejestro­wa­ny jest nasz skrót. Pierw­szy parametr to użyta nazwa, a drugi, to funkcja zwrot­na, którą właśnie opisa­łem.

Blokowanie podstron

Każdą podstro­nę domyśl­nie powin­no dać się wyświe­tlić. Mi zależy na tym, by była tylko strona "kontakt", a pokrew­ne kiero­wa­ły do rodzi­ca. Dlacze­go? By nie dublo­wać treści i przy okazji wskazać najważ­niej­szą treść. W tym celu musimy przechwy­cić infor­ma­cję, że WordPress ma do wyświe­tle­nie potom­ka i przekie­ro­wać adres na rodzi­ca dzięki funkcji wp_​redirect(). W pliku nagłów­ko­wym "header.php" na samym począt­ku dodaje­my warunek:

if (($parent = wp_get_post_parent_id(get_the_ID())) > 0)
{
    switch ($parent)
    {
        case "8":
            wp_redirect(get_permalink($parent), '301');		
            break;
    }
}

Do zmien­nej $parent przypi­su­je­my wynik funkcji wp_​get_​post_​parent_​id(), która zwraca numer ID nadrzęd­nej strony. Jeżeli wynikiem jest 0, to znaczy, że strona jest na najwyż­szym pozio­mie. Jeżeli jest inaczej, to mamy do czynie­nia ze stroną potom­ną. W moim przypad­ku "Kontakt" ma ID równe 8. Jeżeli $parent ma wartość 8, to nastę­pu­je przekie­ro­wa­nie na stronę rodzi­ca. Adres strony mamy dzięki get_​permalink(), a dodat­ko­wo przekie­ro­wa­nie otrzy­mu­je kod odpowie­dzi HTTP 301 – czyli przenie­sio­ny perma­nent­nie.

Widoczność w wyszukiwarce witryny

Możli­wość łatwe­go przeszu­ka­nia zasobów witry­ny opartej na WordPres­sie jest dużą zaletą syste­mu. Funkcja szuka­ją­ca spraw­dza tytuł strony, jej treść z pola edyto­ra i zajaw­ki (post_​excerpt). Nie spraw­dza jednak metada­nych, więc żadna z naszych właśnie stworzo­nych nie zosta­nie odnale­zio­na. Należy zatem w którymś z w/​w pól umieścić dane, tak by system znalazł kontakt. Z pomocą przycho­dzi akcja ACF "acf/​save_​post". Przechwy­ty­wa­ne jest zdarze­nie zapisu postu z numerem ID. W tym przypad­ku uznałem, że kod akcji należy umieścić w wymaga­nym plugi­nie, tzw. "mu-plugins". Nieza­leż­nie od skóry z jakiej korzy­sta­my, 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 stwier­dzi, że ma do czynie­nia z podstro­ną kontak­tu, wykona funkcję wdax_​generate_​kontakt_​page(). Rozbi­łem kod na więcej funkcji dla lepszej czytel­no­ś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 przela­nie treści podstron do edyto­ra rodzi­ca. W linii 27 zasto­so­wa­łem funkcję get_​pages() zamiast pętli wp_​query. Dosta­je­my tabli­cę obiek­tów stron. Jeżeli zmien­na $pages jest tabli­cą, to wykonu­je­my pętlę foreach i do wcześniej utworzo­nej zmien­nej $html dopisu­je­my zawar­tość skrótu "skb_​kontakt" dla strony o ID wskaza­nym w obiek­cie $page.

Znając ID rodzi­ca aktuali­zu­je­my wartość jego pola "post_​content" przypi­su­jąc mu treść zmien­nej $html. Dodat­ko­wo usuną­łem wszyst­kie znacz­ni­ki HTML, żeby odchu­dzić wielość danych ze zbędnych treści. My i tak nie będzie­my pokazy­wać tych infor­ma­cji na zewnątrz, więc warto zadbać o czas ładowa­nia strony i nie dodawać niepo­trzeb­nych bajtów.

Funkcja zwraca wynik wykona­nia wp_​update_​post(). Jeżeli wszyst­ko się udało, to wynikiem będzie liczba 8. Dzięki temu za każdym razem gdy dokona­my zmian na podstro­nach zaktu­ali­zo­wa­na też będzie strona rodzi­ca "Kontakt".

Podsumowanie

Najważ­niej­sza jest wygoda klien­ta. Można oczywi­ście tak wszyst­ko zagma­twać, że tylko my będzie­my mogli dokonać zmian, ale prędzej czy później klient z nas zrezy­gnu­je. Dlate­go lepiej tworzyć syste­my przej­rzy­ste, czytel­ne. Tutaj zrobi­li­śmy coś efektow­ne­go i skrojo­ne­go na miarę. Proste formu­la­rze, dedyko­wa­ny szablon i WordPress poinfor­mo­wa­ny jak się ma zacho­wy­wać. W dodat­ku wszyst­ko łatwe do przenie­sie­nia na inną witry­nę i do dalszej rozbu­do­wy. Mam nadzie­ję, że ten przykład na coś ci się przyda 🙂

Dodaj komentarz

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.