asjs-express 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,883 @@
1
+ # asjs-express
2
+
3
+ ASJS is a lightweight Express view engine built for teams that still want the calm, readable side of server-rendered pages without giving up shared layouts, async page preparation, partials, and modern navigation behavior.
4
+
5
+ Bu depo içindeki örnek yüzey ise WebAS adıyla anlatılıyor. Tonu bilerek daha gerçek tutuldu: gösterişten çok düzeni, süsten çok açıklığı, ilk izlenimden çok sürdürülebilirliği önemseyen bir yapı gibi kurgulandı.
6
+
7
+ ## English
8
+
9
+ ### Why this project exists
10
+
11
+ ASJS was built for a very common problem in Express projects: pages start simple, then real work arrives. A dashboard needs data before render. A contact form needs validation and a meaningful response. A shared layout needs to stay stable while the inside changes. At that point, many projects either become repetitive or drift into a heavy front-end stack before they actually need one.
12
+
13
+ ASJS keeps that middle ground clean. It gives you an EJS-like template syntax, a small but practical view model, optional client-side navigation, and the ability to finish your data work before the first HTML reaches the browser.
14
+
15
+ The WebAS example in this repository is written in that spirit. It presents a small, believable interface structure shaped by two developers:
16
+
17
+ - [Acarfx](https://acarfx.com)
18
+ - [Seyfullah Kısacık](https://seyfooksck.com/)
19
+
20
+ The intention is simple: this should read like something a careful team would actually maintain, not like a throwaway demo assembled for a screenshot.
21
+
22
+ ### What ASJS gives you
23
+
24
+ - EJS-like syntax with `<% %>`, `<%= %>`, and `<%- %>`
25
+ - Layout support without extra ceremony
26
+ - Partial rendering with prop validation
27
+ - Template caching
28
+ - Built-in client router with transitions, prefetch, and loading bar support
29
+ - Async form enhancement for marked forms
30
+ - Plugin and hook support for Express projects that need to grow over time
31
+ - Package-served assets, so you do not have to manually copy router files into your public folder
32
+
33
+ ### Install
34
+
35
+ ```bash
36
+ npm install asjs-express
37
+ ```
38
+
39
+ ### Quick start
40
+
41
+ ```js
42
+ const express = require('express')
43
+ const { setupAsjs } = require('asjs-express')
44
+
45
+ const app = express()
46
+ const asjs = setupAsjs(app, {
47
+ rootDir: __dirname,
48
+ defaultLayout: 'layouts/main',
49
+ debug: true,
50
+ cache: true,
51
+ forms: true,
52
+ transitions: 'lift',
53
+ prefetch: true,
54
+ loadingBar: true
55
+ })
56
+
57
+ app.get('/', asjs.page('home', {
58
+ title: 'Hello ASJS'
59
+ }))
60
+
61
+ app.use(asjs.errors())
62
+ app.listen(3000)
63
+ ```
64
+
65
+ ### Minimal Express template
66
+
67
+ ```js
68
+ const express = require('express')
69
+ const { setupAsjs } = require('asjs-express')
70
+
71
+ const app = express()
72
+ const asjs = setupAsjs(app, { rootDir: __dirname })
73
+
74
+ app.get('/', asjs.page('home', { title: 'Hello ASJS' }))
75
+ app.listen(3000)
76
+ ```
77
+
78
+ ### Layout usage
79
+
80
+ ```asjs
81
+ <!DOCTYPE html>
82
+ <html>
83
+ <head>
84
+ <meta charset="UTF-8">
85
+ <title><%= title %></title>
86
+ <%- asjs.clientTags({ preload: true, theme: true }) %>
87
+ </head>
88
+ <body<%- asjs.bodyAttrs() %>>
89
+ <%- asjs.progressMarkup() %>
90
+ <main<%- asjs.viewAttrs() %>>
91
+ <%- body %>
92
+ </main>
93
+ </body>
94
+ </html>
95
+ ```
96
+
97
+ `asjs.clientTags()` injects the built-in ASJS stylesheet and router script for you.
98
+ `theme: true` optionally loads the packaged light, corporate WebAS theme.
99
+
100
+ ### Work before the page loads
101
+
102
+ Yes. If you pass an async callback to `asjs.page()`, ASJS waits for that work to finish and only then calls `res.render(viewName, locals)`.
103
+
104
+ That means database reads, API requests, permission checks, formatting, and view-model building can all happen before the browser receives the first HTML.
105
+
106
+ #### Database example before render
107
+
108
+ ```js
109
+ const db = {
110
+ companies: {
111
+ async findBySlug(slug) {
112
+ return { id: 7, slug, name: 'Northwind Studio' }
113
+ }
114
+ },
115
+ dashboards: {
116
+ async getMetrics(companyId) {
117
+ return [
118
+ { label: 'Open tasks', value: 12 },
119
+ { label: 'Active pages', value: 8 },
120
+ { label: 'Pending review', value: 3 }
121
+ ]
122
+ }
123
+ }
124
+ }
125
+
126
+ app.get('/dashboard/:slug', asjs.page('dashboard', async (req) => {
127
+ const company = await db.companies.findBySlug(req.params.slug)
128
+ const metrics = await db.dashboards.getMetrics(company.id)
129
+
130
+ return {
131
+ title: `${company.name} Dashboard`,
132
+ company,
133
+ metrics,
134
+ renderSummary: [
135
+ { label: 'Source', value: 'Database' },
136
+ { label: 'Company', value: company.name },
137
+ { label: 'Mode', value: 'Server render' }
138
+ ]
139
+ }
140
+ }))
141
+ ```
142
+
143
+ #### Returned page model
144
+
145
+ The callback above returns a plain object like this:
146
+
147
+ ```js
148
+ {
149
+ title: 'Northwind Studio Dashboard',
150
+ company: { id: 7, slug: 'northwind', name: 'Northwind Studio' },
151
+ metrics: [
152
+ { label: 'Open tasks', value: 12 },
153
+ { label: 'Active pages', value: 8 },
154
+ { label: 'Pending review', value: 3 }
155
+ ],
156
+ renderSummary: [
157
+ { label: 'Source', value: 'Database' },
158
+ { label: 'Company', value: 'Northwind Studio' },
159
+ { label: 'Mode', value: 'Server render' }
160
+ ]
161
+ }
162
+ ```
163
+
164
+ ASJS then turns that object into view locals and renders the final HTML.
165
+ The browser does not receive a loading shell first and wait for the page model later. The work is already done.
166
+
167
+ ### Server-rendered POST example
168
+
169
+ You can use the same pattern for POST routes when you want validation or save results to come back as a fully rendered page state.
170
+
171
+ ```js
172
+ app.use(express.urlencoded({ extended: false }))
173
+
174
+ app.post('/contact', asjs.page('contact', async (req) => {
175
+ const formValues = {
176
+ name: String(req.body.name || '').trim(),
177
+ email: String(req.body.email || '').trim(),
178
+ brief: String(req.body.brief || '').trim()
179
+ }
180
+
181
+ const formErrors = {}
182
+
183
+ if (!formValues.name) {
184
+ formErrors.name = 'Please enter a name.'
185
+ }
186
+
187
+ if (!formValues.email.includes('@')) {
188
+ formErrors.email = 'Please enter a valid email address.'
189
+ }
190
+
191
+ if (Object.keys(formErrors).length) {
192
+ return {
193
+ status: 422,
194
+ title: 'Contact',
195
+ formValues,
196
+ formErrors
197
+ }
198
+ }
199
+
200
+ const savedRecord = await saveProjectNote(formValues)
201
+
202
+ return {
203
+ title: 'Contact received',
204
+ submission: savedRecord
205
+ }
206
+ }))
207
+ ```
208
+
209
+ #### Validation return
210
+
211
+ ```js
212
+ {
213
+ status: 422,
214
+ title: 'Contact',
215
+ formValues: {
216
+ name: '',
217
+ email: 'wrong-address',
218
+ brief: 'Need a redesign'
219
+ },
220
+ formErrors: {
221
+ name: 'Please enter a name.',
222
+ email: 'Please enter a valid email address.'
223
+ }
224
+ }
225
+ ```
226
+
227
+ #### Success return
228
+
229
+ ```js
230
+ {
231
+ title: 'Contact received',
232
+ submission: {
233
+ reference: 'WEBAS-K3J9X2',
234
+ name: 'Lina Howard',
235
+ email: 'lina@northwind.dev',
236
+ brief: 'We need a cleaner delivery and dashboard structure.'
237
+ }
238
+ }
239
+ ```
240
+
241
+ In both cases, the route returns data. ASJS handles the render step after that.
242
+
243
+ ### Inline JSON response without leaving the page
244
+
245
+ Sometimes you do not want a full page transition. You want to post to a dedicated route, get a short answer, and place that answer inside a specific panel.
246
+
247
+ ```asjs
248
+ <form<%- asjs.formAttrs({
249
+ action: '/contact/availability',
250
+ method: 'post',
251
+ mode: 'json',
252
+ target: '#availability-response',
253
+ swap: 'replace'
254
+ }) %>>
255
+ <input type="text" name="company" placeholder="Company name">
256
+ <input type="text" name="scope" placeholder="Dashboard, launch page, corporate site">
257
+ <button type="submit">Check route</button>
258
+ </form>
259
+
260
+ <div id="availability-response"></div>
261
+ ```
262
+
263
+ ```js
264
+ app.post('/contact/availability', async (req, res) => {
265
+ const preview = await availabilityService.check(req.body)
266
+
267
+ res.json({
268
+ html: `
269
+ <div class="asjs-inline-response is-success">
270
+ <strong>${preview.title}</strong>
271
+ <p>${preview.message}</p>
272
+ </div>
273
+ `,
274
+ route: preview.route,
275
+ replyWindow: preview.replyWindow
276
+ })
277
+ })
278
+ ```
279
+
280
+ #### Example JSON response body
281
+
282
+ ```json
283
+ {
284
+ "html": "<div class=\"asjs-inline-response is-success\"><strong>Northwind Studio can be routed into the operational track.</strong><p>The request can continue without leaving the current page shell.</p></div>",
285
+ "route": "Operational UI track",
286
+ "replyWindow": "1 business day"
287
+ }
288
+ ```
289
+
290
+ ASJS reads that response, finds `#availability-response`, and updates only that part of the page.
291
+
292
+ ### Plugins and hooks
293
+
294
+ For projects that need room to grow, `setupAsjs()` now supports plugins and lifecycle hooks.
295
+
296
+ ```js
297
+ const asjs = setupAsjs(app, {
298
+ rootDir: __dirname,
299
+ plugins: [
300
+ function studioPlugin(api) {
301
+ const healthRouter = api.express.Router()
302
+
303
+ healthRouter.get('/', (req, res) => {
304
+ res.json({ ok: true, service: 'studio-plugin' })
305
+ })
306
+
307
+ api.app.use('/health', healthRouter)
308
+
309
+ api.extendLocals({ supportEmail: 'team@example.com' })
310
+
311
+ api.addHook('beforeRender', ({ pageData }) => ({
312
+ pageData: {
313
+ ...pageData,
314
+ supportEmail: 'team@example.com'
315
+ }
316
+ }))
317
+ }
318
+ ]
319
+ })
320
+ ```
321
+
322
+ Available hook points:
323
+
324
+ - `beforePage`
325
+ - `afterPage`
326
+ - `beforeRender`
327
+ - `afterRender`
328
+
329
+ ### Form enhancement
330
+
331
+ ```asjs
332
+ <form class="contact-form"<%- asjs.formAttrs({
333
+ action: '/contact',
334
+ method: 'post',
335
+ transition: 'slide'
336
+ }) %>>
337
+ <input type="text" name="name" placeholder="Project lead">
338
+ <button type="submit">Send</button>
339
+ </form>
340
+ ```
341
+
342
+ `asjs.formAttrs()` marks a form for the built-in ASJS client handler.
343
+ The response may come back as full HTML or as JSON for an inline target update.
344
+
345
+ ### Router lifecycle events
346
+
347
+ - `asjs:before-navigate`
348
+ - `asjs:content-replaced`
349
+ - `asjs:navigated`
350
+ - `asjs:navigation-error`
351
+ - `asjs:before-submit`
352
+ - `asjs:form-response`
353
+ - `asjs:form-success`
354
+ - `asjs:form-error`
355
+
356
+ These events are useful for analytics, loading states, cleanup logic, and custom form feedback.
357
+
358
+ ### Component helper
359
+
360
+ ```js
361
+ const asjs = setupAsjs(app, {
362
+ components: {
363
+ 'partials/card': {
364
+ props: {
365
+ title: 'string',
366
+ text: 'string',
367
+ tone: {
368
+ type: 'string',
369
+ default: 'neutral'
370
+ }
371
+ },
372
+ strict: true
373
+ }
374
+ }
375
+ })
376
+ ```
377
+
378
+ ```asjs
379
+ <%- component('partials/card', { title: 'Card', text: 'Body text' }) %>
380
+ ```
381
+
382
+ ### Built-in asset delivery
383
+
384
+ ASJS serves its client files from inside the package by default.
385
+
386
+ - default mount path: `/_asjs`
387
+ - router script: `/_asjs/asjs-router.js?v=...`
388
+ - core stylesheet: `/_asjs/asjs-core.css?v=...`
389
+ - optional theme stylesheet: `/_asjs/asjs-theme.css?v=...`
390
+
391
+ The `v=...` query string is generated automatically for cache busting.
392
+
393
+ ### Transition presets
394
+
395
+ - `fade`
396
+ - `lift`
397
+ - `slide`
398
+ - `scale`
399
+ - `blur-soft`
400
+
401
+ Use them globally with `transitions`, per-page with `transition`, or per-link with `data-asjs-transition`.
402
+
403
+ ### Highlights
404
+
405
+ - `setupAsjs(app, options)` integrates directly into an existing Express app.
406
+ - `component()` validates props before rendering a partial.
407
+ - `cache: true` keeps compiled templates until file mtimes change.
408
+ - `prefetch` and `loadingBar` improve navigation feel.
409
+ - `forms: true` enables built-in async form enhancement.
410
+ - `plugins` opens room for middleware, routes, and project-specific services.
411
+ - `assetVersion` lets you override or disable cache-busting.
412
+ - `serveClientAssets` serves router assets from the package itself.
413
+
414
+ ### Package utilities
415
+
416
+ ```js
417
+ const { getAsjsPackagePaths } = require('asjs-express')
418
+
419
+ const paths = getAsjsPackagePaths()
420
+ console.log(paths.routerPath)
421
+ ```
422
+
423
+ ### Local demo
424
+
425
+ ```bash
426
+ npm install
427
+ npm run example:express
428
+ ```
429
+
430
+ Open `http://localhost:3001`.
431
+
432
+ ### Repository example app
433
+
434
+ The repository ships a dedicated Express example under `example-express/`.
435
+
436
+ - entry: `example-express/app.js`
437
+ - favicon: `example-express/favicon.svg`
438
+ - views: `example-express/views`
439
+ - start command: `npm run example:express`
440
+ - starter pages: `/`, `/dashboard`, `/products`, `/contact`
441
+ - plugin health route: `/webas-platform/health`
442
+
443
+ The example presents WebAS as a practical interface structure associated with [Acarfx](https://acarfx.com) and [Seyfullah Kısacık](https://seyfooksck.com/). It is intentionally written with a calm, professional tone because the project is meant to feel usable, not theatrical.
444
+
445
+ ## Türkçe
446
+
447
+ ### Bu proje neden var?
448
+
449
+ ASJS, Express projelerinde çok sık karşılaşılan ama çoğu zaman gereksiz yere karmaşık hâle gelen bir alan için yazıldı: sayfalar ilk gün sade olur, sonra gerçek ihtiyaçlar gelir. Dashboard verisi gerekir. Form doğrulaması gerekir. Aynı layout içinde farklı sayfaları sakin biçimde taşımak gerekir. Bir noktadan sonra ya aynı kod tekrar eder ya da ihtiyaç o kadar büyük değilken proje ağır bir ön yüz yapısına sürüklenir.
450
+
451
+ ASJS tam burada devreye girer. Sunucu tarafını sade tutar, şablon katmanını EJS benzeri bir yapıda bırakır, istersen sayfa geçişlerini güçlendirir, istersen hiçbir şeyi zorlamaz. En önemlisi de şu soruya net cevap verir: Evet, sayfa yüklenmeden önce veriyi hazırlayabilirsin.
452
+
453
+ Bu depodaki WebAS örneği de bu anlayışla yazıldı. Dili özellikle daha insanî ve daha gerçek tutuldu. Amaç “vitrinlik demo” hissi vermek değil, gerçekten çalışan küçük bir ekibin kullanabileceği bir yapı duygusu vermekti.
454
+
455
+ Bu örnek anlatımın arkasında iki geliştirici adı özellikle görünür tutuluyor:
456
+
457
+ - [Acarfx](https://acarfx.com)
458
+ - [Seyfullah Kısacık](https://seyfooksck.com/)
459
+
460
+ WebAS yüzeyi, bu iki geliştiricinin birlikte şekillendirdiği düzenli, açık ve kurumsal bir arayüz dili gibi anlatılıyor. Çünkü iyi bir arayüz çoğu zaman bağırmaz; düzeniyle güven verir.
461
+
462
+ ### ASJS neler sağlar?
463
+
464
+ - `<% %>`, `<%= %>`, `<%- %>` tabanlı EJS benzeri sözdizimi
465
+ - Ek yük bindirmeyen layout desteği
466
+ - Prop doğrulamalı partial ve component kullanımı
467
+ - Template cache
468
+ - Geçiş, prefetch ve loading bar destekli istemci yönlendirmesi
469
+ - İşaretli formlar için dahili async form akışı
470
+ - Büyüyen Express projeleri için plugin ve hook desteği
471
+ - Public klasöre dosya kopyalamadan paket içinden asset servisi
472
+
473
+ ### Kurulum
474
+
475
+ ```bash
476
+ npm install asjs-express
477
+ ```
478
+
479
+ ### Hızlı başlangıç
480
+
481
+ ```js
482
+ const express = require('express')
483
+ const { setupAsjs } = require('asjs-express')
484
+
485
+ const app = express()
486
+ const asjs = setupAsjs(app, {
487
+ rootDir: __dirname,
488
+ defaultLayout: 'layouts/main',
489
+ debug: true,
490
+ cache: true,
491
+ forms: true,
492
+ transitions: 'lift',
493
+ prefetch: true,
494
+ loadingBar: true
495
+ })
496
+
497
+ app.get('/', asjs.page('home', {
498
+ title: 'Merhaba ASJS'
499
+ }))
500
+
501
+ app.use(asjs.errors())
502
+ app.listen(3000)
503
+ ```
504
+
505
+ ### Minimal Express şablonu
506
+
507
+ ```js
508
+ const express = require('express')
509
+ const { setupAsjs } = require('asjs-express')
510
+
511
+ const app = express()
512
+ const asjs = setupAsjs(app, { rootDir: __dirname })
513
+
514
+ app.get('/', asjs.page('home', { title: 'Merhaba ASJS' }))
515
+ app.listen(3000)
516
+ ```
517
+
518
+ ### Layout kullanımı
519
+
520
+ ```asjs
521
+ <!DOCTYPE html>
522
+ <html>
523
+ <head>
524
+ <meta charset="UTF-8">
525
+ <title><%= title %></title>
526
+ <%- asjs.clientTags({ preload: true, theme: true }) %>
527
+ </head>
528
+ <body<%- asjs.bodyAttrs() %>>
529
+ <%- asjs.progressMarkup() %>
530
+ <main<%- asjs.viewAttrs() %>>
531
+ <%- body %>
532
+ </main>
533
+ </body>
534
+ </html>
535
+ ```
536
+
537
+ `asjs.clientTags()` çağrısı dahili ASJS stil ve router etiketlerini otomatik üretir.
538
+ `theme: true` ise açık renkli, kurumsal WebAS temasını yükler.
539
+
540
+ ### Sayfa yüklenmeden önce işlem yapmak
541
+
542
+ Evet, mümkün. `asjs.page()` içine async bir callback verirsen ASJS bu işlemin bitmesini bekler, ardından `res.render(viewName, locals)` çağrısını yapar.
543
+
544
+ Yani veritabanı sorgusu, API isteği, yetki kontrolü, veri biçimlendirme ve view model oluşturma gibi işler ilk HTML tarayıcıya gitmeden önce tamamlanabilir.
545
+
546
+ #### Render öncesi veritabanı örneği
547
+
548
+ ```js
549
+ const db = {
550
+ companies: {
551
+ async findBySlug(slug) {
552
+ return { id: 7, slug, name: 'Northwind Studio' }
553
+ }
554
+ },
555
+ dashboards: {
556
+ async getMetrics(companyId) {
557
+ return [
558
+ { label: 'Açık görev', value: 12 },
559
+ { label: 'Aktif sayfa', value: 8 },
560
+ { label: 'Onay bekleyen', value: 3 }
561
+ ]
562
+ }
563
+ }
564
+ }
565
+
566
+ app.get('/dashboard/:slug', asjs.page('dashboard', async (req) => {
567
+ const company = await db.companies.findBySlug(req.params.slug)
568
+ const metrics = await db.dashboards.getMetrics(company.id)
569
+
570
+ return {
571
+ title: `${company.name} Dashboard`,
572
+ company,
573
+ metrics,
574
+ renderSummary: [
575
+ { label: 'Kaynak', value: 'Veritabanı' },
576
+ { label: 'Şirket', value: company.name },
577
+ { label: 'Mod', value: 'Sunucu render' }
578
+ ]
579
+ }
580
+ }))
581
+ ```
582
+
583
+ #### Dönen sayfa modeli
584
+
585
+ Yukarıdaki callback şu tipte bir nesne döndürür:
586
+
587
+ ```js
588
+ {
589
+ title: 'Northwind Studio Dashboard',
590
+ company: { id: 7, slug: 'northwind', name: 'Northwind Studio' },
591
+ metrics: [
592
+ { label: 'Açık görev', value: 12 },
593
+ { label: 'Aktif sayfa', value: 8 },
594
+ { label: 'Onay bekleyen', value: 3 }
595
+ ],
596
+ renderSummary: [
597
+ { label: 'Kaynak', value: 'Veritabanı' },
598
+ { label: 'Şirket', value: 'Northwind Studio' },
599
+ { label: 'Mod', value: 'Sunucu render' }
600
+ ]
601
+ }
602
+ ```
603
+
604
+ ASJS bu nesneyi view locals hâline getirir ve nihai HTML’i üretir.
605
+ Tarayıcı önce boş bir iskelet alıp sonra veriyi beklemez. Veri işi zaten tamamlanmıştır.
606
+
607
+ ### Server-rendered POST dönüşü
608
+
609
+ POST rotalarında da aynı yaklaşımı kullanabilirsin. Özellikle form doğrulaması, kayıt sonucu veya başarı ekranı aynı view içinde geri dönecekse bu çok temiz çalışır.
610
+
611
+ ```js
612
+ app.use(express.urlencoded({ extended: false }))
613
+
614
+ app.post('/contact', asjs.page('contact', async (req) => {
615
+ const formValues = {
616
+ name: String(req.body.name || '').trim(),
617
+ email: String(req.body.email || '').trim(),
618
+ brief: String(req.body.brief || '').trim()
619
+ }
620
+
621
+ const formErrors = {}
622
+
623
+ if (!formValues.name) {
624
+ formErrors.name = 'Lütfen bir isim girin.'
625
+ }
626
+
627
+ if (!formValues.email.includes('@')) {
628
+ formErrors.email = 'Lütfen geçerli bir e-posta adresi girin.'
629
+ }
630
+
631
+ if (Object.keys(formErrors).length) {
632
+ return {
633
+ status: 422,
634
+ title: 'İletişim',
635
+ formValues,
636
+ formErrors
637
+ }
638
+ }
639
+
640
+ const savedRecord = await saveProjectNote(formValues)
641
+
642
+ return {
643
+ title: 'İletişim alındı',
644
+ submission: savedRecord
645
+ }
646
+ }))
647
+ ```
648
+
649
+ #### Hatalı dönüş örneği
650
+
651
+ ```js
652
+ {
653
+ status: 422,
654
+ title: 'İletişim',
655
+ formValues: {
656
+ name: '',
657
+ email: 'yanlis-adres',
658
+ brief: 'Tasarım yenilemesi istiyoruz'
659
+ },
660
+ formErrors: {
661
+ name: 'Lütfen bir isim girin.',
662
+ email: 'Lütfen geçerli bir e-posta adresi girin.'
663
+ }
664
+ }
665
+ ```
666
+
667
+ #### Başarılı dönüş örneği
668
+
669
+ ```js
670
+ {
671
+ title: 'İletişim alındı',
672
+ submission: {
673
+ reference: 'WEBAS-K3J9X2',
674
+ name: 'Lina Howard',
675
+ email: 'lina@northwind.dev',
676
+ brief: 'Daha temiz bir teslim ve dashboard yapısı istiyoruz.'
677
+ }
678
+ }
679
+ ```
680
+
681
+ Her iki durumda da rota veri döndürür. Render adımını ASJS sonradan kendisi yürütür.
682
+
683
+ ### Sayfadan ayrılmadan JSON döndürmek
684
+
685
+ Bazen tam sayfa geçişi istemezsin. Belirli bir rotaya POST atıp kısa bir cevap almak ve yalnızca tek bir paneli güncellemek istersin.
686
+
687
+ ```asjs
688
+ <form<%- asjs.formAttrs({
689
+ action: '/contact/availability',
690
+ method: 'post',
691
+ mode: 'json',
692
+ target: '#availability-response',
693
+ swap: 'replace'
694
+ }) %>>
695
+ <input type="text" name="company" placeholder="Şirket adı">
696
+ <input type="text" name="scope" placeholder="Dashboard, lansman sayfası, kurumsal site">
697
+ <button type="submit">Rotayı kontrol et</button>
698
+ </form>
699
+
700
+ <div id="availability-response"></div>
701
+ ```
702
+
703
+ ```js
704
+ app.post('/contact/availability', async (req, res) => {
705
+ const preview = await availabilityService.check(req.body)
706
+
707
+ res.json({
708
+ html: `
709
+ <div class="asjs-inline-response is-success">
710
+ <strong>${preview.title}</strong>
711
+ <p>${preview.message}</p>
712
+ </div>
713
+ `,
714
+ route: preview.route,
715
+ replyWindow: preview.replyWindow
716
+ })
717
+ })
718
+ ```
719
+
720
+ #### Örnek JSON cevap gövdesi
721
+
722
+ ```json
723
+ {
724
+ "html": "<div class=\"asjs-inline-response is-success\"><strong>Northwind Studio operasyon hattına yönlendirilebilir.</strong><p>İstek mevcut sayfa kabuğu bozulmadan devam edebilir.</p></div>",
725
+ "route": "Operasyonel arayüz hattı",
726
+ "replyWindow": "1 iş günü"
727
+ }
728
+ ```
729
+
730
+ ASJS bu cevabı okur, `#availability-response` alanını bulur ve yalnızca o kısmı günceller.
731
+
732
+ ### Plugin ve hook yapısı
733
+
734
+ Proje büyüdüğünde `setupAsjs()` artık plugin ve yaşam döngüsü hook’ları için de alan açar.
735
+
736
+ ```js
737
+ const asjs = setupAsjs(app, {
738
+ rootDir: __dirname,
739
+ plugins: [
740
+ function studioPlugin(api) {
741
+ const healthRouter = api.express.Router()
742
+
743
+ healthRouter.get('/', (req, res) => {
744
+ res.json({ ok: true, service: 'studio-plugin' })
745
+ })
746
+
747
+ api.app.use('/health', healthRouter)
748
+
749
+ api.extendLocals({ supportEmail: 'team@example.com' })
750
+
751
+ api.addHook('beforeRender', ({ pageData }) => ({
752
+ pageData: {
753
+ ...pageData,
754
+ supportEmail: 'team@example.com'
755
+ }
756
+ }))
757
+ }
758
+ ]
759
+ })
760
+ ```
761
+
762
+ Kullanılabilen hook noktaları:
763
+
764
+ - `beforePage`
765
+ - `afterPage`
766
+ - `beforeRender`
767
+ - `afterRender`
768
+
769
+ ### Form geliştirme
770
+
771
+ ```asjs
772
+ <form class="contact-form"<%- asjs.formAttrs({
773
+ action: '/contact',
774
+ method: 'post',
775
+ transition: 'slide'
776
+ }) %>>
777
+ <input type="text" name="name" placeholder="Proje sorumlusu">
778
+ <button type="submit">Gönder</button>
779
+ </form>
780
+ ```
781
+
782
+ `asjs.formAttrs()` bir formu dahili ASJS istemci akışı için işaretler.
783
+ Gelen cevap tam HTML de olabilir, belirli bir hedef alanı güncelleyen JSON da olabilir.
784
+
785
+ ### Router yaşam döngüsü event’leri
786
+
787
+ - `asjs:before-navigate`
788
+ - `asjs:content-replaced`
789
+ - `asjs:navigated`
790
+ - `asjs:navigation-error`
791
+ - `asjs:before-submit`
792
+ - `asjs:form-response`
793
+ - `asjs:form-success`
794
+ - `asjs:form-error`
795
+
796
+ Bu event’ler loading durumları, analytics, arayüz temizliği ve özel form geri bildirimi için işe yarar.
797
+
798
+ ### Component helper
799
+
800
+ ```js
801
+ const asjs = setupAsjs(app, {
802
+ components: {
803
+ 'partials/card': {
804
+ props: {
805
+ title: 'string',
806
+ text: 'string',
807
+ tone: {
808
+ type: 'string',
809
+ default: 'neutral'
810
+ }
811
+ },
812
+ strict: true
813
+ }
814
+ }
815
+ })
816
+ ```
817
+
818
+ ```asjs
819
+ <%- component('partials/card', { title: 'Kart', text: 'İçerik' }) %>
820
+ ```
821
+
822
+ ### Dahili asset servisi
823
+
824
+ ASJS istemci dosyalarını varsayılan olarak paketin içinden servis eder.
825
+
826
+ - varsayılan mount path: `/_asjs`
827
+ - router script: `/_asjs/asjs-router.js?v=...`
828
+ - ana stil dosyası: `/_asjs/asjs-core.css?v=...`
829
+ - opsiyonel tema dosyası: `/_asjs/asjs-theme.css?v=...`
830
+
831
+ `v=...` query parametresi otomatik cache-busting için üretilir.
832
+
833
+ ### Geçiş preset’leri
834
+
835
+ - `fade`
836
+ - `lift`
837
+ - `slide`
838
+ - `scale`
839
+ - `blur-soft`
840
+
841
+ Bunları global olarak `transitions`, sayfa bazında `transition`, link bazında ise `data-asjs-transition` ile kullanabilirsin.
842
+
843
+ ### Öne çıkanlar
844
+
845
+ - `setupAsjs(app, options)` mevcut Express uygulamasına doğrudan bağlanır.
846
+ - `component()` partial render etmeden önce props doğrular.
847
+ - `cache: true` derlenmiş template’leri dosya mtime değişene kadar saklar.
848
+ - `prefetch` ve `loadingBar` geçiş hissini güçlendirir.
849
+ - `forms: true` işaretli formlar için dahili async form akışını açar.
850
+ - `plugins` middleware, route ve servis katmanı için alan bırakır.
851
+ - `assetVersion` ile dahili asset versiyonlamasını ezebilir veya kapatabilirsin.
852
+ - `serveClientAssets` router asset’lerini doğrudan paket içinden servis eder.
853
+
854
+ ### Paket yardımcıları
855
+
856
+ ```js
857
+ const { getAsjsPackagePaths } = require('asjs-express')
858
+
859
+ const paths = getAsjsPackagePaths()
860
+ console.log(paths.routerPath)
861
+ ```
862
+
863
+ ### Lokal demo
864
+
865
+ ```bash
866
+ npm install
867
+ npm run example:express
868
+ ```
869
+
870
+ Tarayıcıda `http://localhost:3001` adresini aç.
871
+
872
+ ### Repodaki örnek uygulama
873
+
874
+ Repo içinde ayrı bir Express örneği `example-express/` altında durur.
875
+
876
+ - giriş: `example-express/app.js`
877
+ - favicon: `example-express/favicon.svg`
878
+ - view klasörü: `example-express/views`
879
+ - çalıştırma komutu: `npm run example:express`
880
+ - başlangıç rotaları: `/`, `/dashboard`, `/products`, `/contact`
881
+ - plugin sağlık rotası: `/webas-platform/health`
882
+
883
+ Bu örnek, WebAS’i [Acarfx](https://acarfx.com) ve [Seyfullah Kısacık](https://seyfooksck.com/) ile ilişkilendirilen düzenli ve kurumsal bir arayüz dili gibi sunar. Bu ton bilinçli seçildi. Çünkü bazı projelerde asıl değer, daha çok şey göstermek değil, daha çok şeyi sakin biçimde taşıyabilmektir.