

1/1/1970
Bagaimana Saya Melaksanakan SSG dalam 2 Hari
Hai semua! Setahun yang lalu, saya fikir ini mustahil. Tetapi saya baru sahaja selesai melaksanakan Static Site Generation (SSG) untuk Foony dalam masa 2 hari, dan saya cukup teruja dengannya. Ini bukan kali pertama saya cuba menyelesaikan SSG untuk Foony. Saya pernah melihat NextJS, Vike, Astro, Gatsby, dan beberapa penyelesaian lain pada masa lalu. Saya pernah bermula dengan NextJS, tetapi terkandas dengan kerumitan SPA Foony dan beribu-ribu fail. Migrasi itu pasti menjadi mimpi ngeri dan akan mengambil masa berbulan-bulan. Ia juga akan menambah kerumitan untuk semua orang yang bekerja di laman ini kerana mereka perlu belajar NextJS dan kerenahnya.
Saya mahukan sesuatu yang ringan dan mudah dilaksanakan. Sesuatu yang membolehkan kami terus menulis kod dengan cara yang sama tanpa perlu memikirkan SSG (kecuali useMediaQuery, tiada cara sebenar untuk mengelakkan yang itu). Di bawah saya akan menerangkan mengapa saya memilih penyelesaian khas, cabaran khusus yang saya hadapi (terutamanya dengan sempadan Suspense React), dan bagaimana saya menyelesaikannya.
Mengapa Bukan Penyelesaian Standard?
Apabila saya mula-mula memikirkan untuk menambah SSG ke Foony, saya secara semula jadi mempertimbangkan NextJS (standard industri), Vike, dan Astro.
NextJS: Terlalu Banyak Migrasi
NextJS sangat berkuasa, tetapi ia memerlukan migrasi besar-besaran SPA React Foony yang sedia ada. Kami mempunyai beribu-ribu fail, logik penghalaan yang kompleks, dan banyak infrastruktur tersuai. Berhijrah ke NextJS akan bermakna:
- Menulis semula keseluruhan sistem penghalaan kami
- Menyusun semula cara kami memuatkan permainan dan komponen
- Berbulan-bulan kerja hanya untuk kembali ke tahap ciri yang sama
- Perubahan yang berpotensi merosakkan untuk pengguna
- Mengubah cara kami mengendalikan imej
- Masa pembinaan yang jauh lebih perlahan (berpotensi 5-30 minit. Saya tiada angka konkrit untuk menyokong ini selain perbincangan berusia 5 tahun di GitHub)
- Seluruh pasukan perlu belajar sesuatu yang baharu (NextJS), dan kelajuan pembangunan yang lebih perlahan selama-lamanya
- Berhijrah kod setiap kali NextJS membuat perubahan yang merosakkan.
Saya pernah mencuba dengan NextJS, tetapi cepat-cepat sedar kos migrasi terlalu tinggi. Kerumitan itu tidak berbaloi.
Vike: Kerumitan Yang Serupa
Vike (dahulunya vite-plugin-ssr) mempunyai isu yang serupa. Walaupun ia lebih fleksibel daripada NextJS, ia masih memerlukan penyusunan semula yang ketara terhadap kod kami. Lengkung pembelajaran dan usaha migrasi tidak menjustifikasikan manfaatnya.
Astro: Seni Bina Yang Salah
Astro hebat untuk laman yang berat dengan kandungan, tetapi Foony ialah platform permainan berbilang pemain yang kompleks. Kami memerlukan kemas kini masa nyata, sambungan WebSocket, dan komponen React dinamik. Seni bina Astro tidak sesuai dengan apa yang kami bina.
Penyelesaian: SSG Khas
Diberi keberanian oleh pendekatan "SSG palsu" yang saya laksanakan beberapa hari lalu selepas i18n, saya memilih penyelesaian kecil, ringan, dan khas untuk SSG Foony.
Pendekatan "SSG palsu" saya melibatkan pengambilan kandungan blog daripada halaman dengan blog (laluan
/postsdan halaman permainan), dan meletakkannya tepat di tempat klien akan merendernya, khusus untuk enjin carian dan LLM bagi membantu memahami Foony. Ia juga menggunakan skema ld+json dan sedikit perkara SEO kecil.
Pendekatannya mudah:
- Bina di atas SPA React sedia ada: Tiada migrasi diperlukan, cukup tambah penjanaan SSG pada masa pembinaan.
- Gunakan
renderToReadableStream: API SSR penstriman React 18 mengendalikan Suspense secara semula jadi. - Jana fail HTML statik: Pra-render laluan pada masa pembinaan dan sajikannya sebagai fail statik, menggunakan SitemapGenerator kami untuk mendapatkan senarai laluan.
- Perubahan minimum kepada kod sedia ada: Kebanyakan komponen berfungsi seperti sedia ada.
Pelaksanaan teras berada di client/src/generators/GenerateShellSsgFromSitemap.ts. Ia membaca peta laman, merender setiap laluan menggunakan renderToReadableStream React, dan menulis HTML ke fail statik. Mudah, ikut cita rasa saya!
Ini ternyata cukup pantas juga. Lebih kurang 2,800 laluan dirender dalam 10 saat. Bagus. Itu jauh lebih pantas daripada NextJS, Gatsby, dan Astro. <img alt="Log konsol SSG menunjukkan masa yang diambil" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.ssg_speed.webp" style={{ margin: "8px auto", height: 120, display: "block" }} />
Saya boleh bercakap panjang lebar tentang kesederhanaan. Walaupun ia tidak akan memberikan anda kenaikan pangkat di syarikat besar kerana "kurang kerumitan", kod yang ringkas itu cantik, mudah diselenggara, dan secara umumnya jauh lebih baik untuk kelajuan pembangun. Ini sesuatu yang saya benar-benar kagumi tentang prinsip Zen.
Masalah Sempadan Suspense
Jadi sekarang saya ada SSG, dan kandungan muncul dalam HTML, tetapi halaman saya kosong! Macam mana boleh jadi?! <img alt="Halaman kosong SSG" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blank_page.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />
Rupa-rupanya renderToReadableStream masih mempunyai sempadan Suspense, walaupun anda menggunakan await stream.allReady. Tekaan saya, ini kerana ia adalah "stream", dan direka untuk dihantar kepada pelanggan apabila bait diterima.
Apa Yang React Hasilkan
Apabila anda menggunakan renderToReadableStream dengan Suspense, React menghasilkan HTML seperti ini:
<!--$?-->
<template id="B:0"></template>
<!--/$-->
<div hidden id="S:0">
<!-- Actual content here -->
</div>
...
<script>/*Script that replaces the suspense boundaries*/</script>
<template id="B:0"> ialah pemegang tempat di mana kandungan sepatutnya pergi. <div hidden id="S:0"> mengandungi kandungan sebenar yang dirender. B:0 sepadan dengan S:0 mengikut nombor (indeks bermula dari 0).
Tanpa JavaScript, enjin carian (saya pandang awak Bing) dan LLM akan melihat halaman yang hampir kosong dengan hanya pemegang tempat templat. Itu menggagalkan seluruh tujuan SSG!
Saya tidak nampak cara yang bersih untuk mengeluarkan sempadan Suspense ini, jadi penyelesaian saya adalah dengan menulis beberapa ujian dan fungsi resolveSuspenseBoundaries untuk menukarkannya. Ini lebih cepat daripada menghuraikan HTML dan melaksanakan skrip dengan sesuatu seperti JSDOM. Dan, lebih penting lagi, ia adalah keperluan untuk apa yang saya rancangkan: laman yang baik dan boleh dibaca untuk enjin carian / LLM tanpa JavaScript, tetapi dengan sokongan untuk sempadan Suspense dan hidrasi pada klien.
Menguji Transformasi
Saya bermula dengan menulis ujian untuk transformasi dengan mengambil beberapa contoh dalam DOM daripada apa yang saya ada (JavaScript dilumpuhkan), dan apa yang saya inginkan (JavaScript didayakan). Saya memberikan ini kepada LLM dan menyuruhnya mengendalikan penjanaan ujian, sesuatu yang ia cukup mahir.
Ujian-ujian ini berada di client/src/generators/ssr/renderRoute.test.ts dan memastikan transformasi berfungsi dengan betul. Ujian-ujian ini meliputi:
- Penggantian sempadan ringkas (penyenaraian blog)
- Sempadan kompleks dengan kandungan antara templat dan komen penutup
- Pelbagai sempadan
- Sempadan tanpa penanda komen
- Kes tepi
Jenis "TDD" ini sebenarnya cukup berguna untuk kes guna ini di mana anda mempunyai input dan output yang dijangka.
Ini tidak boleh dikelirukan dengan "TDD untuk segala-galanya kerana Robert C. Martin kata begitu" (yang akan memperlahankan kelajuan pembangunan pasukan anda). Anda TIDAK PATUT menggunakan TDD untuk UI atau bahagian kod anda yang sentiasa berubah!
Penyelesaian: resolveSuspenseBoundaries
Setelah ujian disediakan, saya menyuruh LLM menulis fungsi untuk resolveSuspenseBoundaries. Saya memilih cheerio untuk ini bagi mengelakkan kerapuhan RegEx, walaupun menggunakan RegEx di sini akan mengurangkan masa SSG sebanyak kira-kira 40%.
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}> = [];
// Collect hidden divs with their content and positions.
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};
}
// Find templates (B:0) and replace them with the matching hidden content (S:0),
// following React’s internal $RV behavior.
$('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};
}
Ini memastikan bahawa daripada melihat halaman yang hampir kosong, enjin carian dan LLM akan melihat halaman yang dirender sepenuhnya.
Sekarang kita ada SSG yang berfungsi dengan baik tanpa JavaScript!
<img alt="SSG tanpa JavaScript untuk blog Foony" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blog_ssg.webp" style={{ margin: "8px auto", height: 340, display: "block" }} />
Untuk jangka panjang, ada kemungkinan React akan mengubah format Suspense mereka. Saya mungkin akan mengeluarkan kod resolusi Suspense ini setelah saya mempunyai penyelesaian yang lebih baik untuk halaman yang dimuatkan secara malas (dan oleh itu memerlukan sempadan Suspense).
Strategi Hidrasi (Kemas Kini: Ini Mengambil 3 Hari + 1 Hari Tambahan)
Hidrasi mencabar. Saya tahu itu. Tetapi, selepas sedikit kerja, saya berjaya membuatnya berfungsi!
Jumlah masa yang diambil untuk hidrasi: 3 hari, ditambah 1 hari tambahan untuk menggantikan pendekatan dehidrasi.
Bahagian paling rumit adalah mendapatkan hidrasi minimum pertama yang berfungsi. Setelah saya berjaya merender "Hello World" dengan navbar, saya yakin bahawa, ya, ini mungkin tidak akan mengambil masa sebulan penuh!
<img alt="Hello World Foony berjaya dihidratkan dengan navbar" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_mvp.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />
Untuk hidrasi minimum yang berfungsi pertama itu, saya mempunyai cabaran unik: saya mahukan hidrasi, tetapi saya juga mahukan SEO yang baik untuk enjin carian dan LLM tanpa pembangun perlu memikirkan sempadan Suspense.
Cabarannya
Hidrasi React sangat literal: jika DOM tidak kelihatan seperti apa yang React jangkakan untuk render pertama itu, anda akan dapat mesej ralat yang hampir tidak berguna di konsol anda, dan React akan membuang segala-galanya dan merender semula dari awal. Tiada pun diff untuk memberitahu anda apa yang tidak kena!
Dalam kes kami, SSG menjadikan ini lebih buruk dalam beberapa cara:
- Kami memproses HTML selepas itu untuk mengeluarkan/menyelesaikan artifak Suspense penstriman React 18 (yang bagus untuk bot).
- Klien tidak selalu mempunyai data tepat yang sama tersedia pada masa (t = 0) seperti render pelayan (data SSG, metadata blog, dan sebagainya).
- i18n kami "malas" secara lalai, yang bermaksud terjemahan boleh hilang untuk render pertama melainkan anda merekodkan terjemahan mana yang digunakan untuk SSG dan menyuntiknya sebelum React merender.
Apa Yang Berjaya (Pendekatan Awal: Dehidrasi)
Pada mulanya, saya mencuba sesuatu yang bijak dan comel: saya menggunakan corak arahan untuk merekodkan arahan yang digunakan untuk menyelesaikan sempadan Suspense HTML, dan mengembalikan arahan transformasi terbalik supaya saya boleh memulihkan HTML kepada apa yang React perlukan untuk hidrasi.
Harapan saya ialah saya boleh menghantar lebih sedikit bait dalam index.html dengan kaedah arahan ini. Tetapi, seperti kebanyakan penyelesaian bijak, ini gagal kerana penyemak imbas mengubah HTML dengan cara yang halus, seperti mengeluarkan atau menambah ; atau /, yang menyebabkan indeks penggantian tergelincir.
Secara teknikal, anda mungkin boleh mengambil kira perubahan halus penyemak imbas ini, tetapi saya tidak akan menghantar sesuatu yang begitu rapuh.
Daripada cuba "membalikkan" transformasi sempadan Suspense kembali ke markup penstriman React, saya melakukan sesuatu yang sangat ringkas:
Bungkus HTML asal yang belum diselesaikan dalam <script type="text">.
Pendekatan "dehidrasi" ini berfungsi, tetapi saya menghabiskan satu hari tambahan menggantikannya dengan penyelesaian yang lebih baik.
Pendekatan Yang Lebih Baik: Penggantian Sempadan Suspense Laluan Kritikal
Selepas pelaksanaan awal, saya masih menghadapi beberapa isu dengan sempadan Suspense. Pada masa itulah saya sedar ada penyelesaian yang lebih bersih, lebih baik, dan lebih ringkas. Saya menggantikan pendekatan dehidrasi dengan penggantian sempadan Suspense laluan kritikal, yang:
- Memuatkan laluan kritikal sebelum hidrasi: Komponen yang dipra-muat semasa SSR dikenal pasti dan dipra-muat pada klien sebelum
hydrateRoot dipanggil
- Lebih mudah diselenggara: Tiada dalaman React atau penghuraian AST diperlukan (pendekatan dehidrasi perlu menghuraikan dan memulihkan HTML)
- Menghantar lebih sedikit bait: Kami tidak lagi membungkus respons SSR asal dari React dalam tag skrip
- Mengelakkan kemungkinan kelipan: Tiada keperluan untuk dehidrat/rehidrat HTML, menghapuskan kemungkinan kelipan visual
Pelaksanaannya menjejaki komponen malas mana yang dipra-muat semasa SSR (melalui SSRLazyComponentTracker), memasukkan laluan import mereka dalam data hidrasi, dan memuatkannya secara serentak sebelum hidrasi. Komponen laluan kritikal dirender terus tanpa sempadan Suspense, sepadan dengan output SSR dengan tepat.
Untuk segala-galanya yang lain, kami menjadikan render klien pertama bertindak sebagai SSR/SSG. Itu bermaksud menggunakan input yang sama, dan menjadikan input tersebut tersedia secara serentak sebelum hydrateRoot. Ini dilakukan dengan membungkus melalui "ssg-data" kami.
Secara konkrit, pelarasannya adalah:
Bungkus input SSR ke dalam satu skrip teks
- Semasa SSG, kami menyuntik
<script type="text/foony-ssg" id="foony-ssg-data">...</script> tepat sebelum titik masuk modul Vite.
- Skrip itu mengandungi:
html: HTML yang diselesaikan yang sebenarnya kami hantar dalam fail statik
ssgData: SSGData bersiri yang digunakan oleh pembungkus SSR. Saya bercadang untuk mengemas kini ini kepada Proxy atau sesuatu supaya hanya data yang diakses dimasukkan.
translationData: gumpalan kunci-nilai terjemahan yang kami sentuh semasa SSR
Suntik input tersebut tepat sebelum hidrasi
- Dalam
main.tsx, kami secara serentak:
- menetapkan
#root.innerHTML kepada HTML yang diselesaikan bersiri (supaya DOM adalah tepat seperti yang dilihat hidrasi)
- membungkus aplikasi dalam
SSGDataProvider supaya komponen mempunyai SSGData yang sama pada render pertama
Jadikan i18n segera dengan menyuntik nilai terjemahan
- Kami merekodkan objek terjemahan sebenar yang diakses semasa SSR dan menghantarnya dalam skrip SSG.
- Pada klien, kami menyuntiknya terus ke dalam cache
LocaleQueryer melalui kaedah LocaleQueryer.inject() khusus, supaya terjemahan tersedia serta-merta.
Dan dengan itu, render pertama mempunyai data yang sama seperti SSR!
Cangkuk useIsSSRMode() sudah dilaksanakan di client/src/generators/ssr/isSSRMode.ts:
export function useIsSSRMode(): boolean {
const [isSSRMode, setIsSSRMode] = React.useState(true);
React.useEffect(() => {
// After mount (hydration complete), switch to client mode
setIsSSRMode(false);
}, []);
return isSSRMode;
}
Cangkuk ini mengembalikan true semasa SSR dan pada render klien pertama (hidrasi), kemudian bertukar kepada false selepas mount. Komponen seperti UserBanner, Navbar, dan Dialog sudah menggunakan ini untuk mengelakkan ketidakpadanan hidrasi.
- Tampal React untuk diff yang lebih baik
Saya berharap saya boleh menggunakan hydration-overlay. Tetapi ia tidak diselenggara secara aktif, hanya menyokong sehingga React 18, dan bukan sedia pengeluaran. Jadi saya menyuruh LLM mengklon repo itu untuk inspirasi, dan kemudian ia mencipta hidrasi overlay minimum dalam beberapa minit. Saya tidak memerlukan apa-apa yang mewah, hanya sesuatu yang akan muncul semasa pembangunan supaya saya boleh mengetahui di mana silapnya.
Overlay baharu ini sangat asas, jadi diff tidak cukup sempurna. React membuang komen, menambah ; selepas atribut gaya, mengubah ruang putih, dan beberapa perkara kecil lain yang overlay kami tidak ambil kira (lagi). Overlay kami juga termasuk komen HTML yang React abaikan untuk hidrasinya.
<img alt="Overlay hidrasi baharu kami" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_overlay.webp" style={{ margin: "8px auto", height: 315, display: "block" }} />
Tetapi ia cukup baik untuk mengetahui apa yang perlu dibetulkan.
<img alt="diff SSG kami berbanding render halaman pertama klien untuk hidrasi React" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_diff.webp" style={{ margin: "8px auto", height: 85, display: "block" }} />
Mengikut Angka
Untuk memberikan anda gambaran tentang apa yang pelaksanaan ini libatkan:
- 2 hari kerja (dari mula hingga SSG yang berfungsi). Ini hanya lebih sedikit daripada 24 jam semasa bercuti.
- 4 hari kerja untuk mendapatkan hidrasi berfungsi dengan baik tanpa perlumbaan terjemahan async atau
useMediaQuery mengganggu keadaan.
- 1 hari tambahan untuk menggantikan pendekatan dehidrasi dengan penggantian sempadan Suspense laluan kritikal (lebih ringkas, lebih sedikit bait, tiada kemungkinan kelipan).
- ~200 baris kod penjanaan SSG teras (
GenerateShellSsgFromSitemap.ts)
- ~120 baris resolusi sempadan Suspense (
resolveSuspenseBoundaries dalam renderRoute.tsx). Nota: Ini kemudiannya digantikan oleh pendekatan laluan kritikal
- ~50 baris utiliti SSR (
isSSRMode.ts)
- ~100 baris ujian (
renderRoute.test.ts)
- ~150 baris polyfill untuk SSR (
setupSSREnvironment)
- Perubahan minimum kepada komponen sedia ada (kebanyakannya menambah pemeriksaan
useIsSSRMode())
Penyelesaiannya ringan dan boleh diselenggara. Ia tidak memerlukan migrasi kerangka, dan ia berfungsi dengan SPA React kami yang sedia ada.
Pengajaran Utama
Kadangkala Penyelesaian Khas Lebih Baik
Tidak setiap masalah memerlukan kerangka. Untuk Foony, penyelesaian SSG khas yang kecil adalah pilihan yang tepat. Ia:
- Ringan: Tiada kebergantungan berat atau overhed kerangka
- Boleh diselenggara: Kod ringkas yang kami fahami
- Fleksibel: Mudah untuk diubah suai dan dikembangkan mengikut keperluan
- Serasi: Berfungsi dengan SPA React sedia ada kami tanpa migrasi
SSR Penstriman React Mempunyai Kerenah
renderToReadableStream React bagus untuk berurusan dengan Suspense, tetapi ia mempunyai kerenah. Walaupun dengan await stream.allReady, anda masih mendapat sempadan Suspense dalam output. Ini bukan pepijat, ia memang direka begitu untuk penstriman. Tetapi untuk SSG, kami memerlukan HTML yang diselesaikan sepenuhnya. Ia terasa seperti kegagalan oleh pasukan React kerana tidak mengendalikan senario ini dengan cara yang bersih.
Penyelesaian saya adalah memproses HTML selepas itu dan menyelesaikan sempadan. Ia tidak cantik, tetapi ia pantas dan cukup fleksibel untuk kes guna saya.
TDD Boleh Berguna Untuk LLM
Transformasi HTML mudah ralat. Satu pepijat kecil dan anda boleh merosakkan keseluruhan output SSG dan merosakkan pengalaman pengguna akhir. Saya menyuruh LLM menulis ujian komprehensif (dengan input saya) untuk memastikan transformasi berfungsi dengan betul.
Kesimpulan
SSG kini berfungsi untuk Foony. Halaman dirender sepenuhnya untuk enjin carian dan LLM, dan penyelesaiannya boleh diselenggara dan ringan. Hidrasi untuk laluan SSG mengambil masa lebih lama daripada yang saya jangkakan (3 hari), dan saya menghabiskan satu hari tambahan menggantikan pendekatan dehidrasi awal dengan penggantian sempadan Suspense laluan kritikal. Pendekatan baharu ini lebih mudah diselenggara, menghantar lebih sedikit bait, dan mengelakkan kemungkinan kelipan visual daripada mendehidrat/rehidrat HTML.
Saya masih terkejut bahawa ia hanya mengambil masa 2 hari untuk melaksanakan penyelesaian khas untuk SSG. Tetapi kadangkala penyelesaian yang tepat adalah yang paling ringkas.
Kerja masa hadapan termasuk menyiapkan padanan hidrasi dan berkemungkinan menampal React untuk penyahpepijatan yang lebih baik. Tetapi buat masa ini, Foony mempunyai SSG yang berfungsi. Saya akan memerhatikan Google Search Console dan Bing Webmaster Tools sepanjang minggu yang akan datang untuk melihat kesan ini terhadap SEO kami.