@cruxplug/spa 0.0.7 → 0.0.9
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 +138 -219
- package/dist/index.cjs +51 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -13
- package/dist/index.d.ts +29 -13
- package/dist/index.js +51 -42
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
10
|
<div align="center">
|
|
11
|
-
<img src="https://img.shields.io/badge/v-0.0.
|
|
12
|
-
<img src="https://img.shields.io/badge/🔥-@
|
|
11
|
+
<img src="https://img.shields.io/badge/v-0.0.9-black"/>
|
|
12
|
+
<a href="https://github.com/cruxjs-org"><img src="https://img.shields.io/badge/🔥-@cruxjs-black"/></a>
|
|
13
13
|
<br>
|
|
14
|
-
<img src="https://img.shields.io/badge/coverage
|
|
14
|
+
<img src="https://img.shields.io/badge/coverage-~%25-brightgreen" alt="Test Coverage" />
|
|
15
15
|
<img src="https://img.shields.io/github/issues/cruxplug-org/spa?style=flat" alt="Github Repo Issues" />
|
|
16
16
|
<img src="https://img.shields.io/github/stars/cruxplug-org/spa?style=social" alt="GitHub Repo stars" />
|
|
17
17
|
</div>
|
|
@@ -23,256 +23,174 @@
|
|
|
23
23
|
|
|
24
24
|
<!-- ╔══════════════════════════════ DOC ══════════════════════════════╗ -->
|
|
25
25
|
|
|
26
|
-
- ##
|
|
26
|
+
- ## Overview 👀
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
- #### Why ?
|
|
29
|
+
> To provide a production-ready server-side SPA plugin with automatic SEO/CEO metadata generation, structured data (JSON-LD), error handling, and i18n integration for modern full-stack applications.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
- #### When ?
|
|
32
|
+
> When you need to:
|
|
33
|
+
> - Serve SPA pages with full SEO support
|
|
34
|
+
> - Generate OpenGraph and structured data
|
|
35
|
+
> - Handle error pages (404, 500, etc.)
|
|
36
|
+
> - Inject i18n configuration from server to client
|
|
37
|
+
> - Support E-E-A-T signals for AI search
|
|
38
|
+
> - Generate beautiful, formatted HTML
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
> When you use [@cruxjs/app](https://github.com/cruxjs-org/app).
|
|
33
41
|
|
|
34
|
-
>
|
|
42
|
+
<br>
|
|
43
|
+
<br>
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
- ## Quick Start 🔥
|
|
37
46
|
|
|
38
|
-
>
|
|
47
|
+
> install [`hmm`](https://github.com/minejs-org/hmm) first.
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
```bash
|
|
50
|
+
# in your terminal
|
|
51
|
+
hmm i @cruxjs/spa
|
|
52
|
+
```
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
<div align="center"> <img src="./assets/img/line.png" alt="line" style="display: block; margin-top:20px;margin-bottom:20px;width:500px;"/> </div>
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
hmm i @cruxplug/spa
|
|
46
|
-
```
|
|
56
|
+
- #### Setup
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
> Create your SPA plugin configuration:
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
```typescript
|
|
61
|
+
import { serverSPA } from '@cruxjs/spa';
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
const spaPlugin = serverSPA({
|
|
64
|
+
baseUrl : 'http://localhost:3000',
|
|
65
|
+
clientEntry : './src/app/client.ts',
|
|
66
|
+
clientScriptPath : ['/static/dist/js/client.js'],
|
|
67
|
+
clientStylePath : ['/static/dist/css/min.css'],
|
|
68
|
+
|
|
69
|
+
pages: [
|
|
70
|
+
{
|
|
71
|
+
title : 'Home - My App',
|
|
72
|
+
path : '/',
|
|
73
|
+
description : 'Welcome to our application'
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
|
|
77
|
+
errorPages: [
|
|
78
|
+
{
|
|
79
|
+
statusCode : 404,
|
|
80
|
+
title : '404 - Not Found',
|
|
81
|
+
path : '/*',
|
|
82
|
+
description : 'The page you\'re looking for doesn\'t exist'
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
|
|
86
|
+
}, appConfig);
|
|
54
87
|
```
|
|
55
88
|
|
|
56
|
-
|
|
89
|
+
<div align="center"> <img src="./assets/img/line.png" alt="line" style="display: block; margin-top:20px;margin-bottom:20px;width:500px;"/> </div>
|
|
90
|
+
<br>
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
const spaPlugin = serverSPA({
|
|
60
|
-
baseUrl: 'https://example.com',
|
|
61
|
-
clientEntry: './src/client/browser.tsx',
|
|
62
|
-
clientScriptPath: ['/static/dist/js/app.js'],
|
|
63
|
-
clientStylePath: ['/static/dist/css/style.css'],
|
|
64
|
-
enableAutoNotFound: true,
|
|
65
|
-
pages: [
|
|
66
|
-
{
|
|
67
|
-
title: 'Home',
|
|
68
|
-
path: '/',
|
|
69
|
-
description: 'Welcome to our platform',
|
|
70
|
-
keywords: ['home', 'landing']
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
title: 'About Us',
|
|
74
|
-
path: '/about',
|
|
75
|
-
description: 'Learn more about our company',
|
|
76
|
-
contentType: 'page'
|
|
77
|
-
}
|
|
78
|
-
]
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
app.use(spaPlugin);
|
|
82
|
-
```
|
|
92
|
+
- #### Usage
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
> Add the plugin to your application:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { createApp, AppConfig } from '@cruxjs/app';
|
|
98
|
+
import { serverSPA } from '@cruxjs/spa';
|
|
99
|
+
|
|
100
|
+
const appConfig: AppConfig = {
|
|
101
|
+
debug : true,
|
|
102
|
+
server : { port: 3000, host: 'localhost' },
|
|
103
|
+
// ... rest of configuration
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const spaPlugin = serverSPA({
|
|
107
|
+
baseUrl : 'http://localhost:3000',
|
|
108
|
+
clientEntry : './src/app/client.ts',
|
|
109
|
+
clientScriptPath : ['/static/dist/js/client.js'],
|
|
110
|
+
clientStylePath : ['/static/dist/css/min.css'],
|
|
111
|
+
pages : [/* ... */],
|
|
112
|
+
}, appConfig);
|
|
113
|
+
|
|
114
|
+
appConfig.plugins!.push(spaPlugin);
|
|
115
|
+
|
|
116
|
+
const app = createApp(appConfig);
|
|
117
|
+
app.start();
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
<div align="center"> <img src="./assets/img/line.png" alt="line" style="display: block; margin-top:20px;margin-bottom:20px;width:500px;"/> </div>
|
|
121
|
+
|
|
122
|
+
- #### Page Configuration with Translation Support
|
|
85
123
|
|
|
86
124
|
```typescript
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
const pages: SPAPageConfig[] = [
|
|
126
|
+
{
|
|
127
|
+
// Page metadata with translation support
|
|
128
|
+
title : 'meta.home.title',
|
|
129
|
+
path : '/',
|
|
130
|
+
description : 'meta.home.desc',
|
|
131
|
+
keywords : ['home', 'platform', 'app'],
|
|
132
|
+
|
|
133
|
+
// E-E-A-T Signals with translation support
|
|
134
|
+
expertise : 'Full-Stack Web Development',
|
|
135
|
+
experience : '2025+',
|
|
136
|
+
authority : 'meta.authority',
|
|
137
|
+
|
|
138
|
+
// Content type for AI indexing
|
|
139
|
+
contentType : 'page',
|
|
140
|
+
|
|
141
|
+
// OpenGraph for social media
|
|
142
|
+
ogImage : 'http://localhost:3000/static/img/og-home.png',
|
|
143
|
+
canonical : 'http://localhost:3000/'
|
|
144
|
+
}
|
|
145
|
+
];
|
|
106
146
|
```
|
|
107
147
|
|
|
108
|
-
-
|
|
148
|
+
- #### Error Page Configuration with Translation Support
|
|
109
149
|
|
|
110
150
|
```typescript
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
title: '500 - Server Error',
|
|
128
|
-
path: '/500',
|
|
129
|
-
description: 'Something went wrong on our end'
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
});
|
|
151
|
+
const errorPages: ErrorPageConfig[] = [
|
|
152
|
+
{
|
|
153
|
+
statusCode : 404,
|
|
154
|
+
title : 'meta.error.404.title',
|
|
155
|
+
path : '/*',
|
|
156
|
+
description : 'meta.error.404.desc',
|
|
157
|
+
robots : 'noindex, nofollow',
|
|
158
|
+
contentType : 'page'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
statusCode : 500,
|
|
162
|
+
title : 'meta.error.500.title',
|
|
163
|
+
path : '/500',
|
|
164
|
+
description : 'meta.error.500.desc'
|
|
165
|
+
}
|
|
166
|
+
];
|
|
133
167
|
```
|
|
134
168
|
|
|
135
169
|
<br>
|
|
170
|
+
<br>
|
|
136
171
|
|
|
137
|
-
- ## API Reference
|
|
138
|
-
|
|
139
|
-
### Core Plugin
|
|
140
|
-
|
|
141
|
-
- #### `serverSPA(config: ServerSPAPluginConfig): CruxPlugin`
|
|
142
|
-
> Creates and returns the SPA plugin with SEO support
|
|
143
|
-
|
|
144
|
-
**Parameters:**
|
|
145
|
-
- `baseUrl`: Base URL for canonical links and SEO (required)
|
|
146
|
-
- `clientScriptPath`: Array of paths to client-side JS bundles (required)
|
|
147
|
-
- `clientStylePath`: Array of paths to client-side CSS files (optional)
|
|
148
|
-
- `clientEntry`: Path to client entry point (required)
|
|
149
|
-
- `pages`: Array of pages to serve as SPA (optional)
|
|
150
|
-
- `errorPages`: Array of error page configurations (optional)
|
|
151
|
-
- `author`: Author name for structured data (optional)
|
|
152
|
-
- `authorUrl`: Author profile URL (optional)
|
|
153
|
-
- `enableAutoNotFound`: Auto-generate 404 page if true (optional, default: false)
|
|
154
|
-
- `defaultDescription`: Default SEO description (optional)
|
|
155
|
-
- `defaultKeywords`: Default SEO keywords array (optional)
|
|
156
|
-
- `defaultRobots`: Default robots meta tag (optional)
|
|
157
|
-
|
|
158
|
-
**Returns:** CruxPlugin with SEO support
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
const plugin = serverSPA({
|
|
162
|
-
baseUrl: 'https://example.com',
|
|
163
|
-
clientScriptPath: ['/js/app.js'],
|
|
164
|
-
clientStylePath: ['/css/min.css'],
|
|
165
|
-
clientEntry: './src/client/index.tsx',
|
|
166
|
-
enableAutoNotFound: true,
|
|
167
|
-
pages: [
|
|
168
|
-
{ title: 'Home', path: '/', description: 'Home page' }
|
|
169
|
-
]
|
|
170
|
-
});
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Type Definitions
|
|
174
|
-
|
|
175
|
-
- #### `SPAPageConfig`
|
|
176
|
-
> Configuration for a single SPA page
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
interface SPAPageConfig {
|
|
180
|
-
// Required
|
|
181
|
-
title: string; // Page title
|
|
182
|
-
path: string; // Route path
|
|
183
|
-
|
|
184
|
-
// SEO
|
|
185
|
-
description?: string; // Meta description
|
|
186
|
-
keywords?: string[]; // Meta keywords array
|
|
187
|
-
ogImage?: string; // Open Graph image URL
|
|
188
|
-
canonical?: string; // Canonical URL
|
|
189
|
-
robots?: string; // Robots meta tag
|
|
190
|
-
|
|
191
|
-
// E-E-A-T Signals (Google AI Overviews)
|
|
192
|
-
expertise?: string; // Author's expertise
|
|
193
|
-
experience?: string; // Author's experience
|
|
194
|
-
authority?: string; // Author's authority
|
|
195
|
-
|
|
196
|
-
// Content
|
|
197
|
-
contentType?: 'article' | 'product' | 'service' | 'app' | 'workspace' | 'page';
|
|
198
|
-
clientScriptPath?: string[]; // Override client script paths
|
|
199
|
-
clientStylePath?: string[]; // Override client style paths
|
|
200
|
-
clientEntry?: string; // Override client entry
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
- #### `ErrorPageConfig`
|
|
205
|
-
> Configuration for error pages
|
|
206
|
-
|
|
207
|
-
```typescript
|
|
208
|
-
interface ErrorPageConfig extends SPAPageConfig {
|
|
209
|
-
statusCode: number; // HTTP status code (404, 500, etc.)
|
|
210
|
-
}
|
|
211
|
-
```
|
|
172
|
+
- ## API Reference 📚
|
|
212
173
|
|
|
213
|
-
-
|
|
214
|
-
> Main plugin configuration
|
|
174
|
+
- ### ..
|
|
215
175
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
baseUrl: string;
|
|
219
|
-
pages?: SPAPageConfig[];
|
|
220
|
-
errorPages?: ErrorPageConfig[];
|
|
221
|
-
clientEntry: string;
|
|
222
|
-
clientScriptPath: string[]; // Array of script paths
|
|
223
|
-
clientStylePath?: string[]; // Array of style paths
|
|
224
|
-
author?: string;
|
|
225
|
-
authorUrl?: string;
|
|
226
|
-
defaultDescription?: string;
|
|
227
|
-
defaultKeywords?: string[];
|
|
228
|
-
defaultRobots?: string;
|
|
229
|
-
enableAutoNotFound?: boolean;
|
|
230
|
-
}
|
|
176
|
+
```ts
|
|
177
|
+
..
|
|
231
178
|
```
|
|
232
179
|
|
|
233
|
-
### Utility Functions (Advanced)
|
|
234
|
-
|
|
235
|
-
- #### `generateSEOMetaTags(config, baseConfig): string`
|
|
236
|
-
> Generates SEO meta tags with E-E-A-T signals
|
|
237
|
-
|
|
238
|
-
**Features:**
|
|
239
|
-
- Core SEO metadata (charset, viewport, description, keywords)
|
|
240
|
-
- E-E-A-T signals for AI search optimization
|
|
241
|
-
- Mobile optimization (web app capable, status bar)
|
|
242
|
-
- Open Graph protocol tags
|
|
243
|
-
- Performance & security headers
|
|
244
|
-
|
|
245
|
-
- #### `generateStructuredData(pageConfig, baseConfig, contentType): string`
|
|
246
|
-
> Generates JSON-LD structured data
|
|
247
180
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
- Support for multiple content types
|
|
251
|
-
- Rich snippets for search results
|
|
252
|
-
- Author and creator information
|
|
253
|
-
- AI overview optimization
|
|
254
|
-
|
|
255
|
-
- #### `generateSPAHTML(pageConfig, baseConfig): string`
|
|
256
|
-
> Generates complete HTML document
|
|
181
|
+
<br>
|
|
182
|
+
<br>
|
|
257
183
|
|
|
258
|
-
|
|
259
|
-
- Full HTML5 shell with doctype
|
|
260
|
-
- Integrated SEO and structured data
|
|
261
|
-
- App mount point (#app)
|
|
262
|
-
- Multiple module script loading (from array)
|
|
263
|
-
- Multiple stylesheet loading (from array)
|
|
184
|
+
- ## Integration with @cruxjs/client 🔗
|
|
264
185
|
|
|
265
|
-
|
|
266
|
-
> Creates CruxJS route definition
|
|
186
|
+
> When combined with [@cruxjs/client](https://github.com/cruxjs-org/client):
|
|
267
187
|
|
|
268
|
-
-
|
|
269
|
-
|
|
188
|
+
- ✅ i18n automatically injected via meta tag
|
|
189
|
+
- ✅ All plugin lifecycle hooks execute
|
|
190
|
+
- ✅ Routing immediately available
|
|
191
|
+
- ✅ Translations loaded before component render
|
|
192
|
+
- ✅ Zero boilerplate in user code
|
|
270
193
|
|
|
271
|
-
**Features:**
|
|
272
|
-
- Differentiates API vs web requests
|
|
273
|
-
- JSON responses for `/api/*` routes
|
|
274
|
-
- Custom HTML pages for web requests
|
|
275
|
-
- Fallback error handling
|
|
276
194
|
|
|
277
195
|
<!-- ╚═════════════════════════════════════════════════════════════════╝ -->
|
|
278
196
|
|
|
@@ -280,6 +198,7 @@
|
|
|
280
198
|
|
|
281
199
|
<!-- ╔══════════════════════════════ END ══════════════════════════════╗ -->
|
|
282
200
|
|
|
201
|
+
<br>
|
|
283
202
|
<br>
|
|
284
203
|
|
|
285
204
|
---
|
|
@@ -288,4 +207,4 @@
|
|
|
288
207
|
<a href="https://github.com/maysara-elshewehy"><img src="https://img.shields.io/badge/by-Maysara-black"/></a>
|
|
289
208
|
</div>
|
|
290
209
|
|
|
291
|
-
<!-- ╚═════════════════════════════════════════════════════════════════╝ -->
|
|
210
|
+
<!-- ╚═════════════════════════════════════════════════════════════════╝ -->
|
package/dist/index.cjs
CHANGED
|
@@ -1,48 +1,57 @@
|
|
|
1
|
-
'use strict';function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`);return
|
|
1
|
+
'use strict';var server=require('@minejs/server'),i18n=require('@minejs/i18n');function h(e,t=""){if(!e)return t;if(e.includes(".")&&/^[a-z0-9._]+$/.test(e))try{return server.t(e)||e}catch{return e}return e}function u(e,t=""){return h(e,t)}function w(e){return !e||e.length===0?"":e.map(n=>h(n)).filter(Boolean).join(", ")}function P(e){if(!e)return "Page";let t=h(e);try{return v(t)}catch{return t}}function v(e){let t=server.t("app.name");return i18n.isRTL()?`${t} - ${e}`:`${e} - ${t}`}function y(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,r=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",a=P(e.title),i=u(e.description,t.defaultDescription||"A modern single-page application"),s=w(e.keywords||t.defaultKeywords),o=u(e.expertise,""),c=u(e.experience,""),l=u(e.authority,""),p=e.contentType==="article"?"article":"website";return `<!-- \u{1F50D} Core SEO Meta Tags -->
|
|
2
|
+
<meta charset="UTF-8" />
|
|
3
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4
|
+
<title>${a}</title>
|
|
5
|
+
<meta name="description" content="${i}" />
|
|
6
|
+
${s?`<meta name="keywords" content="${s}" />`:""}
|
|
7
|
+
<meta name="robots" content="${r}" />
|
|
8
|
+
<meta name="language" content="en" />
|
|
9
|
+
<meta http-equiv="content-language" content="en-us" />
|
|
10
|
+
<!-- \u{1F465} E-E-A-T Signals for AI Search -->
|
|
11
|
+
${t.author?`<meta name="author" content="${t.author}" />`:""}
|
|
12
|
+
${o?`<meta name="expertise" content="${o}" />`:""}
|
|
13
|
+
${c?`<meta name="experience" content="${c}" />`:""}
|
|
14
|
+
${l?`<meta name="authority" content="${l}" />`:""}
|
|
15
|
+
<!-- \u{1F4F1} Mobile & Performance -->
|
|
16
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
17
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
18
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
19
|
+
<meta name="theme-color" content="#000000" />
|
|
20
|
+
<!-- \u{1F517} Canonical & Prefetch -->
|
|
21
|
+
<link rel="canonical" href="${n}" />
|
|
22
|
+
${(e.clientScriptPath||t.clientScriptPath)?.map(g=>`<link rel="prefetch" href="${g}" />`).join(`
|
|
23
|
+
`)}
|
|
24
|
+
<!-- \u26A1 Performance & Security -->
|
|
25
|
+
<meta name="format-detection" content="telephone=no" />
|
|
26
|
+
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
|
27
|
+
<!-- \u{1F4D8} Open Graph Protocol (Social Media) -->
|
|
28
|
+
<meta property="og:type" content="${p}" />
|
|
29
|
+
<meta property="og:title" content="${a}" />
|
|
30
|
+
<meta property="og:description" content="${i}" />
|
|
31
|
+
<meta property="og:url" content="${n}" />
|
|
32
|
+
<meta property="og:locale" content="en_US" />
|
|
33
|
+
${t.author?`<meta property="og:site_name" content="${t.author}" />`:""}
|
|
34
|
+
${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
|
|
35
|
+
${e.ogImage?`<meta property="og:image:alt" content="${a}" />`:""}
|
|
36
|
+
`}function S(e,t,n="WebPage"){let r=e.canonical||`${t.baseUrl}${e.path}`,a=P(e.title),i=u(e.description,t.defaultDescription),s=u(e.expertise,""),o=u(e.experience,""),c=u(e.authority,""),l={"@context":"https://schema.org","@type":n,name:a,url:r,description:i,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(s||o||c)&&{creator:{"@type":"Person",name:t.author||"Unknown",...s&&{expertise:s},...o&&{experience:o},...c&&{authority:c}}}};return `<script type="application/ld+json">
|
|
37
|
+
${JSON.stringify(l,null,2).split(`
|
|
38
|
+
`).map(g=>g&&` ${g}`).join(`
|
|
39
|
+
`)}
|
|
40
|
+
</script>`}var R=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"];function k(e){let t=[],n=/(<[^>]+>)|([^<]+)/g,r;for(;(r=n.exec(e))!==null;)r[1]?t.push({type:"tag",value:r[1]}):r[2]&&r[2].trim().length>0&&t.push({type:"text",value:r[2]});return t}function L(e,t=4){let n=" ".repeat(t),r=[],a=0;for(let i=0;i<e.length;i++){let s=e[i],o=s.value.trim();o.startsWith("</")&&(a=Math.max(0,a-1));let c=n.repeat(a);if(!(s.type==="text"&&o.length===0)&&(s.type==="text"&&r.length>0&&!r[r.length-1].trim().endsWith(">")?r[r.length-1]+=o:r.push(c+o),o.startsWith("<")&&!o.startsWith("</"))){if(o.startsWith("<!DOCTYPE")||o.startsWith("<!--"))continue;let l=o.match(/<([A-Za-z][A-Za-z0-9\\-]*)/i),p=l?l[1].toLowerCase():"",g=R.includes(p),T=o.endsWith("/>");!g&&!T&&a++;}}return r}function x(e,t=4){let n=e.split(`
|
|
41
|
+
`).map(i=>i.trim()).filter(i=>i.length>0).join(""),r=k(n);return L(r,t).join(`
|
|
42
|
+
`)}function M(e){let t={defaultLanguage:e?.defaultLanguage||"en",supportedLanguages:e?.supportedLanguages||["en"],basePath:e?.basePath||"i18n/",fileExtension:e?.fileExtension||"json"},n=t.basePath;return n=n.replace(/\\/g,"/"),n.includes("shared/static")?n="static"+(n.split("shared/static")[1]||""):n.startsWith("./")?n=n.slice(2):n.startsWith("/")&&(n=n.slice(1)),n.endsWith("/")||(n+="/"),`<meta name="app-i18n" content='${JSON.stringify({defaultLanguage:t.defaultLanguage,supportedLanguages:t.supportedLanguages,basePath:n,fileExtension:t.fileExtension})}' />`}function d(e,t,n){let r=e.clientScriptPath||t.clientScriptPath,a=e.clientStylePath||t.clientStylePath||[],i=r.map(p=>`<script type="module" src="${p}"></script>`).join(`
|
|
43
|
+
`),s=a.map(p=>`<link rel="stylesheet" href="${p}" />`).join(`
|
|
44
|
+
`),o=M(n);o||console.warn("[SPA] WARNING: i18n meta tag is empty!");let c=`<!DOCTYPE html>
|
|
37
45
|
<html lang="en">
|
|
38
46
|
<head>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
${y(e,t)}
|
|
48
|
+
${S(e,t,e.contentType==="article"?"Article":"WebPage")}
|
|
49
|
+
${o}
|
|
50
|
+
${s}
|
|
42
51
|
</head>
|
|
43
52
|
<body>
|
|
44
|
-
|
|
45
|
-
${
|
|
53
|
+
<div id="app"></div>
|
|
54
|
+
${i}
|
|
46
55
|
</body>
|
|
47
|
-
</html
|
|
56
|
+
</html>`;c.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag NOT in raw HTML!");let l=x(c);return l.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag LOST during formatting!"),l}function m(e,t,n){return {method:"GET",path:e.path,handler:r=>{let a=d(e,t,n);return r.html(a)}}}function _(){return global.__cruxjs_i18n_config}function A(e){global.__cruxjs_i18n_config=e;}function j(e,t,n,r){if(r.startsWith("/api/"))return {status:e,headers:{"Content-Type":"application/json"},body:JSON.stringify({error:`Error ${e}`})};if(t.has(e)){let a=t.get(e),i=_();console.log(`[Errors] Generating error page ${e} with i18n:`,!!i);let s=d(a,n,i);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:s}}return console.log(`[Errors] No custom error page for ${e}, returning fallback`),{status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function $(e,t){return (n,r)=>{let a=j(n,e,t,r);return new Response(a.body,{status:a.status,headers:a.headers})}}function b(){return {statusCode:404,title:"404 - Page Not Found",path:"/404",description:"The page you are looking for could not be found.",keywords:["404","not found","error"],robots:"noindex, nofollow"}}function K(e,t){let n=[],r=new Map;A(t?.i18n);let a=t?.i18n;if(e.pages&&e.pages.length>0)for(let o of e.pages)n.push(m(o,e,a));if(e.errorPages&&e.errorPages.length>0)for(let o of e.errorPages)r.set(o.statusCode,o),n.push(m(o,e,a));if(e.enableAutoNotFound&&!r.has(404)){let o=b();r.set(404,o),n.push(m(o,e,a));}let i=$(r,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:n,__spaErrorHandler:i,onRegister:async o=>{if(console.log(`[SPA Plugin] Registered ${n.length} SPA routes`),r.size>0){let c=Array.from(r.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${c}`);}},onAwake:async o=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async o=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async o=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}exports.serverSPA=K;//# sourceMappingURL=index.cjs.map
|
|
48
57
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/seo.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["generateSEOMetaTags","config","baseConfig","canonicalUrl","robots","description","keywords","script","generateStructuredData","pageConfig","contentType","schema","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","createSPARoute","c","html","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.cjs","sourcesContent":["// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n const description = config.description || baseConfig.defaultDescription || 'A modern single-page application';\r\n const keywords = (config.keywords || baseConfig.defaultKeywords || []).join(', ');\r\n\r\n return `\r\n <!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.expertise ? `<meta name=\"expertise\" content=\"${config.expertise}\" />` : ''}\r\n ${config.experience ? `<meta name=\"experience\" content=\"${config.experience}\" />` : ''}\r\n ${config.authority ? `<meta name=\"authority\" content=\"${config.authority}\" />` : ''}\r\n\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n ')}\r\n\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n <meta property=\"og:title\" content=\"${config.title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />`;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': pageConfig.title,\r\n 'url': canonicalUrl,\r\n 'description': pageConfig.description || baseConfig.defaultDescription,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...(pageConfig.expertise || pageConfig.experience || pageConfig.authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(pageConfig.expertise && { 'expertise': pageConfig.expertise }),\r\n ...(pageConfig.experience && { 'experience': pageConfig.experience }),\r\n ...(pageConfig.authority && { 'authority': pageConfig.authority })\r\n }\r\n }\r\n };\r\n\r\n return `<script type=\"application/ld+json\">${JSON.stringify(schema, null, 2)}</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => ` <script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => ` <link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n return `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n ${generateSEOMetaTags(pageConfig, baseConfig)}\r\n ${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n ${styleTags ? styleTags : ''}\r\n</head>\r\n<body>\r\n <div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n * \r\n * Returns HTML for web requests and JSON for API requests\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n const html = generateSPAHTML(errorConfig, baseConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n * \r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl: 'https://example.com',\r\n * clientEntry: './src/client/browser.tsx',\r\n * clientScriptPath: '/static/dist/js/min.js',\r\n * clientStylePath: '/static/dist/css/min.css',\r\n * enableAutoNotFound: true, // Auto-handle 404s\r\n * pages: [\r\n * {\r\n * title: 'Home',\r\n * path: '/',\r\n * description: 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode: 404,\r\n * title: '404 - Not Found',\r\n * path: '/404',\r\n * description: 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config));\r\n }\r\n\r\n // Create error handler function\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/seo.ts","../src/utils/htmlFormatter.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["parseValue","value","defaultValue","t","resolveMetaValue","resolveKeywords","keywords","kw","resolvePageTitle","title","parsedTitle","genPageTitle","val","appName","isRTL","generateSEOMetaTags","config","baseConfig","canonicalUrl","robots","description","expertise","experience","authority","ogType","script","generateStructuredData","pageConfig","contentType","schema","line","VOID_ELEMENTS","tokenize","html","tokens","regex","match","processTokens","indentSize","indent","output","indentLevel","token","padding","tagMatch","tagName","isVoidElement","isSelfClosing","formatHTML","normalized","generateI18nMetaTag","i18nConfig","browserPath","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","i18nMetaTag","rawHTML","formatted","createSPARoute","c","getGlobalI18nConfig","setGlobalI18nConfig","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","appConfig","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.cjs","sourcesContent":["// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { t } from '@minejs/server';\r\n import { isRTL } from '@minejs/i18n';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Smart parser for string values\r\n *\r\n * Intelligently detects if a string is a translation key or plain text:\r\n * - Translation key format (e.g., 'meta.home.title'): must contain dots, attempts translation via t(key)\r\n * - Plain text (e.g., 'My Title', 'cruxjs', 'framework'): returns as-is\r\n *\r\n * Key format: MUST contain at least one dot (.) to be considered a translation key\r\n * Examples:\r\n * - 'meta.home.title' → translation key → tries t('meta.home.title')\r\n * - 'cruxjs' → plain text → returns 'cruxjs'\r\n * - 'My Title' → plain text → returns 'My Title'\r\n *\r\n * @param value - The string to parse\r\n * @param defaultValue - Fallback if value is empty\r\n * @returns Translated value or original string\r\n */\r\n function parseValue(value: string | undefined, defaultValue: string = ''): string {\r\n if (!value) return defaultValue;\r\n\r\n // Check if value looks like a translation key (e.g., 'meta.home.title')\r\n // Key pattern: MUST contain at least one dot AND be lowercase/numeric/underscores/dots\r\n const isTranslationKey = value.includes('.') && /^[a-z0-9._]+$/.test(value);\r\n\r\n if (isTranslationKey) {\r\n try {\r\n const translated = t(value);\r\n // If translation returns a value, use it; otherwise fall back to original\r\n return translated || value;\r\n } catch {\r\n // If translation fails, return the original value\r\n return value;\r\n }\r\n }\r\n\r\n // Not a translation key format, return as direct text\r\n return value;\r\n }\r\n\r\n /**\r\n * Resolve meta tag values with smart translation detection\r\n * Uses parseValue() to automatically detect and translate\r\n */\r\n function resolveMetaValue(value: string | undefined, defaultValue: string = ''): string {\r\n return parseValue(value, defaultValue);\r\n }\r\n\r\n /**\r\n * Resolve keywords with smart translation detection\r\n * Uses parseValue() on each keyword to automatically detect and translate\r\n */\r\n function resolveKeywords(keywords: string[] | undefined): string {\r\n if (!keywords || keywords.length === 0) return '';\r\n\r\n const resolved = keywords\r\n .map(kw => parseValue(kw))\r\n .filter(Boolean);\r\n\r\n return resolved.join(', ');\r\n }\r\n\r\n /**\r\n * Generate page title with translation support\r\n *\r\n * Uses genPageTitle() from @minejs/i18n for RTL-aware titles\r\n * First parses the value to detect and translate if needed\r\n */\r\n function resolvePageTitle(title: string | undefined): string {\r\n if (!title) return 'Page';\r\n\r\n // First parse the value to detect translation keys\r\n const parsedTitle = parseValue(title);\r\n\r\n // Use genPageTitle for RTL-aware title generation\r\n try {\r\n return genPageTitle(parsedTitle);\r\n } catch {\r\n // Fallback: if genPageTitle fails, use parsed value\r\n return parsedTitle;\r\n }\r\n }\r\n\r\n /**\r\n * Generate page title with proper RTL handling\r\n *\r\n * @example\r\n * // English: \"Profile - MyApp\"\r\n * // Arabic: \"MyApp - الملف الشخصي\"\r\n */\r\n export function genPageTitle(val: string): string {\r\n const appName = t('app.name');\r\n return isRTL() ? `${appName} - ${val}` : `${val} - ${appName}`;\r\n }\r\n\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals and translation support\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n * - Translation support for all meta values\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n\r\n // Resolve translated values\r\n const title = resolvePageTitle(config.title);\r\n const description = resolveMetaValue(config.description, baseConfig.defaultDescription || 'A modern single-page application');\r\n const keywords = resolveKeywords(config.keywords || baseConfig.defaultKeywords);\r\n const expertise = resolveMetaValue(config.expertise, '');\r\n const experience = resolveMetaValue(config.experience, '');\r\n const authority = resolveMetaValue(config.authority, '');\r\n\r\n // Determine OG type based on content type\r\n const ogType = config.contentType === 'article' ? 'article' : 'website';\r\n\r\n return `<!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${title}</title>\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${expertise ? `<meta name=\"expertise\" content=\"${expertise}\" />` : ''}\r\n ${experience ? `<meta name=\"experience\" content=\"${experience}\" />` : ''}\r\n ${authority ? `<meta name=\"authority\" content=\"${authority}\" />` : ''}\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n')}\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n <!-- 📘 Open Graph Protocol (Social Media) -->\r\n <meta property=\"og:type\" content=\"${ogType}\" />\r\n <meta property=\"og:title\" content=\"${title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />\r\n <meta property=\"og:locale\" content=\"en_US\" />\r\n ${baseConfig.author ? `<meta property=\"og:site_name\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image:alt\" content=\"${title}\" />` : ''}\r\n `;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n * Handles translation keys in meta values\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n // Resolve translated values for structured data\r\n const title = resolvePageTitle(pageConfig.title);\r\n const description = resolveMetaValue(pageConfig.description, baseConfig.defaultDescription);\r\n const expertise = resolveMetaValue(pageConfig.expertise, '');\r\n const experience = resolveMetaValue(pageConfig.experience, '');\r\n const authority = resolveMetaValue(pageConfig.authority, '');\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': title,\r\n 'url': canonicalUrl,\r\n 'description': description,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...((expertise || experience || authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(expertise && { 'expertise': expertise }),\r\n ...(experience && { 'experience': experience }),\r\n ...(authority && { 'authority': authority })\r\n }\r\n })\r\n };\r\n\r\n const jsonStr = JSON.stringify(schema, null, 2)\r\n .split('\\n')\r\n .map(line => line ? ` ${line}` : line)\r\n .join('\\n');\r\n return `<script type=\"application/ld+json\">\\n${jsonStr}\\n</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/htmlFormatter.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];\r\n\r\n interface Token {\r\n type: 'tag' | 'text' | 'comment';\r\n value: string;\r\n }\r\n\r\n /**\r\n * Tokenize HTML into tags, text, and comments\r\n */\r\n function tokenize(html: string): Token[] {\r\n const tokens: Token[] = [];\r\n const regex = /(<[^>]+>)|([^<]+)/g;\r\n let match;\r\n\r\n while ((match = regex.exec(html)) !== null) {\r\n if (match[1]) {\r\n // It's a tag\r\n tokens.push({ type: 'tag', value: match[1] });\r\n } else if (match[2] && match[2].trim().length > 0) {\r\n // It's text content (not just whitespace)\r\n tokens.push({ type: 'text', value: match[2] });\r\n }\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Process tokens and apply indentation\r\n */\r\n function processTokens(tokens: Token[], indentSize: number = 4): string[] {\r\n const indent = ' '.repeat(indentSize);\r\n const output: string[] = [];\r\n let indentLevel = 0;\r\n\r\n // eslint-disable-next-line @typescript-eslint/prefer-for-of\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n const value = token.value.trim();\r\n\r\n // Decrease indent for closing tags BEFORE adding\r\n if (value.startsWith('</')) {\r\n indentLevel = Math.max(0, indentLevel - 1);\r\n }\r\n\r\n const padding = indent.repeat(indentLevel);\r\n\r\n // Skip empty text nodes\r\n if (token.type === 'text' && value.length === 0) {\r\n continue;\r\n }\r\n\r\n // Add to output\r\n if (token.type === 'text') {\r\n // Text nodes - check if previous line exists\r\n if (output.length > 0 && !output[output.length - 1].trim().endsWith('>')) {\r\n // Append to previous line if it doesn't end with tag\r\n output[output.length - 1] += value;\r\n } else {\r\n output.push(padding + value);\r\n }\r\n } else {\r\n // Tags and comments\r\n output.push(padding + value);\r\n }\r\n\r\n // Increase indent for OPENING tags ONLY (not closing, not void elements, not DOCTYPE/comments)\r\n if (value.startsWith('<') && !value.startsWith('</')) {\r\n // Skip DOCTYPE and comments - they don't increase indent\r\n if (value.startsWith('<!DOCTYPE') || value.startsWith('<!--')) {\r\n continue;\r\n }\r\n\r\n // Extract tag name\r\n const tagMatch = value.match(/<([A-Za-z][A-Za-z0-9\\\\-]*)/i);\r\n const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';\r\n\r\n // Check if it's a void/self-closing element\r\n const isVoidElement = VOID_ELEMENTS.includes(tagName);\r\n const isSelfClosing = value.endsWith('/>');\r\n\r\n // Only increase indent if it's NOT a void element AND NOT self-closing\r\n if (!isVoidElement && !isSelfClosing) {\r\n indentLevel++;\r\n }\r\n }\r\n }\r\n\r\n return output;\r\n }\r\n\r\n /**\r\n * Format HTML with proper indentation and cleanup\r\n *\r\n * - Tokenizes HTML into tags and content\r\n * - Applies proper indentation (4 spaces per level)\r\n * - Handles void elements correctly\r\n * - Preserves script and style tag content\r\n * - Maintains tag hierarchy\r\n *\r\n * @param html Raw HTML string\r\n * @param indentSize Number of spaces per indent level (default: 4)\r\n * @returns Formatted HTML with perfect indentation\r\n */\r\n export function formatHTML(html: string, indentSize: number = 4): string {\r\n // Remove existing whitespace and normalize\r\n const normalized = html\r\n .split('\\n')\r\n .map(line => line.trim())\r\n .filter(line => line.length > 0)\r\n .join('');\r\n\r\n // Tokenize the HTML\r\n const tokens = tokenize(normalized);\r\n\r\n // Process tokens with indentation\r\n const lines = processTokens(tokens, indentSize);\r\n\r\n // Join and return\r\n return lines.join('\\n');\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext, AppConfig } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n import { formatHTML } from './htmlFormatter';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate i18n meta tag for client injection\r\n *\r\n * The browser.tsx template will read this meta tag and inject\r\n * i18n config into the ClientManager config automatically.\r\n *\r\n * Convert server-side filesystem paths to browser-accessible URLs:\r\n * ./src/shared/static/dist/i18n → static/dist/i18n/\r\n *\r\n * Note: NO leading slash - i18n library needs to fetch as URL (not file path)\r\n * Always returns a meta tag - never empty!\r\n */\r\n function generateI18nMetaTag(i18nConfig: AppConfig['i18n']): string {\r\n // Provide defaults if config is missing or incomplete\r\n const config = {\r\n defaultLanguage: i18nConfig?.defaultLanguage || 'en',\r\n supportedLanguages: i18nConfig?.supportedLanguages || ['en'],\r\n basePath: i18nConfig?.basePath || 'i18n/',\r\n fileExtension: i18nConfig?.fileExtension || 'json'\r\n };\r\n\r\n // Convert server-side path to browser URL\r\n let browserPath = config.basePath;\r\n\r\n // Convert path separators to forward slashes\r\n browserPath = browserPath.replace(/\\\\/g, '/');\r\n\r\n // Handle server-side paths like ./src/shared/static/dist/i18n\r\n if (browserPath.includes('shared/static')) {\r\n // Extract everything after \"shared/static\" and prepend static/\r\n const afterStatic = browserPath.split('shared/static')[1] || '';\r\n browserPath = 'static' + afterStatic;\r\n } else if (browserPath.startsWith('./')) {\r\n // Remove leading ./\r\n browserPath = browserPath.slice(2);\r\n } else if (browserPath.startsWith('/')) {\r\n // Remove leading /\r\n browserPath = browserPath.slice(1);\r\n }\r\n\r\n // Ensure it ends with /\r\n if (!browserPath.endsWith('/')) {\r\n browserPath += '/';\r\n }\r\n\r\n const i18nJson = JSON.stringify({\r\n defaultLanguage: config.defaultLanguage,\r\n supportedLanguages: config.supportedLanguages,\r\n basePath: browserPath,\r\n fileExtension: config.fileExtension\r\n });\r\n\r\n return `<meta name=\"app-i18n\" content='${i18nJson}' />`;\r\n }\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - i18n meta tag (read by browser.tsx template)\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n *\r\n * Output is automatically formatted with proper indentation via formatHTML()\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => `<script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => `<link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n // Add i18n meta tag for browser.tsx to read\r\n const i18nMetaTag = generateI18nMetaTag(i18nConfig);\r\n\r\n // DEBUG: Log to see what's being generated\r\n if (!i18nMetaTag) {\r\n console.warn('[SPA] WARNING: i18n meta tag is empty!');\r\n }\r\n\r\n // Build raw HTML - GUARANTEE i18n meta tag is included!\r\n const rawHTML = `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n${generateSEOMetaTags(pageConfig, baseConfig)}\r\n${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n${i18nMetaTag}\r\n${styleTags}\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n\r\n // DEBUG: Check if i18n meta tag is in raw HTML\r\n if (!rawHTML.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag NOT in raw HTML!');\r\n }\r\n\r\n // Format the HTML with proper indentation\r\n const formatted = formatHTML(rawHTML);\r\n\r\n // DEBUG: Check if i18n meta tag made it through formatting\r\n if (!formatted.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag LOST during formatting!');\r\n }\r\n\r\n return formatted;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data and i18n meta tag\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig, i18nConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n // Get global i18n config (set by serverSPA plugin)\r\n export function getGlobalI18nConfig() {\r\n return (global as any).__cruxjs_i18n_config;\r\n }\r\n\r\n export function setGlobalI18nConfig(config: any) {\r\n (global as any).__cruxjs_i18n_config = config;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n *\r\n * Returns HTML for web requests and JSON for API requests\r\n * Automatically uses global i18n config (no need to pass it!)\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n // Use global i18n config - unified approach!\r\n const i18nConfig = getGlobalI18nConfig();\r\n console.log(`[Errors] Generating error page ${statusCode} with i18n:`, !!i18nConfig);\r\n const html = generateSPAHTML(errorConfig, baseConfig, i18nConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n console.log(`[Errors] No custom error page for ${statusCode}, returning fallback`);\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n *\r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n *\r\n * No need to pass i18nConfig - uses global!\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page, setGlobalI18nConfig } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * Features:\r\n * - Server-side rendering with full SEO support (meta tags, structured data)\r\n * - Automatic error page handling (404, 500, etc.)\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl : 'https://example.com',\r\n * clientEntry : './src/client/browser.tsx',\r\n * clientScriptPath : ['/static/dist/js/browser.js'],\r\n * clientStylePath : ['/static/dist/css/index.css'],\r\n * enableAutoNotFound : true,\r\n * pages: [\r\n * {\r\n * title : 'Home',\r\n * path : '/',\r\n * description : 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode : 404,\r\n * title : '404 - Not Found',\r\n * path : '/404',\r\n * description : 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig, appConfig?: any): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Store i18n config globally so ALL handlers can access it (unified!)\r\n setGlobalI18nConfig(appConfig?.i18n);\r\n const i18nConfig = appConfig?.i18n;\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config, i18nConfig));\r\n }\r\n\r\n // Create error handler function (uses global i18nConfig automatically!)\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -3,6 +3,14 @@ import { CruxPlugin } from '@cruxjs/base';
|
|
|
3
3
|
/**
|
|
4
4
|
* SEO Configuration for SPA routes
|
|
5
5
|
* Supports modern E-E-A-T signals and AI Search optimization
|
|
6
|
+
*
|
|
7
|
+
* Translation Support:
|
|
8
|
+
* - title: Use genPageTitle() for RTL-aware page titles
|
|
9
|
+
* - description, keywords, etc: Use t() for translations
|
|
10
|
+
*
|
|
11
|
+
* Meta tag values can be:
|
|
12
|
+
* - Direct string: 'My Title'
|
|
13
|
+
* - Translation key: 'my.translation.key'
|
|
6
14
|
*/
|
|
7
15
|
interface SPAPageConfig {
|
|
8
16
|
title: string;
|
|
@@ -23,6 +31,10 @@ interface SPAPageConfig {
|
|
|
23
31
|
/**
|
|
24
32
|
* Error Page Configuration
|
|
25
33
|
* Define custom error pages (404, 500, etc.)
|
|
34
|
+
*
|
|
35
|
+
* Supports same translation format as SPAPageConfig:
|
|
36
|
+
* - title: ['meta.error.title', 'Default Title']
|
|
37
|
+
* - description: ['meta.error.desc']
|
|
26
38
|
*/
|
|
27
39
|
interface ErrorPageConfig extends SPAPageConfig {
|
|
28
40
|
statusCode: number;
|
|
@@ -51,33 +63,37 @@ interface ServerSPAPluginConfig {
|
|
|
51
63
|
* Generates SPA routes with SEO/CEO metadata and structured data
|
|
52
64
|
* Error pages are handled via CruxJS error hooks
|
|
53
65
|
*
|
|
66
|
+
* Features:
|
|
67
|
+
* - Server-side rendering with full SEO support (meta tags, structured data)
|
|
68
|
+
* - Automatic error page handling (404, 500, etc.)
|
|
69
|
+
*
|
|
54
70
|
* @example
|
|
55
71
|
* ```typescript
|
|
56
72
|
* const spaPlugin = serverSPA({
|
|
57
|
-
* baseUrl: 'https://example.com',
|
|
58
|
-
* clientEntry: './src/client/browser.tsx',
|
|
59
|
-
* clientScriptPath: '/static/dist/js/
|
|
60
|
-
* clientStylePath: '/static/dist/css/
|
|
61
|
-
* enableAutoNotFound: true,
|
|
73
|
+
* baseUrl : 'https://example.com',
|
|
74
|
+
* clientEntry : './src/client/browser.tsx',
|
|
75
|
+
* clientScriptPath : ['/static/dist/js/browser.js'],
|
|
76
|
+
* clientStylePath : ['/static/dist/css/index.css'],
|
|
77
|
+
* enableAutoNotFound : true,
|
|
62
78
|
* pages: [
|
|
63
79
|
* {
|
|
64
|
-
* title: 'Home',
|
|
65
|
-
* path: '/',
|
|
66
|
-
* description: 'Welcome to our platform'
|
|
80
|
+
* title : 'Home',
|
|
81
|
+
* path : '/',
|
|
82
|
+
* description : 'Welcome to our platform'
|
|
67
83
|
* }
|
|
68
84
|
* ],
|
|
69
85
|
* errorPages: [
|
|
70
86
|
* {
|
|
71
|
-
* statusCode: 404,
|
|
72
|
-
* title: '404 - Not Found',
|
|
73
|
-
* path: '/404',
|
|
74
|
-
* description: 'Page not found'
|
|
87
|
+
* statusCode : 404,
|
|
88
|
+
* title : '404 - Not Found',
|
|
89
|
+
* path : '/404',
|
|
90
|
+
* description : 'Page not found'
|
|
75
91
|
* }
|
|
76
92
|
* ]
|
|
77
93
|
* });
|
|
78
94
|
* ```
|
|
79
95
|
*/
|
|
80
|
-
declare function serverSPA(config: ServerSPAPluginConfig): CruxPlugin & {
|
|
96
|
+
declare function serverSPA(config: ServerSPAPluginConfig, appConfig?: any): CruxPlugin & {
|
|
81
97
|
__spaErrorHandler?: any;
|
|
82
98
|
};
|
|
83
99
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ import { CruxPlugin } from '@cruxjs/base';
|
|
|
3
3
|
/**
|
|
4
4
|
* SEO Configuration for SPA routes
|
|
5
5
|
* Supports modern E-E-A-T signals and AI Search optimization
|
|
6
|
+
*
|
|
7
|
+
* Translation Support:
|
|
8
|
+
* - title: Use genPageTitle() for RTL-aware page titles
|
|
9
|
+
* - description, keywords, etc: Use t() for translations
|
|
10
|
+
*
|
|
11
|
+
* Meta tag values can be:
|
|
12
|
+
* - Direct string: 'My Title'
|
|
13
|
+
* - Translation key: 'my.translation.key'
|
|
6
14
|
*/
|
|
7
15
|
interface SPAPageConfig {
|
|
8
16
|
title: string;
|
|
@@ -23,6 +31,10 @@ interface SPAPageConfig {
|
|
|
23
31
|
/**
|
|
24
32
|
* Error Page Configuration
|
|
25
33
|
* Define custom error pages (404, 500, etc.)
|
|
34
|
+
*
|
|
35
|
+
* Supports same translation format as SPAPageConfig:
|
|
36
|
+
* - title: ['meta.error.title', 'Default Title']
|
|
37
|
+
* - description: ['meta.error.desc']
|
|
26
38
|
*/
|
|
27
39
|
interface ErrorPageConfig extends SPAPageConfig {
|
|
28
40
|
statusCode: number;
|
|
@@ -51,33 +63,37 @@ interface ServerSPAPluginConfig {
|
|
|
51
63
|
* Generates SPA routes with SEO/CEO metadata and structured data
|
|
52
64
|
* Error pages are handled via CruxJS error hooks
|
|
53
65
|
*
|
|
66
|
+
* Features:
|
|
67
|
+
* - Server-side rendering with full SEO support (meta tags, structured data)
|
|
68
|
+
* - Automatic error page handling (404, 500, etc.)
|
|
69
|
+
*
|
|
54
70
|
* @example
|
|
55
71
|
* ```typescript
|
|
56
72
|
* const spaPlugin = serverSPA({
|
|
57
|
-
* baseUrl: 'https://example.com',
|
|
58
|
-
* clientEntry: './src/client/browser.tsx',
|
|
59
|
-
* clientScriptPath: '/static/dist/js/
|
|
60
|
-
* clientStylePath: '/static/dist/css/
|
|
61
|
-
* enableAutoNotFound: true,
|
|
73
|
+
* baseUrl : 'https://example.com',
|
|
74
|
+
* clientEntry : './src/client/browser.tsx',
|
|
75
|
+
* clientScriptPath : ['/static/dist/js/browser.js'],
|
|
76
|
+
* clientStylePath : ['/static/dist/css/index.css'],
|
|
77
|
+
* enableAutoNotFound : true,
|
|
62
78
|
* pages: [
|
|
63
79
|
* {
|
|
64
|
-
* title: 'Home',
|
|
65
|
-
* path: '/',
|
|
66
|
-
* description: 'Welcome to our platform'
|
|
80
|
+
* title : 'Home',
|
|
81
|
+
* path : '/',
|
|
82
|
+
* description : 'Welcome to our platform'
|
|
67
83
|
* }
|
|
68
84
|
* ],
|
|
69
85
|
* errorPages: [
|
|
70
86
|
* {
|
|
71
|
-
* statusCode: 404,
|
|
72
|
-
* title: '404 - Not Found',
|
|
73
|
-
* path: '/404',
|
|
74
|
-
* description: 'Page not found'
|
|
87
|
+
* statusCode : 404,
|
|
88
|
+
* title : '404 - Not Found',
|
|
89
|
+
* path : '/404',
|
|
90
|
+
* description : 'Page not found'
|
|
75
91
|
* }
|
|
76
92
|
* ]
|
|
77
93
|
* });
|
|
78
94
|
* ```
|
|
79
95
|
*/
|
|
80
|
-
declare function serverSPA(config: ServerSPAPluginConfig): CruxPlugin & {
|
|
96
|
+
declare function serverSPA(config: ServerSPAPluginConfig, appConfig?: any): CruxPlugin & {
|
|
81
97
|
__spaErrorHandler?: any;
|
|
82
98
|
};
|
|
83
99
|
|
package/dist/index.js
CHANGED
|
@@ -1,48 +1,57 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`);return
|
|
1
|
+
import {t}from'@minejs/server';import {isRTL}from'@minejs/i18n';function h(e,t$1=""){if(!e)return t$1;if(e.includes(".")&&/^[a-z0-9._]+$/.test(e))try{return t(e)||e}catch{return e}return e}function u(e,t=""){return h(e,t)}function w(e){return !e||e.length===0?"":e.map(n=>h(n)).filter(Boolean).join(", ")}function P(e){if(!e)return "Page";let t=h(e);try{return v(t)}catch{return t}}function v(e){let t$1=t("app.name");return isRTL()?`${t$1} - ${e}`:`${e} - ${t$1}`}function y(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,r=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",a=P(e.title),i=u(e.description,t.defaultDescription||"A modern single-page application"),s=w(e.keywords||t.defaultKeywords),o=u(e.expertise,""),c=u(e.experience,""),l=u(e.authority,""),p=e.contentType==="article"?"article":"website";return `<!-- \u{1F50D} Core SEO Meta Tags -->
|
|
2
|
+
<meta charset="UTF-8" />
|
|
3
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4
|
+
<title>${a}</title>
|
|
5
|
+
<meta name="description" content="${i}" />
|
|
6
|
+
${s?`<meta name="keywords" content="${s}" />`:""}
|
|
7
|
+
<meta name="robots" content="${r}" />
|
|
8
|
+
<meta name="language" content="en" />
|
|
9
|
+
<meta http-equiv="content-language" content="en-us" />
|
|
10
|
+
<!-- \u{1F465} E-E-A-T Signals for AI Search -->
|
|
11
|
+
${t.author?`<meta name="author" content="${t.author}" />`:""}
|
|
12
|
+
${o?`<meta name="expertise" content="${o}" />`:""}
|
|
13
|
+
${c?`<meta name="experience" content="${c}" />`:""}
|
|
14
|
+
${l?`<meta name="authority" content="${l}" />`:""}
|
|
15
|
+
<!-- \u{1F4F1} Mobile & Performance -->
|
|
16
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
17
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
18
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
19
|
+
<meta name="theme-color" content="#000000" />
|
|
20
|
+
<!-- \u{1F517} Canonical & Prefetch -->
|
|
21
|
+
<link rel="canonical" href="${n}" />
|
|
22
|
+
${(e.clientScriptPath||t.clientScriptPath)?.map(g=>`<link rel="prefetch" href="${g}" />`).join(`
|
|
23
|
+
`)}
|
|
24
|
+
<!-- \u26A1 Performance & Security -->
|
|
25
|
+
<meta name="format-detection" content="telephone=no" />
|
|
26
|
+
<meta http-equiv="x-ua-compatible" content="IE=edge" />
|
|
27
|
+
<!-- \u{1F4D8} Open Graph Protocol (Social Media) -->
|
|
28
|
+
<meta property="og:type" content="${p}" />
|
|
29
|
+
<meta property="og:title" content="${a}" />
|
|
30
|
+
<meta property="og:description" content="${i}" />
|
|
31
|
+
<meta property="og:url" content="${n}" />
|
|
32
|
+
<meta property="og:locale" content="en_US" />
|
|
33
|
+
${t.author?`<meta property="og:site_name" content="${t.author}" />`:""}
|
|
34
|
+
${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
|
|
35
|
+
${e.ogImage?`<meta property="og:image:alt" content="${a}" />`:""}
|
|
36
|
+
`}function S(e,t,n="WebPage"){let r=e.canonical||`${t.baseUrl}${e.path}`,a=P(e.title),i=u(e.description,t.defaultDescription),s=u(e.expertise,""),o=u(e.experience,""),c=u(e.authority,""),l={"@context":"https://schema.org","@type":n,name:a,url:r,description:i,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(s||o||c)&&{creator:{"@type":"Person",name:t.author||"Unknown",...s&&{expertise:s},...o&&{experience:o},...c&&{authority:c}}}};return `<script type="application/ld+json">
|
|
37
|
+
${JSON.stringify(l,null,2).split(`
|
|
38
|
+
`).map(g=>g&&` ${g}`).join(`
|
|
39
|
+
`)}
|
|
40
|
+
</script>`}var R=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"];function k(e){let t=[],n=/(<[^>]+>)|([^<]+)/g,r;for(;(r=n.exec(e))!==null;)r[1]?t.push({type:"tag",value:r[1]}):r[2]&&r[2].trim().length>0&&t.push({type:"text",value:r[2]});return t}function L(e,t=4){let n=" ".repeat(t),r=[],a=0;for(let i=0;i<e.length;i++){let s=e[i],o=s.value.trim();o.startsWith("</")&&(a=Math.max(0,a-1));let c=n.repeat(a);if(!(s.type==="text"&&o.length===0)&&(s.type==="text"&&r.length>0&&!r[r.length-1].trim().endsWith(">")?r[r.length-1]+=o:r.push(c+o),o.startsWith("<")&&!o.startsWith("</"))){if(o.startsWith("<!DOCTYPE")||o.startsWith("<!--"))continue;let l=o.match(/<([A-Za-z][A-Za-z0-9\\-]*)/i),p=l?l[1].toLowerCase():"",g=R.includes(p),T=o.endsWith("/>");!g&&!T&&a++;}}return r}function x(e,t=4){let n=e.split(`
|
|
41
|
+
`).map(i=>i.trim()).filter(i=>i.length>0).join(""),r=k(n);return L(r,t).join(`
|
|
42
|
+
`)}function M(e){let t={defaultLanguage:e?.defaultLanguage||"en",supportedLanguages:e?.supportedLanguages||["en"],basePath:e?.basePath||"i18n/",fileExtension:e?.fileExtension||"json"},n=t.basePath;return n=n.replace(/\\/g,"/"),n.includes("shared/static")?n="static"+(n.split("shared/static")[1]||""):n.startsWith("./")?n=n.slice(2):n.startsWith("/")&&(n=n.slice(1)),n.endsWith("/")||(n+="/"),`<meta name="app-i18n" content='${JSON.stringify({defaultLanguage:t.defaultLanguage,supportedLanguages:t.supportedLanguages,basePath:n,fileExtension:t.fileExtension})}' />`}function d(e,t,n){let r=e.clientScriptPath||t.clientScriptPath,a=e.clientStylePath||t.clientStylePath||[],i=r.map(p=>`<script type="module" src="${p}"></script>`).join(`
|
|
43
|
+
`),s=a.map(p=>`<link rel="stylesheet" href="${p}" />`).join(`
|
|
44
|
+
`),o=M(n);o||console.warn("[SPA] WARNING: i18n meta tag is empty!");let c=`<!DOCTYPE html>
|
|
37
45
|
<html lang="en">
|
|
38
46
|
<head>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
${y(e,t)}
|
|
48
|
+
${S(e,t,e.contentType==="article"?"Article":"WebPage")}
|
|
49
|
+
${o}
|
|
50
|
+
${s}
|
|
42
51
|
</head>
|
|
43
52
|
<body>
|
|
44
|
-
|
|
45
|
-
${
|
|
53
|
+
<div id="app"></div>
|
|
54
|
+
${i}
|
|
46
55
|
</body>
|
|
47
|
-
</html
|
|
56
|
+
</html>`;c.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag NOT in raw HTML!");let l=x(c);return l.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag LOST during formatting!"),l}function m(e,t,n){return {method:"GET",path:e.path,handler:r=>{let a=d(e,t,n);return r.html(a)}}}function _(){return global.__cruxjs_i18n_config}function A(e){global.__cruxjs_i18n_config=e;}function j(e,t,n,r){if(r.startsWith("/api/"))return {status:e,headers:{"Content-Type":"application/json"},body:JSON.stringify({error:`Error ${e}`})};if(t.has(e)){let a=t.get(e),i=_();console.log(`[Errors] Generating error page ${e} with i18n:`,!!i);let s=d(a,n,i);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:s}}return console.log(`[Errors] No custom error page for ${e}, returning fallback`),{status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function $(e,t){return (n,r)=>{let a=j(n,e,t,r);return new Response(a.body,{status:a.status,headers:a.headers})}}function b(){return {statusCode:404,title:"404 - Page Not Found",path:"/404",description:"The page you are looking for could not be found.",keywords:["404","not found","error"],robots:"noindex, nofollow"}}function K(e,t){let n=[],r=new Map;A(t?.i18n);let a=t?.i18n;if(e.pages&&e.pages.length>0)for(let o of e.pages)n.push(m(o,e,a));if(e.errorPages&&e.errorPages.length>0)for(let o of e.errorPages)r.set(o.statusCode,o),n.push(m(o,e,a));if(e.enableAutoNotFound&&!r.has(404)){let o=b();r.set(404,o),n.push(m(o,e,a));}let i=$(r,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:n,__spaErrorHandler:i,onRegister:async o=>{if(console.log(`[SPA Plugin] Registered ${n.length} SPA routes`),r.size>0){let c=Array.from(r.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${c}`);}},onAwake:async o=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async o=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async o=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}export{K as serverSPA};//# sourceMappingURL=index.js.map
|
|
48
57
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/seo.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["generateSEOMetaTags","config","baseConfig","canonicalUrl","robots","description","keywords","script","generateStructuredData","pageConfig","contentType","schema","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","createSPARoute","c","html","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.js","sourcesContent":["// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n const description = config.description || baseConfig.defaultDescription || 'A modern single-page application';\r\n const keywords = (config.keywords || baseConfig.defaultKeywords || []).join(', ');\r\n\r\n return `\r\n <!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.expertise ? `<meta name=\"expertise\" content=\"${config.expertise}\" />` : ''}\r\n ${config.experience ? `<meta name=\"experience\" content=\"${config.experience}\" />` : ''}\r\n ${config.authority ? `<meta name=\"authority\" content=\"${config.authority}\" />` : ''}\r\n\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n ')}\r\n\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n <meta property=\"og:title\" content=\"${config.title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />`;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': pageConfig.title,\r\n 'url': canonicalUrl,\r\n 'description': pageConfig.description || baseConfig.defaultDescription,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...(pageConfig.expertise || pageConfig.experience || pageConfig.authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(pageConfig.expertise && { 'expertise': pageConfig.expertise }),\r\n ...(pageConfig.experience && { 'experience': pageConfig.experience }),\r\n ...(pageConfig.authority && { 'authority': pageConfig.authority })\r\n }\r\n }\r\n };\r\n\r\n return `<script type=\"application/ld+json\">${JSON.stringify(schema, null, 2)}</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => ` <script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => ` <link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n return `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n ${generateSEOMetaTags(pageConfig, baseConfig)}\r\n ${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n ${styleTags ? styleTags : ''}\r\n</head>\r\n<body>\r\n <div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n * \r\n * Returns HTML for web requests and JSON for API requests\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n const html = generateSPAHTML(errorConfig, baseConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n * \r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl: 'https://example.com',\r\n * clientEntry: './src/client/browser.tsx',\r\n * clientScriptPath: '/static/dist/js/min.js',\r\n * clientStylePath: '/static/dist/css/min.css',\r\n * enableAutoNotFound: true, // Auto-handle 404s\r\n * pages: [\r\n * {\r\n * title: 'Home',\r\n * path: '/',\r\n * description: 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode: 404,\r\n * title: '404 - Not Found',\r\n * path: '/404',\r\n * description: 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config));\r\n }\r\n\r\n // Create error handler function\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/seo.ts","../src/utils/htmlFormatter.ts","../src/utils/spa.ts","../src/utils/errors.ts","../src/index.ts"],"names":["parseValue","value","defaultValue","t","resolveMetaValue","resolveKeywords","keywords","kw","resolvePageTitle","title","parsedTitle","genPageTitle","val","appName","isRTL","generateSEOMetaTags","config","baseConfig","canonicalUrl","robots","description","expertise","experience","authority","ogType","script","generateStructuredData","pageConfig","contentType","schema","line","VOID_ELEMENTS","tokenize","html","tokens","regex","match","processTokens","indentSize","indent","output","indentLevel","token","padding","tagMatch","tagName","isVoidElement","isSelfClosing","formatHTML","normalized","generateI18nMetaTag","i18nConfig","browserPath","generateSPAHTML","clientScripts","clientStyles","scriptTags","styleTags","style","i18nMetaTag","rawHTML","formatted","createSPARoute","c","getGlobalI18nConfig","setGlobalI18nConfig","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","appConfig","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"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","file":"index.js","sourcesContent":["// src/utils/seo.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { t } from '@minejs/server';\r\n import { isRTL } from '@minejs/i18n';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Smart parser for string values\r\n *\r\n * Intelligently detects if a string is a translation key or plain text:\r\n * - Translation key format (e.g., 'meta.home.title'): must contain dots, attempts translation via t(key)\r\n * - Plain text (e.g., 'My Title', 'cruxjs', 'framework'): returns as-is\r\n *\r\n * Key format: MUST contain at least one dot (.) to be considered a translation key\r\n * Examples:\r\n * - 'meta.home.title' → translation key → tries t('meta.home.title')\r\n * - 'cruxjs' → plain text → returns 'cruxjs'\r\n * - 'My Title' → plain text → returns 'My Title'\r\n *\r\n * @param value - The string to parse\r\n * @param defaultValue - Fallback if value is empty\r\n * @returns Translated value or original string\r\n */\r\n function parseValue(value: string | undefined, defaultValue: string = ''): string {\r\n if (!value) return defaultValue;\r\n\r\n // Check if value looks like a translation key (e.g., 'meta.home.title')\r\n // Key pattern: MUST contain at least one dot AND be lowercase/numeric/underscores/dots\r\n const isTranslationKey = value.includes('.') && /^[a-z0-9._]+$/.test(value);\r\n\r\n if (isTranslationKey) {\r\n try {\r\n const translated = t(value);\r\n // If translation returns a value, use it; otherwise fall back to original\r\n return translated || value;\r\n } catch {\r\n // If translation fails, return the original value\r\n return value;\r\n }\r\n }\r\n\r\n // Not a translation key format, return as direct text\r\n return value;\r\n }\r\n\r\n /**\r\n * Resolve meta tag values with smart translation detection\r\n * Uses parseValue() to automatically detect and translate\r\n */\r\n function resolveMetaValue(value: string | undefined, defaultValue: string = ''): string {\r\n return parseValue(value, defaultValue);\r\n }\r\n\r\n /**\r\n * Resolve keywords with smart translation detection\r\n * Uses parseValue() on each keyword to automatically detect and translate\r\n */\r\n function resolveKeywords(keywords: string[] | undefined): string {\r\n if (!keywords || keywords.length === 0) return '';\r\n\r\n const resolved = keywords\r\n .map(kw => parseValue(kw))\r\n .filter(Boolean);\r\n\r\n return resolved.join(', ');\r\n }\r\n\r\n /**\r\n * Generate page title with translation support\r\n *\r\n * Uses genPageTitle() from @minejs/i18n for RTL-aware titles\r\n * First parses the value to detect and translate if needed\r\n */\r\n function resolvePageTitle(title: string | undefined): string {\r\n if (!title) return 'Page';\r\n\r\n // First parse the value to detect translation keys\r\n const parsedTitle = parseValue(title);\r\n\r\n // Use genPageTitle for RTL-aware title generation\r\n try {\r\n return genPageTitle(parsedTitle);\r\n } catch {\r\n // Fallback: if genPageTitle fails, use parsed value\r\n return parsedTitle;\r\n }\r\n }\r\n\r\n /**\r\n * Generate page title with proper RTL handling\r\n *\r\n * @example\r\n * // English: \"Profile - MyApp\"\r\n * // Arabic: \"MyApp - الملف الشخصي\"\r\n */\r\n export function genPageTitle(val: string): string {\r\n const appName = t('app.name');\r\n return isRTL() ? `${appName} - ${val}` : `${val} - ${appName}`;\r\n }\r\n\r\n\r\n /**\r\n * Generate SEO Meta Tags with E-E-A-T signals and translation support\r\n *\r\n * Includes:\r\n * - Core SEO metadata (charset, viewport, description, keywords, robots)\r\n * - E-E-A-T signals (expertise, experience, authority)\r\n * - Mobile optimization (web app capable, status bar style)\r\n * - Performance & security (prefetch, x-ua-compatible)\r\n * - Open Graph protocol tags\r\n * - Translation support for all meta values\r\n */\r\n export function generateSEOMetaTags(\r\n config: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const canonicalUrl = config.canonical || `${baseConfig.baseUrl}${config.path}`;\r\n const robots = config.robots || baseConfig.defaultRobots || 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1';\r\n\r\n // Resolve translated values\r\n const title = resolvePageTitle(config.title);\r\n const description = resolveMetaValue(config.description, baseConfig.defaultDescription || 'A modern single-page application');\r\n const keywords = resolveKeywords(config.keywords || baseConfig.defaultKeywords);\r\n const expertise = resolveMetaValue(config.expertise, '');\r\n const experience = resolveMetaValue(config.experience, '');\r\n const authority = resolveMetaValue(config.authority, '');\r\n\r\n // Determine OG type based on content type\r\n const ogType = config.contentType === 'article' ? 'article' : 'website';\r\n\r\n return `<!-- 🔍 Core SEO Meta Tags -->\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${title}</title>\r\n <meta name=\"description\" content=\"${description}\" />\r\n ${keywords ? `<meta name=\"keywords\" content=\"${keywords}\" />` : ''}\r\n <meta name=\"robots\" content=\"${robots}\" />\r\n <meta name=\"language\" content=\"en\" />\r\n <meta http-equiv=\"content-language\" content=\"en-us\" />\r\n <!-- 👥 E-E-A-T Signals for AI Search -->\r\n ${baseConfig.author ? `<meta name=\"author\" content=\"${baseConfig.author}\" />` : ''}\r\n ${expertise ? `<meta name=\"expertise\" content=\"${expertise}\" />` : ''}\r\n ${experience ? `<meta name=\"experience\" content=\"${experience}\" />` : ''}\r\n ${authority ? `<meta name=\"authority\" content=\"${authority}\" />` : ''}\r\n <!-- 📱 Mobile & Performance -->\r\n <meta name=\"mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\r\n <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\" />\r\n <meta name=\"theme-color\" content=\"#000000\" />\r\n <!-- 🔗 Canonical & Prefetch -->\r\n <link rel=\"canonical\" href=\"${canonicalUrl}\" />\r\n ${(config.clientScriptPath || baseConfig.clientScriptPath)?.map(script => `<link rel=\"prefetch\" href=\"${script}\" />`).join('\\n')}\r\n <!-- ⚡ Performance & Security -->\r\n <meta name=\"format-detection\" content=\"telephone=no\" />\r\n <meta http-equiv=\"x-ua-compatible\" content=\"IE=edge\" />\r\n <!-- 📘 Open Graph Protocol (Social Media) -->\r\n <meta property=\"og:type\" content=\"${ogType}\" />\r\n <meta property=\"og:title\" content=\"${title}\" />\r\n <meta property=\"og:description\" content=\"${description}\" />\r\n <meta property=\"og:url\" content=\"${canonicalUrl}\" />\r\n <meta property=\"og:locale\" content=\"en_US\" />\r\n ${baseConfig.author ? `<meta property=\"og:site_name\" content=\"${baseConfig.author}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image\" content=\"${config.ogImage}\" />` : ''}\r\n ${config.ogImage ? `<meta property=\"og:image:alt\" content=\"${title}\" />` : ''}\r\n `;\r\n }\r\n\r\n /**\r\n * Generate JSON-LD Structured Data\r\n *\r\n * Creates schema.org compatible structured data for:\r\n * - Rich snippets in search results\r\n * - AI overviews and knowledge panels\r\n * - Better indexing and SEO\r\n *\r\n * Supports multiple content types: WebPage, Article, Product, Service, etc.\r\n * Handles translation keys in meta values\r\n */\r\n export function generateStructuredData(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n contentType: string = 'WebPage'\r\n ): string {\r\n const canonicalUrl = pageConfig.canonical || `${baseConfig.baseUrl}${pageConfig.path}`;\r\n\r\n // Resolve translated values for structured data\r\n const title = resolvePageTitle(pageConfig.title);\r\n const description = resolveMetaValue(pageConfig.description, baseConfig.defaultDescription);\r\n const expertise = resolveMetaValue(pageConfig.expertise, '');\r\n const experience = resolveMetaValue(pageConfig.experience, '');\r\n const authority = resolveMetaValue(pageConfig.authority, '');\r\n\r\n const schema = {\r\n '@context': 'https://schema.org',\r\n '@type': contentType,\r\n 'name': title,\r\n 'url': canonicalUrl,\r\n 'description': description,\r\n 'inLanguage': 'en',\r\n ...(pageConfig.contentType && { 'genre': pageConfig.contentType }),\r\n ...(baseConfig.author && {\r\n 'author': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author,\r\n ...(baseConfig.authorUrl && { 'url': baseConfig.authorUrl })\r\n }\r\n }),\r\n ...((expertise || experience || authority) && {\r\n 'creator': {\r\n '@type': 'Person',\r\n 'name': baseConfig.author || 'Unknown',\r\n ...(expertise && { 'expertise': expertise }),\r\n ...(experience && { 'experience': experience }),\r\n ...(authority && { 'authority': authority })\r\n }\r\n })\r\n };\r\n\r\n const jsonStr = JSON.stringify(schema, null, 2)\r\n .split('\\n')\r\n .map(line => line ? ` ${line}` : line)\r\n .join('\\n');\r\n return `<script type=\"application/ld+json\">\\n${jsonStr}\\n</script>`;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/htmlFormatter.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];\r\n\r\n interface Token {\r\n type: 'tag' | 'text' | 'comment';\r\n value: string;\r\n }\r\n\r\n /**\r\n * Tokenize HTML into tags, text, and comments\r\n */\r\n function tokenize(html: string): Token[] {\r\n const tokens: Token[] = [];\r\n const regex = /(<[^>]+>)|([^<]+)/g;\r\n let match;\r\n\r\n while ((match = regex.exec(html)) !== null) {\r\n if (match[1]) {\r\n // It's a tag\r\n tokens.push({ type: 'tag', value: match[1] });\r\n } else if (match[2] && match[2].trim().length > 0) {\r\n // It's text content (not just whitespace)\r\n tokens.push({ type: 'text', value: match[2] });\r\n }\r\n }\r\n\r\n return tokens;\r\n }\r\n\r\n /**\r\n * Process tokens and apply indentation\r\n */\r\n function processTokens(tokens: Token[], indentSize: number = 4): string[] {\r\n const indent = ' '.repeat(indentSize);\r\n const output: string[] = [];\r\n let indentLevel = 0;\r\n\r\n // eslint-disable-next-line @typescript-eslint/prefer-for-of\r\n for (let i = 0; i < tokens.length; i++) {\r\n const token = tokens[i];\r\n const value = token.value.trim();\r\n\r\n // Decrease indent for closing tags BEFORE adding\r\n if (value.startsWith('</')) {\r\n indentLevel = Math.max(0, indentLevel - 1);\r\n }\r\n\r\n const padding = indent.repeat(indentLevel);\r\n\r\n // Skip empty text nodes\r\n if (token.type === 'text' && value.length === 0) {\r\n continue;\r\n }\r\n\r\n // Add to output\r\n if (token.type === 'text') {\r\n // Text nodes - check if previous line exists\r\n if (output.length > 0 && !output[output.length - 1].trim().endsWith('>')) {\r\n // Append to previous line if it doesn't end with tag\r\n output[output.length - 1] += value;\r\n } else {\r\n output.push(padding + value);\r\n }\r\n } else {\r\n // Tags and comments\r\n output.push(padding + value);\r\n }\r\n\r\n // Increase indent for OPENING tags ONLY (not closing, not void elements, not DOCTYPE/comments)\r\n if (value.startsWith('<') && !value.startsWith('</')) {\r\n // Skip DOCTYPE and comments - they don't increase indent\r\n if (value.startsWith('<!DOCTYPE') || value.startsWith('<!--')) {\r\n continue;\r\n }\r\n\r\n // Extract tag name\r\n const tagMatch = value.match(/<([A-Za-z][A-Za-z0-9\\\\-]*)/i);\r\n const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';\r\n\r\n // Check if it's a void/self-closing element\r\n const isVoidElement = VOID_ELEMENTS.includes(tagName);\r\n const isSelfClosing = value.endsWith('/>');\r\n\r\n // Only increase indent if it's NOT a void element AND NOT self-closing\r\n if (!isVoidElement && !isSelfClosing) {\r\n indentLevel++;\r\n }\r\n }\r\n }\r\n\r\n return output;\r\n }\r\n\r\n /**\r\n * Format HTML with proper indentation and cleanup\r\n *\r\n * - Tokenizes HTML into tags and content\r\n * - Applies proper indentation (4 spaces per level)\r\n * - Handles void elements correctly\r\n * - Preserves script and style tag content\r\n * - Maintains tag hierarchy\r\n *\r\n * @param html Raw HTML string\r\n * @param indentSize Number of spaces per indent level (default: 4)\r\n * @returns Formatted HTML with perfect indentation\r\n */\r\n export function formatHTML(html: string, indentSize: number = 4): string {\r\n // Remove existing whitespace and normalize\r\n const normalized = html\r\n .split('\\n')\r\n .map(line => line.trim())\r\n .filter(line => line.length > 0)\r\n .join('');\r\n\r\n // Tokenize the HTML\r\n const tokens = tokenize(normalized);\r\n\r\n // Process tokens with indentation\r\n const lines = processTokens(tokens, indentSize);\r\n\r\n // Join and return\r\n return lines.join('\\n');\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","// src/utils/spa.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { RouteDefinition, AppContext, AppConfig } from '@cruxjs/base';\r\n import type { SPAPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSEOMetaTags, generateStructuredData } from './seo';\r\n import { formatHTML } from './htmlFormatter';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Generate i18n meta tag for client injection\r\n *\r\n * The browser.tsx template will read this meta tag and inject\r\n * i18n config into the ClientManager config automatically.\r\n *\r\n * Convert server-side filesystem paths to browser-accessible URLs:\r\n * ./src/shared/static/dist/i18n → static/dist/i18n/\r\n *\r\n * Note: NO leading slash - i18n library needs to fetch as URL (not file path)\r\n * Always returns a meta tag - never empty!\r\n */\r\n function generateI18nMetaTag(i18nConfig: AppConfig['i18n']): string {\r\n // Provide defaults if config is missing or incomplete\r\n const config = {\r\n defaultLanguage: i18nConfig?.defaultLanguage || 'en',\r\n supportedLanguages: i18nConfig?.supportedLanguages || ['en'],\r\n basePath: i18nConfig?.basePath || 'i18n/',\r\n fileExtension: i18nConfig?.fileExtension || 'json'\r\n };\r\n\r\n // Convert server-side path to browser URL\r\n let browserPath = config.basePath;\r\n\r\n // Convert path separators to forward slashes\r\n browserPath = browserPath.replace(/\\\\/g, '/');\r\n\r\n // Handle server-side paths like ./src/shared/static/dist/i18n\r\n if (browserPath.includes('shared/static')) {\r\n // Extract everything after \"shared/static\" and prepend static/\r\n const afterStatic = browserPath.split('shared/static')[1] || '';\r\n browserPath = 'static' + afterStatic;\r\n } else if (browserPath.startsWith('./')) {\r\n // Remove leading ./\r\n browserPath = browserPath.slice(2);\r\n } else if (browserPath.startsWith('/')) {\r\n // Remove leading /\r\n browserPath = browserPath.slice(1);\r\n }\r\n\r\n // Ensure it ends with /\r\n if (!browserPath.endsWith('/')) {\r\n browserPath += '/';\r\n }\r\n\r\n const i18nJson = JSON.stringify({\r\n defaultLanguage: config.defaultLanguage,\r\n supportedLanguages: config.supportedLanguages,\r\n basePath: browserPath,\r\n fileExtension: config.fileExtension\r\n });\r\n\r\n return `<meta name=\"app-i18n\" content='${i18nJson}' />`;\r\n }\r\n\r\n /**\r\n * Generate SPA HTML shell with SEO metadata\r\n *\r\n * Creates a complete HTML document with:\r\n * - SEO meta tags\r\n * - i18n meta tag (read by browser.tsx template)\r\n * - Structured data (JSON-LD)\r\n * - App mount point (#app)\r\n * - Client-side JavaScript and style entry points\r\n *\r\n * Output is automatically formatted with proper indentation via formatHTML()\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): string {\r\n const clientScripts = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\r\n const clientStyles = pageConfig.clientStylePath || baseConfig.clientStylePath || [];\r\n\r\n const scriptTags = clientScripts\r\n .map(script => `<script type=\"module\" src=\"${script}\"></script>`)\r\n .join('\\n');\r\n\r\n const styleTags = clientStyles\r\n .map(style => `<link rel=\"stylesheet\" href=\"${style}\" />`)\r\n .join('\\n');\r\n\r\n // Add i18n meta tag for browser.tsx to read\r\n const i18nMetaTag = generateI18nMetaTag(i18nConfig);\r\n\r\n // DEBUG: Log to see what's being generated\r\n if (!i18nMetaTag) {\r\n console.warn('[SPA] WARNING: i18n meta tag is empty!');\r\n }\r\n\r\n // Build raw HTML - GUARANTEE i18n meta tag is included!\r\n const rawHTML = `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n${generateSEOMetaTags(pageConfig, baseConfig)}\r\n${generateStructuredData(pageConfig, baseConfig, pageConfig.contentType === 'article' ? 'Article' : 'WebPage')}\r\n${i18nMetaTag}\r\n${styleTags}\r\n</head>\r\n<body>\r\n<div id=\"app\"></div>\r\n${scriptTags}\r\n</body>\r\n</html>`;\r\n\r\n // DEBUG: Check if i18n meta tag is in raw HTML\r\n if (!rawHTML.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag NOT in raw HTML!');\r\n }\r\n\r\n // Format the HTML with proper indentation\r\n const formatted = formatHTML(rawHTML);\r\n\r\n // DEBUG: Check if i18n meta tag made it through formatting\r\n if (!formatted.includes('app-i18n')) {\r\n console.error('[SPA] ERROR: i18n meta tag LOST during formatting!');\r\n }\r\n\r\n return formatted;\r\n }\r\n\r\n /**\r\n * Create SPA route definition for a page\r\n *\r\n * Generates a RouteDefinition that handles GET requests\r\n * and returns the full SPA HTML shell with SEO data and i18n meta tag\r\n */\r\n export function createSPARoute(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig,\r\n i18nConfig?: AppConfig['i18n']\r\n ): RouteDefinition {\r\n return {\r\n method: 'GET',\r\n path: pageConfig.path,\r\n handler: (c: AppContext) => {\r\n const html = generateSPAHTML(pageConfig, baseConfig, i18nConfig);\r\n return c.html(html);\r\n }\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/utils/errors.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type { ErrorPageConfig, ServerSPAPluginConfig } from '../types';\r\n import { generateSPAHTML } from './spa';\r\n\r\n // Get global i18n config (set by serverSPA plugin)\r\n export function getGlobalI18nConfig() {\r\n return (global as any).__cruxjs_i18n_config;\r\n }\r\n\r\n export function setGlobalI18nConfig(config: any) {\r\n (global as any).__cruxjs_i18n_config = config;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ TYPE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Error response definition\r\n */\r\n interface ErrorResponse {\r\n status: number;\r\n headers: Record<string, string>;\r\n body: string;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ UTIL ════════════════════════════════════════╗\r\n\r\n /**\r\n * Build error response with appropriate content type\r\n *\r\n * Returns HTML for web requests and JSON for API requests\r\n * Automatically uses global i18n config (no need to pass it!)\r\n */\r\n export function buildErrorResponse(\r\n statusCode: number,\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig,\r\n path: string\r\n ): ErrorResponse {\r\n // API requests get JSON responses\r\n if (path.startsWith('/api/')) {\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ error: `Error ${statusCode}` })\r\n };\r\n }\r\n\r\n // Try to find custom error page\r\n if (errorPageMap.has(statusCode)) {\r\n const errorConfig = errorPageMap.get(statusCode)!;\r\n // Use global i18n config - unified approach!\r\n const i18nConfig = getGlobalI18nConfig();\r\n console.log(`[Errors] Generating error page ${statusCode} with i18n:`, !!i18nConfig);\r\n const html = generateSPAHTML(errorConfig, baseConfig, i18nConfig);\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\r\n body: html\r\n };\r\n }\r\n\r\n console.log(`[Errors] No custom error page for ${statusCode}, returning fallback`);\r\n // Fallback: plain text error\r\n return {\r\n status: statusCode,\r\n headers: { 'Content-Type': 'text/plain' },\r\n body: `Error ${statusCode}`\r\n };\r\n }\r\n\r\n /**\r\n * Create error handler function for CruxJS\r\n *\r\n * Handles:\r\n * - 404 Not Found pages (with auto-generation support)\r\n * - Custom error pages by status code\r\n * - API vs web request differentiation\r\n * - Fallback error responses\r\n *\r\n * No need to pass i18nConfig - uses global!\r\n */\r\n export function createErrorHandler(\r\n errorPageMap: Map<number, ErrorPageConfig>,\r\n baseConfig: ServerSPAPluginConfig\r\n ): (statusCode: number, path: string) => Response {\r\n return (statusCode: number, path: string) => {\r\n const errorResponse = buildErrorResponse(statusCode, errorPageMap, baseConfig, path);\r\n return new Response(errorResponse.body, {\r\n status: errorResponse.status,\r\n headers: errorResponse.headers\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Create default 404 error page config\r\n */\r\n export function createDefault404Page(): ErrorPageConfig {\r\n return {\r\n statusCode: 404,\r\n title: '404 - Page Not Found',\r\n path: '/404',\r\n description: 'The page you are looking for could not be found.',\r\n keywords: ['404', 'not found', 'error'],\r\n robots: 'noindex, nofollow'\r\n };\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n/* eslint-disable @typescript-eslint/no-explicit-any */\r\n// src/index.ts\r\n//\r\n// Made with ❤️ by Maysara.\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ PACK ════════════════════════════════════════╗\r\n\r\n import type {\r\n CruxPlugin,\r\n AppInstance\r\n } from '@cruxjs/base';\r\n\r\n import * as types from './types';\r\n import { createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page, setGlobalI18nConfig } from './utils/errors';\r\n\r\n export type * from './types';\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝\r\n\r\n\r\n\r\n// ╔════════════════════════════════════════ CORE ════════════════════════════════════════╗\r\n\r\n /**\r\n * Create Server SPA Plugin\r\n *\r\n * Generates SPA routes with SEO/CEO metadata and structured data\r\n * Error pages are handled via CruxJS error hooks\r\n *\r\n * Features:\r\n * - Server-side rendering with full SEO support (meta tags, structured data)\r\n * - Automatic error page handling (404, 500, etc.)\r\n *\r\n * @example\r\n * ```typescript\r\n * const spaPlugin = serverSPA({\r\n * baseUrl : 'https://example.com',\r\n * clientEntry : './src/client/browser.tsx',\r\n * clientScriptPath : ['/static/dist/js/browser.js'],\r\n * clientStylePath : ['/static/dist/css/index.css'],\r\n * enableAutoNotFound : true,\r\n * pages: [\r\n * {\r\n * title : 'Home',\r\n * path : '/',\r\n * description : 'Welcome to our platform'\r\n * }\r\n * ],\r\n * errorPages: [\r\n * {\r\n * statusCode : 404,\r\n * title : '404 - Not Found',\r\n * path : '/404',\r\n * description : 'Page not found'\r\n * }\r\n * ]\r\n * });\r\n * ```\r\n */\r\n export function serverSPA(config: types.ServerSPAPluginConfig, appConfig?: any): CruxPlugin & { __spaErrorHandler?: any } {\r\n const routes = [];\r\n const errorPageMap = new Map<number, types.ErrorPageConfig>();\r\n\r\n // Store i18n config globally so ALL handlers can access it (unified!)\r\n setGlobalI18nConfig(appConfig?.i18n);\r\n const i18nConfig = appConfig?.i18n;\r\n\r\n // Generate routes from config\r\n if (config.pages && config.pages.length > 0) {\r\n for (const pageConfig of config.pages) {\r\n routes.push(createSPARoute(pageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Setup error pages\r\n if (config.errorPages && config.errorPages.length > 0) {\r\n for (const errorPageConfig of config.errorPages) {\r\n errorPageMap.set(errorPageConfig.statusCode, errorPageConfig);\r\n // Register error page as a regular route too (for direct access like /404)\r\n routes.push(createSPARoute(errorPageConfig, config, i18nConfig));\r\n }\r\n }\r\n\r\n // Auto-generate 404 page if enabled and not already defined\r\n if (config.enableAutoNotFound && !errorPageMap.has(404)) {\r\n const defaultErrorPage = createDefault404Page();\r\n errorPageMap.set(404, defaultErrorPage);\r\n // Also register as a regular route\r\n routes.push(createSPARoute(defaultErrorPage, config, i18nConfig));\r\n }\r\n\r\n // Create error handler function (uses global i18nConfig automatically!)\r\n const errorHandler = createErrorHandler(errorPageMap, config);\r\n\r\n const plugin: CruxPlugin & { __spaErrorHandler?: any } = {\r\n name: '@cruxplug/SPA',\r\n version: '0.1.0',\r\n\r\n routes,\r\n\r\n // Attach error handler for CruxJS to use\r\n __spaErrorHandler: errorHandler,\r\n\r\n onRegister: async (app: AppInstance) => {\r\n console.log(`[SPA Plugin] Registered ${routes.length} SPA routes`);\r\n if (errorPageMap.size > 0) {\r\n const statusCodes = Array.from(errorPageMap.keys()).join(', ');\r\n console.log(`[SPA Plugin] Error pages configured for: ${statusCodes}`);\r\n }\r\n },\r\n\r\n onAwake: async (ctx: any) => {\r\n console.log('[SPA Plugin] Awake phase - SPA routes ready');\r\n },\r\n\r\n onStart: async (ctx: any) => {\r\n console.log('[SPA Plugin] Start phase - serving SPA');\r\n },\r\n\r\n onReady: async (ctx: any) => {\r\n console.log('[SPA Plugin] Ready phase - SPA is fully operational');\r\n }\r\n };\r\n\r\n return plugin;\r\n }\r\n\r\n// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxplug/spa",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Single Page Application (SPA) plugin for CruxJS with built-in SEO/CEO support, E-E-A-T signals, and JSON-LD structured data generation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cruxjs",
|
|
@@ -47,11 +47,10 @@
|
|
|
47
47
|
"bun": "^1.3.3"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@cruxjs/base": "^0.0.
|
|
50
|
+
"@cruxjs/base": "^0.0.7",
|
|
51
51
|
"@minejs/db": "^0.0.3",
|
|
52
|
-
"@minejs/server": "^0.0.
|
|
53
|
-
"
|
|
54
|
-
"test": "^3.3.0"
|
|
52
|
+
"@minejs/server": "^0.0.9",
|
|
53
|
+
"@minejs/i18n": "^0.1.0"
|
|
55
54
|
},
|
|
56
55
|
"devDependencies": {
|
|
57
56
|
"@eslint/js": "^9.39.2",
|