@cruxplug/spa 0.0.7 → 0.0.8
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 +146 -219
- package/dist/index.cjs +51 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -19
- package/dist/index.d.ts +36 -19
- package/dist/index.js +51 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.8-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,182 @@
|
|
|
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
|
+
// Format: string | ['translationKey'] | ['translationKey', 'Fallback']
|
|
129
|
+
title : ['meta.home.title', 'Home'],
|
|
130
|
+
path : '/',
|
|
131
|
+
description : ['meta.home.desc', 'Welcome to our platform'],
|
|
132
|
+
|
|
133
|
+
// Keywords: strings are NOT translated, arrays ARE translated
|
|
134
|
+
keywords: [
|
|
135
|
+
'home', // Direct string - no translation
|
|
136
|
+
['meta.keywords.landing'], // Translate this keyword
|
|
137
|
+
['meta.keywords.welcome', 'Welcome'] // With fallback
|
|
138
|
+
],
|
|
139
|
+
|
|
140
|
+
// E-E-A-T Signals with translation support
|
|
141
|
+
expertise : 'Full-Stack Web Development',
|
|
142
|
+
experience : '2025+',
|
|
143
|
+
authority : ['meta.authority', 'CruxJS Framework'],
|
|
144
|
+
|
|
145
|
+
// Content type for AI indexing
|
|
146
|
+
contentType : 'page',
|
|
147
|
+
|
|
148
|
+
// OpenGraph for social media
|
|
149
|
+
ogImage : 'http://localhost:3000/static/img/og-home.png',
|
|
150
|
+
canonical : 'http://localhost:3000/'
|
|
151
|
+
}
|
|
152
|
+
];
|
|
106
153
|
```
|
|
107
154
|
|
|
108
|
-
-
|
|
155
|
+
- #### Error Page Configuration with Translation Support
|
|
109
156
|
|
|
110
157
|
```typescript
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
path: '/500',
|
|
129
|
-
description: 'Something went wrong on our end'
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
});
|
|
158
|
+
const errorPages: ErrorPageConfig[] = [
|
|
159
|
+
{
|
|
160
|
+
statusCode : 404,
|
|
161
|
+
title : ['meta.error.title', '404 - Page Not Found'],
|
|
162
|
+
path : '/*',
|
|
163
|
+
description : ['meta.error.desc', 'The page you\'re looking for doesn\'t exist'],
|
|
164
|
+
keywords : ['error', '404', 'not found'], // Direct strings - no translation
|
|
165
|
+
robots : 'noindex, nofollow',
|
|
166
|
+
contentType : 'page'
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
statusCode : 500,
|
|
170
|
+
title : '500 - Server Error',
|
|
171
|
+
path : '/500',
|
|
172
|
+
description : 'Something went wrong on our end'
|
|
173
|
+
}
|
|
174
|
+
];
|
|
133
175
|
```
|
|
134
176
|
|
|
135
177
|
<br>
|
|
178
|
+
<br>
|
|
136
179
|
|
|
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
|
-
```
|
|
180
|
+
- ## API Reference 📚
|
|
212
181
|
|
|
213
|
-
-
|
|
214
|
-
> Main plugin configuration
|
|
182
|
+
- ### ..
|
|
215
183
|
|
|
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
|
-
}
|
|
184
|
+
```ts
|
|
185
|
+
..
|
|
231
186
|
```
|
|
232
187
|
|
|
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
188
|
|
|
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
|
|
189
|
+
<br>
|
|
190
|
+
<br>
|
|
257
191
|
|
|
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)
|
|
192
|
+
- ## Integration with @cruxjs/client 🔗
|
|
264
193
|
|
|
265
|
-
|
|
266
|
-
> Creates CruxJS route definition
|
|
194
|
+
> When combined with [@cruxjs/client](https://github.com/cruxjs-org/client):
|
|
267
195
|
|
|
268
|
-
-
|
|
269
|
-
|
|
196
|
+
- ✅ i18n automatically injected via meta tag
|
|
197
|
+
- ✅ All plugin lifecycle hooks execute
|
|
198
|
+
- ✅ Routing immediately available
|
|
199
|
+
- ✅ Translations loaded before component render
|
|
200
|
+
- ✅ Zero boilerplate in user code
|
|
270
201
|
|
|
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
202
|
|
|
277
203
|
<!-- ╚═════════════════════════════════════════════════════════════════╝ -->
|
|
278
204
|
|
|
@@ -280,6 +206,7 @@
|
|
|
280
206
|
|
|
281
207
|
<!-- ╔══════════════════════════════ END ══════════════════════════════╗ -->
|
|
282
208
|
|
|
209
|
+
<br>
|
|
283
210
|
<br>
|
|
284
211
|
|
|
285
212
|
---
|
|
@@ -288,4 +215,4 @@
|
|
|
288
215
|
<a href="https://github.com/maysara-elshewehy"><img src="https://img.shields.io/badge/by-Maysara-black"/></a>
|
|
289
216
|
</div>
|
|
290
217
|
|
|
291
|
-
<!-- ╚═════════════════════════════════════════════════════════════════╝ -->
|
|
218
|
+
<!-- ╚═════════════════════════════════════════════════════════════════╝ -->
|
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 E=class{constructor(e){this.translations={},this.currentLanguage="en",this.defaultLanguage="en",this.fallbackLanguage="en",this.supportedLanguages=new Set(["en"]),this.rtlLanguages=new Set(["ar","he","fa","ur","yi","ji","iw","ku"]),this.listeners=new Set,e&&(this.defaultLanguage=e.defaultLanguage||"en",this.fallbackLanguage=e.fallbackLanguage||e.defaultLanguage||"en",this.currentLanguage=e.defaultLanguage||"en",this.storage=e.storage,this.onLanguageChange=e.onLanguageChange,e.supportedLanguages&&(this.supportedLanguages=new Set(e.supportedLanguages)));}async init(){if(this.storage){let e=await this.storage.get("i18n-language");e&&this.supportedLanguages.has(e)&&(this.currentLanguage=e);}}loadLanguage(e,t){this.translations[e]||(this.translations[e]={});let n=this.flattenObject(t);this.translations[e]={...this.translations[e],...n},this.supportedLanguages.add(e);}loadTranslations(e){Object.entries(e).forEach(([t,n])=>{this.loadLanguage(t,n);});}flattenObject(e,t=""){let n={};for(let a in e){if(!Object.prototype.hasOwnProperty.call(e,a))continue;let r=e[a],o=t?`${t}.${a}`:a;typeof r=="object"&&r!==null&&!Array.isArray(r)?Object.assign(n,this.flattenObject(r,o)):n[o]=String(r);}return n}t(e,t){let n=this.getTranslation(e);return t&&Object.entries(t).forEach(([a,r])=>{let o=this.getTranslation(r,r);n=n.replace(new RegExp(`\\{${a}\\}`,"g"),o);}),n}getTranslation(e,t){return this.translations[this.currentLanguage]?.[e]?this.translations[this.currentLanguage][e]:this.fallbackLanguage!==this.currentLanguage&&this.translations[this.fallbackLanguage]?.[e]?this.translations[this.fallbackLanguage][e]:this.defaultLanguage!==this.currentLanguage&&this.defaultLanguage!==this.fallbackLanguage&&this.translations[this.defaultLanguage]?.[e]?this.translations[this.defaultLanguage][e]:(console.warn(`[i18n] Translation key not found: "${e}" (lang: ${this.currentLanguage})`),t||e)}tLang(e,t,n){let a=this.currentLanguage;this.currentLanguage=t;let r=this.t(e,n);return this.currentLanguage=a,r}tParse(e,t){let n=this.t(e,t);n=n.replace(/\\n|\/n/g,"<br>");let a=[],r=/<([a-z]+)>([^<]*)<\/\1>|<([a-z]+)\s*\/?>|([^<]+)/gi,o;for(;(o=r.exec(n))!==null;)o[4]?a.push({type:"text",content:o[4]}):o[1]?a.push({type:"tag",tag:o[1],content:o[2]}):o[3]&&a.push({type:"tag",tag:o[3],content:""});return a.length>0?a:[{type:"text",content:n}]}async setLanguage(e){if(!this.supportedLanguages.has(e)){console.warn(`[i18n] Language "${e}" not supported`);return}this.currentLanguage=e,this.storage&&await this.storage.set("i18n-language",e),this.listeners.forEach(t=>t(e)),this.onLanguageChange&&this.onLanguageChange(e);}getLanguage(){return this.currentLanguage}getSupportedLanguages(){return Array.from(this.supportedLanguages)}isLanguageSupported(e){return this.supportedLanguages.has(e)}hasKey(e){return !!(this.translations[this.currentLanguage]?.[e]||this.translations[this.fallbackLanguage]?.[e]||this.translations[this.defaultLanguage]?.[e])}getTranslations(){return this.translations[this.currentLanguage]||{}}isRTL(){return this.rtlLanguages.has(this.currentLanguage.toLowerCase().substring(0,2))}isRTLLanguage(e){return this.rtlLanguages.has(e.toLowerCase().substring(0,2))}onChange(e){return this.listeners.add(e),()=>this.listeners.delete(e)}};var f=null;function y(){return f||(f=new E),f}var h=(e,t)=>y().t(e,t);var k=()=>y().isRTL();function L(e,t="page."){let n=h("app.name"),a=h(t+e);return k()?`${n} - ${a}`:`${a} - ${n}`}function c(e,t=""){if(!e)return t;if(Array.isArray(e)){let[n,a]=e;if(!n)return a||t;try{return h(n)||a||n||t}catch{return a||n||t}}try{return h(e)||e}catch{return e}}function v(e){return !e||e.length===0?"":e.map(n=>{if(typeof n=="string")return n;if(Array.isArray(n)){let[a,r]=n;if(!a)return r||"";try{return h(a)||r||a}catch{return r||a}}return ""}).filter(Boolean).join(", ")}function P(e){if(!e)return "Page";let t="";Array.isArray(e)?t=e[0]||"":t=e;try{return L(t,"")}catch{return Array.isArray(e)?e[1]||e[0]||"Page":e}}function S(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,a=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",r=P(e.title),o=c(e.description,t.defaultDescription||"A modern single-page application"),i=v(e.keywords||t.defaultKeywords),s=c(e.expertise,""),l=c(e.experience,""),g=c(e.authority,""),u=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>${r}</title>
|
|
5
|
+
<meta name="description" content="${o}" />
|
|
6
|
+
${i?`<meta name="keywords" content="${i}" />`:""}
|
|
7
|
+
<meta name="robots" content="${a}" />
|
|
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
|
+
${s?`<meta name="expertise" content="${s}" />`:""}
|
|
13
|
+
${l?`<meta name="experience" content="${l}" />`:""}
|
|
14
|
+
${g?`<meta name="authority" content="${g}" />`:""}
|
|
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(p=>`<link rel="prefetch" href="${p}" />`).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="${u}" />
|
|
29
|
+
<meta property="og:title" content="${r}" />
|
|
30
|
+
<meta property="og:description" content="${o}" />
|
|
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="${r}" />`:""}
|
|
36
|
+
`}function b(e,t,n="WebPage"){let a=e.canonical||`${t.baseUrl}${e.path}`,r=P(e.title),o=c(e.description,t.defaultDescription),i=c(e.expertise,""),s=c(e.experience,""),l=c(e.authority,""),g={"@context":"https://schema.org","@type":n,name:r,url:a,description:o,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(i||s||l)&&{creator:{"@type":"Person",name:t.author||"Unknown",...i&&{expertise:i},...s&&{experience:s},...l&&{authority:l}}}};return `<script type="application/ld+json">
|
|
37
|
+
${JSON.stringify(g,null,2).split(`
|
|
38
|
+
`).map(p=>p&&` ${p}`).join(`
|
|
39
|
+
`)}
|
|
40
|
+
</script>`}var j=["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"];function R(e){let t=[],n=/(<[^>]+>)|([^<]+)/g,a;for(;(a=n.exec(e))!==null;)a[1]?t.push({type:"tag",value:a[1]}):a[2]&&a[2].trim().length>0&&t.push({type:"text",value:a[2]});return t}function M(e,t=4){let n=" ".repeat(t),a=[],r=0;for(let o=0;o<e.length;o++){let i=e[o],s=i.value.trim();s.startsWith("</")&&(r=Math.max(0,r-1));let l=n.repeat(r);if(!(i.type==="text"&&s.length===0)&&(i.type==="text"&&a.length>0&&!a[a.length-1].trim().endsWith(">")?a[a.length-1]+=s:a.push(l+s),s.startsWith("<")&&!s.startsWith("</"))){if(s.startsWith("<!DOCTYPE")||s.startsWith("<!--"))continue;let g=s.match(/<([A-Za-z][A-Za-z0-9\\-]*)/i),u=g?g[1].toLowerCase():"",p=j.includes(u),T=s.endsWith("/>");!p&&!T&&r++;}}return a}function w(e,t=4){let n=e.split(`
|
|
41
|
+
`).map(o=>o.trim()).filter(o=>o.length>0).join(""),a=R(n);return M(a,t).join(`
|
|
42
|
+
`)}function O(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 m(e,t,n){let a=e.clientScriptPath||t.clientScriptPath,r=e.clientStylePath||t.clientStylePath||[],o=a.map(u=>`<script type="module" src="${u}"></script>`).join(`
|
|
43
|
+
`),i=r.map(u=>`<link rel="stylesheet" href="${u}" />`).join(`
|
|
44
|
+
`),s=O(n);s||console.warn("[SPA] WARNING: i18n meta tag is empty!");let l=`<!DOCTYPE html>
|
|
37
45
|
<html lang="en">
|
|
38
46
|
<head>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
${S(e,t)}
|
|
48
|
+
${b(e,t,e.contentType==="article"?"Article":"WebPage")}
|
|
49
|
+
${s}
|
|
50
|
+
${i}
|
|
42
51
|
</head>
|
|
43
52
|
<body>
|
|
44
|
-
|
|
45
|
-
${
|
|
53
|
+
<div id="app"></div>
|
|
54
|
+
${o}
|
|
46
55
|
</body>
|
|
47
|
-
</html
|
|
56
|
+
</html>`;l.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag NOT in raw HTML!");let g=w(l);return g.includes("app-i18n")||console.error("[SPA] ERROR: i18n meta tag LOST during formatting!"),g}function d(e,t,n){return {method:"GET",path:e.path,handler:a=>{let r=m(e,t,n);return a.html(r)}}}function I(){return global.__cruxjs_i18n_config}function A(e){global.__cruxjs_i18n_config=e;}function C(e,t,n,a){if(a.startsWith("/api/"))return {status:e,headers:{"Content-Type":"application/json"},body:JSON.stringify({error:`Error ${e}`})};if(t.has(e)){let r=t.get(e),o=I();console.log(`[Errors] Generating error page ${e} with i18n:`,!!o);let i=m(r,n,o);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:i}}return console.log(`[Errors] No custom error page for ${e}, returning fallback`),{status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function x(e,t){return (n,a)=>{let r=C(n,e,t,a);return new Response(r.body,{status:r.status,headers:r.headers})}}function $(){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 Y(e,t){let n=[],a=new Map;A(t?.i18n);let r=t?.i18n;if(e.pages&&e.pages.length>0)for(let s of e.pages)n.push(d(s,e,r));if(e.errorPages&&e.errorPages.length>0)for(let s of e.errorPages)a.set(s.statusCode,s),n.push(d(s,e,r));if(e.enableAutoNotFound&&!a.has(404)){let s=$();a.set(404,s),n.push(d(s,e,r));}let o=x(a,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:n,__spaErrorHandler:o,onRegister:async s=>{if(console.log(`[SPA Plugin] Registered ${n.length} SPA routes`),a.size>0){let l=Array.from(a.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${l}`);}},onAwake:async s=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async s=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async s=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}exports.serverSPA=Y;//# sourceMappingURL=index.cjs.map
|
|
48
57
|
//# sourceMappingURL=index.cjs.map
|