

1/1/1970
Paano Ko Na-implement ang SSG sa Loob ng 2 Araw
Kumusta! Isang taon na ang nakalipas, akala ko imposible ito. Pero katatapos ko lang mag-implement ng Static Site Generation (SSG) para sa Foony sa loob ng 2 araw, at sobrang excited ako tungkol dito. Hindi rin ito ang unang pagkakataon na sinubukan kong solusyunan ang SSG para sa Foony. Tiningnan ko na ang NextJS, Vike, Astro, Gatsby, at iba pang solusyon noon. May false start din ako sa NextJS, pero nahirapan ako dahil sa pagkakomplikado ng SPA ng Foony at libo-libong files. Magiging bangungot ang migration at aabutin ng buwan-buwan. Magdaragdag din ito ng komplikasyon para sa lahat ng iba pang nagtatrabaho sa site dahil kailangan nilang matutunan ang NextJS at ang mga kakaiba nitong gawi.
Gusto ko ng isang bagay na magaan at madaling i-implement. Isang bagay na magpapahintulot sa amin na magpatuloy sa pagsulat ng code sa parehong paraan na ginagawa namin nang hindi kinakailangang pag-isipan ang tungkol sa SSG (maliban sa useMediaQuery, walang talagang paraan sa isang iyon). Sa ibaba, ipapaliwanag ko kung bakit ako pumunta sa bespoke solution, ang mga partikular na hamon na nakaharap ko (lalo na sa Suspense boundaries ng React), at kung paano ko sila nalutas.
Bakit Hindi Standard na Solusyon?
Noong una kong tiningnan ang pagdadagdag ng SSG sa Foony, natural na isinaalang-alang ko ang NextJS (industry standard), Vike, at Astro.
NextJS: Sobrang Daming Migration
Makapangyarihan ang NextJS, pero kakailanganin nito ng napakalaking migration ng existing React SPA ng Foony. Mayroon kaming libo-libong files, kumplikadong routing logic, at maraming custom infrastructure. Ang paglipat sa NextJS ay mangangahulugan ng:
- Pagsulat muli ng aming buong routing system
- Pagbabago ng paraan ng pag-load ng mga laro at components
- Buwan-buwan ng trabaho para lang bumalik sa feature parity
- Posibleng breaking changes para sa mga users
- Pagbabago sa paraan ng paghawak namin ng mga images
- Mas mabagal nang malaki ang build times (posibleng 5-30 minuto. Wala akong concrete numbers para suportahan ito maliban sa 5-taong gulang na talakayang ito sa GitHub)
- Buong team na matututo ng bagong bagay (NextJS), at mas mabagal na developer velocity habambuhay
- Pag-migrate ng code tuwing nagpapasya ang NextJS na gumawa ng breaking changes.
Sumubok pa nga ako ng false start sa NextJS, pero mabilis kong na-realize na masyadong mataas ang halaga ng migration. Hindi sulit ang pagiging kumplikado.
Vike: Katulad na Pagkakomplikado
May katulad na isyu ang Vike (dating vite-plugin-ssr). Bagama't mas flexible ito kaysa sa NextJS, kakailanganin pa rin nito ng malaking pagbabago sa aming codebase. Hindi sapat na bigyan ng katwiran ang mga benepisyo ng learning curve at migration effort.
Astro: Maling Architecture
Magaling ang Astro para sa content-heavy na mga site, pero ang Foony ay isang kumplikadong multiplayer game platform. Kailangan namin ng real-time updates, WebSocket connections, at dynamic na React components. Hindi lang talaga akma ang architecture ng Astro sa ginagawa namin.
Ang Solusyon: Bespoke SSG
Pinatatag ng aking "fake SSG" na approach na in-implement ko ilang araw na ang nakalipas pagkatapos ng i18n, pumili ako ng maliit, magaan, at bespoke na solusyon para sa SSG ng Foony.
Ang aking "fake SSG" na approach ay nagsasangkot ng pagkuha ng nilalaman ng blog post mula sa mga page na may mga blog post (
/postsroutes at game pages), at paglagay nila nang eksakto kung saan ire-render sila ng client, partikular para sa search engines at LLMs upang matulungan silang maintindihan ang Foony. Inilapat din nito ang ld+json schema at ilang maliliit na bagay tungkol sa SEO.
Simple lang ang approach:
- Bumuo sa ibabaw ng existing React SPA: Walang kailangang migration, magdagdag lang ng SSG generation sa build time.
- Gamitin ang
renderToReadableStream: Hawakang natural ang Suspense ng streaming SSR API ng React 18. - Bumuo ng static HTML files: Pre-render ang routes sa build time at i-serve sila bilang static files, gamit ang aming SitemapGenerator para makakuha ng listahan ng routes.
- Minimal na pagbabago sa existing codebase: Karamihan sa mga components ay gumagana as-is.
Nakatira ang core implementation sa client/src/generators/GenerateShellSsgFromSitemap.ts. Binabasa nito ang isang sitemap, ire-render ang bawat route gamit ang renderToReadableStream ng React, at isusulat ang HTML sa static files. Simple, sa paraan na gusto ko!
Naging mabilis pala ito. Mga 2,800 routes ang na-render sa loob ng 10 segundo. Astig. Mas mabilis nang husto kaysa sa NextJS, Gatsby, at Astro. <img alt="SSG console log na nagpapakita ng tagal" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.ssg_speed.webp" style={{ margin: "8px auto", height: 120, display: "block" }} />
Pwede pa akong magsalita nang husto tungkol sa pagiging simple. Kahit hindi ka nito makakakuha ng promotion sa malalaking kompanya dahil sa "kakulangan ng pagiging kumplikado", maganda, maintainable, at mas mabuti sa pangkalahatan ang simpleng code para sa developer velocity. Ito ang isang bagay na talagang hinahangaan ko sa Zen principles.
Ang Problema sa Suspense Boundary
Kaya ngayon meron na akong SSG, at lumabas na ang content sa HTML, pero blangko ang aking mga page! Paano?! <img alt="SSG blangkong page" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.blank_page.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />
Lumalabas na ang renderToReadableStream ay meron pa rin ng Suspense boundaries, kahit pa await stream.allReady ka. Ang hula ko, ito ay dahil "stream" ito, at idinisenyo na ipasa sa mga client habang tinatanggap ang mga bytes.
Ano ang Inaalis ng React
Kapag gumagamit ka ng renderToReadableStream na may Suspense, ang inaalis ng React ay HTML na ganito:
<!--$?-->
<template id="B:0"></template>
<!--/$-->
<div hidden id="S:0">
<!-- Actual content here -->
</div>
...
<script>/*Script that replaces the suspense boundaries*/</script>
Ang <template id="B:0"> ay placeholder kung saan dapat pumunta ang content. Ang <div hidden id="S:0"> ay naglalaman ng aktwal na na-render na content. Ang B:0 ay tumutugma sa S:0 ayon sa numero (0-based index).
Kung walang JavaScript, makikita ng search engines (titignan kita, Bing) at LLMs ang halos blangkong page na may template placeholder lang. Sinisira nito ang buong layunin ng SSG!
Hindi ako nakakita ng malinis na paraan upang alisin ang Suspense boundaries na ito, kaya ang aking solusyon ay magsulat ng ilang tests at isang resolveSuspenseBoundaries function para palitan ang mga ito. Mas mabilis ito kaysa sa pag-parse ng HTML at pag-execute ng script gamit ang isang bagay tulad ng JSDOM. At, mas mahalaga, kinakailangan ito para sa pinaplano ko: isang maganda, nababasang site para sa search engines / LLMs nang walang JavaScript, pero may suporta para sa Suspense boundaries at hydration sa client.
Pag-test ng Transformation
Nagsimula ako sa pamamagitan ng pagsusulat ng tests para sa transformation sa pamamagitan ng pagkuha ng ilang halimbawa sa DOM mula sa kung ano ang mayroon ako (JavaScript naka-disable), at kung ano ang gusto ko (JavaScript naka-enable). Pinakain ko ito sa isang LLM at pinahawakan ko ito sa pagbuo ng test, isang bagay na magaling ito.
Nakatira ang tests na ito sa client/src/generators/ssr/renderRoute.test.ts at sinisiguro nila na tama ang gumagana ng transformation. Sinasaklaw ng tests:
- Simpleng boundary replacement (blog listing)
- Kumplikadong boundaries na may content sa pagitan ng template at closing comment
- Maraming boundaries
- Boundaries nang walang comment markers
- Edge cases
Ang ganitong klase ng "TDD" ay talagang kapaki-pakinabang para sa use case na ito kung saan mayroon kang inaasahang inputs at outputs.
Hindi ito dapat malito sa "TDD lahat dahil sinabi ni Robert C. Martin" (na magpapabagal ng development velocity ng iyong team). HINDI ka dapat gumagamit ng TDD para sa UI o mga lugar ng code mo na palaging nagbabago!
Ang Solusyon: resolveSuspenseBoundaries
Ngayon na nakahanda na ang tests, pinagawa ko sa LLM ang function para sa resolveSuspenseBoundaries. Pumili ako ng cheerio para dito upang maiwasan ang pagiging marupok ng RegEx, kahit na ang paggamit ng RegEx dito ay magbabawas ng oras ng SSG ng mga 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};
}
Sinisiguro nito na sa halip na makakita ng halos blangkong page, makikita ng search engines at LLMs ang isang ganap na na-render na page.
Ngayon mayroon na kaming SSG na gumagana nang maayos nang walang JavaScript!
<img alt="Walang JavaScript SSG para sa mga blog ng 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" }} />
Pang-pangmatagalan, posibleng baguhin ng React ang kanilang Suspense format. Maaaring tanggalin ko ang Suspense resolution code kapag mayroon na akong mas magandang solusyon para sa mga page na lazy-loaded (kaya nangangailangan ng Suspense boundaries).
Hydration Strategy (Update: Inabot ito ng 3 Araw + 1 Karagdagang Araw)
Mahirap ang hydration. Alam ko iyon. Pero, pagkatapos ng kaunting trabaho, nagawa kong mapagana ito!
Kabuuang oras na ginugol para sa hydration: 3 araw, plus 1 karagdagang araw upang palitan ang dehydration approach.
Ang pinakamahirap na bahagi ay ang pag-render lang ng unang minimal at gumaganang hydrate. Nang nagawa kong mag-render ng "Hello World" na may navbar, nakuha ko ang kumpiyansa na, oo, baka hindi nga ito aabutin ng buong buwan!
<img alt="Matagumpay na nag-hydrate ang Hello World ng Foony na may 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" }} />
Para sa unang minimal at gumaganang hydrate na iyon, may kakaibang hamon ako: gusto ko ng hydration, pero gusto ko rin ng magandang SEO para sa search engines at LLMs nang hindi na kailangang mag-isip ang mga developer tungkol sa Suspense boundaries.
Ang Hamon
Sobrang literal ang hydration ng React: kung hindi mukhang katulad ng inaasahan ng React para sa unang render iyon ang DOM, makakakuha ka ng magandang at halos walang silbi na error message sa iyong console, at itatapon ng React ang lahat at ire-render muli mula sa simula. Wala man lang diff para malaman mo kung ano ang nangyaring mali!
Sa kaso namin, mas pinalala pa ito ng SSG sa ilang paraan:
- Pina-post-process namin ang HTML para tanggalin/i-resolve ang React 18 streaming Suspense artifacts (na maganda para sa mga bot).
- Hindi laging nakakuha ang client ng eksaktong parehong data na available sa oras (t = 0) tulad ng server render (SSG data, blog metadata, atbp).
- "Lazy" by default ang aming i18n, ibig sabihin maaaring mawawala ang mga translation para sa unang render maliban kung itatala mo kung anong mga translation ang ginamit para sa SSG at i-inject sila bago mag-render ang React.
Ano ang Gumana (Unang Approach: Dehydration)
Sa una, sumubok ako ng isang clever at cute na bagay: gumamit ako ng command pattern para itala ang mga utos na ginamit upang i-resolve ang Suspense boundaries ng HTML, at ibinalik ang reverse transformation commands para maibalik ko ang HTML sa kung ano ang kailangan ng React para sa hydration.
Ang pag-asa ko ay maaari akong magpadala ng mas kakaunting bytes sa index.html sa command method na ito. Pero, tulad ng karamihan sa mga clever solutions, nabigo ito dahil minomodify ng mga browser ang HTML sa banayad na paraan, tulad ng pag-alis o pagdadagdag ng ; o /, na nagpapalihis sa replacement indices.
Sa technical, malamang ay maaari mong i-account ang mga banayad na pagbabago ng browser na ito, pero hindi ako magpapadala ng isang bagay na ganoon kabrittle.
Sa halip na subukang "i-reverse" ang Suspense-boundary transformation pabalik sa streaming markup ng React, ginawa ko ang isang sobrang simple:
I-bundle ang orihinal at hindi nare-resolve na HTML sa isang <script type="text">.
Gumana ang "dehydration" approach na ito, pero gumugol ako ng karagdagang araw para palitan ito ng mas magandang solusyon.
Ang Mas Magandang Approach: Critical Path Suspense Boundary Replacement
Pagkatapos ng unang implementation, may mga isyu pa rin akong naranasan sa Suspense boundaries. Iyon noong nag-realize ako na may mas malinis, mas maganda, at mas simpleng solusyon. Pinalitan ko ang dehydration approach ng critical path Suspense boundary replacement, na:
- Iniload ang critical path bago ang hydration: Ang mga component na pre-loaded sa SSR ay tinukoy at pre-loaded sa client bago tawagin ang
hydrateRoot
- Mas simple sa pag-maintain: Walang React internals o AST parsing na kinakailangan (kailangan ng dehydration approach na mag-parse at magbalik ng HTML)
- Mas kakaunting bytes ang ipinapadala: Hindi na namin pino-bundle ang orihinal na SSR response mula sa React sa isang script tag
- Pinipigilan ang potensyal na flash: Hindi na kailangang i-dehydrate/i-rehydrate ang HTML, tinatanggal ang potensyal na visual flash
Sinusubaybayan ng implementation kung anong mga lazy components ang pre-loaded sa SSR (sa pamamagitan ng SSRLazyComponentTracker), kasama ang kanilang import paths sa hydration data, at pre-loads sila nang synchronous bago ang hydration. Ang critical path components ay nire-render nang direkta nang walang Suspense boundaries, na tumutugma nang eksakto sa output ng SSR.
Para sa lahat ng iba pa, ginagawa naming kumilos bilang SSR/SSG ang unang client render. Ibig sabihin gumagamit ng parehong inputs, at ginagawang available ang mga input na iyon nang synchronous bago ang hydrateRoot. Ginagawa ito sa pamamagitan ng pag-bundle sa pamamagitan ng aming "ssg-data".
Sa kongkretong paraan, ang mga adjustments ay:
I-bundle ang SSR inputs sa isang text script
- Sa SSG, nag-i-inject kami ng
<script type="text/foony-ssg" id="foony-ssg-data">...</script> bago lang ang Vite module entrypoint.
- Naglalaman ang script na iyon ng:
html: ang resolved HTML na aktwal naming pinadala sa static file
ssgData: ang serialized SSGData na ginagamit ng SSR wrapper. Plano kong i-update ito sa Proxy o iba pa para tanging accessed na data lang ang isasama.
translationData: ang translation key-value blobs na ginalaw namin sa SSR
I-inject ang mga input na iyon bago lang ang hydration
- Sa
main.tsx, sa synchronous na paraan:
- itinatakda ang
#root.innerHTML sa serialized resolved HTML (kaya ang DOM ay eksakto kung ano ang nakikita ng hydration)
- binabalot ang app sa
SSGDataProvider para magkaroon ang mga component ng parehong SSGData sa unang render
Gawing instant ang i18n sa pamamagitan ng pag-inject ng translation values
- Itinatala namin ang aktwal na translation objects na na-access sa SSR at ipinapadala sila sa SSG script.
- Sa client, ini-inject namin sila nang direkta sa cache ng
LocaleQueryer sa pamamagitan ng dedicated na LocaleQueryer.inject() method, kaya available kaagad ang mga translation.
At gamit nito, mayroon na ang first render ng parehong data na mayroon ang SSR!
Naka-implement na ang useIsSSRMode() hook sa 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;
}
Nagbabalik ang hook na ito ng true sa SSR at sa unang client render (hydration), pagkatapos ay lumipat sa false pagkatapos ng mount. Ang mga component tulad ng UserBanner, Navbar, at Dialog ay gumagamit na nito upang maiwasan ang hydration mismatches.
- I-patch ang React para sa mas magandang diffs
Umaasa ako na pwede ko lang gamitin ang hydration-overlay. Pero hindi ito aktibong ina-maintain, sinusuportahan lang hanggang React 18, at hindi pa production-ready. Kaya pinakopya ko sa LLM ang repo para sa inspirasyon, at pagkatapos gumawa ito ng minimal hydration overlay sa loob ng ilang minuto. Hindi ako nangangailangan ng anumang fancy, isang bagay lang na ipapakita habang nag-de-develop para malaman ko kung saan nagkamali.
Sobrang basic ang bagong overlay na ito, kaya hindi masyadong perpekto ang diffs. Inaalis ng React ang mga komento, nagdadagdag ng ; pagkatapos ng style attributes, minomodify ang whitespace, at iba pang mga maliliit na bagay na hindi ina-account ng aming overlay (sa ngayon). Kasama rin sa aming overlay ang mga HTML comments na binabalewala ng React para sa hydration nito.
<img alt="Aming bagong hydration overlay" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_overlay.webp" style={{ margin: "8px auto", height: 315, display: "block" }} />
Pero sapat na ito para malaman kung ano ang kailangang ayusin.
<img alt="diff ng aming SSG vs client first-page render para sa React hydration" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_diff.webp" style={{ margin: "8px auto", height: 85, display: "block" }} />
Sa Mga Numero
Para mabigyan ka ng ideya kung ano ang kasangkot sa implementasyong ito:
- 2 araw ng trabaho (mula sa simula hanggang sa gumaganang SSG). Sobra-sobra lang sa 24 oras habang nasa bakasyon.
- 4 na araw ng trabaho upang umayos ang hydration nang walang async translation races o
useMediaQuery na nakakasira ng mga bagay.
- 1 karagdagang araw upang palitan ang dehydration approach ng critical path Suspense boundary replacement (mas simple, mas kakaunting bytes, walang potensyal na flash).
- ~200 na linya ng core SSG generation code (
GenerateShellSsgFromSitemap.ts)
- ~120 na linya ng Suspense boundary resolution (
resolveSuspenseBoundaries sa renderRoute.tsx). Tandaan: Ito ay pinalitan na ng critical path approach
- ~50 na linya ng SSR utilities (
isSSRMode.ts)
- ~100 na linya ng tests (
renderRoute.test.ts)
- ~150 na linya ng polyfills para sa SSR (
setupSSREnvironment)
- Minimal na pagbabago sa existing components (karamihan ay pagdadagdag ng
useIsSSRMode() checks)
Magaan at maintainable ang solusyon. Hindi ito nangangailangan ng framework migration, at gumagana ito sa aming existing React SPA.
Mga Mahahalagang Punto
Minsan Mas Maganda ang Bespoke Solution
Hindi lahat ng problema ay nangangailangan ng framework. Para sa Foony, ang isang maliit na bespoke SSG solution ang tamang pagpipilian. Ito ay:
- Magaan: Walang mabibigat na dependencies o framework overhead
- Maintainable: Simpleng code na naiintindihan namin
- Flexible: Madaling i-modify at palawigin kung kinakailangan
- Compatible: Gumagana sa aming existing React SPA nang walang migration
May Mga Kakaibang Gawi ang Streaming SSR ng React
Maganda ang renderToReadableStream ng React para sa pagharap sa Suspense, pero may mga kakaibang gawi ito. Kahit pa may await stream.allReady, makakakuha ka pa rin ng Suspense boundaries sa output. Hindi ito bug, ginawa itong ganoon para sa streaming. Pero para sa SSG, kailangan namin ng ganap na na-resolve na HTML. Para itong pagkabigo ng React team na hindi pinangangasiwaan ang sitwasyong ito sa malinis na paraan.
Ang aking solusyon ay ang post-process ang HTML at i-resolve ang boundaries. Hindi ito maganda, pero mabilis at flexible enough para sa aking use case.
Maaaring Maging Kapaki-pakinabang ang TDD para sa LLMs
Madaling magkamali sa HTML transformation. Isang maliit na bug at maaari mong sirain ang buong SSG output at ang karanasan ng end-user. Pinagawa ko sa LLM ang malawakang tests (kasama ang aking input) upang masiguro na tama ang gumagana ng transformation.
Konklusyon
Gumagana na ngayon ang SSG para sa Foony. Ganap na nire-render ang mga page para sa search engines at LLMs, at ang solusyon ay maintainable at magaan. Mas matagal ang hydration para sa SSG routes kaysa sa inasahan ko (3 araw), at gumugol ako ng karagdagang araw upang palitan ang unang dehydration approach ng critical path Suspense boundary replacement. Mas simple sa pag-maintain ang bagong approach, mas kaunting bytes ang ipinapadala, at pinipigilan ang potensyal na visual flashes mula sa pag-de-dehydrate/pag-re-rehydrate ng HTML.
Nagulat pa rin ako na 2 araw lang inabot upang mag-implement ng bespoke solution para sa SSG. Pero minsan ang tamang solusyon ay ang pinakasimpleng isa.
Kasama sa future work ang pagkumpleto ng hydration matching at posibleng pag-patch ng React para sa mas magandang debugging. Pero sa ngayon, may gumaganang SSG ang Foony. Babantayan ko ang Google Search Console at ang Bing Webmaster Tools sa mga darating na linggo upang makita kung anong epekto ito sa aming SEO.