Piszemy plugin pozwalający nam dodać źródło obrazka dla obrazków wyświetlanych na stronie. Nauczymy się pisać pluginy, korzystać z tzw. meta-boxes i meta-properties oraz przede wszystkim z bardzo ważnego mechanizmu WordPressa jakim są akcje oraz filtry. Ponadto nasz plugin wyposażymy w funkcjonalność sprawiającą, że obrazek jest jednocześnie linkiem do posta. Do dzieła.

Tworzymy folder i pliki pluginu – podstawy

Jak zawsze w takich przypadkach (to jest – pisanie pluginu) musimy odnaleźć naszą instalację WordPressa. W folderze wp-contnet znaleźć folder plugins i tam utworzyć nasz folder. Nazwijmy go img-source-plug. Wewnątrz tego folderu tworzymy plik index.php o poniższej treści:

<?php
# Silence is golden.

Robimy to rutynowo, dla bezpieczeństwa. Teraz tworzymy kolejny plik, o nazwie img-source-plug.php. Oto jego zawartość:

<?php
/*
Plugin Name: Img src plug
Description: Opis Twojego pluginu
Version: 1.0
Author: Twój autor
*/
if( ! defined( 'ABSPATH') ){
    exit;
}

Autora, opis i nazwę możemy sobie dowolnie dostosować. Kod tam zamieszczony to standardowa procedura bezpieczeństwa, robimy tak przy każdym pluginie. Teraz możemy nasz plugin znaleźć w panelu admina i włączyć.

Teraz dopiszemy sobie naszą funkcję, którą omówimy w następnym punkcie:

<?php
/*
Plugin Name: Img src plug
Description: Opis Twojego pluginu
Version: 1.0
Author: Twój autor
*/
if( ! defined( 'ABSPATH') ){
    exit;
}

function add_thumbnail_link($html, $post_id, $post_image_id) {
    return $html;
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Jeżeli wszystko poszło jak należy, po włączeniu pluginu niewiele się zmieni. Obrazek będzie się wyświetlać tak, jak wcześniej.

Zamiana obrazka na link – filtr post_thumbnail_html

Aby zrozumieć, co robi nasz filtr, powinniśmy zobaczyć, co on zwraca. Aby to osiągnąć i nie widzieć obrazka, ale czysty html, najlepiej to sobie zdebugować przy użyciu htmlentities:

<?php
/*
Plugin Name: Img src plug
Description: Opis Twojego pluginu
Version: 1.0
Author: Twój autor
*/
if( ! defined( 'ABSPATH') ){
    exit;
}

function add_thumbnail_link($html, $post_id, $post_image_id) {
    return htmlentities($html, ENT_QUOTES);
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Otrzymujemy – jako tekst – nasz tag HTML dla naszego thumbnaila. Możemy pokrasić się o tag <pre> aby to lepiej wyświetlić:

function add_thumbnail_link($html, $post_id, $post_image_id) {
    return '<pre>'.htmlentities($html, ENT_QUOTES).'</pre>';
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Jeżeli link naszego obrazka jest zbyt długi, możemy użyć jeszcze funkcji PHP o nazwie wordwrap:

function add_thumbnail_link($html, $post_id, $post_image_id) {
    return wordwrap('<pre>'.htmlentities($html, ENT_QUOTES).'</pre>', 20, "</br>");
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Teraz powinniśmy mieć całkiem ładnie i czytelnie wypisany nasz obrazek. Oczywiście musimy znajdować się na stronie pojedynczego wpisu, do którego jakiś obrazek jest przypisany. Widzimy z czym mamy do czynienia. W moim przypadku jest to coś mniej więcej takiego:

<img
width="1648"
height="736"
src="http://localhost/blocktry/wordpress/wp-content/uploads/2024/05/code.png"
class="attachment-post-thumbnail
size-post-thumbnail
wp-post-image"
alt=""
style="object-fit:cover;"
decoding="async"
fetchpriority="high"
(...)
/>

Teraz chcielibyśmy opleść nasz obrazek tagiem <a> z atrybutem href, którym jest link do naszego posta. Jako że nasza funkcja przyjmuje ID posta jako argument, możemy to łatwo osiągnąć, wrzucając ID w funkcję get_permalink.

function add_thumbnail_link($html, $post_id, $post_image_id) {
    $permalink = get_permalink($post_id);
    $html = '<a href="' . esc_url($permalink) . '">' . $html . '</a>';
    return $html;
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Pamiętamy o bezpieczeństwie – link przechodzi przez esc_url. Cały nasz html zostaje wrzucony wewnątrz tag <a> z odpowiednim atrybutem href – dzięki temu obrazek jest jednocześnie linkiem.

Na razie problem jest jeden – jeżeli obrazka nie ma w ogóle, i tak tworzymy pusty link. Możemy to rozwiązać sprawdzając, czy post o podanym ID w ogóle thumbnail posiada korzystając z funkcji has_post_thumbnail i odpowiednio dostosować nasz kod:

function add_thumbnail_link($html, $post_id, $post_image_id) {
    if(has_post_thumbnail($post_id)) {
        $permalink = get_permalink($post_id);
        $html = '<a href="' . esc_url($permalink) . '">' . $html . '</a>';
    }
    
    return $html;
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Teraz dodajmy jeszcze pustą informację o źródle obrazka. Stworzeniem tej funkcjonalności zajmiemy się w następnej sekcji:

function add_thumbnail_link($html, $post_id, $post_image_id) {
    if(has_post_thumbnail($post_id)) {
        $permalink = get_permalink($post_id);
        $html = '<a href="' . esc_url($permalink) . '">' . $html . '</a>';
        $html = $html . "<p>Źródło:</p>";
    }
    
    return $html;
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Oczywiście możemy naszą informację o źródle trzymać wewnątrz linku, ale moim zdaniem nie wygląda to dobrze i wprowadza użytkownika w błąd.

Źródło obrazka – tworzymy meta-box

Meta-box – czyli miejsce na wpisanie naszej informacji, która zostanie połączona z danym postem – możemy dodać albo w pliku pluginu ( img-source-plug.php) albo w functions.php w naszym szablonie. To już od nas zależy. Potrzebujemy dwóch fragmentów kodu. Pierwszy:

function add_image_source_metabox() {
    add_meta_box(
        'image_source_metabox',
        'Image Source',
        'render_image_source_metabox',
        'post', // Post type
        'side', // Context: 'normal', 'side', or 'advanced'
        'default' // Priority: 'high', 'core', 'default', or 'low'
    );
}
add_action( 'add_meta_boxes', 'add_image_source_metabox' );

function render_image_source_metabox( $post ) {
    // Retrieve the current value of the meta field
    $image_source = get_post_meta( $post->ID, 'image_source', true );
    ?>
    <label for="image_source">Image Source:</label>
    <input type="text" id="image_source" name="image_source" value="<?php echo esc_attr( $image_source ); ?>">
    <?php
}

Tutaj dodajemy metabox o nazwie image_source, o labelu (podpisie) „Image source”, dalej mamy funkcję, która ma nam wyrenderować (jej nazwę), typ posta (post, może być page dla stron albo jakiś customowy typ), argument side (chcemy mieć to po boku, po prawej stronie) oraz domyślny priorytet (występowania po tej prawej stronie, wyżej albo niżej).

Potem nasza funkcja renderująca. Jeżeli mamy już ten metabox wypełniony to jego wartość pokaże się w inpucie. Dalej już tylko potrzebujemy funkcję zapisującą wartość z tego inputu do meta-property podpiętą pod hook save_post:

function save_image_source_metabox( $post_id ) {
    if ( isset( $_POST['image_source'] ) ) {
        update_post_meta( $post_id, 'image_source', sanitize_text_field( $_POST['image_source'] ) );
    }
}
add_action( 'save_post', 'save_image_source_metabox' );

Możemy to albo zrozumieć, albo się tego nauczyć na pamięć tudzież razem z dokumentacją wykorzystywać te kody do tworzenia meta-boxów, zmieniając to i owo według własnych upodobań. Możemy na przykład zmienić Label z „Image source” na jakiś w naszym języku. Albo ustawić nasz meta-box z 'side’ na 'normal’ (wtedy będzie na dole).

Tak czy inaczej, mamy nasz meta-box. Teraz musimy jeszcze odczytać jego wartość i poprawnie podpisać obrazek w naszej funkcji:

function add_thumbnail_link($html, $post_id, $post_image_id) {
    if(has_post_thumbnail($post_id)) {
        $permalink = get_permalink($post_id);
        $html = '<a href="' . esc_url($permalink) . '">' . $html . '</a>';
        $src = get_post_meta( $post_id, 'image_source', true );
        $html = $html . "<p>Źródło: $src</p>";
    }
    
    return $html;
}
add_filter('post_thumbnail_html', 'add_thumbnail_link', 10, 3);

Dodajemy zabezpieczenia – nonce i inne

Teraz musimy dodać pewne zabezpieczenia do naszego metaboxa. Najpierw weźmy się za funkcję, która wyświetla nam pole, w którym możemy go edytować podczas dodawania wpisów. Musimy tam podać funkcję wp_nonce_field z jakąś nazwą. Tworzy ona coś na kształt tokenu CSRF:

function render_image_source_metabox( $post ) {
    // Retrieve the current value of the meta field
    $image_source = get_post_meta( $post->ID, 'image_source', true );
	wp_nonce_field('image_source_meta_box_nonce', 'image_source_meta_box_nonce');
    ?>
    <label for="image_source">Image Source:</label>
    <input type="text" id="image_source" name="image_source" value="<?php echo esc_attr( $image_source ); ?>">
    <?php
}

Teraz możemy zabrać się za funkcję zapisującą nasz metabox. Po pierwsze, sprawdzamy, czy w przesłanym przez metodę POST body znajduje się coś o nazwie 'image_source_meta_box_nonce’

if (!isset($_POST['image_source_meta_box_nonce'])) {
        return $post_id;
    }

Po drugie weryfikujemy wartość naszego tokena:

if (!wp_verify_nonce($_POST['image_source_meta_box_nonce'], 'image_source_meta_box_nonce')) {
        return $post_id;
    }

Po trzecie – obsługujemy przypadek, w którym WordPress sam robi autosave (zapis automatyczny):

if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return $post_id;
    }

Po czwarte – sprawdzamy, czy aby na pewno jesteśmy w typie 'post’ i czy użytkownik aby na pewno ma uprawnienia do edycji postów:

if ('post' == $_POST['post_type']) {
        if (!current_user_can('edit_post', $post_id)) {
            return $post_id;
        }
    }

Na końcu sprawdzamy, czy coś przesłano i oczyszczoną wersję tego zapisujemy:

if ( isset( $_POST['image_source'] ) ) {
        update_post_meta( $post_id, 'image_source', sanitize_text_field( $_POST['image_source'] ) );
    }

Cała nasza funkcja save wygląda teraz tak:

function save_image_source_metabox( $post_id ) {
	if (!isset($_POST['image_source_meta_box_nonce'])) {
        return $post_id;
    }

    // Verify that the nonce is valid.
    if (!wp_verify_nonce($_POST['image_source_meta_box_nonce'], 'image_source_meta_box_nonce')) {
        return $post_id;
    }

    // If this is an autosave, our form has not been submitted, so we don't want to do anything.
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return $post_id;
    }

    // Check the user's permissions.
    if ('post' == $_POST['post_type']) {
        if (!current_user_can('edit_post', $post_id)) {
            return $post_id;
        }
    }
    if ( isset( $_POST['image_source'] ) ) {
        update_post_meta( $post_id, 'image_source', sanitize_text_field( $_POST['image_source'] ) );
    }
}
add_action( 'save_post', 'save_image_source_metabox' );

Oczywiście teraz to działa tak, że jeżeli mamy fail (np. wejdziemy sobie w edycję meta-boxa, otworzymy Chrome Dev Tools i usuniemy pola hidden z tokenem) i spróbujemy zapisać to meta-box nie zostanie zapisany/zaktualizowany, ale żadnej informacji nie będzie. Możemy inaczej te nasze faile z weryfikacją nonce (tokena) obsłużyć, ale sposób który podałem po prostu nie wykona zapisu meta-boxa, nie przeszkadzając w niczym innym.

Domyślne źródło i domyślny obrazek – filtry i hooki

Możemy jeszcze do naszego pluginu dopisać funkcjonalność, która ustawia domyślny opis źródła obrazka. Niech będzie to nazwa strony, pozyskana z funkcji  get_bloginfo(’name’). Podepniemy się w tym celu z naszą akcją pod 'save_post’:

add_action( 'save_post', 'set_post_default_image_source', 10,3 );

function set_post_default_image_source( $post_id, $post, $update ) {
	
}

Jeżeli wykonywany jest update – nie chcemy nic robić:

add_action( 'save_post', 'set_post_default_image_source', 10,3 );

function set_post_default_image_source( $post_id, $post, $update ) {
	// Only want to set if this is a new post!
	if ( $update ){
		return;
	}
	
}

Jeżeli typ naszego posta to nie jest 'post’ (ale np. 'page’ albo customowy typ) – również nie chcemy nic robić:

add_action( 'save_post', 'set_post_default_image_source', 10,3 );

function set_post_default_image_source( $post_id, $post, $update ) {
	// Only want to set if this is a new post!
	if ( $update ){
		return;
	}
	
	// Only set for post_type = post!
	if ( 'post' !== $post->post_type ) {
		return;
	}
	
	
}

Wreszcie – próbujemy uzyskać wartość naszego meta-boxa i jeżeli jest pusta, ustawiamy na domyślną, jaką będzie nazwa naszej strony:

add_action( 'save_post', 'set_post_default_image_source', 10,3 );

function set_post_default_image_source( $post_id, $post, $update ) {
	// Only want to set if this is a new post!
	if ( $update ){
		return;
	}
	
	// Only set for post_type = post!
	if ( 'post' !== $post->post_type ) {
		return;
	}
	
	$src = get_post_meta( $post_id, 'image_source', true );
    if(empty($src)) {
        update_post_meta( $post_id, 'image_source', get_bloginfo('name') );
    }
	
}

Tym sposobem każdy nowy post będzie mieć domyślnie źródło wypełnione. I będzie nim nazwa naszej witryny. Teraz jeszcze możemy pobawić się w napisanie czegoś, co zawsze ustawi nam domyślny obrazek.

Po pierwsze, musimy ten domyślny obrazek gdzieś mieć, w mediach. Po drugie – musimy znać jego ID. Tutaj nie ma łatwych rozwiązań, WordPress ewoluuje, nie zawsze na lepsze i każde rozwiązanie, które znajdowałem do sprawdzania ID u mnie nie działało. Musiałem zainstalować plugin, który pokazywał mi ID każdego obrazka. W moim przypadku była to liczba 19.

Okej, mamy tę liczbę. Teraz pokazuję nasz filtr:

function my_filter_thumbnail_id( $thumbnail_id, $post = null ) {
    if ( ! $thumbnail_id ) {
            $thumbnail_id = 19; //id of default featured image
        }
    
        return  $thumbnail_id;
    }
    
 add_filter( 'post_thumbnail_id', 'my_filter_thumbnail_id', 20, 5 );

I tak to mniej-więcej wygląda.

Dodatek – piszemy własny inspektor ID

Jeżeli nie byliśmy w stanie znaleźć pluginu, który wyświetla nam ID obrazków – a mamy taki problem, że WordPress nie wyświetla – możemy sami sobie taki plugin napisać. Przechodzimy do folderu plugins wewnątrz wp-content w naszej instalacji WordPressa i wpisujemy komendę:

npx @wordpress/create-block@latest media-id-inspector --variant="dynamic"

Następnie przechodzimy tam komendą:

cd media-id-inspector 

Wewnątrz folderu src w folderze, który właśnie stworzyliśmy znajdujemy plik render.php:

<?php
/**
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
	BLOK DO TESTÓW - USUŃ TEN BLOK
</p>

Możemy bawić się w to, aby blok niczego nie wyświetlał albo aby w ogóle się nie zapisywał – jeżeli chcemy. W sumie mogliśmy stworzyć blok statyczny, którego funkcja save.js zwraca 'return null’. Umówmy się jednak – z tego elementu będziemy używać wewnątrz wpisów do debugowania. Nie potrzebujemy go zapisywać.

Dobra, to teraz przechodzimy do edit.js wykonać import:

import { useBlockProps, MediaPlaceholder } from '@wordpress/block-editor';

Jak już to mamy, możemy napisać nasz inspektor:

export default function Edit() {
	return (
		<div { ...useBlockProps() }>
			<MediaPlaceholder
			icon="format-image"
			labels={{
				title: 'Wybierz obraz',
				instructions: 'Przeciągnij i upuść obraz lub kliknij, aby wybrać z biblioteki mediów.',
			}}
			onSelect={(media) => console.log(media.id)}
			accept="image/*"
			allowedTypes={['image']}
			/>
		</div>
	);
}

Teraz tylko komenda:

npm run build

Teraz możemy włączyć nasz plugin, dodać element na stronie, wybrać obrazek i sprawdzić, jaki ma ID. ID zostanie pokazane w console.logu.