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;
} );