Blok Geliştirme: WordPress Gutenberg API Kullanımı
Birkaç yıl önce bir müşteri benden “şu blok editörüyle uyumlu bir ürün galeri bloğu yapabilir misin?” diye sorduğunda, klasik editör eklentisini önerip geçiştirmeyi düşündüm. Ama sonunda oturup Gutenberg API’sini öğrenmek zorunda kaldım. Ve iyi ki öyle yaptım, çünkü bu API düşündüğümden çok daha iyi tasarlanmış.
Gutenberg, aslında React tabanlı bir blok editörü. WordPress 5.0 ile core’a girdi ve o günden beri gelişmeye devam ediyor. Eklenti geliştiriyorsanız artık bu gerçekle yüzleşmek zorundasınız: Klasik editör gidecek, bloklar kalacak. Bu yazıda sıfırdan bir blok nasıl geliştirilir, Gutenberg JavaScript API’si nasıl kullanılır, özel blok özellikleri nasıl eklenir, bunları pratik kod örnekleriyle anlatacağım.
Geliştirme Ortamını Hazırlamak
Gutenberg blok geliştirme için @wordpress/scripts paketini kullanmak hayatı kolaylaştırıyor. Bu paket webpack, babel ve diğer araçları sizin için yapılandırıyor. Manuel webpack config yazmakla uğraşmak yerine buna güvenin.
Önce Node.js ve npm’in kurulu olduğundan emin olun. Ardından eklenti dizinini oluşturun:
mkdir wp-content/plugins/benim-bloklarim
cd wp-content/plugins/benim-bloklarim
npm init -y
npm install --save-dev @wordpress/scripts
package.json dosyasına şu script’leri ekleyin:
{
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"lint:js": "wp-scripts lint-js"
}
}
Geliştirme sırasında npm start komutunu çalıştırırsınız, bu dosyaları izler ve değişiklikte otomatik derler. Production için npm run build kullanırsınız.
Dizin yapısı şöyle olmalı:
- src/: JavaScript kaynak dosyaları
- src/index.js: Blok kayıt dosyası
- build/: Derlenmiş dosyalar (git’e eklemeyin)
- benim-bloklarim.php: Ana PHP dosyası
Ana PHP Dosyasını Oluşturmak
PHP tarafı blokları WordPress’e kayıt etmek ve script’leri enqueue etmek için gerekli. Basit ama doğru yapılması lazım:
<?php
/**
* Plugin Name: Benim Bloklarım
* Description: Özel Gutenberg blokları koleksiyonu
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
*/
defined( 'ABSPATH' ) || exit;
function benim_bloklarim_kaydet() {
$bloklar = array(
'benim-bloklarim/urun-karti',
'benim-bloklarim/tanitim-alani',
);
foreach ( $bloklar as $blok ) {
register_block_type( __DIR__ . '/build/' . explode( '/', $blok )[1] );
}
}
add_action( 'init', 'benim_bloklarim_kaydet' );
register_block_type fonksiyonu block.json dosyasını okuyarak bloğu kaydediyor. Bu modern yöntem, eski registerBlockType ile script handle’larını manuel yönetmekten çok daha temiz.
block.json: Bloğun Kalbi
Her blok için bir block.json dosyası oluşturmanız gerekiyor. Bu dosya bloğun metadata’sını, attribute’larını ve script bağımlılıklarını tanımlıyor:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "benim-bloklarim/urun-karti",
"version": "1.0.0",
"title": "Ürün Kartı",
"category": "media",
"icon": "format-image",
"description": "Ürünlerinizi şık kartlarla sergileyin",
"supports": {
"html": false,
"color": {
"background": true,
"text": true
},
"spacing": {
"padding": true,
"margin": true
}
},
"attributes": {
"baslik": {
"type": "string",
"default": "Ürün Başlığı"
},
"aciklama": {
"type": "string",
"default": ""
},
"fiyat": {
"type": "number",
"default": 0
},
"resimId": {
"type": "number"
},
"resimUrl": {
"type": "string",
"default": ""
},
"butonMetni": {
"type": "string",
"default": "Satın Al"
}
},
"editorScript": "file:../../build/index.js",
"editorStyle": "file:../../build/index.css",
"style": "file:../../build/style-index.css"
}
supports objesi çok önemli. Burada WordPress’in otomatik olarak sunacağı özellikleri seçiyorsunuz. color.background: true diyince WordPress editörde otomatik bir arka plan rengi seçici ekliyor, siz ekstra kod yazmak zorunda kalmıyorsunuz. Bu detayı atlayan çok kişi görüyorum.
İlk Bloğu Yazmak: index.js
Şimdi gerçek işe, JavaScript tarafına geçelim. src/index.js dosyası:
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button, PanelBody, TextControl, __experimentalNumberControl as NumberControl } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import metadata from './urun-karti/block.json';
registerBlockType( metadata.name, {
edit: EditComponent,
save: SaveComponent,
} );
function EditComponent( { attributes, setAttributes } ) {
const { baslik, aciklama, fiyat, resimId, resimUrl, butonMetni } = attributes;
const blockProps = useBlockProps( {
className: 'urun-karti-blok',
} );
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Fiyat Ayarları', 'benim-bloklarim' ) }>
<NumberControl
label={ __( 'Fiyat (TL)', 'benim-bloklarim' ) }
value={ fiyat }
onChange={ ( yeniFiyat ) =>
setAttributes( { fiyat: Number( yeniFiyat ) } )
}
min={ 0 }
/>
<TextControl
label={ __( 'Buton Metni', 'benim-bloklarim' ) }
value={ butonMetni }
onChange={ ( yeniMetin ) =>
setAttributes( { butonMetni: yeniMetin } )
}
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<MediaUploadCheck>
<MediaUpload
onSelect={ ( medya ) =>
setAttributes( {
resimId: medya.id,
resimUrl: medya.url,
} )
}
allowedTypes={ [ 'image' ] }
value={ resimId }
render={ ( { open } ) => (
<Button onClick={ open } variant="secondary">
{ resimUrl
? __( 'Resmi Değiştir', 'benim-bloklarim' )
: __( 'Resim Ekle', 'benim-bloklarim' ) }
</Button>
) }
/>
</MediaUploadCheck>
{ resimUrl && (
<img src={ resimUrl } alt={ baslik } />
) }
<RichText
tagName="h3"
value={ baslik }
onChange={ ( yeniBaslik ) =>
setAttributes( { baslik: yeniBaslik } )
}
placeholder={ __( 'Ürün başlığı yazın...', 'benim-bloklarim' ) }
/>
<RichText
tagName="p"
value={ aciklama }
onChange={ ( yeniAciklama ) =>
setAttributes( { aciklama: yeniAciklama } )
}
placeholder={ __( 'Ürün açıklaması...', 'benim-bloklarim' ) }
/>
<div className="urun-karti-fiyat">
{ fiyat > 0 && <span>{ fiyat } TL</span> }
</div>
</div>
</>
);
}
function SaveComponent( { attributes } ) {
const { baslik, aciklama, fiyat, resimUrl, butonMetni } = attributes;
const blockProps = useBlockProps.save( {
className: 'urun-karti-blok',
} );
return (
<div { ...blockProps }>
{ resimUrl && <img src={ resimUrl } alt={ baslik } /> }
<RichText.Content tagName="h3" value={ baslik } />
<RichText.Content tagName="p" value={ aciklama } />
{ fiyat > 0 && (
<div className="urun-karti-fiyat">
<span>{ fiyat } TL</span>
</div>
) }
<a href="#" className="urun-karti-buton">{ butonMetni }</a>
</div>
);
}
Burada dikkat edilmesi gereken kritik bir nokta var: edit fonksiyonu editörde görünendir, save fonksiyonu veritabanına kaydedilen ve frontend’de gösterilen HTML’i üretir. Bu ikisinin çıktıları tutarsız olursa WordPress “Block Validation Error” hatası verir ve eski içeriği gösterir. Bu hatayı ilk yaşadığımda saatlerce kafa yormuştum.
Dinamik Bloklar: PHP ile Render
Bazı durumlarda bloğun çıktısının her sayfada taze veriden üretilmesi gerekir. Örneğin WooCommerce’den anlık fiyat çekmek istiyorsunuz. Bu durumda save fonksiyonu null döner ve render PHP’ye devredilir:
// PHP tarafında dinamik render fonksiyonu
function render_urun_karti_blok( $attributes, $content, $block ) {
$baslik = esc_html( $attributes['baslik'] ?? '' );
$resim_url = esc_url( $attributes['resimUrl'] ?? '' );
$fiyat = floatval( $attributes['fiyat'] ?? 0 );
$buton_metni = esc_html( $attributes['butonMetni'] ?? 'Satın Al' );
// WooCommerce'den anlık veri çekiyoruz diyelim
if ( function_exists( 'wc_price' ) && $fiyat > 0 ) {
$fiyat_html = wc_price( $fiyat );
} else {
$fiyat_html = $fiyat > 0 ? '<span>' . $fiyat . ' TL</span>' : '';
}
$wrapper_attributes = get_block_wrapper_attributes( array(
'class' => 'urun-karti-blok',
) );
ob_start();
?>
<div <?php echo $wrapper_attributes; ?>>
<?php if ( $resim_url ) : ?>
<img src="<?php echo $resim_url; ?>" alt="<?php echo $baslik; ?>" loading="lazy" />
<?php endif; ?>
<h3><?php echo $baslik; ?></h3>
<div class="urun-karti-fiyat"><?php echo $fiyat_html; ?></div>
<a href="#" class="urun-karti-buton"><?php echo $buton_metni; ?></a>
</div>
<?php
return ob_get_clean();
}
block.json dosyasına da render callback’i belirtmeniz gerekiyor:
{
"render": "file:./render.php"
}
get_block_wrapper_attributes() fonksiyonunu asla atlamayın. Bu fonksiyon supports üzerinden gelen renk, spacing gibi inline style’ları ve CSS class’larını otomatik ekliyor. Atladığınızda editörde ayarladığınız renkler frontend’de görünmüyor, müşteri sizi arayıp “neden çalışmıyor” diyor.
useSelect ve useDispatch ile Veri Yönetimi
Gutenberg’in data layer’ı Redux benzeri çalışıyor. Blok içinden WordPress’in mevcut verilerine erişmek için useSelect hook’unu kullanıyorsunuz. Mesela mevcut yazarın bilgilerini çeken bir blok:
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useBlockProps } from '@wordpress/block-editor';
function YazarKutusuEdit( { attributes } ) {
const blockProps = useBlockProps();
const { yazar, yukleniyor } = useSelect( ( select ) => {
const yazarId = select( 'core/editor' ).getCurrentPostAttribute( 'author' );
const yazarVerisi = select( coreStore ).getEntityRecord(
'root',
'user',
yazarId
);
return {
yazar: yazarVerisi,
yukleniyor: ! select( coreStore ).hasFinishedResolution(
'getEntityRecord',
[ 'root', 'user', yazarId ]
),
};
}, [] );
if ( yukleniyor ) {
return (
<div { ...blockProps }>
<p>Yazar bilgisi yükleniyor...</p>
</div>
);
}
return (
<div { ...blockProps }>
{ yazar && (
<>
<img
src={ yazar.avatar_urls?.[ 96 ] }
alt={ yazar.name }
className="yazar-avatar"
/>
<div className="yazar-bilgi">
<strong>{ yazar.name }</strong>
<p>{ yazar.description }</p>
</div>
</>
) }
</div>
);
}
Bu pattern’i bir haber sitesi için kullanmıştım. Her makalenin altında yazarın biyografisi ve fotoğrafı otomatik çekiliyor. useSelect içinde bağımlılık array’ini doğru yönetmezseniz gereksiz re-render’lara yol açıyorsunuz, performans sorunları çıkıyor.
Block Variations ve Stil Seçenekleri
Tek bir blok tanımından birden fazla varyasyon yaratabilirsiniz. Bu özellik çok işe yarıyor. Örneğin “ürün kartı” bloğunun bir de “öne çıkan ürün” varyasyonu olsun:
import { registerBlockVariation, registerBlockStyle } from '@wordpress/blocks';
// Blok varyasyonu: Öne Çıkan Ürün
registerBlockVariation( 'benim-bloklarim/urun-karti', {
name: 'one-cikan-urun',
title: 'Öne Çıkan Ürün',
description: 'Büyük görselli, öne çıkan ürün kartı',
attributes: {
butonMetni: 'Hemen Satın Al',
fiyat: 0,
},
isActive: ( blockAttributes, variationAttributes ) => {
return blockAttributes.butonMetni === variationAttributes.butonMetni;
},
icon: 'star-filled',
} );
// Blok stili: Koyu Tema
registerBlockStyle( 'benim-bloklarim/urun-karti', {
name: 'koyu-tema',
label: 'Koyu Tema',
} );
registerBlockStyle( 'benim-bloklarim/urun-karti', {
name: 'minimal',
label: 'Minimal',
isDefault: false,
} );
registerBlockStyle çağrısı bloğun toolbar’ına stil seçenekleri ekliyor. Seçilen stil is-style-koyu-tema gibi bir CSS class olarak bloğa ekleniyor. Sonrasında bu class’a göre stilinizi yazıyorsunuz. Müşteriye “şu bloğu seç, stilini değiştir” demek, ayrı bloklar geliştirmekten çok daha pratik.
Deprecations: Blok Güncellemelerini Yönetmek
Bu konuyu atlayan çok geliştirici var ve ileride büyük baş ağrısına yol açıyor. Blok çıktısını değiştirdiğinizde eski içeriklerde “validation error” hatası çıkar. Bunu deprecated array’iyle yönetiyorsunuz:
registerBlockType( metadata.name, {
edit: EditComponent,
save: SaveComponent,
deprecated: [
{
// v1.0 - Fiyat alanı yoktu
attributes: {
baslik: {
type: 'string',
default: 'Ürün Başlığı',
},
aciklama: {
type: 'string',
default: '',
},
resimUrl: {
type: 'string',
default: '',
},
},
save: function( { attributes } ) {
const { baslik, aciklama, resimUrl } = attributes;
return (
<div className="urun-karti-blok">
{ resimUrl && <img src={ resimUrl } alt={ baslik } /> }
<h3>{ baslik }</h3>
<p>{ aciklama }</p>
</div>
);
},
migrate: function( attributes ) {
return {
...attributes,
fiyat: 0,
butonMetni: 'Satın Al',
};
},
},
],
} );
migrate fonksiyonu eski attribute’ları yeni formata dönüştürüyor. Böylece eski içerikler otomatik güncelleniyor, editörde hata görünmüyor. Production’da bu olmadan güncelleme yapmayın.
WordPress CLI ile Blok Scaffolding
Geliştirme sürecini hızlandırmak için WP-CLI’ın blok scaffold komutunu kullanabilirsiniz. Bu komut tüm dosya yapısını otomatik oluşturuyor:
# WP-CLI ile blok scaffold
wp scaffold block yorum-blogu
--title="Yorum Bloğu"
--category=widgets
--namespace=benim-bloklarim
--plugin=benim-bloklarim
# Ya da @wordpress/create-block kullanabilirsiniz
npx @wordpress/create-block@latest tanitim-alani
--namespace=benim-bloklarim
--title="Tanıtım Alanı"
--category=design
--variant=dynamic
--variant=dynamic seçeneği otomatik olarak PHP render dosyası oluşturuyor. İlk blok geliştirirken bu scaffold’ı inceleyerek başlamak çok işe yarıyor, neyin nerede olduğunu anlıyorsunuz.
Performans ve Hata Ayıklama
Geliştirme sırasında tarayıcı konsolunuzu açık tutun. WordPress editör hataların büyük çoğunluğunu oraya yazıyor. Birkaç önemli nokta:
- Block validation hatası:
savefonksiyonunun çıktısı veritabanındaki HTML ile eşleşmiyor.deprecatedekleyin veyamigrateyazın. - useSelect sonsuz döngüsü: Bağımlılık array’ini boş bırakmayı unutmayın:
useSelect( selector, [] ). - Script enqueue sırası:
block.jsonkullandığınızda WordPress bağımlılıkları otomatik çözüyor ama eklenti script’leriniz için hala dikkatli olun. - Build yapmayı unutmak:
npm startçalıştırıp development modda bırakıp production’a çıkmak klasik hata. Her zamannpm run buildile kayıt altına alın.
İzole test için geliştirme ortamında şu komutu kullanın:
# Block validation'ı console'da test etmek
# wp-env ile izole WordPress ortamı kurabilirsiniz
npx @wordpress/env start
# Hata loglarını takip etmek için
npx @wordpress/env run wordpress tail -f /var/log/apache2/error.log
@wordpress/env paketi Docker üzerinde tam izole bir WordPress ortamı kuruyor. Müşteri sitesini bozmadan test etmek için ideal.
Sonuç
Gutenberg API’si ilk bakışta karmaşık görünüyor ama bir kez pattern’leri kavrayınca son derece tutarlı bir yapısı olduğunu anlıyorsunuz. block.json ile metadata, edit ile editör deneyimi, save veya PHP render ile frontend çıktısı. Bu üçlü mantığı oturdurduğunuzda geri kalanı yerine oturuyor.
En çok vurgu yapmak istediğim noktalar şunlar: deprecated olmadan blok güncellemesi yapmayın, get_block_wrapper_attributes() fonksiyonunu asla atlayın, dinamik veri gereken her yerde save: () => null ile PHP render’a gidin. Klasik editör uyumluluğu için enerji harcamak yerine bu API’yi öğrenmeye yatırım yapın, çünkü WordPress bu yönde gitmeye devam edecek.
Bir sonraki yazıda InnerBlocks API’sini ve nested block yapılarını ele alacağım. O konu biraz daha karmaşık ama çok güçlü özellikler sunuyor.
