@ozsarman/clarityjs 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/islands.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Clarity.js — Islands Architecture
3
+ *
4
+ * Statik HTML sayfa üzerinde seçici hidrasyon (selective hydration).
5
+ * Astro Islands / React Server Components benzeri model.
6
+ *
7
+ * Çalışma prensibi:
8
+ * 1. Tüm sayfa sunucuda statik HTML olarak render edilir
9
+ * 2. Sadece `island:client` direktifi olan bileşenler client JS yükler
10
+ * 3. Diğer bileşenler sıfır JavaScript gönderir
11
+ *
12
+ * ── Kullanım (.clarity içinde) ────────────────────────────────────────────────
13
+ *
14
+ * component App() {
15
+ * render {
16
+ * <div>
17
+ * <StaticHeader /> // Server-only — sıfır JS
18
+ * <Counter island:client /> // İnteraktif adacık — JS yüklenir
19
+ * <StaticFooter /> // Server-only — sıfır JS
20
+ *
21
+ * // Yükleme stratejisi seçimi:
22
+ * <HeavyWidget island:client="visible" /> // Görünür olunca
23
+ * <Analytics island:client="idle" /> // Tarayıcı boştayken
24
+ * <Modal island:client="load" /> // Sayfa yüklenir yüklenmez
25
+ * <Tooltip island:client="hover" /> // Hover'da
26
+ * </div>
27
+ * }
28
+ * }
29
+ *
30
+ * ── Programatik API ───────────────────────────────────────────────────────────
31
+ *
32
+ * import { createIsland, hydrateIslands } from '@ozsarman/clarityjs/islands';
33
+ *
34
+ * // Server: adacık HTML'i oluştur + hidrasyon şeridi göm
35
+ * const html = createIsland(Counter, { initialCount: 0 }, { strategy: 'visible' });
36
+ *
37
+ * // Client: sayfadaki tüm adacıkları bul ve hidrate et
38
+ * hydrateIslands();
39
+ *
40
+ * Author: Claude (Anthropic) + Özdemir Sarman
41
+ */
42
+
43
+ import { renderToString } from './ssr.js';
44
+
45
+ // ─── Adacık stratejileri ──────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * @typedef {'load' | 'idle' | 'visible' | 'hover' | 'none'} IslandStrategy
49
+ *
50
+ * load – sayfa yüklenince hemen hidrate et (varsayılan)
51
+ * idle – requestIdleCallback ile boş anında
52
+ * visible – IntersectionObserver ile görünür olunca
53
+ * hover – fareyle üzerine gelince
54
+ * none – hiçbir zaman (sadece statik HTML)
55
+ */
56
+
57
+ // ─── Server-side: adacık oluştur ──────────────────────────────────────────────
58
+
59
+ /**
60
+ * Bir bileşeni sunucuda render et + client hidrasyon bilgisini göm.
61
+ *
62
+ * @param {Function} ComponentFn – Clarity component fonksiyonu
63
+ * @param {object} [props={}] – bileşen prop'ları
64
+ * @param {object} [opts={}]
65
+ * @param {IslandStrategy} [opts.strategy='load'] – hidrasyon stratejisi
66
+ * @param {string} [opts.id] – benzersiz adacık kimliği (otomatik üretilir)
67
+ * @param {string} [opts.import] – client bundle'da bileşenin import yolu
68
+ * @returns {string} HTML string (hidrasyon metadata'sı gömülü)
69
+ */
70
+ export function createIsland(ComponentFn, props = {}, opts = {}) {
71
+ const {
72
+ strategy = 'load',
73
+ id = `island-${Math.random().toString(36).slice(2, 8)}`,
74
+ importPath = null,
75
+ } = opts;
76
+
77
+ // Bileşeni sunucuda render et
78
+ const innerHTML = renderToString(ComponentFn, { props });
79
+
80
+ // Hidrasyon verisi — JSON olarak data attribute'a gömülür
81
+ const islandData = JSON.stringify({
82
+ component: ComponentFn.name || ComponentFn.displayName || 'Unknown',
83
+ props,
84
+ strategy,
85
+ importPath,
86
+ });
87
+
88
+ return `<clarity-island
89
+ id="${id}"
90
+ data-strategy="${strategy}"
91
+ data-island='${_escapeAttr(islandData)}'
92
+ style="display:contents"
93
+ >${innerHTML}</clarity-island>`;
94
+ }
95
+
96
+ // ─── Client-side: adacıkları hidrate et ──────────────────────────────────────
97
+
98
+ /**
99
+ * Sayfadaki tüm `<clarity-island>` elementlerini bulup hidrate eder.
100
+ * Client entry point'te çağrılır.
101
+ *
102
+ * @param {object} [componentMap={}] – bileşen adı → ComponentFn eşlemesi
103
+ * (dinamik import desteklenmiyorsa)
104
+ */
105
+ export function hydrateIslands(componentMap = {}) {
106
+ if (typeof document === 'undefined') return; // SSR ortamda çalışmasın
107
+
108
+ const islands = document.querySelectorAll('clarity-island');
109
+ if (islands.length === 0) return;
110
+
111
+ console.log(`[clarity/islands] ${islands.length} adacık bulundu`);
112
+
113
+ for (const el of islands) {
114
+ _scheduleHydration(el, componentMap);
115
+ }
116
+ }
117
+
118
+ function _scheduleHydration(el, componentMap) {
119
+ const strategy = el.dataset.strategy || 'load';
120
+ const data = _parseIslandData(el);
121
+ if (!data) return;
122
+
123
+ switch (strategy) {
124
+ case 'load':
125
+ _hydrateElement(el, data, componentMap);
126
+ break;
127
+
128
+ case 'idle':
129
+ if ('requestIdleCallback' in window) {
130
+ requestIdleCallback(() => _hydrateElement(el, data, componentMap));
131
+ } else {
132
+ setTimeout(() => _hydrateElement(el, data, componentMap), 200);
133
+ }
134
+ break;
135
+
136
+ case 'visible': {
137
+ const observer = new IntersectionObserver((entries) => {
138
+ for (const entry of entries) {
139
+ if (entry.isIntersecting) {
140
+ observer.unobserve(el);
141
+ _hydrateElement(el, data, componentMap);
142
+ }
143
+ }
144
+ }, { rootMargin: '50px' });
145
+ observer.observe(el);
146
+ break;
147
+ }
148
+
149
+ case 'hover':
150
+ el.addEventListener('mouseenter', () => _hydrateElement(el, data, componentMap), { once: true });
151
+ el.addEventListener('touchstart', () => _hydrateElement(el, data, componentMap), { once: true, passive: true });
152
+ break;
153
+
154
+ case 'none':
155
+ // Statik — hidrasyonsuz
156
+ break;
157
+
158
+ default:
159
+ _hydrateElement(el, data, componentMap);
160
+ }
161
+ }
162
+
163
+ async function _hydrateElement(el, data, componentMap) {
164
+ if (el.dataset.hydrated) return;
165
+ el.dataset.hydrated = 'true';
166
+
167
+ let ComponentFn = componentMap[data.component];
168
+
169
+ // Dinamik import denemeleri
170
+ if (!ComponentFn && data.importPath) {
171
+ try {
172
+ const mod = await import(data.importPath);
173
+ ComponentFn = mod[data.component] ?? mod.default;
174
+ } catch (err) {
175
+ console.error(`[clarity/islands] import hatası (${data.importPath}): ${err.message}`);
176
+ return;
177
+ }
178
+ }
179
+
180
+ if (!ComponentFn) {
181
+ console.warn(`[clarity/islands] Bileşen bulunamadı: ${data.component}`);
182
+ return;
183
+ }
184
+
185
+ try {
186
+ const { hydrateRoot } = await import('./hydrate.js');
187
+ hydrateRoot(ComponentFn, el, data.props);
188
+ console.log(`[clarity/islands] ✅ ${data.component} (${data.strategy})`);
189
+ } catch (err) {
190
+ console.error(`[clarity/islands] hidrasyon hatası (${data.component}): ${err.message}`);
191
+ }
192
+ }
193
+
194
+ function _parseIslandData(el) {
195
+ try {
196
+ return JSON.parse(el.dataset.island || '{}');
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ // ─── defineServerComponent ────────────────────────────────────────────────────
203
+
204
+ /**
205
+ * Bir bileşeni sunucu bileşeni olarak işaretle.
206
+ * Bu bileşenler client bundle'a dahil edilmez.
207
+ *
208
+ * @param {Function} ComponentFn
209
+ * @returns {Function} Aynı fonksiyon + __clarity_server__ = true işareti
210
+ */
211
+ export function defineServerComponent(ComponentFn) {
212
+ ComponentFn.__clarity_server_only__ = true;
213
+ return ComponentFn;
214
+ }
215
+
216
+ /**
217
+ * Bir bileşen sunucu bileşeni mi?
218
+ */
219
+ export function isServerComponent(ComponentFn) {
220
+ return !!ComponentFn.__clarity_server_only__;
221
+ }
222
+
223
+ // ─── Vite plugin: islands dönüşümü ───────────────────────────────────────────
224
+
225
+ /**
226
+ * Vite transform hook'u — `island:client` direktifini işler.
227
+ * clarityPlugin() içinde otomatik kullanılır.
228
+ *
229
+ * `island:client` direktifi olan JSX elementini:
230
+ * <Counter island:client="visible" initialCount={0} />
231
+ *
232
+ * Buna dönüştürür (server):
233
+ * createIsland(Counter, { initialCount: 0 }, { strategy: 'visible' })
234
+ *
235
+ * @internal
236
+ */
237
+ export function transformIslandDirective(code, id) {
238
+ // island:client direktifini tara — basit regex tabanlı dönüşüm
239
+ // CodeGenerator'dan geçtikten sonra üretilen JS'e uygulanır
240
+ return code.replace(
241
+ /(\w+)\(\{\s*([^}]*)"island:client"\s*:\s*"([^"]*)"([^}]*)\}\)/g,
242
+ (match, fnName, beforeProps, strategy, afterProps) => {
243
+ const props = (beforeProps + afterProps).replace(/,\s*,/g, ',').trim().replace(/^,|,$/g, '');
244
+ return `createIsland(${fnName}, {${props}}, { strategy: '${strategy || 'load'}' })`;
245
+ }
246
+ );
247
+ }
248
+
249
+ // ─── SSR helper: adacıkları HTML'e göm ───────────────────────────────────────
250
+
251
+ /**
252
+ * SSR sırasında bir sayfanın adacıklarını render et.
253
+ * renderToDocument() ile birlikte kullanım içindir.
254
+ *
255
+ * @param {Function} PageComponent
256
+ * @param {object} props
257
+ * @param {object} islandDefs – { ComponentName: { ComponentFn, strategy } }
258
+ * @returns {string} HTML
259
+ */
260
+ export function renderPageWithIslands(PageComponent, props = {}, islandDefs = {}) {
261
+ // Server ortamında adacıkları statik HTML + hidrasyon metadata'sı ile render et
262
+ // global'e adacık factory'leri kaydet — ssr.js render sırasında bunları kullanır
263
+ if (typeof globalThis !== 'undefined') {
264
+ globalThis.__clarity_islands__ = islandDefs;
265
+ }
266
+
267
+ try {
268
+ return renderToString(PageComponent, { props });
269
+ } finally {
270
+ if (typeof globalThis !== 'undefined') {
271
+ delete globalThis.__clarity_islands__;
272
+ }
273
+ }
274
+ }
275
+
276
+ // ─── Util ─────────────────────────────────────────────────────────────────────
277
+
278
+ function _escapeAttr(str) {
279
+ return String(str)
280
+ .replace(/&/g, '&amp;')
281
+ .replace(/'/g, '&#39;')
282
+ .replace(/</g, '&lt;')
283
+ .replace(/>/g, '&gt;');
284
+ }
package/src/isr.js ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Clarity.js — Incremental Static Regeneration (ISR)
3
+ *
4
+ * Belirli aralıklarla veya istek üzerine statik sayfaları yeniler.
5
+ * Next.js revalidate / Nuxt routeRules ISR eşdeğeri.
6
+ *
7
+ * ── Kullanım (getStaticProps içinde) ─────────────────────────────────────────
8
+ *
9
+ * // pages/blog/[slug].js
10
+ * export async function getStaticProps({ params }) {
11
+ * const post = await db.posts.findBySlug(params.slug);
12
+ * return {
13
+ * props: { post },
14
+ * revalidate: 60, // 60 saniyede bir arka planda yeniden üret
15
+ * };
16
+ * }
17
+ *
18
+ * ── On-demand revalidation ────────────────────────────────────────────────────
19
+ *
20
+ * // pages/api/revalidate.js
21
+ * import { revalidatePath, revalidateTag } from '@ozsarman/clarityjs/isr';
22
+ *
23
+ * export async function POST(req, res) {
24
+ * const { path, secret } = req.body;
25
+ * if (secret !== process.env.REVALIDATE_SECRET) return res.status(401).json({ error: 'Unauthorized' });
26
+ *
27
+ * await revalidatePath(path, { pagesDir: './pages', outDir: './dist' });
28
+ * res.json({ revalidated: true });
29
+ * }
30
+ *
31
+ * ── Express middleware ────────────────────────────────────────────────────────
32
+ *
33
+ * import { createISRMiddleware } from '@ozsarman/clarityjs/isr';
34
+ *
35
+ * const isr = createISRMiddleware({ outDir: './dist', pagesDir: './pages' });
36
+ * app.use(isr); // Her istekte: bayat mı? → arka planda yenile → statik dosyayı sun
37
+ *
38
+ * Author: Claude (Anthropic) + Özdemir Sarman
39
+ */
40
+
41
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
42
+ import { existsSync } from 'node:fs';
43
+ import { join, dirname, resolve } from 'node:path';
44
+ import { pathToFileURL } from 'node:url';
45
+
46
+ // ─── ISR Metadata Store ───────────────────────────────────────────────────────
47
+
48
+ const ISR_META_FILE = '_clarity_isr.json';
49
+
50
+ /**
51
+ * ISR metadata'yı diskten yükle.
52
+ * @param {string} outDir
53
+ * @returns {Promise<object>}
54
+ */
55
+ async function _loadMeta(outDir) {
56
+ const metaPath = join(resolve(outDir), ISR_META_FILE);
57
+ if (!existsSync(metaPath)) return {};
58
+ try {
59
+ const raw = await readFile(metaPath, 'utf8');
60
+ return JSON.parse(raw);
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ /**
67
+ * ISR metadata'yı diske kaydet.
68
+ */
69
+ async function _saveMeta(outDir, data) {
70
+ const metaPath = join(resolve(outDir), ISR_META_FILE);
71
+ await writeFile(metaPath, JSON.stringify(data, null, 2), 'utf8');
72
+ }
73
+
74
+ // ─── On-demand revalidation ───────────────────────────────────────────────────
75
+
76
+ /**
77
+ * Belirli bir yolu anında yeniden üret.
78
+ *
79
+ * @param {string} path – yenilenecek route (örn: '/blog/my-post')
80
+ * @param {object} opts
81
+ * @param {string} opts.pagesDir
82
+ * @param {string} opts.outDir
83
+ * @param {string} [opts.baseUrl]
84
+ * @param {string} [opts.clientScript]
85
+ * @param {string} [opts.title]
86
+ * @returns {Promise<boolean>} – başarıyla yenilendiyse true
87
+ */
88
+ export async function revalidatePath(path, opts = {}) {
89
+ const { pagesDir = 'pages', outDir = 'dist', baseUrl = '', clientScript = null, title = '' } = opts;
90
+
91
+ const meta = await _loadMeta(outDir);
92
+ const entry = meta[path];
93
+
94
+ if (!entry) {
95
+ console.warn(`[clarity/isr] revalidatePath: '${path}' ISR metadata bulunamadı`);
96
+ return false;
97
+ }
98
+
99
+ const success = await _regeneratePage(path, entry, { outDir, baseUrl, clientScript, title });
100
+
101
+ if (success) {
102
+ meta[path] = {
103
+ ...entry,
104
+ generatedAt: Date.now(),
105
+ expiresAt: Date.now() + entry.revalidate * 1000,
106
+ };
107
+ await _saveMeta(outDir, meta);
108
+ }
109
+
110
+ return success;
111
+ }
112
+
113
+ /**
114
+ * Tag'e göre birden fazla yolu yenile.
115
+ * getStaticProps'ta `tags: ['posts']` ile etiketlenmiş sayfaları yeniler.
116
+ *
117
+ * @param {string} tag
118
+ * @param {object} opts
119
+ */
120
+ export async function revalidateTag(tag, opts = {}) {
121
+ const { outDir = 'dist' } = opts;
122
+ const meta = await _loadMeta(outDir);
123
+ const paths = Object.entries(meta)
124
+ .filter(([, entry]) => Array.isArray(entry.tags) && entry.tags.includes(tag))
125
+ .map(([p]) => p);
126
+
127
+ console.log(`[clarity/isr] Tag '${tag}' için ${paths.length} sayfa yenileniyor...`);
128
+ const results = await Promise.all(paths.map(p => revalidatePath(p, opts)));
129
+ return results.every(Boolean);
130
+ }
131
+
132
+ // ─── Express ISR Middleware ───────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Express middleware factory — stale-while-revalidate pattern.
136
+ *
137
+ * Çalışma prensibi:
138
+ * 1. İstek gelir
139
+ * 2. Statik HTML dosyası var mı? → Varsa hemen sun
140
+ * 3. Dosya bayat mı (expiresAt geçti)? → Arka planda yenile (kullanıcıyı beklettirme)
141
+ * 4. Statik dosya yoksa → SSR ile üret, hem sun hem kaydet
142
+ *
143
+ * @param {object} opts
144
+ * @param {string} opts.outDir
145
+ * @param {string} opts.pagesDir
146
+ * @param {string} [opts.baseUrl]
147
+ * @param {string} [opts.clientScript]
148
+ * @param {string} [opts.title]
149
+ * @param {boolean} [opts.verbose]
150
+ * @returns {Function} Express middleware
151
+ */
152
+ export function createISRMiddleware(opts = {}) {
153
+ const {
154
+ outDir = 'dist',
155
+ pagesDir = 'pages',
156
+ baseUrl = '',
157
+ clientScript = null,
158
+ title = '',
159
+ verbose = false,
160
+ } = opts;
161
+
162
+ const log = (...args) => verbose && console.log('[clarity/isr]', ...args);
163
+
164
+ // Arka planda yenileme kuyrukta mı? Duplicate önlemek için
165
+ const _revalidating = new Set();
166
+
167
+ return async function isrMiddleware(req, res, next) {
168
+ // Sadece GET isteklerini yönet
169
+ if (req.method !== 'GET') return next();
170
+
171
+ const urlPath = req.path === '/' ? '/' : req.path.replace(/\/$/, '');
172
+ const outDirAbs = resolve(outDir);
173
+
174
+ // Statik HTML dosyasını ara
175
+ const htmlFile = urlPath === '/'
176
+ ? join(outDirAbs, 'index.html')
177
+ : join(outDirAbs, urlPath.slice(1), 'index.html');
178
+
179
+ const fileExists = existsSync(htmlFile);
180
+
181
+ if (fileExists) {
182
+ // Dosya var — hemen sun
183
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
184
+ res.setHeader('X-Clarity-SSG', 'hit');
185
+
186
+ // Bayatlık kontrolü — arka planda yenile
187
+ const meta = await _loadMeta(outDir);
188
+ const entry = meta[urlPath];
189
+ if (entry && Date.now() > entry.expiresAt && !_revalidating.has(urlPath)) {
190
+ _revalidating.add(urlPath);
191
+ log(`stale → arka planda yenileniyor: ${urlPath}`);
192
+ _regeneratePage(urlPath, entry, { outDir, baseUrl, clientScript, title })
193
+ .then(async (ok) => {
194
+ if (ok) {
195
+ meta[urlPath] = {
196
+ ...entry,
197
+ generatedAt: Date.now(),
198
+ expiresAt: Date.now() + entry.revalidate * 1000,
199
+ };
200
+ await _saveMeta(outDir, meta);
201
+ log(`✅ arka planda yenilendi: ${urlPath}`);
202
+ }
203
+ })
204
+ .finally(() => _revalidating.delete(urlPath));
205
+ }
206
+
207
+ return res.sendFile(htmlFile);
208
+ }
209
+
210
+ // Dosya yok — SSR ile üret ve kaydet
211
+ log(`miss → SSR ile üretiliyor: ${urlPath}`);
212
+ try {
213
+ const { renderToDocument } = await import('./ssr.js');
214
+ const { FileRouter } = await import('../packages/clarity-ssr/file-router.js');
215
+
216
+ const router = new FileRouter(resolve(pagesDir));
217
+ await router.scan();
218
+ const match = router.match(urlPath);
219
+
220
+ if (!match) return next();
221
+
222
+ const { route, params } = match;
223
+ let pageProps = params;
224
+
225
+ if (route.loader) {
226
+ const loaderData = await route.loader({ params, query: req.query, req });
227
+ pageProps = { ...params, loaderData };
228
+ }
229
+
230
+ const html = renderToDocument(route.component, {
231
+ props: pageProps, title: route.meta?.title ?? title, clientScript,
232
+ });
233
+
234
+ await mkdir(dirname(htmlFile), { recursive: true });
235
+ await writeFile(htmlFile, html, 'utf8');
236
+
237
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
238
+ res.setHeader('X-Clarity-SSG', 'generated');
239
+ res.send(html);
240
+ } catch (err) {
241
+ console.error(`[clarity/isr] SSR hata: ${err.message}`);
242
+ next(err);
243
+ }
244
+ };
245
+ }
246
+
247
+ // ─── Periyodik revalidation scheduler ────────────────────────────────────────
248
+
249
+ /**
250
+ * Tüm bayat ISR sayfalarını periyodik olarak yenile.
251
+ * Sunucu başlatılırken bir kez çağrıl.
252
+ *
253
+ * @param {object} opts
254
+ * @param {string} opts.outDir
255
+ * @param {number} [opts.interval=30000] – kontrol aralığı (ms)
256
+ * @returns {{ stop: Function }}
257
+ */
258
+ export function startISRScheduler(opts = {}) {
259
+ const { outDir = 'dist', interval = 30_000, ...rest } = opts;
260
+
261
+ const timer = setInterval(async () => {
262
+ const meta = await _loadMeta(outDir);
263
+ const stale = Object.entries(meta).filter(([, e]) => Date.now() > e.expiresAt);
264
+
265
+ if (stale.length === 0) return;
266
+ console.log(`[clarity/isr] ${stale.length} bayat sayfa yenileniyor...`);
267
+
268
+ for (const [path, entry] of stale) {
269
+ await revalidatePath(path, { outDir, ...rest });
270
+ }
271
+ }, interval);
272
+
273
+ return { stop: () => clearInterval(timer) };
274
+ }
275
+
276
+ // ─── Internal: single page regen ─────────────────────────────────────────────
277
+
278
+ async function _regeneratePage(routePath, entry, { outDir, baseUrl, clientScript, title }) {
279
+ try {
280
+ const { renderToDocument } = await import('./ssr.js');
281
+ const pageFile = resolve(entry.file);
282
+ const mod = await import(pathToFileURL(pageFile).href + `?t=${Date.now()}`);
283
+
284
+ let pageProps = entry.params ?? {};
285
+ if (typeof mod.getStaticProps === 'function') {
286
+ const result = await mod.getStaticProps({ params: entry.params ?? {} });
287
+ if (result?.notFound) return false;
288
+ pageProps = { ...pageProps, ...(result?.props ?? {}) };
289
+ }
290
+
291
+ const html = renderToDocument(mod.default, {
292
+ props: pageProps, title: mod.meta?.title ?? title, clientScript,
293
+ });
294
+
295
+ const htmlFile = routePath === '/'
296
+ ? join(resolve(outDir), 'index.html')
297
+ : join(resolve(outDir), routePath.slice(1), 'index.html');
298
+
299
+ await mkdir(dirname(htmlFile), { recursive: true });
300
+ await writeFile(htmlFile, html, 'utf8');
301
+ return true;
302
+ } catch (err) {
303
+ console.error(`[clarity/isr] regen hata (${routePath}): ${err.message}`);
304
+ return false;
305
+ }
306
+ }