

1/1/1970
मैंने 2 दिन में SSG कैसे इम्प्लिमेंट किया
नमस्ते! एक साल पहले तक, मुझे लगा था ये चीज़ practically नामुमकिन है। लेकिन अभी–अभी मैंने Foony के लिए Static Site Generation (SSG) सिर्फ 2 दिन में इम्प्लिमेंट कर लिया, और मैं इसके बारे में काफ़ी excited हूँ। ये Foony के लिए SSG solve करने की मेरी पहली कोशिश भी नहीं है। पहले मैं NextJS, Vike, Astro, Gatsby और कुछ और solutions देख चुका हूँ। मैंने NextJS के साथ एक बार शुरुआत भी की थी, लेकिन Foony की complex SPA और हज़ारों फाइलों की वजह से जल्दी ही दिक्कतें आने लगीं। Migration पूरा एक दुःस्वप्न बन जाता और महीनों ले लेता। साथ ही, इससे साइट पर काम करने वाले बाकियों के लिए भी extra complexity बढ़ जाती, क्योंकि सबको NextJS और उसकी quirks सीखनी पड़तीं।
मुझे कुछ हल्का, simple और आसानी से इम्प्लिमेंट होने वाला solution चाहिए था। ऐसा जो हमें अपना कोड वैसे ही लिखने दे, जैसे हम अभी लिख रहे हैं, बिना हर वक्त SSG के बारे में सोचने के (सिवाय useMediaQuery के - उससे बचने का कोई सही तरीका नहीं है)। नीचे मैं बताऊँगा कि मैंने bespoke solution क्यों चुना, कौन-कौन सी specific दिक्कतें आईं (खासकर React की Suspense boundaries के साथ), और मैंने उन्हें कैसे solve किया।
स्टैंडर्ड solutions क्यों नहीं?
जब मैंने पहली बार Foony में SSG जोड़ने के बारे में सोचा, तो naturally दिमाग में NextJS (industry standard), Vike और Astro आए।
NextJS: बहुत ज़्यादा migration
NextJS ताकतवर है, लेकिन उसे use करने के लिए Foony की existing React SPA का बहुत बड़ा migration करना पड़ता। हमारे पास हज़ारों फाइलें हैं, complex routing logic है, और काफ़ी सारा custom infrastructure है। NextJS पर जाने का मतलब होता:
- पूरा routing system दोबारा लिखना
- ये बदलना कि हम games और components कैसे load करते हैं
- सिर्फ feature parity वापस पाने के लिए ही महीनों का काम
- users के लिए potential breaking changes
- images handle करने का तरीका बदलना
- build times काफ़ी ज़्यादा slow हो जाते (शायद 5–30 मिनट के बीच। इसे support करने के लिए मेरे पास पक्के numbers नहीं हैं, बस ये GitHub पर 5 साल पुरानी discussion है)
- पूरी टीम को कुछ नया (NextJS) सीखना पड़ता, और dev velocity हमेशा के लिए धीमी पड़ जाती
- और हर बार जब NextJS breaking changes लाता, तो कोड को migrate करते रहना
मैंने NextJS के साथ एक बार false start भी लिया था, लेकिन जल्दी समझ में आ गया कि migration का खर्च बहुत ज़्यादा है। ये complexity इसके लायक नहीं थी।
Vike: लगभग वही complexity
Vike (पहले vite-plugin-ssr) के साथ भी लगभग वही दिक्कतें थीं। ये NextJS से ज़्यादा flexible है, लेकिन फिर भी हमारे codebase का heavy restructuring करना पड़ता। Learning curve और migration effort, मिलने वाले benefits से justify नहीं हो रहे थे।
Astro: गलत architecture
Astro content-heavy sites के लिए बढ़िया है, लेकिन Foony एक complex multiplayer game platform है। हमें real-time updates, WebSocket connections और dynamic React components की ज़रूरत है। Astro की architecture उस चीज़ से match नहीं बैठती जो हम बना रहे हैं।
Solution: Bespoke SSG
कुछ दिन पहले i18n के बाद जो "fake SSG" approach मैंने लगाई थी, उसी से हिम्मत लेकर मैंने Foony के लिए SSG का एक छोटा, हल्का और bespoke solution चुन लिया।
मेरी "fake SSG" approach में उन pages से blog post content खींचना शामिल था, जहाँ blogs हैं (
/postsroutes और game pages), और उन्हें ठीक वहीं position करना जहाँ client उन्हें render करता, खास तौर पर search engines और LLMs के लिए, ताकि वे Foony को बेहतर समझ सकें। इसके साथ ld+json schema और कुछ छोटे-मोटे SEO tweaks भी लगाए थे।
Approach काफ़ी simple था:
- Existing React SPA के ऊपर build करो: कोई migration नहीं, बस build time पर SSG generation जोड़ दो।
renderToReadableStreamuse करो: React 18 का streaming SSR API Suspense को native तरीके से handle कर लेता है।- Static HTML files generate करो: Build time पर routes pre-render करो और उन्हें static files की तरह serve करो, routes की list पाने के लिए हमारे SitemapGenerator का use करते हुए।
- Existing codebase में minimal changes: ज़्यादातर components वैसे ही चलते रहे जैसे पहले चल रहे थे।
Core implementation client/src/generators/GenerateShellSsgFromSitemap.ts में है। ये एक sitemap पढ़ता है, हर route को React के renderToReadableStream से render करता है, और HTML को static files में लिखता है। Simple, जैसी चीज़ें मुझे पसंद हैं!
ये काफ़ी तेज़ भी निकला। लगभग 2,800 routes 10 सेकंड में render हो गए। बढ़िया। NextJS, Gatsby और Astro से काफ़ी तेज़। <img alt="समय दिखाता SSG console log" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.ssg_speed.webp" style={{ margin: "8px auto", height: 120, display: "block" }} />
मैं simplicity के बारे में बहुत देर तक बात कर सकता हूँ। भले ही बड़ी कंपनियों में "कम complexity" की वजह से आपको promotion न मिले, simple code सुन्दर, maintainable होता है, और general तौर पर developer velocity के लिए ज़्यादा अच्छा होता है। ये चीज़ मैं Zen principles में सच में admire करता हूँ।
Suspense boundary वाली दिक्कत
अब मेरे पास SSG था, और content HTML में दिख भी रहा था... लेकिन मेरी pages खाली दिख रही थीं! ये कैसे हुआ?! <img alt="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" }} />
पता चला कि renderToReadableStream में फिर भी Suspense boundaries रहती हैं, भले ही आप await stream.allReady कर लो। मेरा अंदाज़ा है कि ऐसा इसलिए है क्योंकि ये एक "stream" है और design ही ये है कि bytes जैसे-जैसे आएँ, वैसे-वैसे client को भेजे जाएँ।
React क्या output देता है
जब आप Suspense के साथ renderToReadableStream use करते हो, React कुछ ऐसा HTML output करता है:
<!--$?-->
<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"> वो placeholder है जहाँ content जाना चाहिए। <div hidden id="S:0"> के अंदर actual rendered content होता है। B:0 और S:0 नंबर से match करते हैं (0-based index)।
बिना JavaScript के, search engines (हाँ, तुम्हीं हो Bing) और LLMs को लगभग खाली page दिखेगा जिसमें बस template placeholder होगा। और ये SSG करने के पूरे मक़सद को ही हरा देता है!
मुझे Suspense boundaries हटाने का कोई साफ़-सुथरा तरीका नहीं दिख रहा था, तो मैंने कुछ tests लिखे और एक resolveSuspenseBoundaries function बनाया जो इन्हें swap कर दे। ये HTML parse करके script execute करने (जैसे JSDOM के साथ) से तेज़ था। और सबसे ज़रूरी, ये उस चीज़ के लिए ज़रूरी भी था जो मैं plan कर रहा था: search engines / LLMs के लिए बिना JavaScript के भी साफ़, readable साइट, जो फिर भी Suspense boundaries और client-side hydration support करे।
Transformation को test करना
मैंने DOM में जो examples दिख रहे थे, उनमें से कुछ उठा कर सबसे पहले transformation के लिए tests लिखने शुरू किए: एक वो जो मेरे पास था (JavaScript disabled), और दूसरा वो जैसा मैं चाहता था (JavaScript enabled)। इन्हें मैंने एक LLM को feed किया और उससे test generation handle करवाया, जिसमें वो काफ़ी अच्छा है।
ये tests client/src/generators/ssr/renderRoute.test.ts में हैं और ensure करते हैं कि transformation सही चले। Tests में ये cases cover हैं:
- Simple boundary replacement (blog listing)
- Complex boundaries, जहाँ template और closing comment के बीच content भी है
- Multiple boundaries
- ऐसे boundaries जिनके पास comment markers नहीं हैं
- Edge cases
ऐसा वाला "TDD" इस तरह के use case में काफ़ी काम का होता है, जहाँ आपके पास expected inputs और outputs पहले से हैं।
इसे "Robert C. Martin ने कहा है इसलिए हर चीज़ पर TDD लगा दो" वाले TDD से confuse मत करो (जो आपकी team की development velocity धीमी कर देगा)। आपको UI या code के उन हिस्सों पर TDD नहीं लगाना चाहिए जहाँ चीज़ें लगातार बदलती रहती हैं!
Solution: resolveSuspenseBoundaries
Tests तैयार हो गए, तो मैंने LLM से resolveSuspenseBoundaries function लिखवाया। मैंने इसके लिए cheerio चुना ताकि RegEx की brittleness से बच सकूँ, भले ही RegEx use करने से SSG time लगभग 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};
}
इससे ये सुनिश्चित हो जाता है कि लगभग खाली page दिखने के बजाय, search engines और LLMs को पूरा rendered page दिखे।
अब हमारे पास बिना JavaScript के भी SSG अच्छी तरह से काम कर रहा है!
<img alt="Foony के blogs के लिए बिना JavaScript वाला 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" }} />
Long-term में ये हो सकता है कि React अपना Suspense format बदल दे। जब मेरे पास lazy-loaded pages (जिन्हें Suspense boundaries चाहिए) के लिए बेहतर solution होगा, तब मैं शायद Suspense resolution वाला code हटा दूँ।
Hydration strategy (अपडेट: इसमें लगा 3 दिन + 1 extra दिन)
Hydration मुश्किल होता है, ये मुझे पहले से पता था। लेकिन थोड़े से काम के बाद, आख़िरकार मैं इसे चलाने में सफल हो गया!
Hydration के लिए कुल समय: 3 दिन, और dehydration approach बदलने के लिए extra 1 दिन।
सबसे tricky हिस्सा था वो पहला, minimal, काम करने वाला hydrate हासिल करना। जैसे ही मैं navbar के साथ एक "Hello World" render करने में सफल हुआ, confidence आ गया कि हाँ, ये पूरा महीना नहीं खाएँगा!
<img alt="Foony का Hello World navbar के साथ सफलतापूर्वक hydrate होता हुआ" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_mvp.webp" style={{ margin: "8px auto", height: 205, display: "block" }} />
उस पहले minimal, working hydrate के लिए मेरे पास एक अनोखी challenge थी: मुझे hydration चाहिए थी, लेकिन साथ ही मुझे search engines और LLMs के लिए अच्छा SEO भी चाहिए था, वो भी ऐसे कि developers को Suspense boundaries के बारे में सोचना न पड़े।
Challenge
React hydration बहुत literal है: अगर DOM पहले render पर React की expectation जैसा नहीं दिखता, तो console में एक प्यारा सा, लगभग बेकार error message आता है, और React सब कुछ फेंककर फिर से scratch से re-render कर देता है। कोई diff भी नहीं देता कि कहाँ गड़बड़ हुई!
हमारे case में SSG ने इसे दो तरीकों से और बदतर बना दिया:
- हम HTML को post-process करके React 18 की streaming Suspense artifacts resolve/हटा रहे थे (जो bots के लिए तो बढ़िया है)।
- Client के पास time (t = 0) पर हमेशा वही data available नहीं होता था जो server render के पास था (SSG data, blog metadata, वगैरह)।
- हमारा i18n default से “lazy” है, मतलब पहली render पर translations missing हो सकती हैं, जब तक कि आप SSG के दौरान use हुई translations को record करके React के render करने से पहले inject न कर दो।
क्या काम किया (initial approach: Dehydration)
शुरू में मैंने कुछ clever और cute try किया: मैंने एक command pattern use किया ताकि HTML की Suspense boundaries resolve करने के लिए जो commands लग रहे थे, उन्हें record कर सकूँ और फिर उनकी उलटी commands वापस कर सकूँ, ताकि hydration के समय HTML को फिर से उसी रूप में restore कर पाऊँ जैसा React को चाहिए।
मेरी उम्मीद थी कि इस command method से हम index.html में बहुत कम bytes ship कर पाएँगे। लेकिन जैसे ज़्यादातर clever solutions के साथ होता है, ये fail हो गया, क्योंकि browsers HTML को हल्के-फुल्के तरीक़े से modify कर देते हैं, जैसे कहीं ; या / हटा देना या जोड़ देना, और इससे हमारे replacement indices बिगड़ जाते थे।
Technically आप शायद इन छोटे browser changes को manually account कर सकते हो, लेकिन मैं इतना brittle solution ship करने वाला नहीं था।
Suspense-boundary transformation को React की streaming markup में वापस "reverse" करने की कोशिश करने के बजाय, मैंने एक super simple काम किया:
Original, unresolved HTML को एक <script type="text"> में bundle कर दो।
ये "dehydration" approach काम कर रही थी, लेकिन मैंने इसे replace करने के लिए extra एक दिन और लगाया, ताकि better solution मिल सके।
बेहतर approach: Critical path Suspense boundary replacement
Initial implementation के बाद भी Suspense boundaries से जुड़ी कुछ दिक्कतें आ रही थीं। तब मुझे एहसास हुआ कि इसकी जगह एक साफ़, बेहतर और simple solution हो सकता है। मैंने dehydration approach को replace करके critical path Suspense boundary replacement लगा दिया, जो ये काम करता है:
- Hydration से पहले critical path load करता है: जो components SSR के दौरान preload हुए थे, उन्हें client पर
hydrateRoot call होने से पहले ही preload कर लिया जाता है
- Maintain करना आसान है: इसमें React internals या AST parsing की ज़रूरत नहीं (dehydration वाले approach में HTML parse करके restore करना पड़ता था)
- कम bytes ship करता है: अब हमें React का original SSR response script tag में bundle नहीं करना पड़ता
- Potential flash रोकता है: HTML को dehydrate/rehydrate करने की ज़रूरत नहीं, तो visual flash का risk भी नहीं
Implementation ये track करता है कि SSR के दौरान कौन से lazy components preload हुए थे (via SSRLazyComponentTracker), उनके import paths को hydration data में include करता है, और hydration से पहले client पर उन्हें synchronously preload करता है। Critical path वाले components सीधे render हो जाते हैं, बिना Suspense boundaries के, और SSR output से बिल्कुल match करते हैं।
बाकी सब के लिए, हम पहली client render को ही SSR/SSG जैसा behave कराते हैं। इसका मतलब वही inputs use करना, और उन्हें hydrateRoot से पहले synchronously available करा देना। ये हमारा "ssg-data" bundle करके किया जाता है।
थोड़ा और concrete रूप में, ये changes किए:
SSR inputs को एक single text script में bundle करना
- SSG के दौरान हम Vite module entrypoint से ठीक पहले एक
<script type="text/foony-ssg" id="foony-ssg-data">...</script> inject करते हैं।
- उस script के अंदर होता है:
html: resolved HTML जिसे हम static file में ship कर रहे हैं
ssgData: serialized SSGData जो SSR wrapper use करता है। मैं plan कर रहा हूँ इसे Proxy वगैरह से update करने का, ताकि सिर्फ वही data रहे जिसका सच में इस्तेमाल हुआ।
translationData: translations के वो key-value blobs जिन्हें SSR के दौरान touch किया गया
Hydration से ठीक पहले ये inputs inject करना
main.tsx में हम synchronously:
#root.innerHTML को serialized resolved HTML से set करते हैं (ताकि DOM बिल्कुल वही हो जैसा hydration देख रहा है)
- app को
SSGDataProvider में wrap करते हैं ताकि पहली render पर components के पास वही SSGData हो जो SSR के पास था
i18n को instant बनाना, translation values inject करके
- हम SSR के दौरान access किए गए actual translation objects को record करते हैं और उन्हें SSG script के साथ ship करते हैं।
- Client पर हम उन्हें सीधे
LocaleQueryer की cache में LocaleQueryer.inject() method के ज़रिए inject कर देते हैं, ताकि translations तुरंत available हों।
और इस तरह पहली render पर data वही होता है जो SSR के पास था!
useIsSSRMode() hook पहले से client/src/generators/ssr/isSSRMode.ts में implemented है:
export function useIsSSRMode(): boolean {
const [isSSRMode, setIsSSRMode] = React.useState(true);
React.useEffect(() => {
// After mount (hydration complete), switch to client mode
setIsSSRMode(false);
}, []);
return isSSRMode;
}
ये hook SSR और पहली client render (hydration) के दौरान true return करता है, और mount के बाद false हो जाता है। UserBanner, Navbar और Dialog जैसे components पहले ही hydration mismatches रोकने के लिए इसे use कर रहे हैं।
- React को बेहतर diffs के लिए patch करना
मैं उम्मीद कर रहा था कि मैं बस hydration-overlay use कर पाऊँगा। लेकिन ये actively maintained नहीं है, सिर्फ React 18 तक support करता है, और production-ready भी नहीं था। तो मैंने inspiration के लिए LLM से repo clone करवाया, और फिर उसी ने कुछ ही मिनटों में एक minimal hydration overlay बना दिया। मुझे कुछ fancy नहीं चाहिए था, बस इतना कि development के दौरान दिख जाए कि कहाँ गड़बड़ है।
ये नया overlay बहुत basic है, तो diffs पूरी तरह perfect नहीं हैं। React comments हटा देता है, style attributes के बाद ; जोड़ देता है, whitespace modify कर देता है और कुछ और छोटी-मोटी चीज़ें करता है जिनका हमारा overlay अभी तक हिसाब नहीं रखता। हमारा overlay HTML comments भी दिखाता है जिन्हें React अपनी hydration में ignore कर देता है।
<img alt="हमारा नया 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" }} />
लेकिन इतना काफ़ी है यह समझने के लिए कि कहाँ fix चाहिए।
<img alt="React hydration के लिए SSG vs client की पहली render के बीच diff" loading="lazy" src="/img/posts/en/how-i-implemented-ssg-in-2-days.hydration_diff.webp" style={{ margin: "8px auto", height: 85, display: "block" }} />
Numbers में नज़रिया
ताकि आपको अंदाज़ा लगे कि इस implementation में क्या-क्या शामिल रहा:
- 2 दिन का काम (शुरुआत से working SSG तक)। ये छुट्टी के दौरान लगभग 24 घंटे का काम था।
- 4 दिन का काम, hydration को इस तरह set करने में कि async translation races या
useMediaQuery जैसी चीज़ें गड़बड़ न करें।
- 1 extra दिन dehydration approach को critical path Suspense boundary replacement से बदलने के लिए (ज़्यादा simple, कम bytes, कोई potential flash नहीं)।
- ~200 lines core SSG generation code (
GenerateShellSsgFromSitemap.ts)
- ~120 lines Suspense boundary resolution (
renderRoute.tsx में resolveSuspenseBoundaries) - नोट: बाद में इसे critical path approach से replace कर दिया गया
- ~50 lines SSR utilities (
isSSRMode.ts)
- ~100 lines tests (
renderRoute.test.ts)
- ~150 lines SSR के लिए polyfills (
setupSSREnvironment)
- Minimal changes existing components में (ज़्यादातर
useIsSSRMode() checks जोड़ने वाले)
Solution lightweight और maintainable है। इसके लिए framework migration की ज़रूरत नहीं पड़ी, और ये हमारी existing React SPA के साथ काम करता है।
क्या-क्या सीखा
कभी-कभी bespoke solution ही best होता है
हर problem के लिए framework की ज़रूरत नहीं होती। Foony के लिए छोटा, bespoke SSG solution ही सही choice निकला। ये:
- Lightweight है: कोई भारी dependency या framework overhead नहीं
- Maintainable है: simple code है, जिसे हम खुद अच्छे से समझते हैं
- Flexible है: ज़रूरत पड़ने पर आसानी से बदला और बढ़ाया जा सकता है
- Compatible है: हमारी existing React SPA के साथ बिना migration के काम करता है
React का streaming SSR थोड़ी अजीबियाँ लेकर आता है
React का renderToReadableStream Suspense handle करने के लिए अच्छा है, लेकिन इसकी अपनी quirks हैं। await stream.allReady करने के बाद भी output में Suspense boundaries रहती हैं। ये bug नहीं है, design का हिस्सा है, streaming के लिए। लेकिन SSG के लिए हमें पूरी तरह resolve हुआ HTML चाहिए। ऐसा लगता है जैसे React team ने इस use case को साफ़ तरीके से handle नहीं किया।
मेरी approach थी HTML को post-process करके boundaries resolve करना। ये शायद खूबसूरत नहीं दिखता, लेकिन मेरे use case के लिए तेज़ और काफ़ी flexible है।
TDD, LLMs के लिए काफ़ी काम का हो सकता है
HTML transformation आसानी से टूट सकता है। एक छोटी सी bug भी पूरे SSG output को बिगाड़ सकती है और end-user experience ख़राब कर सकती है। मैंने LLM से (अपनी input के साथ) comprehensive tests लिखवाए ताकि ये ensure हो सके कि transformation सही से काम कर रही है।
निष्कर्ष
अब Foony के लिए SSG काम कर रहा है। Pages search engines और LLMs के लिए पूरी तरह rendered हैं, और solution lightweight और maintainable है। SSG routes के लिए hydration करने में मेरी उम्मीद से ज़्यादा समय लगा (3 दिन), और dehydration approach को critical path Suspense boundary replacement से बदलने में मैंने extra एक दिन और लगाया। नया approach maintain करने में आसान है, कम bytes ship करता है और HTML को dehydrate/rehydrate करने से होने वाले संभावित visual flashes से बचाता है।
मैं अब भी थोड़ा हैरान हूँ कि SSG के लिए bespoke solution इम्प्लिमेंट करने में सिर्फ 2 दिन लगे। लेकिन कई बार सही solution वही होता है जो सबसे simple हो।
आगे का काम है hydration matching को पूरा करना और शायद React को बेहतर debugging के लिए patch करना। लेकिन फिलहाल, Foony के पास working SSG है। आने वाले हफ्तों में मैं Google Search Console और Bing Webmaster Tools पर नज़र रखूँगा, ताकि देख सकूँ कि इसका हमारे SEO पर कितना असर पड़ता है।