jskelet 0.5.5 → 0.6.1

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.
Files changed (156) hide show
  1. package/AGENTS.md +19 -15
  2. package/CHANGELOG.md +165 -15
  3. package/README.md +16 -21
  4. package/bin/jskelet.mjs +23 -9
  5. package/docs/01-baslangic.md +4 -3
  6. package/docs/02-mimari.md +10 -4
  7. package/docs/03-routing.md +14 -7
  8. package/docs/04-render-ve-sablonlar.md +60 -43
  9. package/docs/05-islands.md +12 -8
  10. package/docs/06-cache.md +18 -7
  11. package/docs/07-yapilandirma.md +69 -27
  12. package/docs/08-build.md +15 -9
  13. package/docs/09-dev-araclari.md +22 -8
  14. package/docs/10-dagitim.md +14 -13
  15. package/docs/11-tasima.md +51 -17
  16. package/docs/12-panel-ve-oturum.md +10 -4
  17. package/docs/README.md +10 -33
  18. package/docs/en/01-getting-started.md +4 -3
  19. package/docs/en/02-architecture.md +12 -6
  20. package/docs/en/03-routing.md +15 -8
  21. package/docs/en/04-rendering.md +71 -59
  22. package/docs/en/05-islands.md +13 -8
  23. package/docs/en/06-caching.md +21 -7
  24. package/docs/en/07-configuration.md +69 -29
  25. package/docs/en/08-build.md +16 -10
  26. package/docs/en/09-dev-tools.md +24 -8
  27. package/docs/en/10-deployment.md +14 -14
  28. package/docs/en/11-migration.md +51 -16
  29. package/docs/en/12-dashboards-and-sessions.md +9 -4
  30. package/docs/en/README.md +10 -35
  31. package/package.json +48 -13
  32. package/src/build/tasks/client.mjs +91 -10
  33. package/src/build/tasks/icons.mjs +11 -1
  34. package/src/client/index.js +2 -2
  35. package/src/compile/codegen.js +4 -0
  36. package/src/compile/compile-all.js +12 -21
  37. package/src/compile/expr.js +5 -0
  38. package/src/compile/parse.js +64 -8
  39. package/src/compile/resolve.js +3 -0
  40. package/src/config/defaults.js +48 -5
  41. package/src/config/index.js +138 -27
  42. package/src/dev-server.mjs +26 -3
  43. package/src/http/cookies-entry.js +1 -0
  44. package/src/http/cookies.js +18 -0
  45. package/src/logo.png +0 -0
  46. package/src/migrate/apply.mjs +262 -0
  47. package/src/migrate/babel.mjs +79 -0
  48. package/src/migrate/classify.mjs +155 -0
  49. package/src/migrate/config.mjs +126 -0
  50. package/src/migrate/fs-walk.mjs +191 -0
  51. package/src/migrate/parse.mjs +26 -0
  52. package/src/migrate/scan.mjs +177 -0
  53. package/src/migrate/transform/expr-source.mjs +168 -0
  54. package/src/migrate/transform/island.mjs +67 -0
  55. package/src/migrate/transform/jsx-to-component.mjs +302 -0
  56. package/src/migrate/transform/jsx-to-jsk.mjs +330 -0
  57. package/src/migrate/transform/page-split.mjs +435 -0
  58. package/src/migrate/write.mjs +81 -0
  59. package/src/migrate.mjs +171 -0
  60. package/src/server/auth/handoff.js +94 -11
  61. package/src/server/create-app.js +37 -10
  62. package/src/server/ejs-adapter.js +59 -0
  63. package/src/server/html-cache.js +178 -32
  64. package/src/server/image-optimizer.js +94 -26
  65. package/src/server/middleware/dev-gate.js +21 -8
  66. package/src/server/middleware/robots-txt.js +341 -0
  67. package/src/server/port-guard.js +255 -0
  68. package/src/server/prewarm.js +137 -51
  69. package/src/server/render.js +30 -10
  70. package/src/server/status-page.js +105 -4
  71. package/src/start.mjs +18 -3
  72. package/src/templates/layout.ejs +8 -28
  73. package/src/templates/layout.jsk +30 -0
  74. package/src/templates/layout.render.js +41 -0
  75. package/src/views/helpers/tags.js +86 -3
  76. package/types/build/resolve-peer.d.mts +13 -0
  77. package/types/client/dom.d.ts +55 -0
  78. package/types/client/form.d.ts +19 -0
  79. package/types/client/index.d.ts +20 -0
  80. package/types/client/registry.d.ts +53 -0
  81. package/types/client/safe-image.d.ts +19 -0
  82. package/types/client/shared-cookie.d.ts +82 -0
  83. package/types/client/store.d.ts +18 -0
  84. package/types/client/swap.d.ts +46 -0
  85. package/types/compile/codegen.d.ts +32 -0
  86. package/types/compile/compile-all.d.ts +42 -0
  87. package/types/compile/errors.d.ts +30 -0
  88. package/types/compile/expr.d.ts +67 -0
  89. package/types/compile/index.d.ts +10 -0
  90. package/types/compile/parse.d.ts +82 -0
  91. package/types/compile/resolve.d.ts +46 -0
  92. package/types/compile/scan-exports.d.ts +9 -0
  93. package/types/config/defaults.d.ts +477 -0
  94. package/types/config/index.d.ts +304 -0
  95. package/types/config/pattern.d.ts +38 -0
  96. package/types/http/control-flow.d.ts +45 -0
  97. package/types/http/cookies-entry.d.ts +5 -0
  98. package/types/http/cookies.d.ts +113 -0
  99. package/types/http/request-cache.d.ts +13 -0
  100. package/types/http/request-context.d.ts +67 -0
  101. package/types/http/shared-cookie.d.ts +73 -0
  102. package/types/index.d.ts +30 -0
  103. package/types/log.d.mts +153 -0
  104. package/types/server/admin/actions.d.ts +16 -0
  105. package/types/server/admin/auth.d.ts +52 -0
  106. package/types/server/admin/event-log.d.ts +38 -0
  107. package/types/server/admin/gate.d.ts +43 -0
  108. package/types/server/admin/inventory.d.ts +40 -0
  109. package/types/server/admin/mount.d.ts +6 -0
  110. package/types/server/admin/router.d.ts +6 -0
  111. package/types/server/admin/snapshot.d.ts +6 -0
  112. package/types/server/assets.d.ts +47 -0
  113. package/types/server/auth/handoff.d.ts +12 -0
  114. package/types/server/cache-deps.d.ts +16 -0
  115. package/types/server/cache-vary.d.ts +30 -0
  116. package/types/server/cloudflare.d.ts +163 -0
  117. package/types/server/create-app.d.ts +25 -0
  118. package/types/server/data-cache.d.ts +116 -0
  119. package/types/server/dev/devtools.d.ts +44 -0
  120. package/types/server/dev/report.d.ts +229 -0
  121. package/types/server/dev/socket.d.ts +17 -0
  122. package/types/server/dev/version-check.d.mts +15 -0
  123. package/types/server/ejs-adapter.d.ts +11 -0
  124. package/types/server/head-hints.d.ts +40 -0
  125. package/types/server/html-cache.d.ts +207 -0
  126. package/types/server/image-optimizer.d.ts +68 -0
  127. package/types/server/logs/access-middleware.d.ts +7 -0
  128. package/types/server/logs/file-sink.d.ts +17 -0
  129. package/types/server/logs/pipeline.d.ts +37 -0
  130. package/types/server/logs/s3-put.d.ts +85 -0
  131. package/types/server/logs/s3-sink.d.ts +26 -0
  132. package/types/server/metadata.d.ts +38 -0
  133. package/types/server/middleware/compression.d.ts +17 -0
  134. package/types/server/middleware/csrf.d.ts +4 -0
  135. package/types/server/middleware/dev-gate.d.ts +2 -0
  136. package/types/server/middleware/headers.d.ts +2 -0
  137. package/types/server/middleware/redirects.d.ts +2 -0
  138. package/types/server/middleware/robots-txt.d.ts +33 -0
  139. package/types/server/middleware/static-precompressed.d.ts +5 -0
  140. package/types/server/middleware/trailing-slash.d.ts +11 -0
  141. package/types/server/middleware/upstream-proxy.d.ts +21 -0
  142. package/types/server/og-image.d.ts +149 -0
  143. package/types/server/port-guard.d.ts +50 -0
  144. package/types/server/prewarm.d.ts +131 -0
  145. package/types/server/redis.d.ts +163 -0
  146. package/types/server/render.d.ts +101 -0
  147. package/types/server/router.d.ts +5 -0
  148. package/types/server/status-page.d.ts +24 -0
  149. package/types/server/upstream-limiter.d.ts +123 -0
  150. package/types/server/upstream-tracking.d.ts +42 -0
  151. package/types/shared/cookie-domain.d.ts +29 -0
  152. package/types/templates/layout.render.d.ts +7 -0
  153. package/types/version.d.mts +10 -0
  154. package/types/views/components/loader.d.ts +5 -0
  155. package/types/views/helpers/html.d.ts +39 -0
  156. package/types/views/helpers/tags.d.ts +127 -0
@@ -23,6 +23,10 @@ kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir
23
23
  amaç `npm start`ı doğrudan çalıştıran birinin stilsiz bir sayfayla
24
24
  karşılaşmaması.
25
25
 
26
+ Port doluysa süreç **başlamaz** (PID + ipucu). `jskelet start --murder` o
27
+ porttaki dinleyiciyi öldürüp bağlar — geliştirmede unutulmuş bir süreç için;
28
+ üretim orkestratöründe genelde gerekmez.
29
+
26
30
  Sunucu hazır olduğunda tek satır basar:
27
31
 
28
32
  ```
@@ -46,7 +50,7 @@ ayarlamayı düşünmeniz gerekenler:
46
50
  | `HOST` | `0.0.0.0` | Yalnızca IPv4 dinlemek gerekiyorsa. Varsayılan `::` zaten çift yığın dinler |
47
51
  | `PREWARM_MAX` | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
48
52
  | `PREWARM_INTERVAL_SECONDS` | `0` ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
49
- | `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler |
53
+ | `DEV_GATE` + `DEV_TOKEN` | Yalnızca staging'de | Yayına açılmamış ortamı gizler. Token tek başına siteyi kilitlemez |
50
54
  | `JSKELET_S3_*` | Access log'u S3'e yazıyorsanız | Bucket + credential; ayrıntı [07](./07-yapilandirma.md) |
51
55
 
52
56
  Production'da dosya veya S3 sink açıldığında HTTP access log middleware
@@ -63,7 +67,8 @@ değerin geçerli olduğunu belirsizleştirir; prod imajında `.env` bulundurmam
63
67
  temizidir.
64
68
 
65
69
  **Gizli anahtarlar `clientEnv` listesine konmamalıdır:** oradaki değerler client
66
- bundle'a düz metin olarak gömülür ([08-build.md](./08-build.md)).
70
+ bundle'a düz metin olarak gömülür ([08-build.md](./08-build.md)). Secret benzeri
71
+ isimler (`SECRET`, `API_KEY`, …) artık build'i düşürür.
67
72
 
68
73
  ## Docker
69
74
 
@@ -151,15 +156,11 @@ Build aşaması `npx jskelet build` ile bunları kendisi üretir.
151
156
 
152
157
  Bu depodaki örnekler jskelet'i npm'den değil `"jskelet": "file:../.."` ile
153
158
  alıyor. Coolify, Railway, Render gibi araçlarda "base directory" olarak
154
- `examples/marketing` verilirse build context yalnızca o dizin olur, `../..`
159
+ `examples/blog` verilirse build context yalnızca o dizin olur, `../..`
155
160
  context'in dışında kalır ve kurulum `npm ci`de düşer. Doğru ayar: **base
156
- directory `/`**, Dockerfile konumu `/examples/marketing/Dockerfile`. Çalışan
157
- örnek `examples/marketing/Dockerfile` içinde ve context'i depo kökü kabul eder:
158
-
159
- ```bash
160
- docker build -f examples/marketing/Dockerfile -t jskelet-marketing .
161
- docker run --rm -p 3000:3000 -e SITE_URL=https://example.com jskelet-marketing
162
- ```
161
+ directory `/`** (depo kökü) ve imajı yukarıdaki çok aşamalı Dockerfile ile
162
+ uygulama dizinine göre uyarlamak — ya da jskelet'i npm bağımlılığı olarak
163
+ kurup context'i uygulamanın kendi dizini yapmak.
163
164
 
164
165
  Kendi uygulamanızda jskelet normal bir bağımlılık olacağı için bu kısıt yoktur;
165
166
  yukarıdaki çok aşamalı imaj yeterli.
@@ -168,8 +169,8 @@ yukarıdaki çok aşamalı imaj yeterli.
168
169
 
169
170
  Framework hazır bir sağlık kontrolü ucu **eklemez**; kendi route'unuza koymanız
170
171
  gerekir. Varsayılan `devGateBypass` listesi `/api/healthcheck` yolunu içerdiği
171
- için bu adı kullanmak en az sürprizli seçenektir: `DEV_TOKEN` ayarlı bir ortamda
172
- bile erişilebilir kalır.
172
+ için bu adı kullanmak en az sürprizli seçenektir: dev gate açıkken bile
173
+ erişilebilir kalır.
173
174
 
174
175
  ```js
175
176
  // routes/00-health.mjs
@@ -326,7 +327,7 @@ istek başına iş neredeyse sıfıra iner.
326
327
  - [ ] `hooks.prewarmPaths()` en önemli sayfaları başa koyuyor
327
328
  - [ ] `headers()` içinde CSP ve güvenlik başlıkları tanımlı
328
329
  - [ ] Sağlık kontrolü ucu var ve `devGateBypass` listesinde
329
- - [ ] Staging'de `DEV_TOKEN` ayarlı, prod'da **ayarlı değil**
330
+ - [ ] Staging'de `DEV_GATE=1` ve `DEV_TOKEN` ayarlı, prod'da gate **kapalı**
330
331
  - [ ] Ters proxy `Accept-Encoding`i iletiyor ve kendi sıkıştırmasını yapmıyor
331
332
  - [ ] `clientEnv` listesinde gizli anahtar yok
332
333
 
package/docs/11-tasima.md CHANGED
@@ -7,6 +7,29 @@ alt kümesine benzetildi — `next.config` sözdizimi, Metadata API, `notFound()
7
7
  `revalidate`, `cache()` gibi kavramlar tanıdık gelecek. Farkların *nedenleri*
8
8
  [02-mimari.md](./02-mimari.md)'de.
9
9
 
10
+ ## `jskelet migrate` (codemod)
11
+
12
+ App Router ağacına karşı codemod'u çalıştırın. Babel (`@babel/parser`,
13
+ `@babel/types`) JSkelet ile birlikte gelir — ek kurulum yok.
14
+
15
+ ```bash
16
+ npx jskelet migrate scan ../my-next-app
17
+ npx jskelet migrate apply ../my-next-app --out . --write
18
+ npx jskelet migrate config ../my-next-app --write
19
+ ```
20
+
21
+ | Komut | Ne yapar |
22
+ | --- | --- |
23
+ | `migrate` / `migrate scan` | Sayfa, layout, `"use client"` modülleri ve engelleri (iç içe layout, Server Actions, Suspense) listeler. |
24
+ | `migrate apply` | **Otomatik çeviri:** `page.*` → feature controller + `.jsk`; presentational bileşenler → `views/components/*.js`; client → island `mount()` iskeleti. Varsayılan dry-run; yazmak için `--write`. Üzerine yazmaz (çakışmada `.migrate` soneki). |
25
+ | `migrate config` | `next.config`'ten `jskelet.config.mjs` taslağı (`headers` / `redirects` / `rewrites`, `images.widths`, `NEXT_PUBLIC_*` → `clientEnv`). |
26
+
27
+ Bayraklar: `--out <dir>`, `--only pages,components,islands`, `--json`, `--strict` (partial/skipped → exit 1).
28
+
29
+ **Otomatik çevrilenler:** `className`, `{{ }}` / `{{{ }}}`, `{#if}` / `{#each}`, `next/image` → `<Image />`, `next/link` → `<Link />`, `dangerouslySetInnerHTML`, `revalidate`, basit controller prelude.
30
+
31
+ **Çevrilmeyenler (raporlanır):** React hooks, Server Actions, iç içe layout düzleştirme, Streaming/Suspense, client routing. Dosya başına `ok` / `partial` / `skipped`.
32
+
10
33
  ## Karşılık tablosu
11
34
 
12
35
  ### Yapılandırma
@@ -30,8 +53,8 @@ alt kümesine benzetildi — `next.config` sözdizimi, Metadata API, `notFound()
30
53
  | `app/page.js` (dosya bazlı routing) | `routes/*.mjs` içinde `app.get(...)` | Sıra açık yazılır ([03](./03-routing.md)) |
31
54
  | `app/[slug]/page.js` | `app.get("/:slug", route(...))` | Express desen sözdizimi |
32
55
  | `params`, `searchParams` | `ctx.params`, `ctx.query` | Controller'ın tek argümanı |
33
- | `layout.js` | `views/layout.ejs` + `hooks.layoutContext()` | Tek layout; iç içe layout yok |
34
- | Sunucu bileşeni (RSC) | Controller + EJS şablonu + `views/components/**` | Fonksiyon HTML string döndürür |
56
+ | `layout.js` | `views/layout.jsk` + `hooks.layoutContext()` | Tek layout; iç içe layout yok |
57
+ | Sunucu bileşeni (RSC) | Controller + `.jsk` şablonu + `views/components/**` | Fonksiyon HTML string döndürür |
35
58
  | İstemci bileşeni (`"use client"`) | Island (`data-island` + `mount`) | Sayfanın tamamı hidre edilmez ([05](./05-islands.md)) |
36
59
  | `notFound()` | `notFound()` | Aynı ad, aynı kontrol akışı |
37
60
  | `redirect()` | `redirect()` (307) | Kalıcı için `permanentRedirect()` (308) |
@@ -87,10 +110,13 @@ Bunları taşıma planında baştan hesaba katın:
87
110
 
88
111
  - **React'in kendisi.** Bileşenler HTML string döndüren fonksiyonlara dönüşür.
89
112
  JSX yok, hook yok, sanal DOM yok.
90
- - **TypeScript.** Proje düz JS + JSDoc. `jsconfig.json` içinde `checkJs: true`
91
- ile editörden tip kontrolü alırsınız.
92
- - **İç içe layout'lar.** Tek bir layout var; ortak bölümleri EJS `include` ya da
93
- bileşen fonksiyonlarıyla paylaşırsınız.
113
+ - **TypeScript.** Framework kaynağı düz JS + JSDoc'tur ve tüketiciler için
114
+ `.d.ts` yayınlar. Client entry/island'lar `.ts` / `.mts` olabilir (esbuild tip
115
+ siler; manifest anahtarı `*.js` kalır). Sunucu route, hook ve
116
+ `jskelet.config.mjs` Node ESM JavaScript kalır — orada editör denetimi için
117
+ `jsconfig.json` içinde `checkJs: true` kullanın.
118
+ - **İç içe layout'lar.** Tek bir layout var; ortak bölümleri `{#include}` ya da
119
+ bileşen fonksiyonlarıyla paylaşırsınız (legacy EJS’te `include`).
94
120
  - **Streaming / Suspense / kısmi prerender.** Yanıt tek parça üretilir.
95
121
  - **İstemci tarafı yönlendirme.** Gezinme gerçek sayfa yüklemesidir. Sunucu HTML'i
96
122
  önbellekten geldiği için pratikte çok hızlıdır, ama SPA geçişleri yoktur.
@@ -169,12 +195,12 @@ export default function register(app, { route, notFound }) {
169
195
  }
170
196
  ```
171
197
 
172
- ```ejs
173
- <%# views/pages/article.ejs %>
198
+ ```jsk
199
+ {# views/pages/article.jsk #}
174
200
  <article class="wrapper">
175
- <h1 class="text-3xl font-bold"><%= article.title %></h1>
176
- <%- image({ src: article.cover, alt: article.title, priority: true, width: 1200, height: 630 }) %>
177
- <div><%- article.body %></div>
201
+ <h1 class="text-3xl font-bold">{{ article.title }}</h1>
202
+ <Image :src="article.cover" :alt="article.title" priority :width="1200" :height="630" />
203
+ <div>{{{ article.body }}}</div>
178
204
  </article>
179
205
  ```
180
206
 
@@ -188,12 +214,16 @@ de aynı yazıyı isterse tek upstream isteği yapılmasını sağlar
188
214
 
189
215
  Yeni bir dizinde `npx jskelet init` çalıştırın ve `jskelet dev`in açıldığını
190
216
  görün. Mevcut Next projesini olduğu gibi bırakın; taşıma paralel yürüsün.
217
+ İsterseniz önce `jskelet migrate scan <next-root>` ile sayfa ve engel listesine bakın.
191
218
 
192
219
  `jsconfig.json` içindeki `paths` alias'larınızı taşıyın — `@/` gibi önekler hem
193
220
  sunucuda hem bundle'da aynı şekilde çalışır ([02-mimari.md](./02-mimari.md)).
194
221
 
195
222
  ### 2. `next.config.mjs`'i çevir (1-2 saat)
196
223
 
224
+ `jskelet migrate config <next-root> --write` çoğunu taslaklar; ardından gözden
225
+ geçirin:
226
+
197
227
  `headers()`, `redirects()` ve `rewrites()` bölümleri neredeyse birebir kopyalanır.
198
228
  Desen sözdizimini kontrol edin: JSkelet `:slug`, `:path*`, `/a-:b` ve
199
229
  `/:path*.svg` biçimlerini destekler; daha karmaşık `path-to-regexp` ifadeleri
@@ -214,9 +244,9 @@ değildir; olduğu gibi kopyalanır. İki değişiklik yapın:
214
244
 
215
245
  ### 4. Layout'u kur (yarım gün)
216
246
 
217
- `app/layout.jsx`'i `views/layout.ejs`'e çevirin. Framework'ün varsayılan
218
- layout'unu (`node_modules/jskelet/src/templates/layout.ejs`) kopyalayıp
219
- üzerine yazmak en hızlı yol.
247
+ `app/layout.jsx`'i `views/layout.jsk`'e çevirin (veya `migrate apply` taslağını
248
+ kullanın). Framework'ün varsayılan layout'unu (`jskelet/layout` → `.jsk`)
249
+ kopyalayıp üzerine yazmak en hızlı yol.
220
250
 
221
251
  `layout.jsx` içinde veri çekiyorsanız (navigasyon, site ayarları) bunu
222
252
  `hooks.layoutContext()` içine taşıyın: gövde render'ıyla paralel çalışır ve
@@ -227,6 +257,9 @@ Global metadata varsayılanlarını (`titleTemplate`, `siteUrl`, `description`)
227
257
 
228
258
  ### 5. Bileşenleri çevir (en uzun adım)
229
259
 
260
+ `jskelet migrate apply --only components --write` hooks'suz presentational
261
+ bileşenleri çevirir. Gerisini elle bitirin:
262
+
230
263
  Her React bileşeni bir fonksiyona dönüşür:
231
264
 
232
265
  ```jsx
@@ -260,8 +293,9 @@ Bileşenleri küçük ve saf tutun; veri çekmeyi controller'da bırakın.
260
293
 
261
294
  ### 6. Sayfaları taşı (sayfa başına saatler)
262
295
 
263
- Her `page.jsx` bir controller + bir EJS şablonuna bölünür. Sırayı düşünerek
264
- dosyalayın:
296
+ `jskelet migrate apply --only pages --write` her `page.*` dosyasını feature
297
+ controller + `.jsk` şablonuna böler. `partial` / `skipped` satırlarını gözden
298
+ geçirip TODO'ları bitirin. Sırayı düşünerek dosyalayın:
265
299
 
266
300
  ```
267
301
  routes/
@@ -337,7 +371,7 @@ redirect kurallarının doğruluğunu ölçmek için işe yarar.
337
371
  ## Taşıma sırasında sık yapılan hatalar
338
372
 
339
373
  - **`esc()` unutmak.** JSX'ten gelen alışkanlıkla `${value}` yazmak XSS demektir.
340
- Şablonlarda `<%= %>` (kaçışlı) ile `<%- %>` (ham) ayrımına dikkat edin.
374
+ `.jsk`'de `{{ }}` (kaçışlı) / `{{{ }}}` (ham); bileşenlerde `esc()` kendiniz.
341
375
  - **`@source` eklemeden yeni bir dizin açmak.** Sınıflar sessizce düşer.
342
376
  - **Yakalayıcı route'u yanlış sıraya koymak.** `/:slug` her zaman en sonda.
343
377
  - **Sayfanın tamamını island yapmak.** Kazanç sunucu HTML'inin tam olmasından
@@ -143,7 +143,9 @@ export default {
143
143
  sharedCookieRoots: [".investvio.com", ".localhost"],
144
144
  },
145
145
  auth: {
146
- crossSubdomainHandoff: true, // POST /_jskelet/auth/handoff
146
+ crossSubdomainHandoff: {
147
+ allowedCookieNames: ["sid"], // zorunlu allowlist
148
+ },
147
149
  },
148
150
  };
149
151
  ```
@@ -207,15 +209,19 @@ okunur. Yazımdan sonra **read-back** yapılır; tarayıcı Domain'i reddettiyse
207
209
 
208
210
  `auth.crossSubdomainHandoff` açıkken:
209
211
 
210
- 1. `POST /_jskelet/auth/handoff` `{ name, value, next }` → `{ url }` (`?handoff=` ekli)
212
+ 1. `POST /_jskelet/auth/handoff` `{ name, value, next }` → `{ url }` (`?handoff=` ekli).
213
+ Mint, CSRF middleware'inden **sonra** mount edilir; `name`
214
+ `allowedCookieNames` içinde ve RFC 6265 token olmalı.
211
215
  2. Hedef host'ta GET middleware bileti tek kullanımlık tüketir, cookie yazar
212
216
  (önce shared Domain, olmazsa host-only), `handoff` query'siz 303
213
217
 
214
218
  `next` yalnızca aynı `sharedCookieRoots` altındaki host'lara izinli. Bilet
215
- ~60 sn, süreç belleğinde. JWT URL'ye konmaz.
219
+ ~60 sn, süreç belleğinde; bekleyen bilet ve IP başına mint sınırı vardır.
220
+ JWT URL'ye konmaz.
216
221
 
217
222
  `window.name` köprüsü cookie'siz yedek: kaynakta `handoffViaWindowName`,
218
- hedeefte `consumeWindowNameHandoff`.
223
+ hedeefte `consumeWindowNameHandoff`. Cross-origin tab'da `window.name`
224
+ okunabilir kalır — mümkünse sunucu handoff tercih edin.
219
225
 
220
226
  ## CSRF
221
227
 
package/docs/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # JSkelet belgeleri
2
2
 
3
3
  JSkelet, SEO ve hız odaklı siteler için "framework'süz hissettiren" bir
4
- framework: Express 5 + EJS ile sunucuda tam HTML üretir, etkileşimi vanilla JS
5
- island'larla ekler, CSS'i Tailwind v4 ile tek bir stylesheet'e derler ve ISR
6
- yerine süreç belleğinde yaşayan, stale-while-revalidate'li bir HTML TTL cache
7
- kullanır. React yok, TypeScript yok; düz JavaScript ve JSDoc.
4
+ framework: Express 5 + build-time `.jsk` ile sunucuda tam HTML üretir (EJS
5
+ opsiyonel legacy peer), etkileşimi vanilla JS island'larla ekler, CSS'i
6
+ Tailwind v4 ile tek bir stylesheet'e derler ve ISR yerine süreç belleğinde
7
+ yaşayan, stale-while-revalidate'li bir HTML TTL cache kullanır. React yok;
8
+ framework kaynağı düz JavaScript + JSDoc'tur. Uygulama tarafında client
9
+ island/entry'ler TypeScript yazılabilir ve paket `.d.ts` yayınlar.
8
10
 
9
11
  Bu dizin framework'ün tam referansıdır. Sıralı okumak için baştan başlayın;
10
12
  belirli bir konuyu arıyorsanız doğrudan ilgili başlığa gidin.
@@ -19,7 +21,7 @@ eşlenik tutuluyor; birini değiştiriyorsan diğerini de değiştir.
19
21
  | [01-baslangic.md](./01-baslangic.md) | Kurulum, `jskelet init`, ilk route, ilk island, dizin yapısı, CLI komutları |
20
22
  | [02-mimari.md](./02-mimari.md) | Mimari kararlar ve gerekçeleri: island modeli, tam sunucu HTML'i, cache stratejisi, middleware sırası |
21
23
  | [03-routing.md](./03-routing.md) | Route modülü sözleşmesi, yükleme sırası, controller sözleşmesi, `ctx`, `notFound`/`redirect`, config redirects/rewrites |
22
- | [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md) | EJS layout, sayfalar, otomatik bileşen kaydı, `html`/`tags` yardımcıları, metadata → `<head>`, hook'lar |
24
+ | [04-render-ve-sablonlar.md](./04-render-ve-sablonlar.md) | `.jsk` layout/sayfalar, otomatik bileşen kaydı, `html`/`tags`, metadata → `<head>`, hook'lar; EJS legacy |
23
25
  | [05-islands.md](./05-islands.md) | `data-island` sözleşmesi, hidrasyon stratejileri, `client/entries/*`, `createStore`, DOM yardımcıları, `startSafeImages` |
24
26
  | [06-cache.md](./06-cache.md) | `withHtmlCache`, `revalidate`, stale-while-revalidate, cache anahtarı, `X-JSkelet-Cache`, istek içi cache, degraded render, prewarm |
25
27
  | [07-yapilandirma.md](./07-yapilandirma.md) | `jskelet.config.mjs` tam referansı, `source` desen sözdizimi, ortam değişkenleri tablosu |
@@ -45,7 +47,7 @@ eşlenik tutuluyor; birini değiştiriyorsan diğerini de değiştir.
45
47
 
46
48
  ## Çalışan örnekler
47
49
 
48
- Dördü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
50
+ Üçü de çalışır durumda; belgelerdeki örneklerin çoğu buralardan alınmıştır.
49
51
 
50
52
  **`examples/minimal/`** — iki route, bir bileşen, bir island, minimal config.
51
53
  Framework'ün en küçük çalışan hâli.
@@ -66,32 +68,7 @@ npm --prefix examples/blog install
66
68
  npm --prefix examples/blog run dev
67
69
  ```
68
70
 
69
- **`examples/marketing/`** — framework'ün kendi tanıtım sitesi: hero, kıyaslama
70
- tablosu, canlı gecikme ölçümü, SSS, belgeler dizini, sürüm notları ve indirme
71
- sayfası. Sayfadaki bayt sayıları `lib/payload.js` içinde sitenin **kendi** build
72
- çıktısından, sürüm künyesi ise `lib/release.js` içinde kurulu paketin
73
- `package.json`'ından okunur; gecikme sayıları `latency` island'ında tarayıcıda
74
- ölçülür. Uzun TTL (bir saat) ve tüm sayfaları ısıtan prewarm ile, cache'in en
75
- verimli çalıştığı profili gösterir.
76
-
77
- Site aynı zamanda **bu belgeleri** servis ediyor: `/docs/<bölüm>` adresleri
78
- `node_modules/jskelet/docs/` altındaki markdown dosyalarını okuyup sol gezinme,
79
- "bu sayfada" listesi ve sıralı geçişle basıyor. Çevirici `lib/markdown.js`
80
- içinde küçük bir modül — bağımlılık yok — ve kaynak paketin kendisi olduğu için
81
- site kurulu sürümden hiç ayrışmıyor.
82
-
83
- Site aynı zamanda **iki dilli**: varsayılan İngilizce kökte, Türkçe `/tr`
84
- altında ve route adları iki dilde de aynı. Framework'te i18n yok; dil
85
- çözümlemesi `lib/i18n.js` içinde uygulamanın kendi sözleşmesi olarak duruyor ve
86
- `hooks.layoutContext` ile bir sözlüğe bağlanıyor. Çok dilli bir siteyi bu
87
- yüzeyle nasıl kurabileceğinizi görmek için bakılacak yer burası.
88
-
89
- ```bash
90
- npm --prefix examples/marketing install
91
- npm --prefix examples/marketing run dev
92
- ```
93
-
94
- **`examples/dashboard/`** — diğer üçünün tersi eksen: kişiye özel sayfalar.
71
+ **`examples/dashboard/`** — diğer ikisinin tersi eksen: kişiye özel sayfalar.
95
72
  İmzalı cookie ile giriş, `private: true` korumalı panel, sayfalı tablo
96
73
  fragment'i, CSRF'li mutasyon formu ve temizlik fonksiyonu döndüren bir island.
97
74
  Public bir tanıtım sayfası da var, böylece aynı uygulamada önbelleklenen ve
@@ -102,5 +79,5 @@ npm --prefix examples/dashboard install
102
79
  npm --prefix examples/dashboard run dev
103
80
  ```
104
81
 
105
- Her dört örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
82
+ Her üç örnekte `node smoke.mjs` sunucu ayaktayken uçların beklendiği gibi
106
83
  yanıt verdiğini doğrular.
@@ -255,11 +255,12 @@ ESM resolve hooks (`--import`) at process start.
255
255
 
256
256
  | Command | What it does |
257
257
  | --- | --- |
258
- | `jskelet dev` | Build watch + server, in a single terminal. Live reload, CSS hot-swap, dev overlay. `NODE_ENV=development`. |
258
+ | `jskelet dev` | Build watch + server, in a single terminal. Live reload, CSS hot-swap, dev overlay. `NODE_ENV=development`. Refuses to start if the port is busy; `--murder` kills the listener and binds. |
259
259
  | `jskelet build` | One-shot prod build: fonts → icon sprite → CSS → client JS → images → manifest → precompress. `production` if `NODE_ENV` is not given. |
260
- | `jskelet start` | Prod server. If there is no build output it produces it first. `production` if `NODE_ENV` is not given. |
260
+ | `jskelet start` | Prod server. If there is no build output it produces it first. `production` if `NODE_ENV` is not given. Same port behaviour as `dev` (`--murder`). |
261
261
  | `jskelet init` | Installs a feature-first `.jsk` skeleton into the current directory; leaves existing files alone. |
262
262
  | `jskelet generate` | Scaffolds a `feature` / `page` / `island`. |
263
+ | `jskelet migrate` | Next.js App Router → JSkelet codemod (`scan` / `apply` / `config`). See [11-migration.md](./11-migration.md). |
263
264
 
264
265
  An unknown command, or a call with no arguments, prints the usage text.
265
266
 
@@ -287,7 +288,7 @@ only these specifiers:
287
288
  | `jskelet/tags` | `link`, `image`, `icon`, `preloadImage`, `toKebab` |
288
289
  | `jskelet/log` | Console output helpers (`banner`, `event`, `task`, `size`, `ms`, …) |
289
290
  | `jskelet/register` | Alias + extension hooks via `node --import jskelet/register` |
290
- | `jskelet/layout` | The path to the framework's default `layout.ejs` file |
291
+ | `jskelet/layout` | The path to the framework's default `layout.jsk` file |
291
292
 
292
293
  ## What's next
293
294
 
@@ -36,9 +36,10 @@ Request
36
36
  ├─ rewrites(beforeFiles) config → proxy or a change to req.url
37
37
  ├─ compression brotli/gzip negotiation (quality 5)
38
38
  ├─ headers static cache + config headers()
39
- ├─ devGate if DEV_TOKEN is set, 404 without a token
39
+ ├─ devGate if the gate is on, 404 without a token
40
40
  ├─ redirects config redirects(), first match wins
41
41
  ├─ trailingSlash 308 when config trailingSlash is true
42
+ ├─ robots.txt appends framework Disallow rules to the user's body
42
43
  ├─ staticPrecompressed .br/.gz copies produced at build (quality 11)
43
44
  ├─ express.static files under public/
44
45
  ├─ (dev) devtools only when NODE_ENV=development
@@ -51,7 +52,7 @@ Request
51
52
  │ └─ withHtmlCache TTL + stale-while-revalidate
52
53
  │ └─ withUpstreamTracking
53
54
  │ └─ withRequestCache
54
- │ └─ controller → renderPage → EJS
55
+ │ └─ controller → renderPage → .jsk (or legacy EJS)
55
56
  ├─ 404 → hooks.notFound()
56
57
  └─ error handling redirect/notFound + 500 fallback
57
58
  ```
@@ -71,6 +72,11 @@ position has a reason, and moving things around leads to silent breakage.
71
72
  leak even its redirect rules to the outside. `trailingSlash` sits after config
72
73
  redirects so explicit rules see the requested path first; the canonical slash
73
74
  form is enforced as a second step.
75
+ - **`robots.txt` before static, and inside compression.** The body the
76
+ application wrote is left intact; the framework appends `Disallow` rules
77
+ for its own endpoints. A response that already has `Content-Encoding` is
78
+ not rewritten — the block is added to plain text, and compression stays
79
+ outside.
74
80
  - **`staticPrecompressed` before `express.static`.** If there are `.br`/`.gz`
75
81
  copies produced at build time, those are served (brotli quality 11);
76
82
  otherwise the request falls through to the `static` below it and the
@@ -270,10 +276,10 @@ hard-to-diagnose problems like "why is there no stylesheet".
270
276
 
271
277
  ## Why this dependency list
272
278
 
273
- There are four runtime dependencies: `express`, `ejs`, `esbuild`,
274
- `tailwind-merge`. Everything else (Tailwind, PostCSS, lightningcss, sharp, the
275
- Phosphor icons) is an **optional peer dependency**, and if it is absent the
276
- corresponding build step is skipped.
279
+ There are three runtime dependencies: `express`, `esbuild`, `tailwind-merge`.
280
+ `ejs` is an optional peer only for legacy `.ejs` templates. Everything else
281
+ (Tailwind, PostCSS, lightningcss, sharp, the Phosphor icons) is an **optional
282
+ peer dependency**, and if it is absent the corresponding build step is skipped.
277
283
 
278
284
  Two decisions deserve a separate explanation:
279
285
 
@@ -158,7 +158,9 @@ stored), but the flag is the right place. Details in
158
158
  ## `fragment()` — a partial without the layout
159
159
 
160
160
  For endpoints that refresh a region. No layout is printed, the response is sent
161
- with `private, no-store` and no ETag, and it never touches the HTML cache.
161
+ with `private, no-store` and no ETag, and it never touches the HTML cache. The
162
+ `/_fragment/` prefix is appended to `robots.txt`, so partial endpoints are not
163
+ indexed ([04](./04-rendering.md#robotstxt)).
162
164
 
163
165
  ```js
164
166
  app.get(
@@ -213,7 +215,7 @@ fields:
213
215
 
214
216
  | Field | Type | Default | Meaning |
215
217
  | --- | --- | --- | --- |
216
- | `view` | `string` | — | The template path under `views/`, without the extension: `"pages/home"` → `views/pages/home.ejs`. |
218
+ | `view` | `string` | — | The template path under `views/`, without the extension: `"pages/home"` → `views/pages/home.jsk` (else legacy `.ejs`). |
217
219
  | `data` | `object` | `{}` | Data passed to the template as locals. |
218
220
  | `metadata` | `object` | `{}` | Turned into `<head>` tags; it overrides the output of `hooks.metadata()`. Schema: [04-rendering.md](./04-rendering.md). |
219
221
  | `status` | `number` | `200` | The HTTP status code. Only 200 is written to the cache. |
@@ -332,13 +334,18 @@ handler kicks in, logs the error and returns the framework's own error page with
332
334
  `Cache-Control: no-store`. The status code is read from the error's `statusCode`
333
335
  (or `status`) field; if it is not in the 400–599 range, 500 is used.
334
336
 
335
- The framework's page is deliberately plain: the status code, a one-line heading
336
- and a one-line description. It carries no brand name, no navigation and no error
337
- detail — the innards of the server are not opened up to the visitor. The
338
- language comes from `brand.lang` (`tr` and `en` are built in, others fall back
339
- to `en`).
337
+ **Development** (`NODE_ENV=development`, i.e. `jskelet dev`): for 5xx responses
338
+ the built-in 500 page and `hooks.error()` are skipped; a diagnostic page with
339
+ the message, stack trace, and any `cause` chain is returned instead. 4xx (404
340
+ and friends) still use the usual status page in development.
340
341
 
341
- To provide your own page, `hooks.error()`:
342
+ **Production**: the framework's page is deliberately plain — status code, a
343
+ one-line heading and a one-line description. It carries no brand name, no
344
+ navigation and no error detail; the innards of the server are not opened up to
345
+ the visitor. The language comes from `brand.lang` (`tr` and `en` are built in,
346
+ others fall back to `en`).
347
+
348
+ To provide your own page, `hooks.error()` (production / 4xx only):
342
349
 
343
350
  ```js
344
351
  // jskelet.config.mjs
@@ -52,7 +52,7 @@ controller data → imported render(data, helpers) → HTML
52
52
  {/if}
53
53
 
54
54
  {#each items as item, i}
55
- <li data-i="{{ i }}">{{ item }}</li>
55
+ <li :data-i="i">{{ item }}</li>
56
56
  {/each}
57
57
 
58
58
  <Link href="/" text="Home" />
@@ -68,7 +68,7 @@ controller data → imported render(data, helpers) → HTML
68
68
  | Loop | `{#each list as item}` or `as item, i` |
69
69
  | Include | `{#include "partials/header"}` (compiled `.jsk`) |
70
70
  | Component | PascalCase tag; `:prop="expr"`, `prop="literal"`, boolean `disabled` |
71
- | Built-ins | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage` |
71
+ | Built-ins | `Link`, `Image`, `Icon`, `CsrfField`, `PreloadImage`, `Stylesheets`, `BodyScripts`, `JsonLd` |
72
72
 
73
73
  The expression language is intentionally small (access, compare, ternary,
74
74
  `.length`). No assignments, object literals, or arbitrary calls — keep logic in
@@ -104,12 +104,15 @@ See the extension README for details.
104
104
 
105
105
  If a compiled `.jsk` exists for a view id it wins; otherwise `.ejs` is rendered
106
106
  with EJS. Existing apps keep working unchanged. `jskelet init` scaffolds `.jsk`.
107
+ Legacy `.ejs` needs the optional `ejs` peer installed in the application
108
+ (`npm install ejs`); without it only `.jsk` templates run.
107
109
 
108
110
  ## The EJS engine (legacy)
109
111
 
110
- EJS remains supported. The engine is set up once on the first render; the
111
- component scan touches the file system, so it cannot be done on every request
112
- and cannot be computed before the config is loaded.
112
+ EJS remains supported as an **optional peer dependency** for legacy templates.
113
+ The engine is set up once on the first render; the component scan touches the
114
+ file system, so it cannot be done on every request and cannot be computed
115
+ before the config is loaded.
113
116
 
114
117
  Settings:
115
118
 
@@ -130,66 +133,54 @@ normal flow because the dev server restarts the process.
130
133
 
131
134
  1. `jskelet.config.mjs` → if `layout` is given, it is used. The path is
132
135
  resolved relative to the **parent directory of the views directory**: if
133
- `views` is the default, `layout: "views/custom.ejs"` → `<root>/views/custom.ejs`.
136
+ `views` is the default, `layout: "views/custom.jsk"` → `<root>/views/custom.jsk`.
134
137
  2. If it is not given and `views/layout.jsk` exists (compiled), that is used.
135
- 3. Else if `views/layout.ejs` exists, that is used.
138
+ 3. Else if `views/layout.ejs` exists (legacy), that is used.
136
139
  4. If that does not exist either, the framework's own minimal layout is used
137
- (`node_modules/jskelet/src/templates/layout.ejs`, also reachable through the
140
+ (`node_modules/jskelet/src/templates/layout.jsk`, also reachable through the
138
141
  `jskelet/layout` specifier).
139
142
 
140
143
  These fallbacks exist so that a new project can work with a single route. The
141
144
  most practical way to move to your own layout is to copy that file to
142
- `views/layout.ejs` or author `views/layout.jsk`.
145
+ `views/layout.jsk`.
143
146
 
144
147
  ### The framework's default layout
145
148
 
146
- ```ejs
149
+ ```html
147
150
  <!DOCTYPE html>
148
- <html lang="<%= lang %>">
151
+ <html :lang="lang">
149
152
  <head>
150
153
  <meta charset="utf-8">
151
154
  <meta name="viewport" content="width=device-width, initial-scale=1">
152
- <%- extraHead %>
153
- <% if (hasAsset('app.css')) { %>
154
- <link rel="stylesheet" href="<%= asset('app.css') %>" data-jskelet-css="app.css">
155
- <% } %>
156
- <% styles.forEach(function (sheet) { %>
157
- <% if (hasAsset(sheet)) { %>
158
- <link rel="stylesheet" href="<%= asset(sheet) %>" data-jskelet-css="<%= sheet %>">
159
- <% } %>
160
- <% }); %>
161
- <%- headMeta %>
162
- <% structuredData.forEach(function (item) { %>
163
- <script type="application/ld+json"><%- jsonScript(item) %></script>
164
- <% }); %>
155
+ {{{ extraHead }}}
156
+ <Stylesheets :styles="styles" />
157
+ {{{ headMeta }}}
158
+ <JsonLd :items="structuredData" />
165
159
  </head>
166
- <body class="<%= bodyClass %>">
167
- <%- body %>
168
- <% if (hasAsset('main.js')) { %>
169
- <script type="module" src="<%= asset('main.js') %>"></script>
170
- <% } %>
171
- <% entries.forEach(function (entry) { %>
172
- <script type="module" src="<%= asset(entry) %>"></script>
173
- <% }); %>
174
- <% if (devtools) { %>
175
- <script type="module" src="<%= devBasePath %>/overlay.js"></script>
176
- <% } %>
160
+ <body :class="bodyClass">
161
+ {{{ body }}}
162
+ <BodyScripts :entries="entries" :devtools="devtools" :devBasePath="devBasePath" />
177
163
  </body>
178
164
  </html>
179
165
  ```
180
166
 
167
+ The `.jsk` expression language has no function calls, so asset loops live in the
168
+ built-in `<Stylesheets />`, `<BodyScripts />` and `<JsonLd />` tags instead of
169
+ inline `hasAsset` / `asset` / `forEach` in the layout.
170
+
181
171
  Points to watch:
182
172
 
183
173
  - **`extraHead` comes first.** Delaying resource hints (`preconnect`, LCP
184
174
  `preload`) writes straight into LCP.
185
- - **Global `app.css` is render-blocking**, with the reasoning in
186
- [02-architecture.md](./02-architecture.md). Controller `styles: [...]` adds
187
- page sheets the same way. If the build has not run, `hasAsset` is false and
188
- the tag is never emitted.
189
- - **The `hasAsset` checks** keep the page from requesting files that 404 when
190
- the build is missing.
191
- - **The devtools script** is emitted only when `NODE_ENV=development`; it does
192
- not exist at all in production output.
175
+ - **`<Stylesheets />` emits global `app.css` (render-blocking)** plus controller
176
+ `styles: [...]`, with the reasoning in
177
+ [02-architecture.md](./02-architecture.md). If the build has not run,
178
+ `hasAsset` is false inside the tag and nothing is emitted.
179
+ - **`<BodyScripts />` emits `main.js`, page `entries`, and the
180
+ development-only overlay.** The overlay script exists only when
181
+ `NODE_ENV=development`; it is absent from production output.
182
+ - **`<JsonLd />` turns `structuredData` into safe
183
+ `application/ld+json` scripts.**
193
184
 
194
185
  ### Layout locals
195
186
 
@@ -219,30 +210,27 @@ of bug where every page thinks it is the home page and renders the logo as an
219
210
  ## Page templates
220
211
 
221
212
  The `view` field gives the path under `views/` without an extension:
222
- `"pages/home"` → `views/pages/home.ejs`. The locals passed to the template are
223
- the contents of the `data` field plus `metadata` — **not** the layout locals.
224
- The page template still has access to all helpers and components.
213
+ `"pages/home"` → `views/pages/home.jsk` (else legacy `home.ejs`). The locals
214
+ passed to the template are the contents of the `data` field plus `metadata` —
215
+ **not** the layout locals. The page template still has access to all helpers
216
+ and components.
225
217
 
226
- ```ejs
227
- <%# views/pages/home.ejs %>
218
+ ```html
219
+ {# views/pages/home.jsk #}
228
220
  <section class="wrapper">
229
- <h1 class="text-3xl font-bold"><%= heading %></h1>
221
+ <h1 class="text-3xl font-bold">{{ heading }}</h1>
230
222
 
231
- <%# `list` is defined in views/components/list.js; no import needed. %>
232
- <%- list({ items }) %>
223
+ {# `list` is defined in views/components/list.js; no import needed. #}
224
+ <List :items="items" />
233
225
 
234
226
  <div class="mt-8" data-island="counter" data-island-props='{"start":5}'></div>
235
227
  </section>
236
228
  ```
237
229
 
238
- Do not mix up the two output forms in EJS:
239
-
240
- - `<%= value %>` — HTML escaped. **Always** this for user/upstream data.
241
- - `<%- html %>` — raw. Only for HTML strings you produced yourself and know to
242
- be safe (component calls, `headMeta`, `body`).
243
-
244
- Because `async: true` is on, `await` can also be used inside a template, but
245
- keeping data fetching in the controller makes diagnosis easier.
230
+ In `.jsk`, `{{ }}` escapes and `{{{ }}}` emits raw HTML (trusted strings only).
231
+ Legacy EJS keeps `<%= %>` / `<%- %>` with the same meaning; because `async: true`
232
+ is on there, `await` can also be used inside an `.ejs` template, but keeping
233
+ data fetching in the controller makes diagnosis easier.
246
234
 
247
235
  ## Components: `views/components/**`
248
236
 
@@ -522,6 +510,30 @@ The `renderHeadMeta(metadata)` function is exported; it can be used when you
522
510
  need to produce the same tags outside the layout (for example in a fragment or
523
511
  an email).
524
512
 
513
+ ## robots.txt
514
+
515
+ The application writes `robots.txt`: `public/robots.txt` or a plain route.
516
+ The framework does not change that body; it appends a JSkelet note and
517
+ `Disallow` rules **under** a successful text response. If there is no file
518
+ and no route, the framework does not invent a `robots.txt`.
519
+
520
+ Paths added:
521
+
522
+ - `/_jskelet/` — admin panel, remote image proxy, auth handoff
523
+ - `/__jskelet/` — development tools
524
+ - `/_fragment/` — partial responses without a layout
525
+
526
+ An endpoint moved off those prefixes is added too, but only when it is
527
+ actually mounted: `admin.basePath`, `images.remote.path`,
528
+ `auth.crossSubdomainHandoff.path`. `brand.devBasePath` is written only in
529
+ development; in production that path may be the application's own page.
530
+
531
+ The note starts with the configured brand name (`brand.name`, default
532
+ `JSkelet`). The trailing group repeats `User-agent: *` together with every
533
+ other agent already named in the file. Google does not merge a
534
+ crawler-specific group with `*`; it does merge a second group for the same
535
+ agent. If the note is already in the file, it is not appended again.
536
+
525
537
  ## Dynamic OG images
526
538
 
527
539
  Counterpart to Next.js `ImageResponse` / `opengraph-image.tsx`. There is no JSX: