jskelet 0.6.2 → 0.6.3
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 +620 -596
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +130 -130
- package/docs/01-baslangic.md +291 -291
- package/docs/02-mimari.md +310 -309
- package/docs/03-routing.md +515 -515
- package/docs/04-render-ve-sablonlar.md +661 -661
- package/docs/05-islands.md +486 -486
- package/docs/06-cache.md +1443 -1423
- package/docs/07-yapilandirma.md +12 -6
- package/docs/08-build.md +429 -428
- package/docs/09-dev-araclari.md +364 -364
- package/docs/10-dagitim.md +338 -338
- package/docs/12-panel-ve-oturum.md +478 -478
- package/docs/README.md +83 -83
- package/docs/en/01-getting-started.md +298 -298
- package/docs/en/02-architecture.md +329 -328
- package/docs/en/03-routing.md +531 -531
- package/docs/en/04-rendering.md +669 -669
- package/docs/en/05-islands.md +497 -497
- package/docs/en/06-caching.md +1453 -1431
- package/docs/en/07-configuration.md +1219 -1214
- package/docs/en/08-build.md +447 -446
- package/docs/en/09-dev-tools.md +373 -373
- package/docs/en/10-deployment.md +340 -340
- package/docs/en/11-migration.md +398 -398
- package/docs/en/12-dashboards-and-sessions.md +488 -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 +17 -1
- package/src/config/index.js +13 -0
- 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 +230 -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 -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 -462
- 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 -0
- package/src/server/ejs-adapter.js +59 -59
- package/src/server/html-cache.js +1196 -1122
- package/src/server/image-optimizer.js +500 -407
- package/src/server/logs/access-middleware.js +66 -66
- package/src/server/logs/file-sink.js +193 -66
- package/src/server/logs/pipeline.js +165 -158
- 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 -100
- package/src/server/middleware/trailing-slash.js +53 -53
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/og-image.js +356 -356
- package/src/server/port-guard.js +255 -255
- package/src/server/prewarm.js +1082 -1058
- package/src/server/redis.js +588 -569
- package/src/server/render.js +4 -4
- 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 +15 -1
- package/types/config/index.d.ts +8 -0
- package/types/server/cache-blob.d.ts +13 -0
- package/types/server/data-cache.d.ts +9 -0
- package/types/server/disk-cache.d.ts +36 -0
- package/types/server/html-cache.d.ts +26 -3
- package/types/server/logs/file-sink.d.ts +16 -5
- package/types/server/redis.d.ts +2 -1
|
@@ -1,661 +1,661 @@
|
|
|
1
|
-
# 04 — Render ve şablonlar
|
|
2
|
-
|
|
3
|
-
Bu belge sunucu HTML'inin nasıl üretildiğini anlatır: EJS motorunun ayarları,
|
|
4
|
-
layout dosyasının çözümü ve kullanabildiği local'ler, `views/pages` altındaki
|
|
5
|
-
sayfa şablonları, `views/components/**` altındaki bileşenlerin otomatik kaydı,
|
|
6
|
-
şablonlara hazır gelen `html`/`tags` yardımcıları, `metadata` nesnesinin `<head>`
|
|
7
|
-
etiketlerine çevrilmesi ve üç render hook'u. Controller'ın bu katmana ne
|
|
8
|
-
gönderdiği [03-routing.md](./03-routing.md)'de, varlık URL'lerini üreten
|
|
9
|
-
`asset()`/`hasAsset()` [08-build.md](./08-build.md)'de anlatılıyor.
|
|
10
|
-
|
|
11
|
-
## Render hattı
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
route(controller)
|
|
15
|
-
└─ produce()
|
|
16
|
-
├─ controller(ctx) → sayfa tanımı
|
|
17
|
-
└─ renderPage(page)
|
|
18
|
-
├─ hooks.metadata(page) + page.metadata → metadata
|
|
19
|
-
├─ Promise.all([
|
|
20
|
-
│ renderView(page.view, { …data, metadata }), → body
|
|
21
|
-
│ hooks.layoutContext({ pathname, metadata }), → context
|
|
22
|
-
│ ])
|
|
23
|
-
└─ layout (.jsk derlenmiş veya .ejs) → tam HTML
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Layout bağlamı ve gövde **paralel** üretilir. Sebebi ölçümden geliyor:
|
|
27
|
-
navigasyon çoğu projede upstream'den geliyor ve gövde render'ıyla sırayla
|
|
28
|
-
beklemek her sayfaya gereksiz gecikme ekliyor.
|
|
29
|
-
|
|
30
|
-
## `.jsk` — build-time derlenmiş şablonlar
|
|
31
|
-
|
|
32
|
-
Yeni uygulamalarda varsayılan şablon biçimi `.jsk`'dir. Build sırasında
|
|
33
|
-
(`.jskelet/templates/*.mjs`) normal ESM modüllerine çevrilir; **istek anında
|
|
34
|
-
parse / `eval` / `new Function` yoktur**. Production yolu:
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
controller data → import edilmiş render(data, helpers) → HTML
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Sözdizimi özeti
|
|
41
|
-
|
|
42
|
-
```html
|
|
43
|
-
<section class="wrapper">
|
|
44
|
-
<h1>{{ title }}</h1>
|
|
45
|
-
<div>{{{ trustedHtml }}}</div>
|
|
46
|
-
|
|
47
|
-
{#if items.length}
|
|
48
|
-
<List :items="items" />
|
|
49
|
-
{#else}
|
|
50
|
-
<p>Boş</p>
|
|
51
|
-
{/if}
|
|
52
|
-
|
|
53
|
-
{#each items as item, i}
|
|
54
|
-
<li :data-i="i">{{ item }}</li>
|
|
55
|
-
{/each}
|
|
56
|
-
|
|
57
|
-
<Link href="/" text="Home" />
|
|
58
|
-
<div data-island="counter" data-island-props='{"start":0}'></div>
|
|
59
|
-
</section>
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
| Özellik | Yazım |
|
|
63
|
-
| --- | --- |
|
|
64
|
-
| Kaçışlı metin | `{{ expr }}` |
|
|
65
|
-
| Ham HTML | `{{{ expr }}}` |
|
|
66
|
-
| Koşul | `{#if expr}` … `{#else}` … `{/if}` |
|
|
67
|
-
| Döngü | `{#each list as item}` veya `as item, i` |
|
|
68
|
-
| Include | `{#include "partials/header"}` (derlenmiş `.jsk`) |
|
|
69
|
-
| Bileşen | PascalCase etiket; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
|
|
70
|
-
| Yerleşikler | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
|
|
71
|
-
|
|
72
|
-
İfade dili kasıtlı olarak dardır (erişim, karşılaştırma, ternary, `.length`).
|
|
73
|
-
Atama, object literal ve rastgele fonksiyon çağrısı yok — mantık controller
|
|
74
|
-
veya JS bileşende kalır.
|
|
75
|
-
|
|
76
|
-
#### Şablon mu, bileşen mi?
|
|
77
|
-
|
|
78
|
-
EJS’den geçerken sınırı erken çizmek işe yarar:
|
|
79
|
-
|
|
80
|
-
| Burada kalsın (`.jsk`) | JS bileşene taşı |
|
|
81
|
-
| --- | --- |
|
|
82
|
-
| Metin, koşul, liste, prop bağlama | Fonksiyon çağrısı, nesne üretimi, biçimlendirme |
|
|
83
|
-
| Yerleşik etiketler (`Link`, `Image`, …) | Birden fazla yardımcıdan HTML birleştirme |
|
|
84
|
-
| Controller’dan gelen hazır veri | Upstream / hata ayırt eden UI (`LoadErrorState`) |
|
|
85
|
-
|
|
86
|
-
Şablonda `format(x)` veya `{ a: 1 }` yazılamıyorsa bu bir eksik değil: o iş
|
|
87
|
-
`views/components/*.js` veya controller’ındır. Karmaşık sayfalar bileşene
|
|
88
|
-
kaçıyorsa ifade dilini genişletmek yerine bileşen sınırını net tutmak tercih
|
|
89
|
-
edilir.
|
|
90
|
-
|
|
91
|
-
### Editör desteği
|
|
92
|
-
|
|
93
|
-
Repo içinde `extensions/vscode-jsk` VS Code / Cursor uzantısı vardır: sözdizimi
|
|
94
|
-
renklendirme, dil yapılandırması ve snippet'ler. Yerel kurulum:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
code --install-extension extensions/vscode-jsk
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Ayrıntılar uzantı README'sinde.
|
|
101
|
-
|
|
102
|
-
### Yerleşik layout etiketleri
|
|
103
|
-
|
|
104
|
-
`.jsk` ifade dilinde `asset()` / `hasAsset()` çağrılamaz. Layout’ta stylesheet,
|
|
105
|
-
script ve JSON-LD döngüleri için yerleşikler:
|
|
106
|
-
|
|
107
|
-
| Etiket | Props | Çıktı |
|
|
108
|
-
| --- | --- | --- |
|
|
109
|
-
| `Stylesheets` | `styles` | `app.css` + sayfa sheet’leri (`data-jskelet-css`) |
|
|
110
|
-
| `BodyScripts` | `entries`, `devtools`, `devBasePath` | `main.js`, entry’ler, isteğe bağlı overlay |
|
|
111
|
-
| `JsonLd` | `items` (`structuredData`) | `application/ld+json` script’leri |
|
|
112
|
-
|
|
113
|
-
### EJS ile birlikte yaşam
|
|
114
|
-
|
|
115
|
-
Aynı `view` id için derlenmiş `.jsk` varsa o kullanılır; yoksa `.ejs` dosyası
|
|
116
|
-
**yalnızca `ejs` peer’i kuruluysa** render edilir. `jskelet init` yeni iskeleti
|
|
117
|
-
`.jsk` ile kurar.
|
|
118
|
-
|
|
119
|
-
## EJS motoru (legacy peer)
|
|
120
|
-
|
|
121
|
-
EJS opsiyonel peer bağımlılıktır (`npm i ejs`). `.jsk`-only uygulamalar kurmak
|
|
122
|
-
zorunda değildir. Bir `.ejs` view veya layout istendiğinde paket uygulamadan
|
|
123
|
-
yüklenir; yoksa göç yolunu gösteren bir hata fırlatılır.
|
|
124
|
-
|
|
125
|
-
Motor ilk EJS render’da bir kez kurulur. Ayarlar:
|
|
126
|
-
|
|
127
|
-
| Ayar | Değer | Sebebi |
|
|
128
|
-
| --- | --- | --- |
|
|
129
|
-
| `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
|
|
130
|
-
| `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
|
|
131
|
-
| `rmWhitespace` | `true` | çıktı boyutu |
|
|
132
|
-
| `async` | `true` | şablon içinde `await` kullanılabilir |
|
|
133
|
-
|
|
134
|
-
Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık.
|
|
135
|
-
|
|
136
|
-
## Layout
|
|
137
|
-
|
|
138
|
-
### Layout dosyası nasıl bulunur
|
|
139
|
-
|
|
140
|
-
1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
|
|
141
|
-
dizininin üst dizinine** göre çözülür: `views` varsayılansa
|
|
142
|
-
`layout: "views/ozel.jsk"` → `<root>/views/ozel.jsk`.
|
|
143
|
-
2. Verilmemişse `views/layout.jsk` (derlenmiş) varsa o kullanılır.
|
|
144
|
-
3. Yoksa `views/layout.ejs` varsa o kullanılır (EJS peer gerekir).
|
|
145
|
-
4. O da yoksa framework'ün kendi minimal layout'u kullanılır
|
|
146
|
-
(`node_modules/jskelet/src/templates/layout.jsk`, `jskelet/layout`;
|
|
147
|
-
legacy kopya `jskelet/layout/ejs`).
|
|
148
|
-
|
|
149
|
-
Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
|
|
150
|
-
layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.jsk` olarak
|
|
151
|
-
kopyalamaktır.
|
|
152
|
-
|
|
153
|
-
### Framework'ün varsayılan layout'u
|
|
154
|
-
|
|
155
|
-
```jsk
|
|
156
|
-
<!DOCTYPE html>
|
|
157
|
-
<html :lang="lang">
|
|
158
|
-
<head>
|
|
159
|
-
<meta charset="utf-8">
|
|
160
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
161
|
-
{{{ extraHead }}}
|
|
162
|
-
<Stylesheets :styles="styles" />
|
|
163
|
-
{{{ headMeta }}}
|
|
164
|
-
<JsonLd :items="structuredData" />
|
|
165
|
-
</head>
|
|
166
|
-
<body :class="bodyClass">
|
|
167
|
-
{{{ body }}}
|
|
168
|
-
<BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
|
|
169
|
-
</body>
|
|
170
|
-
</html>
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Dikkat edilecek noktalar:
|
|
174
|
-
|
|
175
|
-
- **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
|
|
176
|
-
geciktirmek doğrudan LCP'ye yazılır.
|
|
177
|
-
- **Global `app.css` render-blocking** ve gerekçesi
|
|
178
|
-
[02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
|
|
179
|
-
sheet'leri de aynı şekilde basılır. Build çalışmadıysa `hasAsset` false olur
|
|
180
|
-
ve etiket hiç basılmaz.
|
|
181
|
-
- **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
|
|
182
|
-
istememesini sağlar (`Stylesheets` / `BodyScripts` içinde).
|
|
183
|
-
- **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
|
|
184
|
-
çıktısında hiç yoktur.
|
|
185
|
-
|
|
186
|
-
### Layout local'leri
|
|
187
|
-
|
|
188
|
-
| Local | Tip | Kaynağı |
|
|
189
|
-
| --- | --- | --- |
|
|
190
|
-
| `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
|
|
191
|
-
| `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
|
|
192
|
-
| `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
|
|
193
|
-
| `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
|
|
194
|
-
| `body` | `string` | Sayfa şablonunun render çıktısı |
|
|
195
|
-
| `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
|
|
196
|
-
| `entries` | `string[]` | controller `entries`; varsayılan `[]` |
|
|
197
|
-
| `styles` | `string[]` | controller `styles`; varsayılan `[]` |
|
|
198
|
-
| `pathname` | `string` | `req.path`; **varsayılan boş string** |
|
|
199
|
-
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
200
|
-
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
|
201
|
-
| `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
|
|
202
|
-
| `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
|
|
203
|
-
| html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
204
|
-
| `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
|
|
205
|
-
| `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
|
|
206
|
-
|
|
207
|
-
`pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
|
|
208
|
-
sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
|
|
209
|
-
|
|
210
|
-
## Sayfa şablonları
|
|
211
|
-
|
|
212
|
-
`view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
|
|
213
|
-
`views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
|
|
214
|
-
`metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
|
|
215
|
-
ve bileşenlere erişir.
|
|
216
|
-
|
|
217
|
-
```ejs
|
|
218
|
-
<%# views/pages/home.ejs %>
|
|
219
|
-
<section class="wrapper">
|
|
220
|
-
<h1 class="text-3xl font-bold"><%= heading %></h1>
|
|
221
|
-
|
|
222
|
-
<%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
|
|
223
|
-
<%- list({ items }) %>
|
|
224
|
-
|
|
225
|
-
<div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
|
|
226
|
-
</section>
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
EJS'te iki çıktı biçimini karıştırmayın:
|
|
230
|
-
|
|
231
|
-
- `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
|
|
232
|
-
- `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
|
|
233
|
-
(bileşen çağrıları, `headMeta`, `body`).
|
|
234
|
-
|
|
235
|
-
`async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
|
|
236
|
-
veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
|
|
237
|
-
|
|
238
|
-
## Bileşenler: `views/components/**`
|
|
239
|
-
|
|
240
|
-
Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
|
|
241
|
-
`views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
|
|
242
|
-
şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
|
|
243
|
-
eklemek için dosyayı oluşturmak yeterli.
|
|
244
|
-
|
|
245
|
-
```js
|
|
246
|
-
// views/components/list.js
|
|
247
|
-
import { esc } from "jskelet/html";
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* @param {{ items: string[] }} props
|
|
251
|
-
* @returns {string}
|
|
252
|
-
*/
|
|
253
|
-
export function list({ items }) {
|
|
254
|
-
if (!items?.length) return "";
|
|
255
|
-
|
|
256
|
-
const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
|
|
257
|
-
return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Şablonda:
|
|
262
|
-
|
|
263
|
-
```ejs
|
|
264
|
-
<%- list({ items }) %>
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Kurallar:
|
|
268
|
-
|
|
269
|
-
- Tarama özyinelemelidir; alt dizinler de kapsanır.
|
|
270
|
-
- `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
|
|
271
|
-
- Compile-time bilinen bileşen listesi **dosya adından değil**, kaynak
|
|
272
|
-
metindeki named export'lardan okunur. `ui.js` içindeki `sectionHead` →
|
|
273
|
-
şablonda `<SectionHead />` (runtime zaten camelCase export'a PascalCase
|
|
274
|
-
alias ekler). Dosya adına göre stub re-export eklemeye gerek yoktur.
|
|
275
|
-
- `loader.js` ve `index.js` bileşen dosyası sayılmaz.
|
|
276
|
-
- `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
|
|
277
|
-
önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
|
|
278
|
-
bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
|
|
279
|
-
- Aynı ad (veya aynı PascalCase etiket) iki farklı bileşen dosyasında
|
|
280
|
-
tanımlıysa **uyarı değil hata**: build ve sunucu açılışı
|
|
281
|
-
`Component 'card' is defined twice: …` ile durur. Barrel üzerine yazmak
|
|
282
|
-
bilinçli istisnadır.
|
|
283
|
-
- `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
|
|
284
|
-
bir proje de çalışır.
|
|
285
|
-
|
|
286
|
-
## Yardımcılar: `jskelet/html`
|
|
287
|
-
|
|
288
|
-
Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
|
|
289
|
-
ile alınır.
|
|
290
|
-
|
|
291
|
-
### `esc(value)`
|
|
292
|
-
|
|
293
|
-
Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
|
|
294
|
-
`null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
|
|
295
|
-
`false && "…"` gibi ifadeler `"false"` basmaz.
|
|
296
|
-
|
|
297
|
-
```js
|
|
298
|
-
esc('<b>"x"</b>'); // "<b>"x"</b>"
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
### `attrs(object)`
|
|
302
|
-
|
|
303
|
-
Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
|
|
304
|
-
boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
|
|
305
|
-
değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
|
|
306
|
-
doğru biçimlenir.
|
|
307
|
-
|
|
308
|
-
```js
|
|
309
|
-
`<input${attrs({ type: "text", required: true, value: null })}>`;
|
|
310
|
-
// '<input type="text" required>'
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### `cx(...inputs)`
|
|
314
|
-
|
|
315
|
-
`clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
|
|
316
|
-
falsy değerleri atar. Tailwind çakışması **çözmez**.
|
|
317
|
-
|
|
318
|
-
```js
|
|
319
|
-
cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### `cn(...inputs)`
|
|
323
|
-
|
|
324
|
-
`cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
|
|
325
|
-
Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
|
|
326
|
-
bunu kullanın.
|
|
327
|
-
|
|
328
|
-
```js
|
|
329
|
-
cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
`tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
|
|
333
|
-
yalnızca sunucuda yapılır; client bundle'a hiç girmez.
|
|
334
|
-
|
|
335
|
-
### `jsonScript(value)`
|
|
336
|
-
|
|
337
|
-
`<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
|
|
338
|
-
ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
|
|
339
|
-
kapatamaz.
|
|
340
|
-
|
|
341
|
-
```ejs
|
|
342
|
-
<script type="application/ld+json"><%- jsonScript(article) %></script>
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
## Yardımcılar: `jskelet/tags`
|
|
346
|
-
|
|
347
|
-
`next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
|
|
348
|
-
string döndürür ve EJS içinden `<%- %>` ile basılır.
|
|
349
|
-
|
|
350
|
-
### `link(props)`
|
|
351
|
-
|
|
352
|
-
```js
|
|
353
|
-
link({
|
|
354
|
-
href: "/hakkinda",
|
|
355
|
-
text: "Hakkında",
|
|
356
|
-
class: "font-semibold",
|
|
357
|
-
// opsiyonel: html, title, ariaLabel, target, rel, attrs
|
|
358
|
-
});
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
- `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
|
|
362
|
-
doldurulur.
|
|
363
|
-
- `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
|
|
364
|
-
`rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
|
|
365
|
-
kullanılır.
|
|
366
|
-
- `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
|
|
367
|
-
- `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
|
|
368
|
-
|
|
369
|
-
### `image(props)`
|
|
370
|
-
|
|
371
|
-
```js
|
|
372
|
-
image({
|
|
373
|
-
src: "/hero.png",
|
|
374
|
-
alt: "Kapak",
|
|
375
|
-
priority: true,
|
|
376
|
-
// opsiyonel: width, height, class, sizes, srcset, fill, loading,
|
|
377
|
-
// unoptimized, attrs
|
|
378
|
-
});
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Davranış:
|
|
382
|
-
|
|
383
|
-
- `public/` altındaki yerel raster görseller için build'de üretilen webp
|
|
384
|
-
varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
|
|
385
|
-
`width`/`height` olarak eklenir. Manifest'te olmayan yerel yollar olduğu
|
|
386
|
-
gibi basılır.
|
|
387
|
-
- `images.remote.allowHosts` açıksa uzak `http(s)` URL'leri
|
|
388
|
-
`/_jskelet/image?url=&w=` proxy'sine çevrilir (webp). `width` varsa 1x/2x
|
|
389
|
-
+ config `widths` ile `srcset` üretilir.
|
|
390
|
-
- `srcset` elle verilmişse ya da `unoptimized: true` ise ne manifest ne de
|
|
391
|
-
remote proxy kullanılır.
|
|
392
|
-
- Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
|
|
393
|
-
yazılmaz; gürültüden ibaret olurdu. Remote'da tek genişlikte bile `src`
|
|
394
|
-
yine optimize URL'dir.
|
|
395
|
-
- `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
|
|
396
|
-
genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
|
|
397
|
-
(`(max-width: Npx) 100vw, Npx`).
|
|
398
|
-
- `priority: true` → `loading="eager"`, `decoding="sync"`,
|
|
399
|
-
`fetchpriority="high"`. LCP görseli için.
|
|
400
|
-
- `priority` yoksa → `loading="lazy"`, `decoding="async"`.
|
|
401
|
-
- `fill: true` → `width`/`height` yazılmaz ve
|
|
402
|
-
`absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
|
|
403
|
-
|
|
404
|
-
### `icon(props)`
|
|
405
|
-
|
|
406
|
-
Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
|
|
407
|
-
|
|
408
|
-
```js
|
|
409
|
-
icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
|
|
410
|
-
// <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
|
|
411
|
-
// fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
- `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
|
|
415
|
-
edilir ve `arrow-right`'a çevrilir (`toKebab()`).
|
|
416
|
-
- `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
|
|
417
|
-
`bold`, `fill`, `duotone`.
|
|
418
|
-
- `size` varsayılan 24; `width` ve `height` olarak yazılır.
|
|
419
|
-
- Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
|
|
420
|
-
basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
|
|
421
|
-
çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
|
|
422
|
-
sessizce boşluk kalır ([08-build.md](./08-build.md)).
|
|
423
|
-
|
|
424
|
-
### `preloadImage(props)`
|
|
425
|
-
|
|
426
|
-
```js
|
|
427
|
-
preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
|
|
428
|
-
// <link rel="preload" as="image" href="…" fetchpriority="high">
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
|
|
432
|
-
|
|
433
|
-
```js
|
|
434
|
-
import { headHints } from "jskelet";
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
view: "pages/article",
|
|
438
|
-
head: headHints({ href: cover, imageSrcSet, imageSizes }),
|
|
439
|
-
};
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
`headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
|
|
443
|
-
Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
|
|
444
|
-
|
|
445
|
-
## Metadata → `<head>`
|
|
446
|
-
|
|
447
|
-
Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
|
|
448
|
-
Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
|
|
449
|
-
gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
|
|
450
|
-
için sürüm çıkarmak zorunda kalmaz.
|
|
451
|
-
|
|
452
|
-
| Alan | Tip | Anlamı |
|
|
453
|
-
| --- | --- | --- |
|
|
454
|
-
| `title` | `string` | `<title>` |
|
|
455
|
-
| `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
|
|
456
|
-
| `description` | `string` | `<meta name="description">` |
|
|
457
|
-
| `canonical` | `string` | Mutlak ya da göreli URL |
|
|
458
|
-
| `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
|
|
459
|
-
| `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
|
|
460
|
-
| `locale` | `string` | `og:locale` |
|
|
461
|
-
| `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
|
|
462
|
-
| `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
|
|
463
|
-
| `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
|
|
464
|
-
|
|
465
|
-
Üretim kuralları:
|
|
466
|
-
|
|
467
|
-
- **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
|
|
468
|
-
olmalı: `robots: { index: false }` → `noindex, follow`.
|
|
469
|
-
- **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
|
|
470
|
-
yazılmış og etiketlerini görmezden geliyor.
|
|
471
|
-
- **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
|
|
472
|
-
`description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
|
|
473
|
-
yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
|
|
474
|
-
- **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
|
|
475
|
-
`summary`.
|
|
476
|
-
- **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
|
|
477
|
-
etiket üretmez.
|
|
478
|
-
- `og:type` verilmezse `website`.
|
|
479
|
-
|
|
480
|
-
Örnek:
|
|
481
|
-
|
|
482
|
-
```js
|
|
483
|
-
return {
|
|
484
|
-
view: "pages/article",
|
|
485
|
-
metadata: {
|
|
486
|
-
title: article.title,
|
|
487
|
-
description: article.summary,
|
|
488
|
-
canonical: `/haber/${article.slug}`,
|
|
489
|
-
openGraph: {
|
|
490
|
-
type: "article",
|
|
491
|
-
image: article.cover,
|
|
492
|
-
imageWidth: 1200,
|
|
493
|
-
imageHeight: 630,
|
|
494
|
-
},
|
|
495
|
-
extraTags: [`<meta property="article:published_time" content="${article.date}">`],
|
|
496
|
-
},
|
|
497
|
-
};
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
`titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
|
|
501
|
-
`hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
|
|
502
|
-
|
|
503
|
-
`renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
|
|
504
|
-
fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
|
|
505
|
-
|
|
506
|
-
## robots.txt
|
|
507
|
-
|
|
508
|
-
`robots.txt`'i uygulama yazar: `public/robots.txt` ya da düz bir route.
|
|
509
|
-
Framework bu gövdeyi değiştirmez; başarılı metin yanıtının **altına** bir
|
|
510
|
-
JSkelet notu ve `Disallow` kuralları ekler. Dosya ya da route yoksa framework
|
|
511
|
-
bir `robots.txt` uydurmaz.
|
|
512
|
-
|
|
513
|
-
Eklenen yollar:
|
|
514
|
-
|
|
515
|
-
- `/_jskelet/` — yönetim paneli, uzak görsel proxy, auth handoff
|
|
516
|
-
- `/__jskelet/` — geliştirme araçları
|
|
517
|
-
- `/_fragment/` — layout'suz parça yanıtları
|
|
518
|
-
|
|
519
|
-
Bu öneklerin dışına taşınmış bir uç da eklenir, ama yalnızca gerçekten
|
|
520
|
-
mount edildiyse: `admin.basePath`, `images.remote.path`,
|
|
521
|
-
`auth.crossSubdomainHandoff.path`. `brand.devBasePath` yalnızca
|
|
522
|
-
development'ta yazılır; production'da o yol uygulamanın kendi sayfası
|
|
523
|
-
olabilir.
|
|
524
|
-
|
|
525
|
-
Not, config'teki marka adıyla başlar (`brand.name`, varsayılan `JSkelet`).
|
|
526
|
-
Alttaki grup `User-agent: *` ile birlikte dosyada adı geçen diğer ajanları
|
|
527
|
-
da tekrarlar. Google, belirli bir ajana ait grubu `*` ile birleştirmez;
|
|
528
|
-
aynı ajanın ikinci grubunu birleştirir. Not dosyada zaten varsa ikinci kez
|
|
529
|
-
eklenmez.
|
|
530
|
-
|
|
531
|
-
## Dinamik OG görselleri
|
|
532
|
-
|
|
533
|
-
Next.js `ImageResponse` / `opengraph-image.tsx` karşılığı. JSX yok: kart
|
|
534
|
-
alanları (`title`, `description`, `siteName`, renkler) ya da ham `svg` verilir.
|
|
535
|
-
`sharp` (opsiyonel peer) kuruluysa PNG, yoksa SVG döner. Sosyal kazıyıcıların
|
|
536
|
-
çoğu PNG beklediği için prod'da `sharp` önerilir.
|
|
537
|
-
|
|
538
|
-
HTML değil görsel döndüğü için `route()` kullanılmaz — `ogHandler` düz bir
|
|
539
|
-
Express handler üretir. `notFound()` ve `null` dönüşü 404 olur.
|
|
540
|
-
|
|
541
|
-
```js
|
|
542
|
-
// routes/35-og.mjs
|
|
543
|
-
export default function register(app, { ogHandler, notFound }) {
|
|
544
|
-
app.get(
|
|
545
|
-
"/og/blog/:slug.png",
|
|
546
|
-
ogHandler(async ({ params }) => {
|
|
547
|
-
const post = getPost(params.slug);
|
|
548
|
-
if (!post) notFound();
|
|
549
|
-
return {
|
|
550
|
-
title: post.title,
|
|
551
|
-
description: post.excerpt,
|
|
552
|
-
siteName: "Blog",
|
|
553
|
-
};
|
|
554
|
-
}),
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
Sayfa metadata'sında mutlak URL ve boyut verin:
|
|
560
|
-
|
|
561
|
-
```js
|
|
562
|
-
openGraph: {
|
|
563
|
-
type: "article",
|
|
564
|
-
image: `${SITE_URL}/og/blog/${post.slug}.png`,
|
|
565
|
-
imageWidth: 1200,
|
|
566
|
-
imageHeight: 630,
|
|
567
|
-
},
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
Ham SVG veya Next benzeri sınıf:
|
|
571
|
-
|
|
572
|
-
```js
|
|
573
|
-
import { ImageResponse, sendOgImage, OG_SIZE } from "jskelet";
|
|
574
|
-
|
|
575
|
-
app.get("/og/custom.png", async (req, res) => {
|
|
576
|
-
const image = new ImageResponse(
|
|
577
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">…</svg>`,
|
|
578
|
-
OG_SIZE,
|
|
579
|
-
);
|
|
580
|
-
await image.send(res);
|
|
581
|
-
// veya: await sendOgImage(res, { title: "…", format: "svg" });
|
|
582
|
-
});
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
Varsayılan `Cache-Control`:
|
|
586
|
-
`public, max-age=0, s-maxage=86400, stale-while-revalidate=604800`.
|
|
587
|
-
`cacheControl` seçeneğiyle ezilir. Çalışan örnek: `examples/blog/routes/35-og.mjs`.
|
|
588
|
-
|
|
589
|
-
## Hook'lar
|
|
590
|
-
|
|
591
|
-
Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
|
|
592
|
-
hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
|
|
593
|
-
varsayılanına döner ve uyarır.
|
|
594
|
-
|
|
595
|
-
### `hooks.metadata(page)`
|
|
596
|
-
|
|
597
|
-
Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
|
|
598
|
-
alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
|
|
599
|
-
**üzerine biner** (alan bazında, sığ birleştirme).
|
|
600
|
-
|
|
601
|
-
```js
|
|
602
|
-
hooks: {
|
|
603
|
-
metadata() {
|
|
604
|
-
return {
|
|
605
|
-
titleTemplate: "%s | JSkelet",
|
|
606
|
-
description: "JSkelet ile kurulmuş bir site.",
|
|
607
|
-
siteUrl: "https://ornek.com",
|
|
608
|
-
};
|
|
609
|
-
},
|
|
610
|
-
}
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### `hooks.layoutContext({ pathname, metadata })`
|
|
614
|
-
|
|
615
|
-
Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
|
|
616
|
-
layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
|
|
617
|
-
|
|
618
|
-
- `lang` → `<html lang>`
|
|
619
|
-
- `structuredData` → JSON-LD script'leri (dizi)
|
|
620
|
-
- `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
|
|
621
|
-
- `bodyClass` → controller `bodyClass` vermemişse kullanılır
|
|
622
|
-
|
|
623
|
-
```js
|
|
624
|
-
hooks: {
|
|
625
|
-
async layoutContext({ pathname }) {
|
|
626
|
-
return {
|
|
627
|
-
bodyClass: "min-h-full",
|
|
628
|
-
navigation: await getNavigation(),
|
|
629
|
-
isHome: pathname === "/",
|
|
630
|
-
};
|
|
631
|
-
},
|
|
632
|
-
}
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
|
|
636
|
-
sıralı gecikme eklemez.
|
|
637
|
-
|
|
638
|
-
### `hooks.notFound()`
|
|
639
|
-
|
|
640
|
-
404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
|
|
641
|
-
verilir. Ayrıntı: [03-routing.md](./03-routing.md).
|
|
642
|
-
|
|
643
|
-
### Diğer hook'lar
|
|
644
|
-
|
|
645
|
-
`hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
|
|
646
|
-
[06-cache.md](./06-cache.md).
|
|
647
|
-
|
|
648
|
-
## Overlay portal noktası
|
|
649
|
-
|
|
650
|
-
`jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
|
|
651
|
-
hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
|
|
652
|
-
`body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
|
|
653
|
-
`position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
|
|
654
|
-
layout'un `<body>` sonuna eklemek yeterli
|
|
655
|
-
([05-islands.md](./05-islands.md)).
|
|
656
|
-
|
|
657
|
-
## Sırada ne var
|
|
658
|
-
|
|
659
|
-
- Island'lar ve `entries`: [05-islands.md](./05-islands.md)
|
|
660
|
-
- `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
|
|
661
|
-
- Hook'ların config içindeki yeri: [07-yapilandirma.md](./07-yapilandirma.md)
|
|
1
|
+
# 04 — Render ve şablonlar
|
|
2
|
+
|
|
3
|
+
Bu belge sunucu HTML'inin nasıl üretildiğini anlatır: EJS motorunun ayarları,
|
|
4
|
+
layout dosyasının çözümü ve kullanabildiği local'ler, `views/pages` altındaki
|
|
5
|
+
sayfa şablonları, `views/components/**` altındaki bileşenlerin otomatik kaydı,
|
|
6
|
+
şablonlara hazır gelen `html`/`tags` yardımcıları, `metadata` nesnesinin `<head>`
|
|
7
|
+
etiketlerine çevrilmesi ve üç render hook'u. Controller'ın bu katmana ne
|
|
8
|
+
gönderdiği [03-routing.md](./03-routing.md)'de, varlık URL'lerini üreten
|
|
9
|
+
`asset()`/`hasAsset()` [08-build.md](./08-build.md)'de anlatılıyor.
|
|
10
|
+
|
|
11
|
+
## Render hattı
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
route(controller)
|
|
15
|
+
└─ produce()
|
|
16
|
+
├─ controller(ctx) → sayfa tanımı
|
|
17
|
+
└─ renderPage(page)
|
|
18
|
+
├─ hooks.metadata(page) + page.metadata → metadata
|
|
19
|
+
├─ Promise.all([
|
|
20
|
+
│ renderView(page.view, { …data, metadata }), → body
|
|
21
|
+
│ hooks.layoutContext({ pathname, metadata }), → context
|
|
22
|
+
│ ])
|
|
23
|
+
└─ layout (.jsk derlenmiş veya .ejs) → tam HTML
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Layout bağlamı ve gövde **paralel** üretilir. Sebebi ölçümden geliyor:
|
|
27
|
+
navigasyon çoğu projede upstream'den geliyor ve gövde render'ıyla sırayla
|
|
28
|
+
beklemek her sayfaya gereksiz gecikme ekliyor.
|
|
29
|
+
|
|
30
|
+
## `.jsk` — build-time derlenmiş şablonlar
|
|
31
|
+
|
|
32
|
+
Yeni uygulamalarda varsayılan şablon biçimi `.jsk`'dir. Build sırasında
|
|
33
|
+
(`.jskelet/templates/*.mjs`) normal ESM modüllerine çevrilir; **istek anında
|
|
34
|
+
parse / `eval` / `new Function` yoktur**. Production yolu:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
controller data → import edilmiş render(data, helpers) → HTML
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Sözdizimi özeti
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<section class="wrapper">
|
|
44
|
+
<h1>{{ title }}</h1>
|
|
45
|
+
<div>{{{ trustedHtml }}}</div>
|
|
46
|
+
|
|
47
|
+
{#if items.length}
|
|
48
|
+
<List :items="items" />
|
|
49
|
+
{#else}
|
|
50
|
+
<p>Boş</p>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
{#each items as item, i}
|
|
54
|
+
<li :data-i="i">{{ item }}</li>
|
|
55
|
+
{/each}
|
|
56
|
+
|
|
57
|
+
<Link href="/" text="Home" />
|
|
58
|
+
<div data-island="counter" data-island-props='{"start":0}'></div>
|
|
59
|
+
</section>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Özellik | Yazım |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Kaçışlı metin | `{{ expr }}` |
|
|
65
|
+
| Ham HTML | `{{{ expr }}}` |
|
|
66
|
+
| Koşul | `{#if expr}` … `{#else}` … `{/if}` |
|
|
67
|
+
| Döngü | `{#each list as item}` veya `as item, i` |
|
|
68
|
+
| Include | `{#include "partials/header"}` (derlenmiş `.jsk`) |
|
|
69
|
+
| Bileşen | PascalCase etiket; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
|
|
70
|
+
| Yerleşikler | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
|
|
71
|
+
|
|
72
|
+
İfade dili kasıtlı olarak dardır (erişim, karşılaştırma, ternary, `.length`).
|
|
73
|
+
Atama, object literal ve rastgele fonksiyon çağrısı yok — mantık controller
|
|
74
|
+
veya JS bileşende kalır.
|
|
75
|
+
|
|
76
|
+
#### Şablon mu, bileşen mi?
|
|
77
|
+
|
|
78
|
+
EJS’den geçerken sınırı erken çizmek işe yarar:
|
|
79
|
+
|
|
80
|
+
| Burada kalsın (`.jsk`) | JS bileşene taşı |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| Metin, koşul, liste, prop bağlama | Fonksiyon çağrısı, nesne üretimi, biçimlendirme |
|
|
83
|
+
| Yerleşik etiketler (`Link`, `Image`, …) | Birden fazla yardımcıdan HTML birleştirme |
|
|
84
|
+
| Controller’dan gelen hazır veri | Upstream / hata ayırt eden UI (`LoadErrorState`) |
|
|
85
|
+
|
|
86
|
+
Şablonda `format(x)` veya `{ a: 1 }` yazılamıyorsa bu bir eksik değil: o iş
|
|
87
|
+
`views/components/*.js` veya controller’ındır. Karmaşık sayfalar bileşene
|
|
88
|
+
kaçıyorsa ifade dilini genişletmek yerine bileşen sınırını net tutmak tercih
|
|
89
|
+
edilir.
|
|
90
|
+
|
|
91
|
+
### Editör desteği
|
|
92
|
+
|
|
93
|
+
Repo içinde `extensions/vscode-jsk` VS Code / Cursor uzantısı vardır: sözdizimi
|
|
94
|
+
renklendirme, dil yapılandırması ve snippet'ler. Yerel kurulum:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
code --install-extension extensions/vscode-jsk
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Ayrıntılar uzantı README'sinde.
|
|
101
|
+
|
|
102
|
+
### Yerleşik layout etiketleri
|
|
103
|
+
|
|
104
|
+
`.jsk` ifade dilinde `asset()` / `hasAsset()` çağrılamaz. Layout’ta stylesheet,
|
|
105
|
+
script ve JSON-LD döngüleri için yerleşikler:
|
|
106
|
+
|
|
107
|
+
| Etiket | Props | Çıktı |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `Stylesheets` | `styles` | `app.css` + sayfa sheet’leri (`data-jskelet-css`) |
|
|
110
|
+
| `BodyScripts` | `entries`, `devtools`, `devBasePath` | `main.js`, entry’ler, isteğe bağlı overlay |
|
|
111
|
+
| `JsonLd` | `items` (`structuredData`) | `application/ld+json` script’leri |
|
|
112
|
+
|
|
113
|
+
### EJS ile birlikte yaşam
|
|
114
|
+
|
|
115
|
+
Aynı `view` id için derlenmiş `.jsk` varsa o kullanılır; yoksa `.ejs` dosyası
|
|
116
|
+
**yalnızca `ejs` peer’i kuruluysa** render edilir. `jskelet init` yeni iskeleti
|
|
117
|
+
`.jsk` ile kurar.
|
|
118
|
+
|
|
119
|
+
## EJS motoru (legacy peer)
|
|
120
|
+
|
|
121
|
+
EJS opsiyonel peer bağımlılıktır (`npm i ejs`). `.jsk`-only uygulamalar kurmak
|
|
122
|
+
zorunda değildir. Bir `.ejs` view veya layout istendiğinde paket uygulamadan
|
|
123
|
+
yüklenir; yoksa göç yolunu gösteren bir hata fırlatılır.
|
|
124
|
+
|
|
125
|
+
Motor ilk EJS render’da bir kez kurulur. Ayarlar:
|
|
126
|
+
|
|
127
|
+
| Ayar | Değer | Sebebi |
|
|
128
|
+
| --- | --- | --- |
|
|
129
|
+
| `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
|
|
130
|
+
| `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
|
|
131
|
+
| `rmWhitespace` | `true` | çıktı boyutu |
|
|
132
|
+
| `async` | `true` | şablon içinde `await` kullanılabilir |
|
|
133
|
+
|
|
134
|
+
Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık.
|
|
135
|
+
|
|
136
|
+
## Layout
|
|
137
|
+
|
|
138
|
+
### Layout dosyası nasıl bulunur
|
|
139
|
+
|
|
140
|
+
1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
|
|
141
|
+
dizininin üst dizinine** göre çözülür: `views` varsayılansa
|
|
142
|
+
`layout: "views/ozel.jsk"` → `<root>/views/ozel.jsk`.
|
|
143
|
+
2. Verilmemişse `views/layout.jsk` (derlenmiş) varsa o kullanılır.
|
|
144
|
+
3. Yoksa `views/layout.ejs` varsa o kullanılır (EJS peer gerekir).
|
|
145
|
+
4. O da yoksa framework'ün kendi minimal layout'u kullanılır
|
|
146
|
+
(`node_modules/jskelet/src/templates/layout.jsk`, `jskelet/layout`;
|
|
147
|
+
legacy kopya `jskelet/layout/ejs`).
|
|
148
|
+
|
|
149
|
+
Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
|
|
150
|
+
layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.jsk` olarak
|
|
151
|
+
kopyalamaktır.
|
|
152
|
+
|
|
153
|
+
### Framework'ün varsayılan layout'u
|
|
154
|
+
|
|
155
|
+
```jsk
|
|
156
|
+
<!DOCTYPE html>
|
|
157
|
+
<html :lang="lang">
|
|
158
|
+
<head>
|
|
159
|
+
<meta charset="utf-8">
|
|
160
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
161
|
+
{{{ extraHead }}}
|
|
162
|
+
<Stylesheets :styles="styles" />
|
|
163
|
+
{{{ headMeta }}}
|
|
164
|
+
<JsonLd :items="structuredData" />
|
|
165
|
+
</head>
|
|
166
|
+
<body :class="bodyClass">
|
|
167
|
+
{{{ body }}}
|
|
168
|
+
<BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
|
|
169
|
+
</body>
|
|
170
|
+
</html>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Dikkat edilecek noktalar:
|
|
174
|
+
|
|
175
|
+
- **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
|
|
176
|
+
geciktirmek doğrudan LCP'ye yazılır.
|
|
177
|
+
- **Global `app.css` render-blocking** ve gerekçesi
|
|
178
|
+
[02-mimari.md](./02-mimari.md)'de. Controller `styles: [...]` ile ek sayfa
|
|
179
|
+
sheet'leri de aynı şekilde basılır. Build çalışmadıysa `hasAsset` false olur
|
|
180
|
+
ve etiket hiç basılmaz.
|
|
181
|
+
- **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
|
|
182
|
+
istememesini sağlar (`Stylesheets` / `BodyScripts` içinde).
|
|
183
|
+
- **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
|
|
184
|
+
çıktısında hiç yoktur.
|
|
185
|
+
|
|
186
|
+
### Layout local'leri
|
|
187
|
+
|
|
188
|
+
| Local | Tip | Kaynağı |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
|
|
191
|
+
| `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
|
|
192
|
+
| `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
|
|
193
|
+
| `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
|
|
194
|
+
| `body` | `string` | Sayfa şablonunun render çıktısı |
|
|
195
|
+
| `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
|
|
196
|
+
| `entries` | `string[]` | controller `entries`; varsayılan `[]` |
|
|
197
|
+
| `styles` | `string[]` | controller `styles`; varsayılan `[]` |
|
|
198
|
+
| `pathname` | `string` | `req.path`; **varsayılan boş string** |
|
|
199
|
+
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
200
|
+
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
|
201
|
+
| `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
|
|
202
|
+
| `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
|
|
203
|
+
| html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
204
|
+
| `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
|
|
205
|
+
| `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
|
|
206
|
+
|
|
207
|
+
`pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
|
|
208
|
+
sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
|
|
209
|
+
|
|
210
|
+
## Sayfa şablonları
|
|
211
|
+
|
|
212
|
+
`view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
|
|
213
|
+
`views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
|
|
214
|
+
`metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
|
|
215
|
+
ve bileşenlere erişir.
|
|
216
|
+
|
|
217
|
+
```ejs
|
|
218
|
+
<%# views/pages/home.ejs %>
|
|
219
|
+
<section class="wrapper">
|
|
220
|
+
<h1 class="text-3xl font-bold"><%= heading %></h1>
|
|
221
|
+
|
|
222
|
+
<%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
|
|
223
|
+
<%- list({ items }) %>
|
|
224
|
+
|
|
225
|
+
<div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
|
|
226
|
+
</section>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
EJS'te iki çıktı biçimini karıştırmayın:
|
|
230
|
+
|
|
231
|
+
- `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
|
|
232
|
+
- `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
|
|
233
|
+
(bileşen çağrıları, `headMeta`, `body`).
|
|
234
|
+
|
|
235
|
+
`async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
|
|
236
|
+
veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
|
|
237
|
+
|
|
238
|
+
## Bileşenler: `views/components/**`
|
|
239
|
+
|
|
240
|
+
Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
|
|
241
|
+
`views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
|
|
242
|
+
şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
|
|
243
|
+
eklemek için dosyayı oluşturmak yeterli.
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
// views/components/list.js
|
|
247
|
+
import { esc } from "jskelet/html";
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {{ items: string[] }} props
|
|
251
|
+
* @returns {string}
|
|
252
|
+
*/
|
|
253
|
+
export function list({ items }) {
|
|
254
|
+
if (!items?.length) return "";
|
|
255
|
+
|
|
256
|
+
const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
|
|
257
|
+
return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Şablonda:
|
|
262
|
+
|
|
263
|
+
```ejs
|
|
264
|
+
<%- list({ items }) %>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Kurallar:
|
|
268
|
+
|
|
269
|
+
- Tarama özyinelemelidir; alt dizinler de kapsanır.
|
|
270
|
+
- `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
|
|
271
|
+
- Compile-time bilinen bileşen listesi **dosya adından değil**, kaynak
|
|
272
|
+
metindeki named export'lardan okunur. `ui.js` içindeki `sectionHead` →
|
|
273
|
+
şablonda `<SectionHead />` (runtime zaten camelCase export'a PascalCase
|
|
274
|
+
alias ekler). Dosya adına göre stub re-export eklemeye gerek yoktur.
|
|
275
|
+
- `loader.js` ve `index.js` bileşen dosyası sayılmaz.
|
|
276
|
+
- `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
|
|
277
|
+
önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
|
|
278
|
+
bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
|
|
279
|
+
- Aynı ad (veya aynı PascalCase etiket) iki farklı bileşen dosyasında
|
|
280
|
+
tanımlıysa **uyarı değil hata**: build ve sunucu açılışı
|
|
281
|
+
`Component 'card' is defined twice: …` ile durur. Barrel üzerine yazmak
|
|
282
|
+
bilinçli istisnadır.
|
|
283
|
+
- `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
|
|
284
|
+
bir proje de çalışır.
|
|
285
|
+
|
|
286
|
+
## Yardımcılar: `jskelet/html`
|
|
287
|
+
|
|
288
|
+
Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
|
|
289
|
+
ile alınır.
|
|
290
|
+
|
|
291
|
+
### `esc(value)`
|
|
292
|
+
|
|
293
|
+
Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
|
|
294
|
+
`null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
|
|
295
|
+
`false && "…"` gibi ifadeler `"false"` basmaz.
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
esc('<b>"x"</b>'); // "<b>"x"</b>"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `attrs(object)`
|
|
302
|
+
|
|
303
|
+
Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
|
|
304
|
+
boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
|
|
305
|
+
değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
|
|
306
|
+
doğru biçimlenir.
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
`<input${attrs({ type: "text", required: true, value: null })}>`;
|
|
310
|
+
// '<input type="text" required>'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### `cx(...inputs)`
|
|
314
|
+
|
|
315
|
+
`clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
|
|
316
|
+
falsy değerleri atar. Tailwind çakışması **çözmez**.
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### `cn(...inputs)`
|
|
323
|
+
|
|
324
|
+
`cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
|
|
325
|
+
Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
|
|
326
|
+
bunu kullanın.
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
|
|
333
|
+
yalnızca sunucuda yapılır; client bundle'a hiç girmez.
|
|
334
|
+
|
|
335
|
+
### `jsonScript(value)`
|
|
336
|
+
|
|
337
|
+
`<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
|
|
338
|
+
ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
|
|
339
|
+
kapatamaz.
|
|
340
|
+
|
|
341
|
+
```ejs
|
|
342
|
+
<script type="application/ld+json"><%- jsonScript(article) %></script>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Yardımcılar: `jskelet/tags`
|
|
346
|
+
|
|
347
|
+
`next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
|
|
348
|
+
string döndürür ve EJS içinden `<%- %>` ile basılır.
|
|
349
|
+
|
|
350
|
+
### `link(props)`
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
link({
|
|
354
|
+
href: "/hakkinda",
|
|
355
|
+
text: "Hakkında",
|
|
356
|
+
class: "font-semibold",
|
|
357
|
+
// opsiyonel: html, title, ariaLabel, target, rel, attrs
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
|
|
362
|
+
doldurulur.
|
|
363
|
+
- `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
|
|
364
|
+
`rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
|
|
365
|
+
kullanılır.
|
|
366
|
+
- `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
|
|
367
|
+
- `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
|
|
368
|
+
|
|
369
|
+
### `image(props)`
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
image({
|
|
373
|
+
src: "/hero.png",
|
|
374
|
+
alt: "Kapak",
|
|
375
|
+
priority: true,
|
|
376
|
+
// opsiyonel: width, height, class, sizes, srcset, fill, loading,
|
|
377
|
+
// unoptimized, attrs
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Davranış:
|
|
382
|
+
|
|
383
|
+
- `public/` altındaki yerel raster görseller için build'de üretilen webp
|
|
384
|
+
varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
|
|
385
|
+
`width`/`height` olarak eklenir. Manifest'te olmayan yerel yollar olduğu
|
|
386
|
+
gibi basılır.
|
|
387
|
+
- `images.remote.allowHosts` açıksa uzak `http(s)` URL'leri
|
|
388
|
+
`/_jskelet/image?url=&w=` proxy'sine çevrilir (webp). `width` varsa 1x/2x
|
|
389
|
+
+ config `widths` ile `srcset` üretilir.
|
|
390
|
+
- `srcset` elle verilmişse ya da `unoptimized: true` ise ne manifest ne de
|
|
391
|
+
remote proxy kullanılır.
|
|
392
|
+
- Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
|
|
393
|
+
yazılmaz; gürültüden ibaret olurdu. Remote'da tek genişlikte bile `src`
|
|
394
|
+
yine optimize URL'dir.
|
|
395
|
+
- `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
|
|
396
|
+
genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
|
|
397
|
+
(`(max-width: Npx) 100vw, Npx`).
|
|
398
|
+
- `priority: true` → `loading="eager"`, `decoding="sync"`,
|
|
399
|
+
`fetchpriority="high"`. LCP görseli için.
|
|
400
|
+
- `priority` yoksa → `loading="lazy"`, `decoding="async"`.
|
|
401
|
+
- `fill: true` → `width`/`height` yazılmaz ve
|
|
402
|
+
`absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
|
|
403
|
+
|
|
404
|
+
### `icon(props)`
|
|
405
|
+
|
|
406
|
+
Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
|
|
410
|
+
// <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
|
|
411
|
+
// fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
- `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
|
|
415
|
+
edilir ve `arrow-right`'a çevrilir (`toKebab()`).
|
|
416
|
+
- `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
|
|
417
|
+
`bold`, `fill`, `duotone`.
|
|
418
|
+
- `size` varsayılan 24; `width` ve `height` olarak yazılır.
|
|
419
|
+
- Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
|
|
420
|
+
basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
|
|
421
|
+
çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
|
|
422
|
+
sessizce boşluk kalır ([08-build.md](./08-build.md)).
|
|
423
|
+
|
|
424
|
+
### `preloadImage(props)`
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
|
|
428
|
+
// <link rel="preload" as="image" href="…" fetchpriority="high">
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
import { headHints } from "jskelet";
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
view: "pages/article",
|
|
438
|
+
head: headHints({ href: cover, imageSrcSet, imageSizes }),
|
|
439
|
+
};
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
|
|
443
|
+
Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
|
|
444
|
+
|
|
445
|
+
## Metadata → `<head>`
|
|
446
|
+
|
|
447
|
+
Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
|
|
448
|
+
Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
|
|
449
|
+
gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
|
|
450
|
+
için sürüm çıkarmak zorunda kalmaz.
|
|
451
|
+
|
|
452
|
+
| Alan | Tip | Anlamı |
|
|
453
|
+
| --- | --- | --- |
|
|
454
|
+
| `title` | `string` | `<title>` |
|
|
455
|
+
| `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
|
|
456
|
+
| `description` | `string` | `<meta name="description">` |
|
|
457
|
+
| `canonical` | `string` | Mutlak ya da göreli URL |
|
|
458
|
+
| `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
|
|
459
|
+
| `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
|
|
460
|
+
| `locale` | `string` | `og:locale` |
|
|
461
|
+
| `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
|
|
462
|
+
| `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
|
|
463
|
+
| `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
|
|
464
|
+
|
|
465
|
+
Üretim kuralları:
|
|
466
|
+
|
|
467
|
+
- **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
|
|
468
|
+
olmalı: `robots: { index: false }` → `noindex, follow`.
|
|
469
|
+
- **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
|
|
470
|
+
yazılmış og etiketlerini görmezden geliyor.
|
|
471
|
+
- **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
|
|
472
|
+
`description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
|
|
473
|
+
yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
|
|
474
|
+
- **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
|
|
475
|
+
`summary`.
|
|
476
|
+
- **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
|
|
477
|
+
etiket üretmez.
|
|
478
|
+
- `og:type` verilmezse `website`.
|
|
479
|
+
|
|
480
|
+
Örnek:
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
return {
|
|
484
|
+
view: "pages/article",
|
|
485
|
+
metadata: {
|
|
486
|
+
title: article.title,
|
|
487
|
+
description: article.summary,
|
|
488
|
+
canonical: `/haber/${article.slug}`,
|
|
489
|
+
openGraph: {
|
|
490
|
+
type: "article",
|
|
491
|
+
image: article.cover,
|
|
492
|
+
imageWidth: 1200,
|
|
493
|
+
imageHeight: 630,
|
|
494
|
+
},
|
|
495
|
+
extraTags: [`<meta property="article:published_time" content="${article.date}">`],
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
|
|
501
|
+
`hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
|
|
502
|
+
|
|
503
|
+
`renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
|
|
504
|
+
fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
|
|
505
|
+
|
|
506
|
+
## robots.txt
|
|
507
|
+
|
|
508
|
+
`robots.txt`'i uygulama yazar: `public/robots.txt` ya da düz bir route.
|
|
509
|
+
Framework bu gövdeyi değiştirmez; başarılı metin yanıtının **altına** bir
|
|
510
|
+
JSkelet notu ve `Disallow` kuralları ekler. Dosya ya da route yoksa framework
|
|
511
|
+
bir `robots.txt` uydurmaz.
|
|
512
|
+
|
|
513
|
+
Eklenen yollar:
|
|
514
|
+
|
|
515
|
+
- `/_jskelet/` — yönetim paneli, uzak görsel proxy, auth handoff
|
|
516
|
+
- `/__jskelet/` — geliştirme araçları
|
|
517
|
+
- `/_fragment/` — layout'suz parça yanıtları
|
|
518
|
+
|
|
519
|
+
Bu öneklerin dışına taşınmış bir uç da eklenir, ama yalnızca gerçekten
|
|
520
|
+
mount edildiyse: `admin.basePath`, `images.remote.path`,
|
|
521
|
+
`auth.crossSubdomainHandoff.path`. `brand.devBasePath` yalnızca
|
|
522
|
+
development'ta yazılır; production'da o yol uygulamanın kendi sayfası
|
|
523
|
+
olabilir.
|
|
524
|
+
|
|
525
|
+
Not, config'teki marka adıyla başlar (`brand.name`, varsayılan `JSkelet`).
|
|
526
|
+
Alttaki grup `User-agent: *` ile birlikte dosyada adı geçen diğer ajanları
|
|
527
|
+
da tekrarlar. Google, belirli bir ajana ait grubu `*` ile birleştirmez;
|
|
528
|
+
aynı ajanın ikinci grubunu birleştirir. Not dosyada zaten varsa ikinci kez
|
|
529
|
+
eklenmez.
|
|
530
|
+
|
|
531
|
+
## Dinamik OG görselleri
|
|
532
|
+
|
|
533
|
+
Next.js `ImageResponse` / `opengraph-image.tsx` karşılığı. JSX yok: kart
|
|
534
|
+
alanları (`title`, `description`, `siteName`, renkler) ya da ham `svg` verilir.
|
|
535
|
+
`sharp` (opsiyonel peer) kuruluysa PNG, yoksa SVG döner. Sosyal kazıyıcıların
|
|
536
|
+
çoğu PNG beklediği için prod'da `sharp` önerilir.
|
|
537
|
+
|
|
538
|
+
HTML değil görsel döndüğü için `route()` kullanılmaz — `ogHandler` düz bir
|
|
539
|
+
Express handler üretir. `notFound()` ve `null` dönüşü 404 olur.
|
|
540
|
+
|
|
541
|
+
```js
|
|
542
|
+
// routes/35-og.mjs
|
|
543
|
+
export default function register(app, { ogHandler, notFound }) {
|
|
544
|
+
app.get(
|
|
545
|
+
"/og/blog/:slug.png",
|
|
546
|
+
ogHandler(async ({ params }) => {
|
|
547
|
+
const post = getPost(params.slug);
|
|
548
|
+
if (!post) notFound();
|
|
549
|
+
return {
|
|
550
|
+
title: post.title,
|
|
551
|
+
description: post.excerpt,
|
|
552
|
+
siteName: "Blog",
|
|
553
|
+
};
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Sayfa metadata'sında mutlak URL ve boyut verin:
|
|
560
|
+
|
|
561
|
+
```js
|
|
562
|
+
openGraph: {
|
|
563
|
+
type: "article",
|
|
564
|
+
image: `${SITE_URL}/og/blog/${post.slug}.png`,
|
|
565
|
+
imageWidth: 1200,
|
|
566
|
+
imageHeight: 630,
|
|
567
|
+
},
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Ham SVG veya Next benzeri sınıf:
|
|
571
|
+
|
|
572
|
+
```js
|
|
573
|
+
import { ImageResponse, sendOgImage, OG_SIZE } from "jskelet";
|
|
574
|
+
|
|
575
|
+
app.get("/og/custom.png", async (req, res) => {
|
|
576
|
+
const image = new ImageResponse(
|
|
577
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630">…</svg>`,
|
|
578
|
+
OG_SIZE,
|
|
579
|
+
);
|
|
580
|
+
await image.send(res);
|
|
581
|
+
// veya: await sendOgImage(res, { title: "…", format: "svg" });
|
|
582
|
+
});
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Varsayılan `Cache-Control`:
|
|
586
|
+
`public, max-age=0, s-maxage=86400, stale-while-revalidate=604800`.
|
|
587
|
+
`cacheControl` seçeneğiyle ezilir. Çalışan örnek: `examples/blog/routes/35-og.mjs`.
|
|
588
|
+
|
|
589
|
+
## Hook'lar
|
|
590
|
+
|
|
591
|
+
Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
|
|
592
|
+
hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
|
|
593
|
+
varsayılanına döner ve uyarır.
|
|
594
|
+
|
|
595
|
+
### `hooks.metadata(page)`
|
|
596
|
+
|
|
597
|
+
Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
|
|
598
|
+
alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
|
|
599
|
+
**üzerine biner** (alan bazında, sığ birleştirme).
|
|
600
|
+
|
|
601
|
+
```js
|
|
602
|
+
hooks: {
|
|
603
|
+
metadata() {
|
|
604
|
+
return {
|
|
605
|
+
titleTemplate: "%s | JSkelet",
|
|
606
|
+
description: "JSkelet ile kurulmuş bir site.",
|
|
607
|
+
siteUrl: "https://ornek.com",
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### `hooks.layoutContext({ pathname, metadata })`
|
|
614
|
+
|
|
615
|
+
Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
|
|
616
|
+
layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
|
|
617
|
+
|
|
618
|
+
- `lang` → `<html lang>`
|
|
619
|
+
- `structuredData` → JSON-LD script'leri (dizi)
|
|
620
|
+
- `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
|
|
621
|
+
- `bodyClass` → controller `bodyClass` vermemişse kullanılır
|
|
622
|
+
|
|
623
|
+
```js
|
|
624
|
+
hooks: {
|
|
625
|
+
async layoutContext({ pathname }) {
|
|
626
|
+
return {
|
|
627
|
+
bodyClass: "min-h-full",
|
|
628
|
+
navigation: await getNavigation(),
|
|
629
|
+
isHome: pathname === "/",
|
|
630
|
+
};
|
|
631
|
+
},
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
|
|
636
|
+
sıralı gecikme eklemez.
|
|
637
|
+
|
|
638
|
+
### `hooks.notFound()`
|
|
639
|
+
|
|
640
|
+
404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
|
|
641
|
+
verilir. Ayrıntı: [03-routing.md](./03-routing.md).
|
|
642
|
+
|
|
643
|
+
### Diğer hook'lar
|
|
644
|
+
|
|
645
|
+
`hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
|
|
646
|
+
[06-cache.md](./06-cache.md).
|
|
647
|
+
|
|
648
|
+
## Overlay portal noktası
|
|
649
|
+
|
|
650
|
+
`jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
|
|
651
|
+
hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
|
|
652
|
+
`body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
|
|
653
|
+
`position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
|
|
654
|
+
layout'un `<body>` sonuna eklemek yeterli
|
|
655
|
+
([05-islands.md](./05-islands.md)).
|
|
656
|
+
|
|
657
|
+
## Sırada ne var
|
|
658
|
+
|
|
659
|
+
- Island'lar ve `entries`: [05-islands.md](./05-islands.md)
|
|
660
|
+
- `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
|
|
661
|
+
- Hook'ların config içindeki yeri: [07-yapilandirma.md](./07-yapilandirma.md)
|