jskelet 0.2.3 → 0.2.5
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 +132 -132
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +103 -103
- package/docs/01-baslangic.md +285 -285
- package/docs/02-mimari.md +287 -287
- package/docs/03-routing.md +480 -480
- package/docs/04-render-ve-sablonlar.md +490 -490
- package/docs/05-islands.md +482 -482
- package/docs/06-cache.md +1209 -1202
- package/docs/08-build.md +366 -366
- package/docs/09-dev-araclari.md +335 -335
- package/docs/10-dagitim.md +329 -329
- package/docs/12-panel-ve-oturum.md +384 -384
- package/docs/README.md +105 -105
- package/docs/en/01-getting-started.md +292 -292
- package/docs/en/02-architecture.md +305 -305
- package/docs/en/03-routing.md +497 -497
- package/docs/en/04-rendering.md +504 -504
- package/docs/en/05-islands.md +492 -492
- package/docs/en/06-caching.md +1239 -1232
- package/docs/en/07-configuration.md +986 -986
- package/docs/en/08-build.md +383 -383
- package/docs/en/09-dev-tools.md +342 -342
- package/docs/en/10-deployment.md +332 -332
- package/docs/en/11-migration.md +359 -359
- package/docs/en/12-dashboards-and-sessions.md +392 -392
- package/docs/en/README.md +112 -112
- package/package.json +102 -102
- package/src/build/ensure-build.mjs +15 -15
- package/src/build/paths.mjs +143 -143
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +268 -268
- package/src/build/tasks/css.mjs +124 -124
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +224 -224
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/client/cache-panel/i18n.js +670 -0
- package/src/client/cache-panel/login.html +74 -71
- package/src/client/cache-panel/panel.css +756 -740
- package/src/client/cache-panel/panel.html +308 -307
- package/src/client/cache-panel/panel.js +915 -808
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +725 -725
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +35 -35
- package/src/client/registry.js +297 -297
- package/src/client/safe-image.js +91 -91
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/config/pattern.js +107 -107
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies.js +257 -257
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +162 -162
- package/src/index.js +83 -83
- package/src/init.mjs +221 -221
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/assets.js +147 -147
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-panel.js +759 -738
- package/src/server/cloudflare.js +607 -595
- package/src/server/create-app.js +291 -291
- package/src/server/data-cache.js +462 -462
- package/src/server/dev/report.js +369 -369
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/html-cache.js +817 -817
- 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 +62 -62
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/static-precompressed.js +100 -100
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/prewarm.js +601 -601
- package/src/server/redis.js +569 -569
- package/src/server/router.js +128 -128
- package/src/server/status-page.js +164 -164
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/start.mjs +7 -7
- package/src/templates/layout.ejs +44 -44
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +85 -85
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +245 -245
|
@@ -1,490 +1,490 @@
|
|
|
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.ejs render → 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
|
-
## EJS motoru
|
|
31
|
-
|
|
32
|
-
Motor ilk render'da bir kez kurulur; bileşen taraması dosya sistemine
|
|
33
|
-
dokunduğu için her istekte yapılamaz ve config yüklenmeden hesaplanamaz.
|
|
34
|
-
|
|
35
|
-
Ayarlar:
|
|
36
|
-
|
|
37
|
-
| Ayar | Değer | Sebebi |
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
|
|
40
|
-
| `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
|
|
41
|
-
| `rmWhitespace` | `true` | çıktı boyutu |
|
|
42
|
-
| `async` | `true` | şablon içinde `await` kullanılabilir |
|
|
43
|
-
|
|
44
|
-
Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık: bileşen
|
|
45
|
-
dosyaları değişince kaydı yeniler. Dev sunucusu süreci yeniden başlattığı için
|
|
46
|
-
normal akışta gerekmez.
|
|
47
|
-
|
|
48
|
-
## Layout
|
|
49
|
-
|
|
50
|
-
### Layout dosyası nasıl bulunur
|
|
51
|
-
|
|
52
|
-
1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
|
|
53
|
-
dizininin üst dizinine** göre çözülür: `views` varsayılansa
|
|
54
|
-
`layout: "views/ozel.ejs"` → `<root>/views/ozel.ejs`.
|
|
55
|
-
2. Verilmemişse `views/layout.ejs` varsa o kullanılır.
|
|
56
|
-
3. O da yoksa framework'ün kendi minimal layout'u kullanılır
|
|
57
|
-
(`node_modules/jskelet/src/templates/layout.ejs`, ayrıca
|
|
58
|
-
`jskelet/layout` belirteciyle de erişilebilir).
|
|
59
|
-
|
|
60
|
-
Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
|
|
61
|
-
layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.ejs` olarak
|
|
62
|
-
kopyalamaktır.
|
|
63
|
-
|
|
64
|
-
### Framework'ün varsayılan layout'u
|
|
65
|
-
|
|
66
|
-
```ejs
|
|
67
|
-
<!DOCTYPE html>
|
|
68
|
-
<html lang="<%= lang %>">
|
|
69
|
-
<head>
|
|
70
|
-
<meta charset="utf-8">
|
|
71
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
72
|
-
<%- extraHead %>
|
|
73
|
-
<% if (hasAsset('app.css')) { %>
|
|
74
|
-
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
75
|
-
<% } %>
|
|
76
|
-
<%- headMeta %>
|
|
77
|
-
<% structuredData.forEach(function (item) { %>
|
|
78
|
-
<script type="application/ld+json"><%- jsonScript(item) %></script>
|
|
79
|
-
<% }); %>
|
|
80
|
-
</head>
|
|
81
|
-
<body class="<%= bodyClass %>">
|
|
82
|
-
<%- body %>
|
|
83
|
-
<% if (hasAsset('main.js')) { %>
|
|
84
|
-
<script type="module" src="<%= asset('main.js') %>"></script>
|
|
85
|
-
<% } %>
|
|
86
|
-
<% entries.forEach(function (entry) { %>
|
|
87
|
-
<script type="module" src="<%= asset(entry) %>"></script>
|
|
88
|
-
<% }); %>
|
|
89
|
-
<% if (devtools) { %>
|
|
90
|
-
<script type="module" src="<%= devBasePath %>/overlay.js"></script>
|
|
91
|
-
<% } %>
|
|
92
|
-
</body>
|
|
93
|
-
</html>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Dikkat edilecek noktalar:
|
|
97
|
-
|
|
98
|
-
- **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
|
|
99
|
-
geciktirmek doğrudan LCP'ye yazılır.
|
|
100
|
-
- **Tek, render-blocking stylesheet** ve gerekçesi
|
|
101
|
-
[02-mimari.md](./02-mimari.md)'de. Build çalışmadıysa `hasAsset('app.css')`
|
|
102
|
-
false olur ve etiket hiç basılmaz.
|
|
103
|
-
- **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
|
|
104
|
-
istememesini sağlar.
|
|
105
|
-
- **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
|
|
106
|
-
çıktısında hiç yoktur.
|
|
107
|
-
|
|
108
|
-
### Layout local'leri
|
|
109
|
-
|
|
110
|
-
| Local | Tip | Kaynağı |
|
|
111
|
-
| --- | --- | --- |
|
|
112
|
-
| `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
|
|
113
|
-
| `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
|
|
114
|
-
| `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
|
|
115
|
-
| `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
|
|
116
|
-
| `body` | `string` | Sayfa şablonunun render çıktısı |
|
|
117
|
-
| `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
|
|
118
|
-
| `entries` | `string[]` | controller `entries`; varsayılan `[]` |
|
|
119
|
-
| `pathname` | `string` | `req.path`; **varsayılan boş string** |
|
|
120
|
-
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
121
|
-
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
|
122
|
-
| `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
|
|
123
|
-
| `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
|
|
124
|
-
| html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
125
|
-
| `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
|
|
126
|
-
| `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
|
|
127
|
-
|
|
128
|
-
`pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
|
|
129
|
-
sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
|
|
130
|
-
|
|
131
|
-
## Sayfa şablonları
|
|
132
|
-
|
|
133
|
-
`view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
|
|
134
|
-
`views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
|
|
135
|
-
`metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
|
|
136
|
-
ve bileşenlere erişir.
|
|
137
|
-
|
|
138
|
-
```ejs
|
|
139
|
-
<%# views/pages/home.ejs %>
|
|
140
|
-
<section class="wrapper">
|
|
141
|
-
<h1 class="text-3xl font-bold"><%= heading %></h1>
|
|
142
|
-
|
|
143
|
-
<%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
|
|
144
|
-
<%- list({ items }) %>
|
|
145
|
-
|
|
146
|
-
<div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
|
|
147
|
-
</section>
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
EJS'te iki çıktı biçimini karıştırmayın:
|
|
151
|
-
|
|
152
|
-
- `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
|
|
153
|
-
- `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
|
|
154
|
-
(bileşen çağrıları, `headMeta`, `body`).
|
|
155
|
-
|
|
156
|
-
`async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
|
|
157
|
-
veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
|
|
158
|
-
|
|
159
|
-
## Bileşenler: `views/components/**`
|
|
160
|
-
|
|
161
|
-
Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
|
|
162
|
-
`views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
|
|
163
|
-
şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
|
|
164
|
-
eklemek için dosyayı oluşturmak yeterli.
|
|
165
|
-
|
|
166
|
-
```js
|
|
167
|
-
// views/components/list.js
|
|
168
|
-
import { esc } from "jskelet/html";
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* @param {{ items: string[] }} props
|
|
172
|
-
* @returns {string}
|
|
173
|
-
*/
|
|
174
|
-
export function list({ items }) {
|
|
175
|
-
if (!items?.length) return "";
|
|
176
|
-
|
|
177
|
-
const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
|
|
178
|
-
return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Şablonda:
|
|
183
|
-
|
|
184
|
-
```ejs
|
|
185
|
-
<%- list({ items }) %>
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Kurallar:
|
|
189
|
-
|
|
190
|
-
- Tarama özyinelemelidir; alt dizinler de kapsanır.
|
|
191
|
-
- `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
|
|
192
|
-
- `loader.js` ve `index.js` bileşen dosyası sayılmaz.
|
|
193
|
-
- `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
|
|
194
|
-
önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
|
|
195
|
-
bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
|
|
196
|
-
- Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
|
|
197
|
-
kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
|
|
198
|
-
one wins.`
|
|
199
|
-
- `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
|
|
200
|
-
bir proje de çalışır.
|
|
201
|
-
|
|
202
|
-
## Yardımcılar: `jskelet/html`
|
|
203
|
-
|
|
204
|
-
Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
|
|
205
|
-
ile alınır.
|
|
206
|
-
|
|
207
|
-
### `esc(value)`
|
|
208
|
-
|
|
209
|
-
Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
|
|
210
|
-
`null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
|
|
211
|
-
`false && "…"` gibi ifadeler `"false"` basmaz.
|
|
212
|
-
|
|
213
|
-
```js
|
|
214
|
-
esc('<b>"x"</b>'); // "<b>"x"</b>"
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### `attrs(object)`
|
|
218
|
-
|
|
219
|
-
Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
|
|
220
|
-
boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
|
|
221
|
-
değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
|
|
222
|
-
doğru biçimlenir.
|
|
223
|
-
|
|
224
|
-
```js
|
|
225
|
-
`<input${attrs({ type: "text", required: true, value: null })}>`;
|
|
226
|
-
// '<input type="text" required>'
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### `cx(...inputs)`
|
|
230
|
-
|
|
231
|
-
`clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
|
|
232
|
-
falsy değerleri atar. Tailwind çakışması **çözmez**.
|
|
233
|
-
|
|
234
|
-
```js
|
|
235
|
-
cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### `cn(...inputs)`
|
|
239
|
-
|
|
240
|
-
`cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
|
|
241
|
-
Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
|
|
242
|
-
bunu kullanın.
|
|
243
|
-
|
|
244
|
-
```js
|
|
245
|
-
cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
`tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
|
|
249
|
-
yalnızca sunucuda yapılır; client bundle'a hiç girmez.
|
|
250
|
-
|
|
251
|
-
### `jsonScript(value)`
|
|
252
|
-
|
|
253
|
-
`<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
|
|
254
|
-
ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
|
|
255
|
-
kapatamaz.
|
|
256
|
-
|
|
257
|
-
```ejs
|
|
258
|
-
<script type="application/ld+json"><%- jsonScript(article) %></script>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
## Yardımcılar: `jskelet/tags`
|
|
262
|
-
|
|
263
|
-
`next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
|
|
264
|
-
string döndürür ve EJS içinden `<%- %>` ile basılır.
|
|
265
|
-
|
|
266
|
-
### `link(props)`
|
|
267
|
-
|
|
268
|
-
```js
|
|
269
|
-
link({
|
|
270
|
-
href: "/hakkinda",
|
|
271
|
-
text: "Hakkında",
|
|
272
|
-
class: "font-semibold",
|
|
273
|
-
// opsiyonel: html, title, ariaLabel, target, rel, attrs
|
|
274
|
-
});
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
- `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
|
|
278
|
-
doldurulur.
|
|
279
|
-
- `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
|
|
280
|
-
`rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
|
|
281
|
-
kullanılır.
|
|
282
|
-
- `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
|
|
283
|
-
- `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
|
|
284
|
-
|
|
285
|
-
### `image(props)`
|
|
286
|
-
|
|
287
|
-
```js
|
|
288
|
-
image({
|
|
289
|
-
src: "/hero.png",
|
|
290
|
-
alt: "Kapak",
|
|
291
|
-
priority: true,
|
|
292
|
-
// opsiyonel: width, height, class, sizes, srcset, fill, loading,
|
|
293
|
-
// unoptimized, attrs
|
|
294
|
-
});
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Davranış:
|
|
298
|
-
|
|
299
|
-
- `public/` altındaki yerel raster görseller için build'de üretilen webp
|
|
300
|
-
varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
|
|
301
|
-
`width`/`height` olarak eklenir. Manifest'te olmayan ya da uzak görseller
|
|
302
|
-
olduğu gibi basılır.
|
|
303
|
-
- `srcset` elle verilmişse ya da `unoptimized: true` ise manifest'e hiç
|
|
304
|
-
bakılmaz.
|
|
305
|
-
- Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
|
|
306
|
-
yazılmaz; gürültüden ibaret olurdu.
|
|
307
|
-
- `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
|
|
308
|
-
genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
|
|
309
|
-
(`(max-width: Npx) 100vw, Npx`).
|
|
310
|
-
- `priority: true` → `loading="eager"`, `decoding="sync"`,
|
|
311
|
-
`fetchpriority="high"`. LCP görseli için.
|
|
312
|
-
- `priority` yoksa → `loading="lazy"`, `decoding="async"`.
|
|
313
|
-
- `fill: true` → `width`/`height` yazılmaz ve
|
|
314
|
-
`absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
|
|
315
|
-
|
|
316
|
-
### `icon(props)`
|
|
317
|
-
|
|
318
|
-
Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
|
|
319
|
-
|
|
320
|
-
```js
|
|
321
|
-
icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
|
|
322
|
-
// <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
|
|
323
|
-
// fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
- `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
|
|
327
|
-
edilir ve `arrow-right`'a çevrilir (`toKebab()`).
|
|
328
|
-
- `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
|
|
329
|
-
`bold`, `fill`, `duotone`.
|
|
330
|
-
- `size` varsayılan 24; `width` ve `height` olarak yazılır.
|
|
331
|
-
- Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
|
|
332
|
-
basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
|
|
333
|
-
çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
|
|
334
|
-
sessizce boşluk kalır ([08-build.md](./08-build.md)).
|
|
335
|
-
|
|
336
|
-
### `preloadImage(props)`
|
|
337
|
-
|
|
338
|
-
```js
|
|
339
|
-
preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
|
|
340
|
-
// <link rel="preload" as="image" href="…" fetchpriority="high">
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
|
|
344
|
-
|
|
345
|
-
```js
|
|
346
|
-
import { headHints } from "jskelet";
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
view: "pages/article",
|
|
350
|
-
head: headHints({ href: cover, imageSrcSet, imageSizes }),
|
|
351
|
-
};
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
`headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
|
|
355
|
-
Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
|
|
356
|
-
|
|
357
|
-
## Metadata → `<head>`
|
|
358
|
-
|
|
359
|
-
Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
|
|
360
|
-
Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
|
|
361
|
-
gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
|
|
362
|
-
için sürüm çıkarmak zorunda kalmaz.
|
|
363
|
-
|
|
364
|
-
| Alan | Tip | Anlamı |
|
|
365
|
-
| --- | --- | --- |
|
|
366
|
-
| `title` | `string` | `<title>` |
|
|
367
|
-
| `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
|
|
368
|
-
| `description` | `string` | `<meta name="description">` |
|
|
369
|
-
| `canonical` | `string` | Mutlak ya da göreli URL |
|
|
370
|
-
| `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
|
|
371
|
-
| `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
|
|
372
|
-
| `locale` | `string` | `og:locale` |
|
|
373
|
-
| `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
|
|
374
|
-
| `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
|
|
375
|
-
| `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
|
|
376
|
-
|
|
377
|
-
Üretim kuralları:
|
|
378
|
-
|
|
379
|
-
- **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
|
|
380
|
-
olmalı: `robots: { index: false }` → `noindex, follow`.
|
|
381
|
-
- **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
|
|
382
|
-
yazılmış og etiketlerini görmezden geliyor.
|
|
383
|
-
- **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
|
|
384
|
-
`description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
|
|
385
|
-
yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
|
|
386
|
-
- **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
|
|
387
|
-
`summary`.
|
|
388
|
-
- **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
|
|
389
|
-
etiket üretmez.
|
|
390
|
-
- `og:type` verilmezse `website`.
|
|
391
|
-
|
|
392
|
-
Örnek:
|
|
393
|
-
|
|
394
|
-
```js
|
|
395
|
-
return {
|
|
396
|
-
view: "pages/article",
|
|
397
|
-
metadata: {
|
|
398
|
-
title: article.title,
|
|
399
|
-
description: article.summary,
|
|
400
|
-
canonical: `/haber/${article.slug}`,
|
|
401
|
-
openGraph: {
|
|
402
|
-
type: "article",
|
|
403
|
-
image: article.cover,
|
|
404
|
-
imageWidth: 1200,
|
|
405
|
-
imageHeight: 630,
|
|
406
|
-
},
|
|
407
|
-
extraTags: [`<meta property="article:published_time" content="${article.date}">`],
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
`titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
|
|
413
|
-
`hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
|
|
414
|
-
|
|
415
|
-
`renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
|
|
416
|
-
fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
|
|
417
|
-
|
|
418
|
-
## Hook'lar
|
|
419
|
-
|
|
420
|
-
Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
|
|
421
|
-
hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
|
|
422
|
-
varsayılanına döner ve uyarır.
|
|
423
|
-
|
|
424
|
-
### `hooks.metadata(page)`
|
|
425
|
-
|
|
426
|
-
Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
|
|
427
|
-
alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
|
|
428
|
-
**üzerine biner** (alan bazında, sığ birleştirme).
|
|
429
|
-
|
|
430
|
-
```js
|
|
431
|
-
hooks: {
|
|
432
|
-
metadata() {
|
|
433
|
-
return {
|
|
434
|
-
titleTemplate: "%s | JSkelet",
|
|
435
|
-
description: "JSkelet ile kurulmuş bir site.",
|
|
436
|
-
siteUrl: "https://ornek.com",
|
|
437
|
-
};
|
|
438
|
-
},
|
|
439
|
-
}
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
### `hooks.layoutContext({ pathname, metadata })`
|
|
443
|
-
|
|
444
|
-
Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
|
|
445
|
-
layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
|
|
446
|
-
|
|
447
|
-
- `lang` → `<html lang>`
|
|
448
|
-
- `structuredData` → JSON-LD script'leri (dizi)
|
|
449
|
-
- `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
|
|
450
|
-
- `bodyClass` → controller `bodyClass` vermemişse kullanılır
|
|
451
|
-
|
|
452
|
-
```js
|
|
453
|
-
hooks: {
|
|
454
|
-
async layoutContext({ pathname }) {
|
|
455
|
-
return {
|
|
456
|
-
bodyClass: "min-h-full",
|
|
457
|
-
navigation: await getNavigation(),
|
|
458
|
-
isHome: pathname === "/",
|
|
459
|
-
};
|
|
460
|
-
},
|
|
461
|
-
}
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
|
|
465
|
-
sıralı gecikme eklemez.
|
|
466
|
-
|
|
467
|
-
### `hooks.notFound()`
|
|
468
|
-
|
|
469
|
-
404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
|
|
470
|
-
verilir. Ayrıntı: [03-routing.md](./03-routing.md).
|
|
471
|
-
|
|
472
|
-
### Diğer hook'lar
|
|
473
|
-
|
|
474
|
-
`hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
|
|
475
|
-
[06-cache.md](./06-cache.md).
|
|
476
|
-
|
|
477
|
-
## Overlay portal noktası
|
|
478
|
-
|
|
479
|
-
`jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
|
|
480
|
-
hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
|
|
481
|
-
`body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
|
|
482
|
-
`position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
|
|
483
|
-
layout'un `<body>` sonuna eklemek yeterli
|
|
484
|
-
([05-islands.md](./05-islands.md)).
|
|
485
|
-
|
|
486
|
-
## Sırada ne var
|
|
487
|
-
|
|
488
|
-
- Island'lar ve `entries`: [05-islands.md](./05-islands.md)
|
|
489
|
-
- `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
|
|
490
|
-
- 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.ejs render → 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
|
+
## EJS motoru
|
|
31
|
+
|
|
32
|
+
Motor ilk render'da bir kez kurulur; bileşen taraması dosya sistemine
|
|
33
|
+
dokunduğu için her istekte yapılamaz ve config yüklenmeden hesaplanamaz.
|
|
34
|
+
|
|
35
|
+
Ayarlar:
|
|
36
|
+
|
|
37
|
+
| Ayar | Değer | Sebebi |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `root`, `views` | `views` dizini | `include('partials/header')` çağrıları views kökünden çözülür |
|
|
40
|
+
| `cache` | dev'de `false`, prod'da `true` | dev'de şablon düzenlemesi anında görünsün |
|
|
41
|
+
| `rmWhitespace` | `true` | çıktı boyutu |
|
|
42
|
+
| `async` | `true` | şablon içinde `await` kullanılabilir |
|
|
43
|
+
|
|
44
|
+
Gömülü kullanımlar (test, script) için `resetRenderEngine()` dışa açık: bileşen
|
|
45
|
+
dosyaları değişince kaydı yeniler. Dev sunucusu süreci yeniden başlattığı için
|
|
46
|
+
normal akışta gerekmez.
|
|
47
|
+
|
|
48
|
+
## Layout
|
|
49
|
+
|
|
50
|
+
### Layout dosyası nasıl bulunur
|
|
51
|
+
|
|
52
|
+
1. `jskelet.config.mjs` → `layout` verilmişse o kullanılır. Yol, **views
|
|
53
|
+
dizininin üst dizinine** göre çözülür: `views` varsayılansa
|
|
54
|
+
`layout: "views/ozel.ejs"` → `<root>/views/ozel.ejs`.
|
|
55
|
+
2. Verilmemişse `views/layout.ejs` varsa o kullanılır.
|
|
56
|
+
3. O da yoksa framework'ün kendi minimal layout'u kullanılır
|
|
57
|
+
(`node_modules/jskelet/src/templates/layout.ejs`, ayrıca
|
|
58
|
+
`jskelet/layout` belirteciyle de erişilebilir).
|
|
59
|
+
|
|
60
|
+
Üçüncü seçenek yeni bir projenin tek route ile çalışabilmesi için var. Kendi
|
|
61
|
+
layout'unuza geçmenin en pratik yolu o dosyayı `views/layout.ejs` olarak
|
|
62
|
+
kopyalamaktır.
|
|
63
|
+
|
|
64
|
+
### Framework'ün varsayılan layout'u
|
|
65
|
+
|
|
66
|
+
```ejs
|
|
67
|
+
<!DOCTYPE html>
|
|
68
|
+
<html lang="<%= lang %>">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
72
|
+
<%- extraHead %>
|
|
73
|
+
<% if (hasAsset('app.css')) { %>
|
|
74
|
+
<link rel="stylesheet" href="<%= asset('app.css') %>">
|
|
75
|
+
<% } %>
|
|
76
|
+
<%- headMeta %>
|
|
77
|
+
<% structuredData.forEach(function (item) { %>
|
|
78
|
+
<script type="application/ld+json"><%- jsonScript(item) %></script>
|
|
79
|
+
<% }); %>
|
|
80
|
+
</head>
|
|
81
|
+
<body class="<%= bodyClass %>">
|
|
82
|
+
<%- body %>
|
|
83
|
+
<% if (hasAsset('main.js')) { %>
|
|
84
|
+
<script type="module" src="<%= asset('main.js') %>"></script>
|
|
85
|
+
<% } %>
|
|
86
|
+
<% entries.forEach(function (entry) { %>
|
|
87
|
+
<script type="module" src="<%= asset(entry) %>"></script>
|
|
88
|
+
<% }); %>
|
|
89
|
+
<% if (devtools) { %>
|
|
90
|
+
<script type="module" src="<%= devBasePath %>/overlay.js"></script>
|
|
91
|
+
<% } %>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Dikkat edilecek noktalar:
|
|
97
|
+
|
|
98
|
+
- **`extraHead` en başta.** Kaynak ipuçlarını (`preconnect`, LCP `preload`)
|
|
99
|
+
geciktirmek doğrudan LCP'ye yazılır.
|
|
100
|
+
- **Tek, render-blocking stylesheet** ve gerekçesi
|
|
101
|
+
[02-mimari.md](./02-mimari.md)'de. Build çalışmadıysa `hasAsset('app.css')`
|
|
102
|
+
false olur ve etiket hiç basılmaz.
|
|
103
|
+
- **`hasAsset` kontrolleri** build eksikken sayfanın 404 veren dosyaları
|
|
104
|
+
istememesini sağlar.
|
|
105
|
+
- **Devtools script'i** yalnızca `NODE_ENV=development` iken basılır; prod
|
|
106
|
+
çıktısında hiç yoktur.
|
|
107
|
+
|
|
108
|
+
### Layout local'leri
|
|
109
|
+
|
|
110
|
+
| Local | Tip | Kaynağı |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `metadata` | `object` | `hooks.metadata()` + controller `metadata` (controller kazanır) |
|
|
113
|
+
| `headMeta` | `string` | `metadata`dan üretilmiş hazır `<head>` etiketleri |
|
|
114
|
+
| `extraHead` | `string` | `preconnect` ipuçları + `navigation` ipuçları + controller `head` + `context.extraHead` |
|
|
115
|
+
| `structuredData` | `unknown[]` | `hooks.layoutContext()` → `structuredData`; varsayılan `[]` |
|
|
116
|
+
| `body` | `string` | Sayfa şablonunun render çıktısı |
|
|
117
|
+
| `bodyClass` | `string` | controller `bodyClass` → `context.bodyClass` → `""` |
|
|
118
|
+
| `entries` | `string[]` | controller `entries`; varsayılan `[]` |
|
|
119
|
+
| `pathname` | `string` | `req.path`; **varsayılan boş string** |
|
|
120
|
+
| `lang` | `string` | `context.lang` → `brand.lang` → `"en"` |
|
|
121
|
+
| `devtools` | `boolean` | `NODE_ENV === "development"` |
|
|
122
|
+
| `devBasePath` | `string` | `brand.devBasePath`, varsayılan `/__jskelet/dev` |
|
|
123
|
+
| `asset`, `hasAsset` | fonksiyon | Manifest erişimi |
|
|
124
|
+
| html/tags yardımcıları | fonksiyon | `esc`, `attrs`, `cx`, `cn`, `jsonScript`, `link`, `image`, `icon`, `preloadImage`, `toKebab` |
|
|
125
|
+
| `views/components/**` export'ları | fonksiyon | Otomatik kayıt |
|
|
126
|
+
| `hooks.layoutContext()` çıktısındaki her alan | — | Doğrudan local olur |
|
|
127
|
+
|
|
128
|
+
`pathname`'in boş varsayılanı bilinçli: `"/"` yazmak her sayfayı ana sayfa
|
|
129
|
+
sanıp logoyu `<h1>` olarak bastıran türde hatalara yol açıyor.
|
|
130
|
+
|
|
131
|
+
## Sayfa şablonları
|
|
132
|
+
|
|
133
|
+
`view` alanı `views/` altındaki yolu uzantısız verir: `"pages/home"` →
|
|
134
|
+
`views/pages/home.ejs`. Şablona geçen local'ler `data` alanının içeriği artı
|
|
135
|
+
`metadata`dır — layout local'leri **değil**. Sayfa şablonu yine tüm yardımcılara
|
|
136
|
+
ve bileşenlere erişir.
|
|
137
|
+
|
|
138
|
+
```ejs
|
|
139
|
+
<%# views/pages/home.ejs %>
|
|
140
|
+
<section class="wrapper">
|
|
141
|
+
<h1 class="text-3xl font-bold"><%= heading %></h1>
|
|
142
|
+
|
|
143
|
+
<%# `list` views/components/list.js içinde tanımlı; import gerekmiyor. %>
|
|
144
|
+
<%- list({ items }) %>
|
|
145
|
+
|
|
146
|
+
<div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
|
|
147
|
+
</section>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
EJS'te iki çıktı biçimini karıştırmayın:
|
|
151
|
+
|
|
152
|
+
- `<%= value %>` — HTML kaçışlı. Kullanıcı/upstream verisi için **daima** bu.
|
|
153
|
+
- `<%- html %>` — ham. Yalnızca kendi ürettiğin, güvenli HTML string'leri için
|
|
154
|
+
(bileşen çağrıları, `headMeta`, `body`).
|
|
155
|
+
|
|
156
|
+
`async: true` açık olduğu için şablon içinde `await` da kullanılabilir, ancak
|
|
157
|
+
veri çekmeyi controller'da tutmak teşhisi kolaylaştırır.
|
|
158
|
+
|
|
159
|
+
## Bileşenler: `views/components/**`
|
|
160
|
+
|
|
161
|
+
Bileşenler EJS partial'ı değil, **HTML string döndüren fonksiyonlardır**.
|
|
162
|
+
`views/components/**` altındaki her `.js` dosyası taranır ve **her named export**
|
|
163
|
+
şablon local'i olur. Elle bakılan bir barrel dosyası yok: yeni bir bileşen
|
|
164
|
+
eklemek için dosyayı oluşturmak yeterli.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// views/components/list.js
|
|
168
|
+
import { esc } from "jskelet/html";
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {{ items: string[] }} props
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
export function list({ items }) {
|
|
175
|
+
if (!items?.length) return "";
|
|
176
|
+
|
|
177
|
+
const rows = items.map((item) => `<li class="py-1">${esc(item)}</li>`).join("");
|
|
178
|
+
return `<ul class="mt-6 list-disc pl-6">${rows}</ul>`;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Şablonda:
|
|
183
|
+
|
|
184
|
+
```ejs
|
|
185
|
+
<%- list({ items }) %>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Kurallar:
|
|
189
|
+
|
|
190
|
+
- Tarama özyinelemelidir; alt dizinler de kapsanır.
|
|
191
|
+
- `default` export'lar yok sayılır — yalnızca named export'lar kaydedilir.
|
|
192
|
+
- `loader.js` ve `index.js` bileşen dosyası sayılmaz.
|
|
193
|
+
- `views/components/index.js` varsa **barrel** olarak, en düşük öncelikle en
|
|
194
|
+
önce yüklenir. Tek amacı `lib/` yeniden ihraçlarını şablon local'i yapmak;
|
|
195
|
+
bileşenlerin kendi dosyaları sonradan gelip sessizce üzerine yazar.
|
|
196
|
+
- Aynı ad iki farklı bileşen dosyasında tanımlıysa uyarı basılır ve **ikincisi
|
|
197
|
+
kazanır**: `[components] 'card' is defined twice: a.js and b.js — the second
|
|
198
|
+
one wins.`
|
|
199
|
+
- `views/components` dizini yoksa bileşen kaydı boş kalır; bileşen kullanmayan
|
|
200
|
+
bir proje de çalışır.
|
|
201
|
+
|
|
202
|
+
## Yardımcılar: `jskelet/html`
|
|
203
|
+
|
|
204
|
+
Şablonlara otomatik geçer; bileşen dosyalarında `import { … } from "jskelet/html"`
|
|
205
|
+
ile alınır.
|
|
206
|
+
|
|
207
|
+
### `esc(value)`
|
|
208
|
+
|
|
209
|
+
Metin içeriği ve attribute değerleri için kaçış (`&`, `<`, `>`, `"`, `'`).
|
|
210
|
+
`null`, `undefined` ve `false` boş string'e çevrilir — koşullu render'da
|
|
211
|
+
`false && "…"` gibi ifadeler `"false"` basmaz.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
esc('<b>"x"</b>'); // "<b>"x"</b>"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `attrs(object)`
|
|
218
|
+
|
|
219
|
+
Attribute nesnesini string'e çevirir. `null`/`undefined`/`false` atlanır, `true`
|
|
220
|
+
boolean attribute olarak yazılır, geri kalan değerler kaçışlanır. Çıktı boş
|
|
221
|
+
değilse **başında bir boşluk** ile döner, böylece `<div${attrs(...)}>` her zaman
|
|
222
|
+
doğru biçimlenir.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
`<input${attrs({ type: "text", required: true, value: null })}>`;
|
|
226
|
+
// '<input type="text" required>'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `cx(...inputs)`
|
|
230
|
+
|
|
231
|
+
`clsx` karşılığı: string, sayı, dizi ve `{ sınıf: koşul }` nesnesi kabul eder,
|
|
232
|
+
falsy değerleri atar. Tailwind çakışması **çözmez**.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
cx("btn", isActive && "btn-active", { "btn-lg": size === "lg" });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### `cn(...inputs)`
|
|
239
|
+
|
|
240
|
+
`cx()` ile birleştirir, sonra `tailwind-merge` ile Tailwind çakışmalarını çözer.
|
|
241
|
+
Bir bileşenin varsayılan sınıflarının çağıran tarafından ezilmesi gerektiğinde
|
|
242
|
+
bunu kullanın.
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
cn("px-4 py-2 bg-slate-100", className); // className "bg-white" ise bg-slate-100 düşer
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`tailwind-merge` çalışma zamanı bağımlılığı olarak korunur çünkü sınıf hesabı
|
|
249
|
+
yalnızca sunucuda yapılır; client bundle'a hiç girmez.
|
|
250
|
+
|
|
251
|
+
### `jsonScript(value)`
|
|
252
|
+
|
|
253
|
+
`<script type="application/ld+json">` gövdesi için güvenli JSON: `<`, `>`, `&`
|
|
254
|
+
ve U+2028/U+2029 kaçırılır, böylece `</script` ya da `<!--` dizileri gövdeyi
|
|
255
|
+
kapatamaz.
|
|
256
|
+
|
|
257
|
+
```ejs
|
|
258
|
+
<script type="application/ld+json"><%- jsonScript(article) %></script>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Yardımcılar: `jskelet/tags`
|
|
262
|
+
|
|
263
|
+
`next/link`, `next/image` ve `@phosphor-icons/react` karşılıkları. Hepsi HTML
|
|
264
|
+
string döndürür ve EJS içinden `<%- %>` ile basılır.
|
|
265
|
+
|
|
266
|
+
### `link(props)`
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
link({
|
|
270
|
+
href: "/hakkinda",
|
|
271
|
+
text: "Hakkında",
|
|
272
|
+
class: "font-semibold",
|
|
273
|
+
// opsiyonel: html, title, ariaLabel, target, rel, attrs
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- `title` verilmezse `ariaLabel` → `text` → `href` sırasıyla otomatik
|
|
278
|
+
doldurulur.
|
|
279
|
+
- `href` `http://` ya da `https://` ile başlıyorsa `target="_blank"` ve
|
|
280
|
+
`rel="noopener noreferrer"` otomatik eklenir; açıkça verirsen senin değerin
|
|
281
|
+
kullanılır.
|
|
282
|
+
- `html` verilirse içerik ham basılır; `text` verilirse kaçışlanır.
|
|
283
|
+
- `attrs` nesnesi ek attribute'ları geçirir ve öncekilerin üzerine biner.
|
|
284
|
+
|
|
285
|
+
### `image(props)`
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
image({
|
|
289
|
+
src: "/hero.png",
|
|
290
|
+
alt: "Kapak",
|
|
291
|
+
priority: true,
|
|
292
|
+
// opsiyonel: width, height, class, sizes, srcset, fill, loading,
|
|
293
|
+
// unoptimized, attrs
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Davranış:
|
|
298
|
+
|
|
299
|
+
- `public/` altındaki yerel raster görseller için build'de üretilen webp
|
|
300
|
+
varyantları (`.jskelet/images.json`) otomatik olarak `srcset` + intrinsic
|
|
301
|
+
`width`/`height` olarak eklenir. Manifest'te olmayan ya da uzak görseller
|
|
302
|
+
olduğu gibi basılır.
|
|
303
|
+
- `srcset` elle verilmişse ya da `unoptimized: true` ise manifest'e hiç
|
|
304
|
+
bakılmaz.
|
|
305
|
+
- Yalnızca **tek** varyant üretilmişse (kaynak zaten küçükse) `srcset`/`sizes`
|
|
306
|
+
yazılmaz; gürültüden ibaret olurdu.
|
|
307
|
+
- `sizes` verilmezse makul bir varsayılan üretilir: görsel kendi intrinsic
|
|
308
|
+
genişliğinden büyütülmez, dar ekranlarda viewport'u kaplar
|
|
309
|
+
(`(max-width: Npx) 100vw, Npx`).
|
|
310
|
+
- `priority: true` → `loading="eager"`, `decoding="sync"`,
|
|
311
|
+
`fetchpriority="high"`. LCP görseli için.
|
|
312
|
+
- `priority` yoksa → `loading="lazy"`, `decoding="async"`.
|
|
313
|
+
- `fill: true` → `width`/`height` yazılmaz ve
|
|
314
|
+
`absolute inset-0 h-full w-full object-cover` sınıfları `cn()` ile birleştirilir.
|
|
315
|
+
|
|
316
|
+
### `icon(props)`
|
|
317
|
+
|
|
318
|
+
Build zamanı üretilen SVG sprite'tan `<use>` çıkarır.
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
icon({ name: "ArrowRight", weight: "bold", size: 20, class: "text-slate-500" });
|
|
322
|
+
// <svg width="20" height="20" class="…" aria-hidden="true" focusable="false"
|
|
323
|
+
// fill="currentColor" viewBox="0 0 256 256"><use href="/assets/sprite.<hash>.svg#arrow-right-bold"></use></svg>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- `name` Phosphor adıdır; `ArrowRightIcon` ve `ArrowRight` biçimleri de kabul
|
|
327
|
+
edilir ve `arrow-right`'a çevrilir (`toKebab()`).
|
|
328
|
+
- `weight` sprite id'sine dâhildir: `thin`, `light`, `regular` (varsayılan),
|
|
329
|
+
`bold`, `fill`, `duotone`.
|
|
330
|
+
- `size` varsayılan 24; `width` ve `height` olarak yazılır.
|
|
331
|
+
- Development'ta sprite'ta olmayan bir sembol istendiğinde tek seferlik uyarı
|
|
332
|
+
basılır. Sprite yalnızca kaynakta **statik olarak** görülen adları içerir; adı
|
|
333
|
+
çalışma anında hesaplanan bir çağrı eksik sembole işaret ederse ekranda
|
|
334
|
+
sessizce boşluk kalır ([08-build.md](./08-build.md)).
|
|
335
|
+
|
|
336
|
+
### `preloadImage(props)`
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
preloadImage({ href: "/assets/img/hero-1280.abc.webp", imagesrcset, imagesizes });
|
|
340
|
+
// <link rel="preload" as="image" href="…" fetchpriority="high">
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Pratikte doğrudan çağırmak yerine `headHints()` kullanılır:
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
import { headHints } from "jskelet";
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
view: "pages/article",
|
|
350
|
+
head: headHints({ href: cover, imageSrcSet, imageSizes }),
|
|
351
|
+
};
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`headHints()` `href` yoksa boş string döner, yani koşul yazmak gerekmez.
|
|
355
|
+
Preconnect'leri layout zaten her sayfaya bastığı için burada tekrarlanmaz.
|
|
356
|
+
|
|
357
|
+
## Metadata → `<head>`
|
|
358
|
+
|
|
359
|
+
Controller `metadata` döndürür, framework onu etiketlere çevirir (Next.js'in
|
|
360
|
+
Metadata API'sinin karşılığı). Şema bilinçli olarak küçük; daha fazlası
|
|
361
|
+
gerekirse `extraTags` ile ham HTML eklenir, böylece framework her yeni meta türü
|
|
362
|
+
için sürüm çıkarmak zorunda kalmaz.
|
|
363
|
+
|
|
364
|
+
| Alan | Tip | Anlamı |
|
|
365
|
+
| --- | --- | --- |
|
|
366
|
+
| `title` | `string` | `<title>` |
|
|
367
|
+
| `titleTemplate` | `string` | `"%s \| Site"` — `title` buna gömülür. Yalnızca `title` da varsa uygulanır. |
|
|
368
|
+
| `description` | `string` | `<meta name="description">` |
|
|
369
|
+
| `canonical` | `string` | Mutlak ya da göreli URL |
|
|
370
|
+
| `siteUrl` | `string` | Göreli `canonical`ı mutlaklaştırmak için taban |
|
|
371
|
+
| `robots` | `{ index?: boolean, follow?: boolean }` | Varsayılan `index, follow` |
|
|
372
|
+
| `locale` | `string` | `og:locale` |
|
|
373
|
+
| `openGraph` | `{ title, description, url, type, siteName, image, imageWidth, imageHeight }` | `og:*` etiketleri |
|
|
374
|
+
| `twitter` | `{ card, site, creator, title, description, image }` | `twitter:*` etiketleri |
|
|
375
|
+
| `extraTags` | `string[]` | Olduğu gibi basılacak ham etiketler |
|
|
376
|
+
|
|
377
|
+
Üretim kuralları:
|
|
378
|
+
|
|
379
|
+
- **Robots varsayılanı indekslenebilir.** Bir sayfayı gizlemek açık bir karar
|
|
380
|
+
olmalı: `robots: { index: false }` → `noindex, follow`.
|
|
381
|
+
- **OpenGraph `property` kullanır, `name` değil.** Bazı kazıyıcılar `name` ile
|
|
382
|
+
yazılmış og etiketlerini görmezden geliyor.
|
|
383
|
+
- **Devralma zinciri:** `og:title` yoksa `title`, `og:description` yoksa
|
|
384
|
+
`description`, `og:url` yoksa mutlaklaştırılmış `canonical`, `twitter:title`
|
|
385
|
+
yoksa `og:title` → `title`, `twitter:image` yoksa `og:image`.
|
|
386
|
+
- **`twitter:card`** verilmezse `og:image` varsa `summary_large_image`, yoksa
|
|
387
|
+
`summary`.
|
|
388
|
+
- **Boş değerler hiç basılmaz:** `null`, `undefined` ve `""` olan alanlar
|
|
389
|
+
etiket üretmez.
|
|
390
|
+
- `og:type` verilmezse `website`.
|
|
391
|
+
|
|
392
|
+
Örnek:
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
return {
|
|
396
|
+
view: "pages/article",
|
|
397
|
+
metadata: {
|
|
398
|
+
title: article.title,
|
|
399
|
+
description: article.summary,
|
|
400
|
+
canonical: `/haber/${article.slug}`,
|
|
401
|
+
openGraph: {
|
|
402
|
+
type: "article",
|
|
403
|
+
image: article.cover,
|
|
404
|
+
imageWidth: 1200,
|
|
405
|
+
imageHeight: 630,
|
|
406
|
+
},
|
|
407
|
+
extraTags: [`<meta property="article:published_time" content="${article.date}">`],
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`titleTemplate` ve `siteUrl` gibi her sayfada aynı olan alanları
|
|
413
|
+
`hooks.metadata()` içine koyun; controller yalnızca sayfaya özel olanı verir.
|
|
414
|
+
|
|
415
|
+
`renderHeadMeta(metadata)` fonksiyonu dışa açıktır; layout dışında (ör. bir
|
|
416
|
+
fragment ya da e-posta) aynı etiketleri üretmek gerekirse kullanılabilir.
|
|
417
|
+
|
|
418
|
+
## Hook'lar
|
|
419
|
+
|
|
420
|
+
Hook'lar `jskelet.config.mjs` → `hooks` altında tanımlanır. Hepsi opsiyonel,
|
|
421
|
+
hepsi `async` olabilir. **Bir hook hata verirse sayfa düşmez:** framework kendi
|
|
422
|
+
varsayılanına döner ve uyarır.
|
|
423
|
+
|
|
424
|
+
### `hooks.metadata(page)`
|
|
425
|
+
|
|
426
|
+
Her sayfanın metadata varsayılanı. Argüman olarak render edilen sayfa tanımını
|
|
427
|
+
alır, bir metadata nesnesi döndürür. Controller'ın `metadata` alanı bunun
|
|
428
|
+
**üzerine biner** (alan bazında, sığ birleştirme).
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
hooks: {
|
|
432
|
+
metadata() {
|
|
433
|
+
return {
|
|
434
|
+
titleTemplate: "%s | JSkelet",
|
|
435
|
+
description: "JSkelet ile kurulmuş bir site.",
|
|
436
|
+
siteUrl: "https://ornek.com",
|
|
437
|
+
};
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### `hooks.layoutContext({ pathname, metadata })`
|
|
443
|
+
|
|
444
|
+
Layout'a her render'da eklenen local'ler. Döndürülen nesnenin **her alanı**
|
|
445
|
+
layout local'i olur; ayrıca üç alan özel olarak yorumlanır:
|
|
446
|
+
|
|
447
|
+
- `lang` → `<html lang>`
|
|
448
|
+
- `structuredData` → JSON-LD script'leri (dizi)
|
|
449
|
+
- `extraHead` → `<head>`e eklenir (controller `head`inden sonra)
|
|
450
|
+
- `bodyClass` → controller `bodyClass` vermemişse kullanılır
|
|
451
|
+
|
|
452
|
+
```js
|
|
453
|
+
hooks: {
|
|
454
|
+
async layoutContext({ pathname }) {
|
|
455
|
+
return {
|
|
456
|
+
bodyClass: "min-h-full",
|
|
457
|
+
navigation: await getNavigation(),
|
|
458
|
+
isHome: pathname === "/",
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Bu hook gövde render'ıyla **paralel** çalışır; içinde upstream çağırmak sayfaya
|
|
465
|
+
sıralı gecikme eklemez.
|
|
466
|
+
|
|
467
|
+
### `hooks.notFound()`
|
|
468
|
+
|
|
469
|
+
404 sayfası tanımı. Döndürdüğü nesne `renderPage`'e `pathname: "/404"` ile
|
|
470
|
+
verilir. Ayrıntı: [03-routing.md](./03-routing.md).
|
|
471
|
+
|
|
472
|
+
### Diğer hook'lar
|
|
473
|
+
|
|
474
|
+
`hooks.prewarmPaths()` render katmanına değil ısıtmaya aittir; bkz.
|
|
475
|
+
[06-cache.md](./06-cache.md).
|
|
476
|
+
|
|
477
|
+
## Overlay portal noktası
|
|
478
|
+
|
|
479
|
+
`jskelet/client` → `getOverlayRoot()` modal ve drawer içeriğini taşıyacağı
|
|
480
|
+
hedefi verir: layout'ta `<div id="jskelet-overlays"></div>` varsa oraya, yoksa
|
|
481
|
+
`body`ye. Portal, `overflow` ya da `transform` taşıyan bir ata elementin
|
|
482
|
+
`position: fixed` overlay'i kırpmasını engeller. Modal kullanacaksanız bu div'i
|
|
483
|
+
layout'un `<body>` sonuna eklemek yeterli
|
|
484
|
+
([05-islands.md](./05-islands.md)).
|
|
485
|
+
|
|
486
|
+
## Sırada ne var
|
|
487
|
+
|
|
488
|
+
- Island'lar ve `entries`: [05-islands.md](./05-islands.md)
|
|
489
|
+
- `asset()`, manifest ve Tailwind taraması: [08-build.md](./08-build.md)
|
|
490
|
+
- Hook'ların config içindeki yeri: [07-yapilandirma.md](./07-yapilandirma.md)
|