0 php /** * SEO i porządki (18.10.2026): * - alt zdjęć w treści wpisu i w zdjęciu głównym (zamiast „- fraza kluczowa” z Rank Math Image SEO), * - opis meta stron wypraw i strony bloga (wyjazdowo_meta_opis_wyprawy() + uzupełnienie w locie, gdy pole puste), * - schema TouristTrip dla stron wypraw (zamiast BlogPosting), sameAs z ustawień kontaktu, * - kategorie kontynentów → strony /europa/, /azja/… (linki + przekierowanie 301 archiwów). */ defined( 'ABSPATH' ) || exit; /* ============================== Alt zdjęć ============================== */ /** Tekst alternatywny zdjęć wpisu: tytuł bez dopisku wyprawy („Lodowiec Perito Moreno z dziećmi”). */ function wyjazdowo_alt_wpisu( int $id ): string { $t = function_exists( 'wyjazdowo_wyprawy_krotki_tytul' ) ? wyjazdowo_wyprawy_krotki_tytul( $id ) : get_the_title( $id ); return trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( $t ) ) ); } /** Puste alt="" (albo brak alt) w zdjęciach treści wpisu → podpis zdjęcia, a bez podpisu tytuł wpisu. */ add_filter( 'the_content', function ( $html ) { if ( ! is_singular( 'post' ) || ! in_the_loop() || false === strpos( $html, '……
Podpis
- podpis ma pierwszeństwo. $html = preg_replace_callback( '#]*>(?:(?!).)*?]*>(.*?)\s*#su', function ( $m ) { $pod = trim( wp_strip_all_tags( $m[1] ) ); if ( '' === $pod ) { return $m[0]; } return preg_replace( '#(]*?)\salt=""#u', '$1 alt="' . esc_attr( $pod ) . '"', $m[0], 1 ); }, $html ); $html = preg_replace( '#(]*?)\salt=""#u', '$1 alt="' . esc_attr( $alt ) . '"', $html ); return preg_replace_callback( '#]*\balt=)[^>]*>#u', fn( $m ) => substr( $m[0], 0, 4 ) . ' alt="' . esc_attr( $alt ) . '"' . substr( $m[0], 4 ), $html ); }, 30 ); /** Zdjęcie główne na stronie wpisu (nagłówek) bez alt → tytuł wpisu. */ add_filter( 'post_thumbnail_html', function ( $html, $post_id ) { if ( ! $html || ! is_singular() || (int) $post_id !== (int) get_queried_object_id() || ! preg_match( '#\salt=""#', $html ) ) { return $html; } return preg_replace( '#\salt=""#', ' alt="' . esc_attr( wyjazdowo_alt_wpisu( (int) $post_id ) ) . '"', $html, 1 ); }, 10, 2 ); /* ============================== Opis meta ============================== */ /** Opis meta strony wyprawy: pierwszy akapit opisu, a bez niego zdanie z danych wyprawy (typ, rok, kraje). */ function wyjazdowo_meta_opis_wyprawy( int $id ): string { $w = function_exists( 'wyjazdowo_wyprawa_dane' ) ? wyjazdowo_wyprawa_dane( $id ) : null; if ( ! $w ) { return ''; } $tekst = ''; foreach ( parse_blocks( (string) get_post_field( 'post_content', $id ) ) as $b ) { if ( 'core/paragraph' === $b['blockName'] ) { $t = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( render_block( $b ) ) ) ); if ( mb_strlen( $t ) >= 50 ) { $tekst = $t; break; } } } if ( '' === $tekst ) { $nazwa = function_exists( 'wyjazdowo_wyprawa_nazwa' ) ? wyjazdowo_wyprawa_nazwa( $w + [ 'tytul' => get_the_title( $id ) ] ) : get_the_title( $id ); $nazwa = trim( preg_replace( '/\s*\b(19|20)\d\d\b\s*/u', ' ', $nazwa ) ); $brak = array_filter( $w['kraje'], fn( $k ) => false === mb_stripos( $nazwa, $k ) ); // kraje, których nie ma w nazwie $kraje = $brak ? ', ' . implode( ', ', $brak ) : ''; $ma = function_exists( 'wyjazdowo_wpisy_strony_wyprawy' ) && wyjazdowo_wpisy_strony_wyprawy( $id, false ); $glowa = in_array( $w['typ'], [ 'meska', 'mamowa' ], true ) ? sprintf( '%s (%d%s).', $nazwa, $w['rok'], $kraje ) : sprintf( 'Rodzinna wyprawa: %s %d%s.', $nazwa, $w['rok'], $kraje ); $tekst = $glowa . ' ' . ( $ma ? 'Relacje dzień po dniu, zdjęcia, trasa na mapie i kilometry całej wyprawy.' : 'Trasa na mapie, transport i kilometry wyprawy.' ); } return wyjazdowo_meta_przytnij( $tekst ); } /** * Opis meta wpisu (20.10.2026), gdy Rank Math nie ma opisu ani zajawki: pierwszy akapit treści (min. 50 znaków), * a we wpisach złożonych z samej galerii - zdanie z danych wyprawy: miejsce, wyprawa, numer wpisu, liczba zdjęć. */ function wyjazdowo_meta_opis_wpisu( int $id ): string { foreach ( parse_blocks( (string) get_post_field( 'post_content', $id ) ) as $b ) { if ( 'core/paragraph' !== $b['blockName'] ) { continue; } $t = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( render_block( $b ) ) ) ); if ( mb_strlen( $t ) >= 50 ) { return wyjazdowo_meta_przytnij( $t ); } } $miejsce = function_exists( 'wyjazdowo_wyprawy_krotki_tytul' ) ? wyjazdowo_wyprawy_krotki_tytul( $id ) : get_the_title( $id ); $miejsce = html_entity_decode( wp_strip_all_tags( $miejsce ), ENT_QUOTES, 'UTF-8' ); $w = function_exists( 'wyjazdowo_wyprawa_wpisu' ) ? wyjazdowo_wyprawa_wpisu( $id ) : null; $zdjecia = function_exists( 'wyjazdowo_liczba_zdjec' ) ? wyjazdowo_liczba_zdjec( $id ) : 0; $odm = ( $zdjecia % 10 >= 2 && $zdjecia % 10 <= 4 && ( $zdjecia % 100 < 12 || $zdjecia % 100 > 14 ) ) ? 'zdjęcia' : 'zdjęć'; $foto = $zdjecia >= 5 ? sprintf( '%d %s z drogi', $zdjecia, $odm ) : 'Zdjęcia z drogi'; if ( $w && ! empty( $w['tytul'] ) ) { $poz = array_search( $id, array_map( 'intval', (array) ( $w['ids'] ?? [] ) ), true ); $nr = false !== $poz ? sprintf( ' (wpis %d z %d)', $poz + 1, count( $w['ids'] ) ) : ''; $t = sprintf( '%s – relacja z rodzinnej wyprawy %s%s. %s, miejsce na mapie i trasa całej wyprawy.', $miejsce, $w['tytul'], $nr, $foto ); } else { $kraj = function_exists( 'wyjazdowo_kraj' ) ? wyjazdowo_kraj( $id ) : null; $t = sprintf( '%s%s – relacja z rodzinnej podróży. %s i miejsce na mapie.', $miejsce, $kraj ? ', ' . $kraj->name : '', $foto ); } return wyjazdowo_meta_przytnij( $t ); } function wyjazdowo_meta_przytnij( string $t, int $max = 160 ): string { $t = trim( $t ); if ( mb_strlen( $t ) <= $max ) { return $t; } $c = mb_substr( $t, 0, $max - 1 ); $c = preg_replace( '/[\s,;:–—-]+[^\s]*$/u', '', $c ); return rtrim( $c, " ,;:–—-" ) . '…'; } const WYJAZDOWO_OPIS_BLOGA = 'Wszystkie relacje z naszych rodzinnych wypraw: dzień po dniu, z trasą na mapie, zdjęciami i komentarzami całej ekipy. Europa, Azja, Ameryka i Afryka.'; add_filter( 'rank_math/frontend/description', function ( $d ) { if ( '' !== trim( (string) $d ) ) { return $d; } if ( is_home() ) { return WYJAZDOWO_OPIS_BLOGA; } if ( is_page() && ( $o = wyjazdowo_meta_opis_wyprawy( (int) get_queried_object_id() ) ) ) { return $o; } if ( is_singular( 'post' ) && ( $o = wyjazdowo_meta_opis_wpisu( (int) get_queried_object_id() ) ) ) { return $o; } return $d; } ); /* ============================== Schema ============================== */ add_filter( 'rank_math/json_ld', function ( $data ) { if ( ! is_array( $data ) ) { return $data; } // sameAs organizacji/osoby: profile z ustawień kontaktu. $profile = function_exists( 'wyjazdowo_kontakt' ) ? array_values( array_filter( array_column( wyjazdowo_kontakt()['profile'], 'url' ) ) ) : []; if ( $profile ) { foreach ( $data as &$n ) { if ( is_array( $n ) && isset( $n['@id'] ) && ( str_ends_with( (string) $n['@id'], '/#person' ) || str_ends_with( (string) $n['@id'], '/#organization' ) ) ) { $n['sameAs'] = $profile; } } unset( $n ); } if ( ! is_page() || ! function_exists( 'wyjazdowo_wyprawa_dane' ) ) { return $data; } $id = (int) get_queried_object_id(); $w = wyjazdowo_wyprawa_dane( $id ); if ( ! $w ) { return $data; } foreach ( $data as $k => $n ) { if ( ! is_array( $n ) || ! isset( $n['@id'] ) || ! str_ends_with( (string) $n['@id'], '#richSnippet' ) ) { continue; } $url = get_permalink( $id ); $trip = [ '@type' => 'TouristTrip', '@id' => $n['@id'], 'name' => html_entity_decode( get_the_title( $id ), ENT_QUOTES, 'UTF-8' ), 'description' => wyjazdowo_meta_opis_wyprawy( $id ), 'url' => $url, 'inLanguage' => 'pl-PL', 'touristType' => [ 'meska' => 'Ojciec z synami', 'mamowa' => 'Mama z synami' ][ $w['typ'] ] ?? 'Rodzina z dziećmi', 'mainEntityOfPage' => [ '@id' => $url . '#webpage' ], 'provider' => [ '@id' => home_url( '/#organization' ) ], ]; if ( ! empty( $n['image'] ) ) { $trip['image'] = $n['image']; } if ( $w['rok'] ) { $trip['temporalCoverage'] = (string) $w['rok']; } $dom = function_exists( 'wyjazdowo_transport_dom' ) ? wyjazdowo_transport_dom() : null; if ( $dom ) { $trip['tripOrigin'] = [ '@type' => 'Place', 'name' => $dom['m'] ?? 'Warszawa' ]; } $el = []; if ( function_exists( 'wyjazdowo_trasa_strony' ) ) { foreach ( wyjazdowo_trasa_strony( $id, false ) as $x ) { if ( ! empty( $x['dom'] ) || empty( $x['p'] ) ) { continue; } $pl = [ '@type' => 'Place', 'name' => (string) $x['m'], 'geo' => [ '@type' => 'GeoCoordinates', 'latitude' => $x['p'][0], 'longitude' => $x['p'][1] ] ]; if ( ! empty( $x['w'] ) ) { $pl['url'] = get_permalink( (int) $x['w'] ); } $el[] = [ '@type' => 'ListItem', 'position' => count( $el ) + 1, 'item' => $pl ]; } } if ( $el ) { $trip['itinerary'] = [ '@type' => 'ItemList', 'numberOfItems' => count( $el ), 'itemListElement' => $el ]; } if ( $w['czesc'] ) { $trip['partOfTrip'] = [ '@type' => 'TouristTrip', 'url' => get_permalink( $w['czesc'] ) ]; } $data[ $k ] = $trip; break; } return $data; }, 999 ); // po Rank Math (dokłada richSnippet z wysokim priorytetem) /* ============================== Kontynenty: strony zamiast kategorii ============================== */ /** Kategorie, które mają własną stronę (slug kategorii => ścieżka strony). Archiwum kategorii przekierowuje na stronę. */ function wyjazdowo_kategorie_stron(): array { // Wydajność (19.10.2026): mapa liczona raz na żądanie i trzymana w cache razem z danymi wypraw // (filtr stron krajów szuka kategorii 33 krajów — bez cache to setki zapytań na każdą stronę z kartami). static $mem = null; if ( null !== $mem ) { return $mem; } $klucz = 'wy_kat_stron_' . ( function_exists( 'wyjazdowo_wyprawy_wersja' ) ? wyjazdowo_wyprawy_wersja() : 0 ); $c = get_transient( $klucz ); if ( is_array( $c ) ) { return $mem = $c; } $mem = apply_filters( 'wyjazdowo_kategorie_stron', [ 'europa' => 'europa', 'azja-wyjazdy' => 'azja', 'ameryka' => 'ameryka', 'afryka' => 'afryka', 'polska' => 'europa/polska', 'meski-wyjazd' => 'trzy-czwarte/meskie', ] ); set_transient( $klucz, $mem, DAY_IN_SECONDS ); return $mem; } function wyjazdowo_strona_kategorii( $term ): string { static $mem = []; if ( ! $term instanceof WP_Term || 'category' !== $term->taxonomy ) { return ''; } if ( isset( $mem[ $term->slug ] ) ) { return $mem[ $term->slug ]; } $sciezka = wyjazdowo_kategorie_stron()[ $term->slug ] ?? ''; if ( '' === $sciezka ) { return $mem[ $term->slug ] = ''; } $p = get_page_by_path( $sciezka ); return $mem[ $term->slug ] = ( $p && 'publish' === $p->post_status ) ? get_permalink( $p ) : ''; } add_filter( 'term_link', function ( $link, $term ) { if ( is_admin() && ! wp_doing_ajax() ) { return $link; } // kokpit: zwykłe linki kategorii $u = wyjazdowo_strona_kategorii( $term ); return $u ?: $link; }, 30, 2 ); add_action( 'template_redirect', function () { if ( ! is_category() || is_feed() || is_admin() ) { return; } $u = wyjazdowo_strona_kategorii( get_queried_object() ); if ( $u ) { wp_safe_redirect( $u, 301 ); exit; } }, 5 ); /** Mapa witryny: bez kategorii, które przekierowują na stronę (strona jest już w page-sitemap). */ add_filter( 'rank_math/sitemap/entry', function ( $url, $type, $obj ) { return ( 'term' === $type && $obj instanceof WP_Term && wyjazdowo_strona_kategorii( $obj ) ) ? false : $url; }, 20, 3 ); /* * Zwykłe strony (20.10.2026): Rank Math daje im BlogPosting (domyślny typ stron = artykuł, potrzebny, bo z tego węzła * powstają TouristTrip wypraw i dane stron krajów). Gdy węzeł nadal jest artykułem - usuwamy go, strona zostaje WebPage * wydawcy Wyjazdowo; znika też osierocony węzeł autora. */ add_filter( 'rank_math/json_ld', function ( $data ) { if ( ! is_array( $data ) || ! is_page() || is_front_page() ) { return $data; } $usuniety = false; foreach ( $data as $k => $n ) { if ( ! is_array( $n ) || ! str_ends_with( (string) ( $n['@id'] ?? '' ), '#richSnippet' ) ) { continue; } if ( array_intersect( (array) ( $n['@type'] ?? [] ), [ 'BlogPosting', 'Article', 'NewsArticle' ] ) ) { unset( $data[ $k ] ); $usuniety = true; } } if ( $usuniety ) { $json = wp_json_encode( $data ); foreach ( $data as $k => $n ) { if ( is_array( $n ) && 'Person' === ( $n['@type'] ?? '' ) && ! empty( $n['@id'] ) && substr_count( $json, '"' . str_replace( '/', '\/', $n['@id'] ) . '"' ) <= 1 ) { unset( $data[ $k ] ); } } } return $data; }, 1000 ); // wyjazdowo_schema_stron_bez_artykulu - po zamianie na TouristTrip (999) /** * Rank Math: spis treści wpisu renderuje blok wyjazdowo/panel-boczny (z nagłówków H2/H3), * więc analiza treści ma traktować wtyczkę wyjazdowo-blocks jako wtyczkę spisu treści. */ add_filter( 'rank_math/researches/toc_plugins', function ( $wtyczki ) { $wtyczki['wyjazdowo-blocks/wyjazdowo-blocks.php'] = 'Wyjazdowo – spis treści'; return $wtyczki; } );