Blog

💬 'Gutenberg ve Ayrık Uygulamalar' için Yeni Bir Yaklaşım Önerisi

Leonardo Losoviz
Yazan: Leonardo Losoviz ·

Birkaç gün önce, WPGraphQL'in yaratıcısı Jason Bahl, Gutenberg and Decoupled Applications başlıklı yazısını yayımladı; bu yazıda GraphQL'i Gutenberg ile entegre etmek için 3 yaklaşımın avantajlarını ve eksikliklerini analiz etti.

Bir hafta önce ise Twitter'da şunu söylemişti: Gato GraphQL'in Gutenberg'i modelleme yaklaşımı uygun değildir:

Bence bu, övünülecek bir şey değil. GraphQL'in Tipli Şema ile çözmeye çalıştığı şeylerden biri, istemcilere öngörülebilirlik ve tutarlılık sağlamak, onlara alan düzeyine kadar istediklerini sorma kontrolü vermektir.

Öngörülebilir bir şekli olmayan genel bir "Object" Tipi döndürmek, sunucu ile istemci arasında artık bir sözleşme olmadığı için istemci uygulamalarının her an bozulabileceği anlamına gelir. Sunucu artık istemciden kontrolü almış olur.

Bu makale aracılığıyla ben de bu tartışmaya katılıyorum. Jason'ın eleştirisini ele alacak ve bunu yaparken eklentimin yaklaşımını açıklayacak, neden Gutenberg için gerçekten uygun olduğuna inandığımı göstereceğim.

Gutenberg Metadata'sını Çıkarmak için COPE Kullanımı

Benim çözümüm 4. yaklaşım olarak değerlendirilebilir ve şu şekildedir:

GraphQL'e güç sağlamak için Gutenberg verilerini elde etmek amacıyla PHP tarafında ek bir şema oluşturmayın veya mevcut verileri çoğaltmayın. Bunun yerine, COPE ("Create Once, Publish Everywhere") stratejisini kullanarak verileri blokların depolanmış içeriğinden çıkarın.

(COPE, içeriğin tek bir gerçeklik kaynağına sahip olmasını ve farklı uygulamalara sunulmasını sağlayan bir stratejidir. Bizim durumumuzda tek gerçeklik kaynağı, veritabanında depolandığı haliyle Gutenberg blok verileridir. COPE'u ve WordPress için uygulamasını bu makalede açıkladım.)

Son olarak, tüm blokları tek bir Block tipine eşleyerek herhangi bir Gutenberg bloğu için çıkarılan verileri almak amacıyla GraphQL kullanabiliriz.

Bu Strateji Bir Uzlaşıdır, Kesin Bir Çözüm Değil

Bu strateji, Jason'ın işaret ettiği sorunu çözmez: sunucu ile istemci arasında bir sözleşme oluşturmaya olanak tanıyacak sunucu tarafı şemasının eksikliği.

COPE bu sorunu çözemez çünkü yalnızca depolanmış içerikten şemayı yeniden oluşturamayız:

  • Depolanmış içerik, alanın tipini belirtmez
  • Depolanmış içerik, alanın hangi kısıtlamalara sahip olduğunu belirtmez (null olabilir mi? pozitif bir tam sayı mıdır? string bir e-posta mı yoksa URL mi içindir?)
  • Null olabilen alanlarda depolanmış içerikte bulunmayacak olan bir varsayılan değer olabilir

Ancak COPE stratejisini ve tüm blokları temsil etmek için tek bir Block tipini kullanarak, Gato GraphQL mevcut sınırlamaları aşan oldukça iyi bir Gutenberg entegrasyonu oluşturabilir.

Bunu bu makale boyunca açıklayacağım.

Gato GraphQL'in Gutenberg ile Entegrasyonu

Bu çözüm hâlâ geliştirilmekte, ancak nasıl davranacağını şimdiden açıklayabilirim.

Blok başına farklı bir tipe bağlı kalmak yerine (WPGraphQL'in WPGraphQL for Gutenberg eklentisine dayanırken yaptığı gibi), Gato GraphQL tüm blokları temsil etmek için tek bir Block tipi sağlayacak.

Bu query'de, Post.blockDataItems alanı, gönderiden Block öğelerinin bir listesini alır (paragraflar, resimler, listeler ve diğerleri dahil farklı Gutenberg blokları için):

{
  post(by: { id: 1499 }) {
    title
    blockDataItems
  }
}

Belirli bir bloğun verilerini almak istiyorsak, bloğun adına göre filtreleme yapabiliriz (core/paragraph, core/quote, vb.).

Bu query'de yalnızca resim bloklarını alıyoruz:

{
  post(by: { id: 1177 }) {
    title
    blockDataItems(
      filterBy: { include: "core/image" }
    )
  }
}

Tek Block Tipinin İncelenmesi

Bu yaklaşımda yanıt, bir şemaya değil depolanmış içeriğe göre değişebilir. Bu nitelik hem avantajı (API'yi esnek kılar) hem de dezavantajıdır (sunucu-istemci sözleşmelerini zorunlu kılamayız).

Her Block öğesi iki özellik içerir:

  • name: Bloğun adı (core/paragraph, core/quote, vb.)
  • meta: Blokta yer alan metadata

Her Gutenberg bloğu farklıdır; farklı veriler içerir (paragraf içeriği, Youtube videosu, resim kaynak URL'si ve boyutları vb.). Bu nedenle meta alanı için yanıtta yer alan veriler de farklı olacaktır.

Bu nedenle meta alanı, GraphQL şemasında ilgili bir JSONObject tipi aracılığıyla basitçe bir JSON nesnesi olarak eşlenmiştir ("ham" veriler içerebilir).

Şu yanıtı üretir:

{
  "data": {
    "post": {
      "title": "COPE with WordPress: Post demo containing plenty of blocks",
      "blockDataItems": [
        {
          "name": "core/paragraph",
          "attributes": {
            "content": "Lorem ipsum dolor sit amet"
          }
        },
        {
          "name": "core/image",
          "attributes": {
            "src": "https://ps.w.org/gutenberg/assets/banner-1544x500.jpg"
          }
        },
        {
          "name": "core/quote",
          "attributes": {
            "quote": "Etiam tempor orci eu lobortis elementum nibh tellus molestie",
            "cite": "Aristoteles"
          }
        },
        {
          "name": "core/heading",
          "attributes": {
            "size": "xl",
            "heading": "Welcome to my site"
          }
        },
        {
          "name": "core/list",
          "attributes": {
            "items": [
              "First element",
              "Second element",
              "Third element"
            ]
          }
        },
      ]
    }
  }
}

Gördüğümüz gibi, farklı bloklar farklı özellikler almaktadır:

  • core/paragraph, content özelliğine sahiptir
  • core/image, src özelliğine ve isteğe bağlı olarak width, height ve caption özelliklerine sahiptir (yukarıdaki yanıtta görünmüyor)
  • core/quote, quote ve cite (alıntı yapılan kişi için) özelliklerine sahiptir
  • core/heading, header ve size özelliklerine sahiptir (xl değeri <h2> temsil eder, çünkü COPE değeri hedef uygulamadan, bu durumda bir web sitesinden ayırır)
  • core/list, bir öğeler listesi olan items özelliğine sahiptir

JSONObject Tipinin Neden Spesifikasyonun Parçası Olmadığı

Yukarıda açıkladığım JSONObject tipi, GraphQL'in "dinamik" alanları (bilmediğimiz alanlar gibi) veya birden fazla yapılandırmaya sahip olabilen alanları (Gutenberg bloklarında olduğu gibi) almasına olanak tanır.

Şimdi, GraphQL spesifikasyonu şu anda JSONObject veya Map tiplerini desteklememektedir. Destek eklenmesi talep edilmiştir, şunlar gibi nedenlerle](https://github.com/graphql/graphql-spec/issues/101#issuecomment-386836767):

[...] bu özelliğin eksikliği, özellikle sorunludur çünkü GraphQL'in arayüz kurduğu tip sistemlerinin ve hizmetlerin çoğunda desteklenmektedir.

Bu, sunucumun bir Map gönderdiği ve istemcimin bir Map istediği ve GraphQL'in ortada Map desteği olmadan durduğu durumlarla başa çıkmak için sunucuda özel çözümleyiciler uygulamaya, ardından istemcide özel dönüşümler yapmaya yol açar. Evet, bu mümkündür ve ben yaptım, ancak API spesifikasyonunu GraphQL ile yazmak amacını yenen oldukça fazla standart kod ve soyutlamadır.

Bu özellik, dinamik alanları ele almanın GraphQL'in güçlü tipleme davranışına aykırı olması nedeniyle spesifikasyon tarafından desteklenmemektedir; bu durum sunucu ile istemci arasındaki sözleşmeyi bozar.

Yine de bu tip, daha sonra göstereceğim gibi Gutenberg için faydalı olabilir.

Blok Başına Farklı Tip Kullanımında ve Sunucu Tarafı Kayıt Defterinde Sorunlar

Blok başına yeni bir GraphQL tipi oluşturulursa, tüm eklentilerin bloklarının GraphQL şemasına eklenmesi gerekir. Bu, tüm blokların özelliklerini önerilen yeni sunucu tarafı kayıt defterinde tanımlamasıyla otomatik olarak gerçekleştirilebilir.

Bunu yapmazlarsa, blokları API için kullanılamaz hale gelir ve bu ek sonuçlar doğurabilir. Bazı durumlarda, sorgulanan gönderinin tüm içeriği güvenilmez hale gelebilir.

Bu durum, GraphQL'in bir gönderideki tüm bloklara bir işlev uygulayan harici bir bulut tabanlı hizmetle etkileşime girdiği durumlarda söz konusu olabilir (çeviri, dilbilgisi düzeltme, SEO önerileri, analitik vb. düşünün).

Buna bir örnek görelim.

Çok dilli yetenekler Gutenberg'e 4. aşamada ekleneceğinden, @strTranslate direktifi aracılığıyla yürütülen Google Translate API çağrısıyla eklentideki tüm blokların nasıl çevrileceğini modelleyelim.

(Bu API tabanlı ilk çeviri işleminin ardından kullanıcı, blog gönderisini her zaman WordPress editörü içinde çevrilmiş dilde düzenlemeye devam edebilir.)

Farklı bloklar, çevrilmesi gereken farklı bilgiler içerir:

  • core/paragraph: metin
  • core/image: açıklama yazısı
  • core/quote: alıntı ve alıntı yapılan kişi (kişinin unvanı olabileceğinden, örneğin "Okul müdürü")
  • core/heading: başlık
  • core/list: listedeki tüm öğeler

Blok başına farklı bir tip kullanıldığında, elde edilen query şöyle bir şey olabilir:

{
  post(by: { id: 1 }) {
    blocks {
      ... on CoreParagraphBlock {
        content @strTranslate
      }
      ... on CoreImageBlock {
        caption @strTranslate
      }
      ... on CoreQuoteBlock {
        quote @strTranslate
        cite @strTranslate
      }
      ... on CoreHeadingBlock {
        heading @strTranslate
      }
      ... on CoreListBlock {
        items @strTranslateList
      }
      ... on EmbedTwitterBlock {
        caption @strTranslate
      }
      ... on EmbedYoutubeBlock {
        caption @strTranslate
      }
      ... on EmbedVimeoBlock {
        caption @strTranslate
      }
    }
  }
}

Ve böyle devam eder. Ne kadar çok bloğumuz varsa, bu query o kadar uzun olur; kolayca yüz satırı ve daha fazlasını kapsayabilir.

Açık sorun, query'nin bakımını yapmamız gereken vahşi bir canavara dönüşmesidir.

Ayrıca her blok için çalışmasını sağlamak amacıyla özel işlevsellik eklememiz gerekir. Örneğin @strTranslate, bir dize listesi döndüren CoreListBlock.items ile çalışmaz (yani [String] döndürür, direktif ise String bekler), bu nedenle @strTranslateList oluşturmamız gerekir.

Ve sonra core/table kendi özel direktifine ihtiyaç duyacaktır (@strTranslateTable?).

Ve özel üçüncü taraf blokların kendi özel direktiflerine ihtiyacı olabilir.

Ve sonra birkaç sorun daha görüyorum.

Ya hep ya hiç

Bir blog gönderisi, WordPress editörüne yüklü herhangi bir blok içerebilir. Ve query'yi kodlarken hangi blokları gönderinin kullandığını önceden bilmiyoruz.

O zaman, blok başına bir tip olduğunda, query'de ele alınması gereken tip sayısı gönderideki blok sayısına eşdeğer olmayacaktır. Bunun yerine, WordPress editörüne yüklü blok sayısına eşdeğer olacaktır.

Hem WordPress çekirdeğinden hem de eklentilerden 100 bloğumuz varsa ne olur? O zaman GraphQL şemasına eşlenmiş 100 tipimiz olması gerekir. Eşlenmemiş tek bir blok, bazı blokların İngilizce'den Fransızca'ya çevrilmesine, diğerlerinin ise İngilizce kalmaya devam etmesine yol açarak "içerik sözleşmesini" bozabilir.

Sonuç olarak, sorunlu bloğu içerip içermediklerine bakılmaksızın çevrilmiş gönderilere artık güvenemeyeceğiz. Dolayısıyla tüm bloklar kayıt defterine eklenmezse uygulama güvenilmez hale gelebilir.

Yeni bir blok yüklendiğinde query güncellenmelidir

Benzer şekilde, her bloğun GraphQL query'sinde ele alınması gerekir. Bu, yeni bir blok yüklediğimizde uygulamamızın koduna gitmemiz, onu güncellememiz ve yeniden dağıtmamız gerektiği anlamına gelir.

Bu yalnızca ekstra bürokratik işlemler değildir: Tüm queries güncellenmeden canlı bir siteye blok yükleyemeyeceğiz; aksi takdirde uygulamayı bozma korkusu yaşarız.

GraphQL WordPress'e Hizmet Etmeli, Tersi Değil

JSONObject'in GraphQL spesifikasyonuna neden eklenmediğini tekrar düşündüğümüzde, bunun GraphQL'in yapma biçimiyle uyuşmaması olduğunu görürüz.

Ancak burada gerçekten GraphQL ile ilgilenmiyoruz. Yalnızca WordPress'i ve bu özel durumda Gutenberg'i önemsiyoruz.

GraphQL'i Gutenberg ile entegre ederken, GraphQL WordPress bağlamında çalışacaktır. Bu, WordPress'in GraphQL gereksinimlerini karşılaması gerektiği anlamına gelir. Ama daha da önemlisi, GraphQL'in WordPress gereksinimlerini karşılaması gerekmektedir.

Ve çatışma durumunda, WordPress önceliklidir.

Bir özellik GraphQL'e uymuyorsa, ancak yine de Gutenberg'e uyuyorsa, bu dikkate alınmalı mıdır?

Bence alınmalıdır.

Tek bir Block tipinin Gutenberg'e nasıl daha iyi hizmet edebileceğini görelim.

Tek Bir Block Tipi Aracılığıyla Önceki Sorunları Çözmek

Önceki örneği takiben, tek bir Block tipi kullanarak bir gönderideki tüm blokları İngilizce'den Fransızca'ya çevirmek şu şekilde yapılacak (ya da bu kavramla ilgili bir şey):

{
  post(by: { id: 1 }) {
    blocks {
      name
      meta
        @advancePointersInArray(paths: "{{ translatablePaths }}")
          @underEachArrayItem
            @strTranslate(from: "en", to: "fr")
    }
  }
}

Bu kadar mı? Tüm query? Tüm blokları çevirmek için? Evet.

Hem çekirdekten hem de eklentilerden tüm bloklar için, mevcut veya henüz oluşturulmamış olanlar için çalışacak mı? Evet.

Bu query size biraz garip görünüyor mu? Öyleyse, bu yalnızca Gato GraphQL tarafından desteklenen standart dışı GraphQL özelliklerini kullandığı içindir:

  • {{ translatablePaths }}, bir alanın değerini başka bir alana veya direktife argüman olarak girmek için gömülebilir bir alandır (bu durumda, Block tipinin translatableFields adlı bir alanı olacak ve değeri @advancePointersInArray direktifine enjekte edilecektir)
  • direktifler diğer direktifler tarafından oluşturulabilir

Şimdi, bir özellik tam olarak CMS'nin ihtiyaç duyduğu şeyi karşılıyorsa, ancak standart dışıysa, yine de kullanmalı mıyız? Bence kullanmalıyız.

Bu özellikleri GraphQL spesifikasyonu için de talep ettim (kabul edilmeyecekler olsa da):

Tek Block Tipinin Nasıl Çalıştığı

Uyarı: teknik bölüm geliyor.

Block tipinin, çevrilmesi gereken JSONObject'teki özelliklerin bir dizisini döndüren translatablePaths adlı bir alanı olacak:

  • core/paragraph, ["content"] döndürür
  • core/image, ["caption"] döndürür
  • core/quote, ["quote", "cite"] döndürür
  • core/heading, ["header"] döndürür
  • core/list, ["items.0", "items.1", "items.2", ...] döndürür

@advancePointersInArray, bir meta-direktiftir: sonraki direktif için bağlamı değiştirir. Sonraki direktifin sorgulanan JSONObject içinden bir alt öğe almasını sağlar; örneğin paragraf bloğundaki content özelliği gibi. Yolların listesi, aynı sorgulanan varlık üzerinde değerlendirilen translatablePaths alanı aracılığıyla elde edilir.

Ardından, @underEachArrayItem başka bir meta-direktiftir; sorgulanan varlıktan öğelerin bir listesi üzerinde yineler ve yinelenen öğeye bir referansı sonraki direktife iletir. Bu durumda, her biri String tipinde olan tüm varlıklar için çevrilecek özellikler listesini alır ve tek tek String öğelerini aşağıya iletir.

Son olarak, @strTranslate direktifi, JSONObject içinde yer alan String tipinde bir öğe alır ve onu doğrudan JSONObject'in içinde çevirir.

Bu çözümün ne kadar esnek olduğuna dikkat edin. JSONObject içindeki dizeye giden yolu sağlamak, değere erişmek, onu @strTranslate (veya başka herhangi bir direktif) ile değiştirmek ve hatta muhtemelen değeri yeniden veritabanına kaydetmek için yeterlidir (bunu gerçekleştirme çalışması şu anda devam etmektedir).

core/list için zaten çalışmaktadır; listeki tüm öğelere kendi yolları altında ulaşılabilir (items.0 dizideki 1. öğedir vb.). Ardından her birinden String değerine erişebilir ve @strTranslate'e iletebilir, böylece @strTranslateList oluşturmaya gerek kalmaz.

Benzer şekilde, core/table ile de çalışacaktır. Sadece verileri cells özelliği aracılığıyla açığa çıkarmamız gerekir; bu, 2 boyutlu bir dizi olacaktır (sütunlar için bir tane içeren satırlar için bir tane). Ardından translatablePaths, tüm öğelere ["cells.0.0", "cells.0.1", "cells.1.0", ...] şeklinde ulaşabilir.

Ve herhangi bir üçüncü taraf blok için de çalışacaktır. Bunun için blok verilerinin nasıl depolandığına dikkat etmemiz ve oradan özelliklerine giden yolu çıkarmamız gerekir.

Tek Bir Block, PHP Koduna Dayalı Yapılandırma Gerektirir

Blokları eşlemek, metadata özelliklerini nerede bulacağımızı bilmek amacıyla yapılandırma yoluyla gerçekleştirilebilir. Böylece bununla çok esnek bir şekilde başa çıkabiliriz.

Gutenberg'de bir bloktaki özelliğin depolanabileceği iki yer vardır: bir özellik olarak veya işlenmiş içerik içinde.

Örneğin, core/image bloğu şu şekilde depolanır:

<!-- wp:image {"id":1670,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large">
<img src="https://newapi.getpop.org/wp/wp-content/uploads/2021/01/dynamic-include-first-query.webp" alt="" class="wp-image-1670"/>
</figure>
<!-- /wp:image -->

Bu durumda:

  1. id, sizeSlug ve linkDestination özellikleri özellik olarak depolanır
  2. src özelliği işlenmiş içerik içinde depolanır

Şimdi, API sorgulandığında, core/image bloğu için yanıt şu şekilde olacaktır:

{
  "data": {
    "blocks": [
      {
        "name": "core/image",
        "meta": {
          "id": 1670,
          "sizeSlug": "large",
          "linkDestination": "none",
          "src": "https://newapi.getpop.org/wp/wp-content/uploads/2021/01/dynamic-include-first-query.webp"
        }
      }
    ]
  }
}

API, Gutenberg'de depolanan bloğu ayrıştırarak özellikleri nasıl alacağını bilir (bu COPE stratejisidir). Bu işlem belirli bir dereceye kadar otomatik olarak yapılabilir ve ardından hook'lar veya bir kullanıcı arayüzü aracılığıyla biraz manuel girdi gerektirir.

Özellik olarak doğrudan eşlenmiş özellikleri elde etmek önemsizdir. GraphQL sunucusu zaten bloktan tüm özellikleri alabilir ve bunları özellik olarak kullanılabilir kılabilir. Ya da hangilerini açığa çıkaracağımızı açıkça tanımlamak istiyorsak, bunu filtre hook'ları aracılığıyla yapabiliriz:

$attrs = apply_filters("blockPropsAsAttr:core/image", []);
 
add_filter("blockPropsAsAttr:core/image", function ($attrs) {
  return array_merge($attrs, ['id', 'sizeSlug', 'linkDestination']);
})

İçerikte depolanan özellikler bir regex ile çıkarılabilir:

$propRegexes = apply_filters("blockPropsAsRegex:core/image", []);
 
add_filter("blockPropsAsRegex:core/image", function ($propRegexes) {
  $propRegexes['src'] = '/<img src="(.*?)"/';
  return $propRegexes;
})

Son olarak, @strTranslate'in üzerinde çalışacağı bloğun çevrilebilir özelliklerini belirtiyoruz:

$propRegexes = apply_filters("translatableProperties:core/image", []);
 
add_filter("translatableProperties:core/image", function ($properties) {
  $properties[] = 'caption';
  return $properties;
})

Şimdi, bu özellikler yine de birisi tarafından, büyük olasılıkla eklenti geliştiricisi tarafından karşılanmalıdır. Bu nedenle, sunucu tarafı kayıt defterine sahip olmak bu hedefe ulaşmaya yardımcı olacaktır.

Peki ya WordPress topluluğu önerilen sunucu tarafı kayıt defterini eklemek istemezse? Bu strateji kolayca uyum sağlayabilir, çünkü eşleme az önce gösterildiği gibi PHP kodu aracılığıyla yapılabilir.

Herhangi bir blok eşlenmemişse, kullanıcı da bunu yapabilir; Gutenberg hakkında biraz bilgi sahibi olması yeterli, GraphQL veya şemalar hakkında hiçbir şey bilmesi gerekmez.

Ayrıca, GraphQL'in kullanıcıyı eşlenmemiş bir blok olduğunda (ve dolayısıyla çevrilemeyeceğinde) uyarmasını sağlayabiliriz. Bunu, koşul geçerliyse @sendEmail direktifini çalıştıran bir @if meta-direktifi ekleyerek yapabiliriz:

{
  post(by: { id: 1 }) {
    blocks {
      name
      meta
        @advancePointersInArray(paths: "{{ translatablePaths }}")
          @underEachArrayItem
            @strTranslate(from: "en", to: "fr")
        @if(condition: "{{ isTranslatablePathsUnmapped }}")
          @sendEmail(
            to: "{{ root.adminEmail }}",
            subject: "Block with name {{ name }} has 'translatablePaths' unmapped"
          )
    }
  }
}

Bu çözüm esnek ve basittir; GraphQL'in WordPress'e hizmet etmesini sağlar, geliştiricilerin yeni bir teknoloji öğrenmesini veya Gutenberg'in nasıl çalıştığını değiştirmesini gerektirmez.

Sonuç

GraphQL ile Gutenberg arasında olası bir entegrasyonun nasıl görüneceğini düşünürken (WordPress çekirdeğine olası bir dahil edilme açısından), GraphQL'in Gutenberg'in gelecekteki tüm gereksinimlerini karşılayabildiğinden emin olmalıyız; bunlar şunları tam olarak desteklemeyi içerir:

  • çok dilli bloklar
  • Tam Site Düzenleme (Full Site Editing)
  • işbirlikçi düzenleme
  • canlı bir sitede üçüncü taraf hizmetleriyle etkileşim

Tüm bunlar, umarız Gutenberg'i değiştirmeye gerek kalmadan (en azından önemli ölçüde değil) ve eklenti geliştiricilerinden gereken yeni görevleri azaltarak gerçekleştirilmelidir.

Bunları göz önünde bulundurarak, burada önerdiğim 4. yaklaşımın gerçekten çok iyi çalışabileceğine inanıyorum.


Bültenimize abone olun

Gato GraphQL'deki tüm güncellemelerden haberdar olun.