background blurbackground mobile blur

1/1/1970

SSG'yi 2 Günde Nasıl Hayata Geçirdim

Selamlar! Bir yıl önce bunun imkânsız olduğunu düşünüyordum. Ama Foony için Statik Site Üretimi'ni (SSG) sadece 2 günde tamamladım ve bu konuda bayağı heyecanlıyım. Bu, Foony için SSG'yi çözmeye çalıştığım ilk seferim de değil. Geçmişte NextJS, Vike, Astro, Gatsby ve birkaç başka çözüme baktım. Hatta NextJS ile yanlış bir başlangıç bile yaptım, ama Foony'nin SPA karmaşıklığı ve binlerce dosyasıyla zorluklara takıldım. Geçiş bir kâbus olurdu ve aylar sürerdi. Ayrıca sitede çalışan herkesin NextJS'i ve onun garipliklerini öğrenmek zorunda kalması yüzünden ek bir karmaşıklık katmanı da getirirdi.

Hafif ve uygulaması kolay bir şey istiyordum. Kod yazma şeklimizi değiştirmeden, SSG hakkında düşünmek zorunda kalmadan devam etmemize izin verecek bir şey (useMediaQuery hariç, onun gerçek bir çözümü yok). Aşağıda neden özel bir çözüme yöneldiğimi, karşılaştığım belirli zorlukları (özellikle React'in Suspense sınırlarıyla) ve bunları nasıl çözdüğümü anlatacağım.

Neden Standart Çözümler Olmadı?

Foony'ye SSG eklemeyi ilk düşündüğümde, doğal olarak NextJS (sektör standardı), Vike ve Astro'yu değerlendirdim.

NextJS: Çok Fazla Geçiş

NextJS güçlü, ama Foony'nin mevcut React SPA'sının devasa bir geçişini gerektirirdi. Binlerce dosyamız, karmaşık yönlendirme mantığımız ve epey özel altyapımız var. NextJS'e geçmek şu anlama gelirdi:

  • Tüm yönlendirme sistemimizi yeniden yazmak
  • Oyunları ve bileşenleri yükleme şeklimizi yeniden yapılandırmak
  • Sadece özellik denkliğine geri dönmek için aylarca çalışmak
  • Kullanıcılar için potansiyel bozucu değişiklikler
  • Görselleri ele alma şeklimizi değiştirmek
  • Önemli ölçüde yavaşlamış derleme süreleri (potansiyel olarak 5-30 dakika. Bunu destekleyecek somut rakamlarım yok, sadece GitHub'daki 5 yıllık şu tartışma var)
  • Tüm ekibin yeni bir şey (NextJS) öğrenmesi ve sonsuza dek daha yavaş geliştirici hızı
  • NextJS bozucu değişiklikler yapmaya karar verdiğinde her seferinde kodu taşımak.

NextJS ile yanlış bir başlangıç bile denedim, ama geçiş maliyetinin çok yüksek olduğunu hızla fark ettim. Karmaşıklık buna değmezdi.

Vike: Benzer Karmaşıklık

Vike (eski adıyla vite-plugin-ssr) benzer sorunlara sahipti. NextJS'ten daha esnek olsa da, kod tabanımızın önemli ölçüde yeniden yapılandırılmasını yine de gerektirirdi. Öğrenme eğrisi ve geçiş çabası faydaları haklı çıkarmıyordu.

Astro: Yanlış Mimari

Astro içerik yoğun siteler için harika, ama Foony karmaşık bir çok oyunculu oyun platformu. Gerçek zamanlı güncellemelere, WebSocket bağlantılarına ve dinamik React bileşenlerine ihtiyacımız var. Astro'nun mimarisi inşa ettiğimiz şeye uymuyor.

Çözüm: Özel SSG

Birkaç gün önce i18n sonrası uyguladığım "sahte SSG" yaklaşımımdan cesaret alarak, Foony'nin SSG'si için küçük, hafif, özel bir çözümde karar kıldım.

"Sahte SSG" yaklaşımım, blog yazısı içeren sayfalardan (/posts rotaları ve oyun sayfaları) blog yazısı içeriğini çekip, istemcinin onları render edeceği yere tam olarak yerleştirmeyi içeriyordu. Bu özellikle arama motorlarının ve LLM'lerin Foony'yi anlamasına yardımcı olmak içindi. Ayrıca ld+json şeması ve bazı küçük SEO işleri uyguluyordu.

Yaklaşım basit:

  1. Mevcut React SPA'nın üzerine inşa et: Geçiş gerekmez, sadece derleme zamanında SSG üretimi ekle.
  2. renderToReadableStream kullan: React 18'in akış SSR API'si Suspense'i yerel olarak ele alır.
  3. Statik HTML dosyaları üret: Rotaları derleme zamanında önceden render et ve statik dosyalar olarak servis et, rotaların listesini almak için SitemapGenerator'ımızı kullan.
  4. Mevcut kod tabanında minimum değişiklik: Çoğu bileşen olduğu gibi çalışıyor.

Çekirdek uygulama client/src/generators/GenerateShellSsgFromSitemap.ts içinde yaşıyor. Bir site haritası okur, her rotayı React'in renderToReadableStream'ini kullanarak render eder ve HTML'i statik dosyalara yazar. Basit, tam istediğim gibi!

Bu aynı zamanda epey hızlı çıktı. Yaklaşık 2.800 rota 10 saniyede render edildi. Güzel. Bu NextJS, Gatsby ve Astro'dan önemli ölçüde daha hızlı. <img alt="Geçen süreyi gösteren SSG konsol logu" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.ssg_speed.webp" style={{ margin: "8px auto", height: 120, display: "block" }} />

Sadelik konusunda saatlerce konuşabilirim. Büyük şirketlerde "karmaşıklık eksikliği" nedeniyle terfi getirmese bile, basit kod güzel, sürdürülebilir ve genel olarak geliştirici hızı için çok daha iyi. Bu, Zen prensiplerinde gerçekten hayran olduğum bir şey.

Suspense Sınırı Sorunu

Şimdi SSG'm vardı ve içerik HTML'de görünüyordu... ama sayfalarım boştu! Nasıl yani?! <img alt="SSG boş sayfa" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blank_page.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />

Meğer renderToReadableStream, await stream.allReady yapsanız bile hâlâ Suspense sınırlarına sahipmiş. Tahminim bu, "akış" olduğu ve baytlar alındıkça istemcilere iletilmek üzere tasarlandığı için.

React'in Çıktısı

renderToReadableStream'i Suspense ile kullandığınızda, React şu şekilde HTML çıktısı verir:

<!--$?-->
<template id="B:0"></template>
<!--/$-->
<div hidden id="S:0">
  <!-- Asıl içerik burada -->
</div>
...
<script>/*Suspense sınırlarını değiştiren script*/</script>

<template id="B:0"> içeriğin gitmesi gereken yer için bir yer tutucu. <div hidden id="S:0"> asıl render edilmiş içeriği barındırıyor. B:0, numaraya göre S:0 ile eşleşiyor (0 tabanlı indeks).

JavaScript olmadan, arama motorları (sana bakıyorum, Bing) ve LLM'ler sadece şablon yer tutucusuyla neredeyse boş bir sayfa görürdü. Bu SSG'nin tüm amacını çürütür!

Bu Suspense sınırlarını kaldırmanın temiz bir yolunu göremedim, bu yüzden çözümüm bunları değiştirmek için bazı testler ve bir resolveSuspenseBoundaries fonksiyonu yazmaktı. Bu, HTML'i ayrıştırıp script'i JSDOM gibi bir şeyle çalıştırmaktan daha hızlıydı. Ve daha da önemlisi, planladığım şey için bir gereklilikti: arama motorları / LLM'ler için JavaScript olmadan güzel, okunabilir bir site, ama istemcide Suspense sınırları ve hidrasyon desteğiyle.

Dönüşümü Test Etme

Sahip olduğum şeyden (JavaScript devre dışı) ve istediğim şeyden (JavaScript etkin) DOM'da bazı örnekler alarak dönüşüm için testler yazmaya başladım. Bunları bir LLM'e besledim ve test üretimini ona yaptırdım, bu konuda oldukça iyi. Bu testler client/src/generators/ssr/renderRoute.test.ts içinde yaşıyor ve dönüşümün doğru çalıştığından emin oluyor. Testler şunları kapsıyor:

  • Basit sınır değiştirme (blog listesi)
  • Şablon ile kapanış yorumu arasında içerik bulunan karmaşık sınırlar
  • Birden çok sınır
  • Yorum işaretleri olmayan sınırlar
  • Sınır durumlar

Bu tür "TDD", beklenen girdi ve çıktıların olduğu bu kullanım durumu için aslında oldukça yararlı.

Bu, "Robert C. Martin öyle dedi diye her şeye TDD" ile karıştırılmamalı (bu ekibinizin geliştirme hızını yavaşlatır). UI veya sürekli değişen kod bölgeleri için TDD kullanmamalısınız!

Çözüm: resolveSuspenseBoundaries

Testler hazır olunca, resolveSuspenseBoundaries fonksiyonunu LLM'e yazdırdım. RegEx'in kırılganlığından kaçınmak için cheerio ile gittim, RegEx kullanmak SSG süresini yaklaşık %40 kısaltacak olsa bile.

export function resolveSuspenseBoundaries(html: string): {html: string; didResolveSuspense: boolean} {
  const originalHtml = html;
  const $ = cheerio.load(originalHtml, {xml: false, isDocument: false, sourceCodeLocationInfo: true});
  const operations: Array<{index: number; removeLength: number; insertText?: string}> = [];

  // Gizli div'leri içerikleri ve konumlarıyla topla.
  const hiddenDivs = new Map<string, {content: string; divStartIndex: number; divEndIndex: number}>();
  $('div[hidden][id^="S:"]').each((_, el) => {
    const id = $(el).attr('id');
    if (!id) {
      return;
    }
    const boundaryId = id.substring(2);
    const content = $(el).html() || '';
    const {startOffset, endOffset} = el.sourceCodeLocation ?? {};
    if (typeof startOffset === 'number' && typeof endOffset === 'number') {
      hiddenDivs.set(boundaryId, {content, divStartIndex: startOffset, divEndIndex: endOffset});
    }
  });

  if (hiddenDivs.size === 0) {
    return {html: originalHtml, didResolveSuspense: false};
  }

  // Şablonları (B:0) bul ve eşleşen gizli içerikle (S:0) değiştir,
  // React'in dahili $RV davranışını takip ederek.
  $('template[id^="B:"]').each((_, el) => {
    const id = $(el).attr('id');
    if (!id) {
      return;
    }
    const boundaryId = id.substring(2);
    const divInfo = hiddenDivs.get(boundaryId);
    if (!divInfo) {
      return;
    }
    const {startOffset, endOffset} = el.sourceCodeLocation ?? {};
    if (typeof startOffset !== 'number' || typeof endOffset !== 'number') {
      return;
    }

    const templateIndex = startOffset;
    const templateLength = endOffset - startOffset;
    const afterTemplate = originalHtml.substring(templateIndex + templateLength);
    const closingCommentMatch = afterTemplate.match(/<!--\/[
amp;]-->/); const removeEndIndex = closingCommentMatch ? templateIndex + templateLength + closingCommentMatch.index! : templateIndex + templateLength; const divContentStartIndex = originalHtml.indexOf('>', divInfo.divStartIndex) + 1; const divContentEndIndex = originalHtml.lastIndexOf('</', divInfo.divEndIndex); const divContent = originalHtml.substring(divContentStartIndex, divContentEndIndex); operations.push({index: templateIndex, removeLength: removeEndIndex - templateIndex}); operations.push({index: templateIndex, removeLength: 0, insertText: divContent}); operations.push({index: divContentStartIndex, removeLength: divContentEndIndex - divContentStartIndex}); operations.push({index: divInfo.divStartIndex, removeLength: divContentStartIndex - divInfo.divStartIndex}); operations.push({index: divContentEndIndex, removeLength: divInfo.divEndIndex - divContentEndIndex}); }); operations.sort((a, b) => (a.index !== b.index ? b.index - a.index : b.removeLength - a.removeLength)); let resultHtml = originalHtml; for (const operation of operations) { resultHtml = resultHtml.slice(0, operation.index) + (operation.insertText ?? '') + resultHtml.slice(operation.index + operation.removeLength); } return {html: resultHtml, didResolveSuspense: true}; }

Bu, neredeyse boş bir sayfa görmek yerine arama motorlarının ve LLM'lerin tamamen render edilmiş bir sayfa görmesini sağlıyor.

Şimdi JavaScript olmadan SSG düzgün çalışıyor! <img alt="Foony'nin blogları için JavaScript'siz SSG" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blog_ssg.webp" style={{ margin: "8px auto", height: 340, display: "block" }} />

Uzun vadede, React'in Suspense formatını değiştirmesi mümkün. Tembel yüklenen (ve dolayısıyla Suspense sınırları gerektiren) sayfalar için daha iyi bir çözümüm olduğunda Suspense çözümleme kodunu kaldırabilirim.

Hidrasyon Stratejisi (Güncelleme: Bu 3 Gün + 1 Ek Gün Sürdü)

Hidrasyon zorlu. Bunu biliyordum. Ama biraz çalışmadan sonra, çalışmasını sağlamayı başardım!

Hidrasyon için toplam süre: 3 gün, artı dehidrasyon yaklaşımını değiştirmek için 1 ek gün.

En zor kısmı sadece o ilk minimum, çalışan hidrasyonu elde etmekti. Navbar ile bir "Hello World" render etmeyi başardığımda, evet, bunun bir ay sürmeyebileceğine dair güven kazandım!

<img alt="Foony'nin Hello World'ünün navbar ile başarıyla hidrasyonu" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_mvp.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />

O ilk minimum, çalışan hidrasyon için benzersiz bir zorluğum vardı: hidrasyon istiyordum ama aynı zamanda geliştiricilerin Suspense sınırları hakkında düşünmesine gerek kalmadan arama motorları ve LLM'ler için iyi bir SEO da istiyordum.

Zorluk

React hidrasyonu son derece harfiyendir: DOM, React'in o ilk render için beklediği gibi görünmüyorsa, konsolunuzda bu hoş, neredeyse işe yaramaz hata mesajını alırsınız ve React her şeyi atıp sıfırdan yeniden render eder. Neyin yanlış gittiğini bildirmek için bir diff bile yok!

Bizim durumumuzda, SSG bunu birkaç şekilde daha kötüleştirdi:

  1. React 18 akış Suspense kalıntılarını kaldırmak/çözmek için HTML'i son işleme tabi tutuyorduk (botlar için harika).
  2. İstemci, sunucu render'ının t = 0 anında sahip olduğu verilere her zaman tam olarak sahip değildi (SSG verisi, blog metadatası vb.).
  3. i18n'imiz varsayılan olarak "tembel", bu da SSG için hangi çevirilerin kullanıldığını kaydedip React render etmeden önce enjekte etmediğiniz sürece ilk render için çevirilerin eksik olabileceği anlamına gelir.

Ne İşe Yaradı (İlk Yaklaşım: Dehidrasyon)

İlk olarak akıllıca ve sevimli bir şey denedim: HTML'in Suspense sınırlarını çözmek için kullanılan komutları kaydetmek üzere bir komut deseni kullandım ve HTML'i React'in hidrasyon için ihtiyaç duyduğu hale geri yüklemem için ters dönüşüm komutlarını döndürdüm. Umudum, bu komut yöntemiyle index.html'de çok daha az bayt göndermekti. Ama çoğu akıllı çözüm gibi, bu da başarısız oldu çünkü tarayıcılar HTML'i ince yollarla değiştiriyor, mesela bir ; veya / ekleyip kaldırıyor, bu da değiştirme indekslerini bozuyordu. Teknik olarak bu ince tarayıcı değişikliklerini hesaba katabilirsiniz, ama bu kadar kırılgan bir şey göndermeye niyetim yoktu. Suspense sınır dönüşümünü React'in akış işaretlemesine geri "tersine çevirmeye" çalışmak yerine, çok basit bir şey yaptım:

Orijinal, çözülmemiş HTML'i bir <script type="text"> içinde paketle.

Bu "dehidrasyon" yaklaşımı işe yaradı, ama onu daha iyi bir çözümle değiştirmek için ekstra bir gün harcadım.

Daha İyi Yaklaşım: Kritik Yol Suspense Sınır Değişimi

İlk uygulamadan sonra, hâlâ Suspense sınırlarıyla bazı sorunlar yaşıyordum. İşte o zaman daha temiz, daha iyi, daha basit bir çözüm olduğunu fark ettim. Dehidrasyon yaklaşımını kritik yol Suspense sınır değişimi ile değiştirdim, bu da:

  • Hidrasyondan önce kritik yolu yükler: SSR sırasında önceden yüklenen bileşenler tanımlanır ve hydrateRoot çağrılmadan önce istemcide önceden yüklenir
  • Sürdürmek daha basittir: React iç bileşenleri veya AST ayrıştırma gerektirmez (dehidrasyon yaklaşımı HTML'i ayrıştırıp geri yüklemek zorundaydı)
  • Daha az bayt gönderir: React'in orijinal SSR yanıtını artık bir script etiketinde paketlemiyoruz
  • Olası bir flaş'ı önler: HTML'i dehidrate/yeniden hidrate etmeye gerek yok, olası görsel flaş'ı ortadan kaldırır

Uygulama, SSR sırasında hangi tembel bileşenlerin önceden yüklendiğini izler (SSRLazyComponentTracker aracılığıyla), import yollarını hidrasyon verisine dahil eder ve hidrasyondan önce bunları senkron olarak önceden yükler. Kritik yol bileşenleri Suspense sınırları olmadan doğrudan render edilir, SSR çıktısıyla tam olarak eşleşir.

Diğer her şey için, ilk istemci render'ını SSR/SSG gibi davranır hale getiriyoruz. Bu, aynı girdileri kullanmak ve bu girdileri hydrateRoot'tan önce senkron olarak kullanılabilir kılmak demek. Bu, "ssg-data"mız aracılığıyla paketleyerek yapılır.

Somut olarak, ayarlamalar şunlardı:

  1. SSR girdilerini tek bir text script'ine paketle

    • SSG sırasında, Vite modül giriş noktasından hemen önce bir <script type="text/foony-ssg" id="foony-ssg-data">...</script> enjekte ediyoruz.
    • Bu script şunları içerir:
      • html: statik dosyada gerçekten gönderdiğimiz çözülmüş HTML
      • ssgData: SSR sarmalayıcısı tarafından kullanılan serileştirilmiş SSGData. Bunu sadece erişilen verilerin dahil edilmesi için bir Proxy veya benzerine güncellemeyi planlıyorum.
      • translationData: SSR sırasında dokunduğumuz çeviri anahtar-değer blokları
  2. Bu girdileri hidrasyondan hemen önce enjekte et

    • main.tsx içinde, senkron olarak:
      • #root.innerHTML'i serileştirilmiş çözülmüş HTML'e ayarlıyoruz (böylece DOM, hidrasyonun gördüğüyle tam olarak aynı oluyor)
      • uygulamayı SSGDataProvider içinde sarıyoruz, böylece bileşenler ilk render'da aynı SSGData'ya sahip oluyor
  3. Çeviri değerlerini enjekte ederek i18n'i anında yap

    • SSR sırasında erişilen gerçek çeviri nesnelerini kaydediyoruz ve onları SSG script'inde gönderiyoruz.
    • İstemcide, onları özel bir LocaleQueryer.inject() yöntemi aracılığıyla doğrudan LocaleQueryer'ın önbelleğine enjekte ediyoruz, böylece çeviriler hemen kullanılabilir oluyor.

Ve bununla birlikte, ilk render'ın SSR'ın sahip olduğu verilerin aynısına sahip olması sağlanıyor!

useIsSSRMode() hook'u zaten client/src/generators/ssr/isSSRMode.ts içinde uygulanmış durumda:

export function useIsSSRMode(): boolean {
  const [isSSRMode, setIsSSRMode] = React.useState(true);
  
  React.useEffect(() => {
    // Mount sonrası (hidrasyon tamamlandı), istemci moduna geç
    setIsSSRMode(false);
  }, []);
  
  return isSSRMode;
}

Bu hook, SSR sırasında ve ilk istemci render'ında (hidrasyon) true döndürür, sonra mount'tan sonra false'a geçer. UserBanner, Navbar ve Dialog gibi bileşenler hidrasyon uyumsuzluklarını önlemek için bunu zaten kullanıyor.

  1. Daha iyi diff'ler için React'i yamala

Sadece hydration-overlay kullanabilmeyi umuyordum. Ama aktif olarak sürdürülmüyor, sadece React 18'e kadar destekleniyor ve üretime hazır değildi. Bu yüzden bir LLM'e ilham almak için repoyu klonlattım ve birkaç dakika içinde minimal bir hidrasyon overlay'i oluşturdu. Süslü bir şeye ihtiyacım yoktu, sadece geliştirme sırasında görünecek ve nerede yanlış gittiğini bulmamı sağlayacak bir şey istiyordum.

Bu yeni overlay çok temel, bu yüzden diff'ler tam olarak mükemmel değil. React yorumları kaldırır, stil özniteliklerinden sonra ;'ler ekler, boşlukları değiştirir ve overlay'imizin (henüz) hesaba katmadığı birkaç başka küçük şey yapar. Overlay'imiz ayrıca React'in hidrasyonu için yok saydığı HTML yorumlarını da içerir.

<img alt="Yeni hidrasyon overlay'imiz" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_overlay.webp" style={{ margin: "8px auto", height: 315, display: "block" }} />

Ama nelerin düzeltilmesi gerektiğini anlamak için yeterince iyi.

<img alt="React hidrasyonu için SSG'mizin istemci ilk sayfa render'ına karşı diff'i" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_diff.webp" style={{ margin: "8px auto", height: 85, display: "block" }} />

Rakamlarla

Bu uygulamanın ne içerdiğine dair size bir fikir vermek için:

  • 2 gün çalışma (başlangıçtan çalışan SSG'ye). Bu, tatildeyken sadece 24 saatten biraz fazlaydı.
  • 4 gün çalışma, hidrasyonun async çeviri yarışları veya useMediaQuery işleri bozmadan düzgün davranmasını sağlamak için.
  • 1 ek gün dehidrasyon yaklaşımını kritik yol Suspense sınır değişimi ile değiştirmek için (daha basit, daha az bayt, olası flaş yok).
  • ~200 satır çekirdek SSG üretim kodu (GenerateShellSsgFromSitemap.ts)
  • ~120 satır Suspense sınır çözümlemesi (renderRoute.tsx içinde resolveSuspenseBoundaries) - Not: Bu daha sonra kritik yol yaklaşımıyla değiştirildi
  • ~50 satır SSR yardımcı işlevleri (isSSRMode.ts)
  • ~100 satır test (renderRoute.test.ts)
  • ~150 satır SSR için polyfill'ler (setupSSREnvironment)
  • Mevcut bileşenlere minimum değişiklik (çoğunlukla useIsSSRMode() kontrolleri eklemek)

Çözüm hafif ve sürdürülebilir. Bir framework geçişi gerektirmiyor ve mevcut React SPA'mızla çalışıyor.

Önemli Çıkarımlar

Bazen Özel Bir Çözüm Daha İyidir

Her sorun bir framework gerektirmez. Foony için, küçük, özel bir SSG çözümü doğru tercihti. Bu:

  • Hafif: Ağır bağımlılıklar veya framework yükü yok
  • Sürdürülebilir: Anladığımız basit kod
  • Esnek: Gerektiğinde değiştirmesi ve genişletmesi kolay
  • Uyumlu: Mevcut React SPA'mızla geçişe gerek kalmadan çalışıyor

React'in Akış SSR'sının Tuhaflıkları Var

React'in renderToReadableStream'i Suspense ile başa çıkmak için güzel, ama tuhaflıkları var. await stream.allReady ile bile, çıktıda hâlâ Suspense sınırları alıyorsunuz. Bu bir bug değil, akış için tasarım gereği. Ama SSG için, tamamen çözülmüş HTML'e ihtiyacımız var. Bu, React ekibinin bu senaryoyu temiz bir şekilde ele almamasının bir başarısızlığı gibi hissettiriyor.

Çözümüm HTML'i son işleme tabi tutmak ve sınırları çözmekti. Hoş değil, ama hızlı ve kullanım durumum için yeterince esnek.

TDD, LLM'ler İçin Yararlı Olabilir

HTML dönüşümü hata yapmaya açık. Küçük bir bug ve tüm SSG çıktısını bozup son kullanıcı deneyimini bozabilirsiniz. Dönüşümün doğru çalıştığından emin olmak için bir LLM'e (benim girdim ile) kapsamlı testler yazdırdım.

Sonuç

SSG artık Foony için çalışıyor. Sayfalar arama motorları ve LLM'ler için tamamen render ediliyor ve çözüm sürdürülebilir ve hafif. SSG rotaları için hidrasyon beklediğimden uzun sürdü (3 gün) ve ilk dehidrasyon yaklaşımını kritik yol Suspense sınır değişimi ile değiştirmek için ekstra bir gün harcadım. Yeni yaklaşım sürdürmesi daha basit, daha az bayt gönderiyor ve HTML'i dehidrate/yeniden hidrate etmekten kaynaklanan olası görsel flaş'ları önlüyor.

Hâlâ SSG için özel bir çözüm uygulamanın sadece 2 gün almasının şokunu yaşıyorum. Ama bazen doğru çözüm en basit olanıdır.

Gelecekteki çalışmalar arasında hidrasyon eşleştirmesini tamamlamak ve daha iyi hata ayıklama için React'i potansiyel olarak yamalamak yer alıyor. Ama şimdilik, Foony'de çalışan SSG var. Önümüzdeki haftalarda SEO'muza ne etkisi olacağını görmek için Google Search Console ve Bing Webmaster Tools'a göz kulak olacağım.

8 Ball Pool online multiplayer billiards icon