jskelet 0.6.3 → 0.6.4
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/AGENTS.md +136 -136
- package/CHANGELOG.md +628 -620
- package/LICENSE +21 -21
- package/README.md +2 -0
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -310
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +667 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1467 -1443
- package/docs/07-yapilandirma.md +1208 -1197
- package/docs/08-build.md +429 -429
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +348 -338
- package/docs/12-panel-ve-oturum.md +479 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -329
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +675 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1476 -1453
- package/docs/en/07-configuration.md +1229 -1219
- package/docs/en/08-build.md +447 -447
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +351 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +489 -488
- package/docs/en/README.md +87 -87
- package/package.json +137 -137
- package/src/build/ensure-build.mjs +19 -19
- package/src/build/paths.mjs +153 -153
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +349 -349
- package/src/build/tasks/css.mjs +235 -235
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +357 -357
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/build/tasks/templates.mjs +20 -20
- package/src/client/admin/i18n.js +764 -764
- package/src/client/admin/login.html +74 -74
- package/src/client/admin/panel.css +809 -809
- package/src/client/admin/panel.html +495 -495
- package/src/client/admin/panel.js +1251 -1251
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +745 -745
- package/src/client/devtools/seo.js +628 -628
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +45 -45
- package/src/client/registry.js +305 -305
- package/src/client/safe-image.js +91 -91
- package/src/client/shared-cookie.js +225 -225
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/compile/codegen.js +336 -336
- package/src/compile/compile-all.js +149 -149
- package/src/compile/errors.js +66 -66
- package/src/compile/expr.js +409 -409
- package/src/compile/index.js +17 -17
- package/src/compile/parse.js +541 -541
- package/src/compile/resolve.js +211 -211
- package/src/compile/scan-exports.js +51 -51
- package/src/config/defaults.js +541 -534
- package/src/config/index.js +1500 -1469
- package/src/config/pattern.js +107 -107
- package/src/generate.mjs +163 -163
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies-entry.js +21 -21
- package/src/http/cookies.js +277 -277
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +165 -165
- package/src/http/shared-cookie.js +178 -178
- package/src/index.js +101 -101
- package/src/init.mjs +232 -230
- package/src/migrate/apply.mjs +262 -262
- package/src/migrate/babel.mjs +79 -79
- package/src/migrate/classify.mjs +155 -155
- package/src/migrate/config.mjs +126 -126
- package/src/migrate/fs-walk.mjs +191 -191
- package/src/migrate/parse.mjs +26 -26
- package/src/migrate/scan.mjs +177 -177
- package/src/migrate/transform/expr-source.mjs +168 -168
- package/src/migrate/transform/island.mjs +67 -67
- package/src/migrate/transform/jsx-to-component.mjs +302 -302
- package/src/migrate/transform/jsx-to-jsk.mjs +330 -330
- package/src/migrate/transform/page-split.mjs +435 -435
- package/src/migrate/write.mjs +81 -81
- package/src/migrate.mjs +171 -171
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/admin/actions.js +229 -229
- package/src/server/admin/auth.js +125 -125
- package/src/server/admin/event-log.js +151 -151
- package/src/server/admin/gate.js +209 -209
- package/src/server/admin/inventory.js +188 -188
- package/src/server/admin/mount.js +56 -56
- package/src/server/admin/router.js +216 -216
- package/src/server/admin/snapshot.js +241 -241
- package/src/server/assets.js +147 -147
- package/src/server/auth/handoff.js +309 -309
- package/src/server/cache-blob.js +70 -70
- package/src/server/cache-control.js +45 -0
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-vary.js +113 -113
- package/src/server/cloudflare.js +607 -607
- package/src/server/create-app.js +366 -366
- package/src/server/data-cache.js +553 -553
- package/src/server/dev/report.js +485 -485
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/disk-cache.js +233 -233
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1196
- package/src/server/image-optimizer.js +500 -500
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -193
- package/src/server/logs/pipeline.js +165 -165
- package/src/server/logs/s3-put.js +214 -214
- package/src/server/logs/s3-sink.js +112 -112
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +75 -75
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/robots-txt.js +341 -341
- package/src/server/middleware/static-precompressed.js +121 -121
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +369 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1082
- package/src/server/redis.js +588 -588
- package/src/server/render.js +910 -910
- package/src/server/router.js +157 -157
- package/src/server/status-page.js +265 -265
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/shared/cookie-domain.js +66 -66
- package/src/start.mjs +22 -22
- package/src/templates/layout.ejs +30 -30
- package/src/templates/layout.jsk +30 -30
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +101 -101
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +375 -375
- package/types/config/defaults.d.ts +6 -0
- package/types/config/index.d.ts +6 -0
- package/types/server/cache-control.d.ts +28 -0
- package/types/server/og-image.d.ts +5 -0
package/src/server/og-image.js
CHANGED
|
@@ -1,356 +1,369 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dinamik Open Graph görselleri — Next.js `ImageResponse` /
|
|
3
|
-
* `opengraph-image.tsx` karşılığı.
|
|
4
|
-
*
|
|
5
|
-
* JSX yok: ya hazır kart alanları (`title`, `description`, `siteName`) ya da
|
|
6
|
-
* ham `svg` verilir. sharp (opsiyonel peer) varsa PNG üretilir; yoksa SVG
|
|
7
|
-
* döner. Sosyal kazıyıcıların çoğu PNG beklediği için prod'da sharp önerilir.
|
|
8
|
-
*
|
|
9
|
-
* Domain bilgisi taşınmaz — metin, renk ve SVG uygulama tarafındandır.
|
|
10
|
-
*/
|
|
11
|
-
import { tryImportFromApp } from "../build/resolve-peer.mjs";
|
|
12
|
-
import { getConfig } from "../config/index.js";
|
|
13
|
-
import { isNotFoundError } from "../http/control-flow.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* @
|
|
31
|
-
* @property {string} [
|
|
32
|
-
* @property {string} [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* @
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
lines.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return
|
|
355
|
-
}
|
|
356
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Dinamik Open Graph görselleri — Next.js `ImageResponse` /
|
|
3
|
+
* `opengraph-image.tsx` karşılığı.
|
|
4
|
+
*
|
|
5
|
+
* JSX yok: ya hazır kart alanları (`title`, `description`, `siteName`) ya da
|
|
6
|
+
* ham `svg` verilir. sharp (opsiyonel peer) varsa PNG üretilir; yoksa SVG
|
|
7
|
+
* döner. Sosyal kazıyıcıların çoğu PNG beklediği için prod'da sharp önerilir.
|
|
8
|
+
*
|
|
9
|
+
* Domain bilgisi taşınmaz — metin, renk ve SVG uygulama tarafındandır.
|
|
10
|
+
*/
|
|
11
|
+
import { tryImportFromApp } from "../build/resolve-peer.mjs";
|
|
12
|
+
import { getConfig } from "../config/index.js";
|
|
13
|
+
import { isNotFoundError } from "../http/control-flow.js";
|
|
14
|
+
import { setEdgeCacheHeaders } from "./cache-control.js";
|
|
15
|
+
|
|
16
|
+
/** @type {((input: Buffer, opts?: object) => import('sharp').Sharp) | null | undefined} */
|
|
17
|
+
let sharpModule;
|
|
18
|
+
|
|
19
|
+
/** Sosyal kartlar için yaygın boyut (Facebook / X / LinkedIn). */
|
|
20
|
+
export const OG_SIZE = Object.freeze({ width: 1200, height: 630 });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* OG süreleri HTML TTL'ye bağlanmaz. Yalnızca `s-maxage` kalkar; edge
|
|
24
|
+
* `CDN-Cache-Control` üzerinde aynı pencereyi görür.
|
|
25
|
+
*/
|
|
26
|
+
const OG_EDGE_MAX_AGE = 86400;
|
|
27
|
+
const OG_EDGE_STALE = 604800;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} OgCardOptions
|
|
31
|
+
* @property {string} [title]
|
|
32
|
+
* @property {string} [description]
|
|
33
|
+
* @property {string} [siteName]
|
|
34
|
+
* @property {string} [background] Düz SVG rengi (`#0f172a`)
|
|
35
|
+
* @property {string} [color] Ana metin rengi
|
|
36
|
+
* @property {string} [mutedColor] Açıklama / site adı
|
|
37
|
+
* @property {string} [accent] Sol şerit rengi
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {OgCardOptions & {
|
|
42
|
+
* svg?: string,
|
|
43
|
+
* width?: number,
|
|
44
|
+
* height?: number,
|
|
45
|
+
* format?: 'png' | 'svg',
|
|
46
|
+
* cacheControl?: string,
|
|
47
|
+
* }} OgImageOptions
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {object} OgImageResult
|
|
52
|
+
* @property {Buffer} body
|
|
53
|
+
* @property {string} contentType
|
|
54
|
+
* @property {number} width
|
|
55
|
+
* @property {number} height
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* XML metin kaçışı — kullanıcı başlığı SVG'ye gömülür.
|
|
60
|
+
* @param {unknown} value
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function escapeXml(value) {
|
|
64
|
+
return String(value ?? "")
|
|
65
|
+
.replace(/&/g, "&")
|
|
66
|
+
.replace(/</g, "<")
|
|
67
|
+
.replace(/>/g, ">")
|
|
68
|
+
.replace(/"/g, """)
|
|
69
|
+
.replace(/'/g, "'");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Kelime sınırında satır kır. Uzun kelime kesilir; taşan içerik son satırda `…`.
|
|
74
|
+
* @param {string} text
|
|
75
|
+
* @param {number} maxChars
|
|
76
|
+
* @param {number} maxLines
|
|
77
|
+
* @returns {string[]}
|
|
78
|
+
*/
|
|
79
|
+
export function wrapText(text, maxChars, maxLines) {
|
|
80
|
+
const words = String(text ?? "")
|
|
81
|
+
.trim()
|
|
82
|
+
.split(/\s+/)
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
if (!words.length || maxLines < 1 || maxChars < 1) return [];
|
|
85
|
+
|
|
86
|
+
/** @type {string[]} */
|
|
87
|
+
const lines = [];
|
|
88
|
+
let current = "";
|
|
89
|
+
let overflow = false;
|
|
90
|
+
|
|
91
|
+
const pushCurrent = () => {
|
|
92
|
+
if (!current) return;
|
|
93
|
+
lines.push(current);
|
|
94
|
+
current = "";
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
for (const word of words) {
|
|
98
|
+
if (lines.length >= maxLines) {
|
|
99
|
+
overflow = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
104
|
+
if (candidate.length <= maxChars) {
|
|
105
|
+
current = candidate;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pushCurrent();
|
|
110
|
+
if (lines.length >= maxLines) {
|
|
111
|
+
overflow = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (word.length <= maxChars) {
|
|
116
|
+
current = word;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let rest = word;
|
|
121
|
+
while (rest.length > maxChars) {
|
|
122
|
+
if (lines.length >= maxLines) {
|
|
123
|
+
overflow = true;
|
|
124
|
+
rest = "";
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
lines.push(rest.slice(0, maxChars));
|
|
128
|
+
rest = rest.slice(maxChars);
|
|
129
|
+
}
|
|
130
|
+
current = rest;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (current && lines.length < maxLines) {
|
|
134
|
+
lines.push(current);
|
|
135
|
+
} else if (current) {
|
|
136
|
+
overflow = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (overflow && lines.length) {
|
|
140
|
+
const last = lines[lines.length - 1];
|
|
141
|
+
const base = last.endsWith("…") ? last.slice(0, -1) : last;
|
|
142
|
+
const trimmed = base.slice(0, Math.max(1, maxChars - 1));
|
|
143
|
+
lines[lines.length - 1] = `${trimmed}…`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return lines;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Hazır kart SVG'si. Uygulama kendi SVG'sini vermek isterse `svg` kullanır.
|
|
151
|
+
* @param {OgCardOptions & { width?: number, height?: number }} options
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
export function buildOgSvg(options = {}) {
|
|
155
|
+
const width = options.width ?? OG_SIZE.width;
|
|
156
|
+
const height = options.height ?? OG_SIZE.height;
|
|
157
|
+
const background = options.background ?? "#0f172a";
|
|
158
|
+
const color = options.color ?? "#f8fafc";
|
|
159
|
+
const muted = options.mutedColor ?? "#94a3b8";
|
|
160
|
+
const accent = options.accent ?? "#38bdf8";
|
|
161
|
+
|
|
162
|
+
const titleLines = wrapText(options.title ?? "", 28, 3);
|
|
163
|
+
const descLines = wrapText(options.description ?? "", 52, 2);
|
|
164
|
+
const siteName = options.siteName ? escapeXml(options.siteName) : "";
|
|
165
|
+
|
|
166
|
+
const titleTs = titleLines
|
|
167
|
+
.map((line, i) => {
|
|
168
|
+
const dy = i === 0 ? 0 : 72;
|
|
169
|
+
return `<tspan x="80" dy="${dy}">${escapeXml(line)}</tspan>`;
|
|
170
|
+
})
|
|
171
|
+
.join("");
|
|
172
|
+
|
|
173
|
+
const descTs = descLines
|
|
174
|
+
.map((line, i) => {
|
|
175
|
+
const dy = i === 0 ? 0 : 40;
|
|
176
|
+
return `<tspan x="80" dy="${dy}">${escapeXml(line)}</tspan>`;
|
|
177
|
+
})
|
|
178
|
+
.join("");
|
|
179
|
+
|
|
180
|
+
const titleY = 200;
|
|
181
|
+
const descY = titleY + Math.max(titleLines.length, 1) * 72 + 36;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` +
|
|
185
|
+
`<rect width="100%" height="100%" fill="${escapeXml(background)}"/>` +
|
|
186
|
+
`<rect x="0" y="0" width="14" height="${height}" fill="${escapeXml(accent)}"/>` +
|
|
187
|
+
(titleTs
|
|
188
|
+
? `<text x="80" y="${titleY}" font-family="system-ui, -apple-system, Segoe UI, sans-serif" font-size="64" font-weight="700" fill="${escapeXml(color)}">${titleTs}</text>`
|
|
189
|
+
: "") +
|
|
190
|
+
(descTs
|
|
191
|
+
? `<text x="80" y="${descY}" font-family="system-ui, -apple-system, Segoe UI, sans-serif" font-size="30" font-weight="400" fill="${escapeXml(muted)}">${descTs}</text>`
|
|
192
|
+
: "") +
|
|
193
|
+
(siteName
|
|
194
|
+
? `<text x="80" y="${height - 64}" font-family="system-ui, -apple-system, Segoe UI, sans-serif" font-size="24" font-weight="600" fill="${escapeXml(muted)}">${siteName}</text>`
|
|
195
|
+
: "") +
|
|
196
|
+
`</svg>`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @returns {Promise<((input: Buffer, opts?: object) => import('sharp').Sharp) | null>}
|
|
202
|
+
*/
|
|
203
|
+
async function loadSharp() {
|
|
204
|
+
if (sharpModule !== undefined) return sharpModule;
|
|
205
|
+
|
|
206
|
+
/** @type {any} */
|
|
207
|
+
let mod = null;
|
|
208
|
+
try {
|
|
209
|
+
mod = await tryImportFromApp(getConfig().root, "sharp");
|
|
210
|
+
} catch {
|
|
211
|
+
// Config yoksa (birim test) doğrudan çözümle.
|
|
212
|
+
try {
|
|
213
|
+
mod = await import("sharp");
|
|
214
|
+
} catch {
|
|
215
|
+
mod = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
sharpModule = mod?.default ?? mod ?? null;
|
|
220
|
+
return sharpModule;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* SVG veya kart alanlarından PNG/SVG gövde üretir.
|
|
225
|
+
* @param {OgImageOptions} [options]
|
|
226
|
+
* @returns {Promise<OgImageResult>}
|
|
227
|
+
*/
|
|
228
|
+
export async function ogImage(options = {}) {
|
|
229
|
+
const width = options.width ?? OG_SIZE.width;
|
|
230
|
+
const height = options.height ?? OG_SIZE.height;
|
|
231
|
+
const svg =
|
|
232
|
+
typeof options.svg === "string" && options.svg.trim()
|
|
233
|
+
? options.svg
|
|
234
|
+
: buildOgSvg({ ...options, width, height });
|
|
235
|
+
|
|
236
|
+
const preferSvg = options.format === "svg";
|
|
237
|
+
const sharp = preferSvg ? null : await loadSharp();
|
|
238
|
+
|
|
239
|
+
if (!sharp) {
|
|
240
|
+
return {
|
|
241
|
+
body: Buffer.from(svg, "utf8"),
|
|
242
|
+
contentType: "image/svg+xml; charset=utf-8",
|
|
243
|
+
width,
|
|
244
|
+
height,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const body = await sharp(Buffer.from(svg, "utf8"))
|
|
249
|
+
.resize(width, height, { fit: "fill" })
|
|
250
|
+
.png()
|
|
251
|
+
.toBuffer();
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
body,
|
|
255
|
+
contentType: "image/png",
|
|
256
|
+
width,
|
|
257
|
+
height,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Express yanıtına OG görseli basar.
|
|
263
|
+
*
|
|
264
|
+
* Varsayılan edge penceresi 86400 / 604800'tür ve HTML TTL'ye bağlı değildir.
|
|
265
|
+
* `cacheControl` verilirse yalnızca `Cache-Control` yazılır;
|
|
266
|
+
* `CDN-Cache-Control` basılmaz.
|
|
267
|
+
*
|
|
268
|
+
* @param {import('express').Response} res
|
|
269
|
+
* @param {OgImageOptions} [options]
|
|
270
|
+
* @returns {Promise<OgImageResult>}
|
|
271
|
+
*/
|
|
272
|
+
export async function sendOgImage(res, options = {}) {
|
|
273
|
+
const result = await ogImage(options);
|
|
274
|
+
|
|
275
|
+
res.status(200);
|
|
276
|
+
res.setHeader("Content-Type", result.contentType);
|
|
277
|
+
if (options.cacheControl != null) {
|
|
278
|
+
res.setHeader("Cache-Control", options.cacheControl);
|
|
279
|
+
} else {
|
|
280
|
+
setEdgeCacheHeaders(res, OG_EDGE_MAX_AGE, OG_EDGE_STALE);
|
|
281
|
+
}
|
|
282
|
+
res.setHeader("Content-Length", String(result.body.length));
|
|
283
|
+
// Kazıyıcılar ve CDN'ler için boyut ipucu (meta ile de verilir).
|
|
284
|
+
res.setHeader("X-Og-Width", String(result.width));
|
|
285
|
+
res.setHeader("X-Og-Height", String(result.height));
|
|
286
|
+
res.end(result.body);
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Next `opengraph-image` route handler'ına yakın Express sarmalayıcı.
|
|
292
|
+
*
|
|
293
|
+
* Factory `null` dönerse veya `notFound()` fırlatırsa 404.
|
|
294
|
+
*
|
|
295
|
+
* @param {(ctx: { params: Record<string, string>, query: import('express').Request['query'], req: import('express').Request }) =>
|
|
296
|
+
* OgImageOptions | null | Promise<OgImageOptions | null>} factory
|
|
297
|
+
* @param {OgImageOptions} [defaults] Her istekte birleşen varsayılanlar
|
|
298
|
+
* @returns {import('express').RequestHandler}
|
|
299
|
+
*/
|
|
300
|
+
export function ogHandler(factory, defaults = {}) {
|
|
301
|
+
return async (req, res, next) => {
|
|
302
|
+
try {
|
|
303
|
+
const result = await factory({
|
|
304
|
+
params: req.params ?? {},
|
|
305
|
+
query: req.query,
|
|
306
|
+
req,
|
|
307
|
+
});
|
|
308
|
+
if (result == null) {
|
|
309
|
+
res.status(404).end();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await sendOgImage(res, { ...defaults, ...result });
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (isNotFoundError(error)) {
|
|
315
|
+
res.status(404).end();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
next(error);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Next.js `new ImageResponse(...)` DX'si. JSX yok — ilk argüman SVG string
|
|
325
|
+
* veya kart alanları nesnesi.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```js
|
|
329
|
+
* return new ImageResponse(
|
|
330
|
+
* { title: post.title, description: post.excerpt, siteName: "Blog" },
|
|
331
|
+
* { width: 1200, height: 630 },
|
|
332
|
+
* );
|
|
333
|
+
* // handler içinde: await image.send(res)
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export class ImageResponse {
|
|
337
|
+
/** @type {OgImageOptions} */
|
|
338
|
+
#options;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {string | OgCardOptions} element
|
|
342
|
+
* @param {Omit<OgImageOptions, keyof OgCardOptions | 'svg'> & { width?: number, height?: number }} [init]
|
|
343
|
+
*/
|
|
344
|
+
constructor(element, init = {}) {
|
|
345
|
+
if (typeof element === "string") {
|
|
346
|
+
this.#options = { ...init, svg: element };
|
|
347
|
+
} else {
|
|
348
|
+
this.#options = { ...element, ...init };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** @returns {OgImageOptions} */
|
|
353
|
+
get options() {
|
|
354
|
+
return this.#options;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** @returns {Promise<OgImageResult>} */
|
|
358
|
+
async buffer() {
|
|
359
|
+
return ogImage(this.#options);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* @param {import('express').Response} res
|
|
364
|
+
* @returns {Promise<OgImageResult>}
|
|
365
|
+
*/
|
|
366
|
+
async send(res) {
|
|
367
|
+
return sendOgImage(res, this.#options);
|
|
368
|
+
}
|
|
369
|
+
}
|