@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 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.7-black"/>
12
- <img src="https://img.shields.io/badge/🔥-@cruxplug-black"/>
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----%25-brightgreen" alt="Test 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
- - ## Quick Start 🔥
26
+ - ## Overview 👀
27
27
 
28
- > A production-ready **CruxJS plugin** for serving Single Page Applications (SPAs) with built-in **SEO/CEO support**, **E-E-A-T signals**, and **JSON-LD structured data generation**.
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
- > Advanced SEO metadata and AI Search optimization
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
- > Mobile-first meta tags and web app capabilities
40
+ > When you use [@cruxjs/app](https://github.com/cruxjs-org/app).
33
41
 
34
- > Structured data for rich snippets and knowledge panels
42
+ <br>
43
+ <br>
35
44
 
36
- > Automatic error page handling (404, 500, etc.)
45
+ - ## Quick Start 🔥
37
46
 
38
- > Performance optimization (canonical URLs, prefetching)
47
+ > install [`hmm`](https://github.com/minejs-org/hmm) first.
39
48
 
40
- - ### Setup
49
+ ```bash
50
+ # in your terminal
51
+ hmm i @cruxjs/spa
52
+ ```
41
53
 
42
- > install [`hmm`](https://github.com/maysara-elshewehy/hmm) first.
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
- ```bash
45
- hmm i @cruxplug/spa
46
- ```
56
+ - #### Setup
47
57
 
48
- <div align="center"> <img src="./assets/img/line.png" alt="line" style="display: block; margin-top:20px;margin-bottom:20px;width:500px;"/> <br> </div>
58
+ > Create your SPA plugin configuration:
49
59
 
50
- - ### Usage
60
+ ```typescript
61
+ import { serverSPA } from '@cruxjs/spa';
51
62
 
52
- ```ts
53
- import { serverSPA } from '@cruxplug/spa';
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
- - #### 1. Basic Usage
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
- ```typescript
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
- - #### 2. With E-E-A-T Signals (for AI Search)
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 spaPlugin = serverSPA({
88
- baseUrl: 'https://example.com',
89
- clientScriptPath: ['/js/app.js'],
90
- clientStylePath: ['/css/min.css'],
91
- clientEntry: './src/client/index.tsx',
92
- author: 'Your Company Name',
93
- authorUrl: 'https://example.com/about',
94
- pages: [
95
- {
96
- title: 'Blog Post',
97
- path: '/blog/seo-guide',
98
- description: 'Complete SEO guide for 2026',
99
- contentType: 'article',
100
- expertise: 'SEO and digital marketing',
101
- experience: '10+ years in the industry',
102
- authority: 'Published in major tech blogs'
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
- - ### 3. With Custom Error Pages
148
+ - #### Error Page Configuration with Translation Support
109
149
 
110
150
  ```typescript
111
- const spaPlugin = serverSPA({
112
- baseUrl: 'https://example.com',
113
- clientScriptPath: ['/js/app.js'],
114
- clientStylePath: ['/css/min.css'],
115
- clientEntry: './src/client/index.tsx',
116
- enableAutoNotFound: true,
117
- errorPages: [
118
- {
119
- statusCode: 404,
120
- title: '404 - Page Not Found',
121
- path: '/404',
122
- description: 'The page you're looking for doesn\'t exist',
123
- robots: 'noindex, nofollow'
124
- },
125
- {
126
- statusCode: 500,
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
- - #### `ServerSPAPluginConfig`
214
- > Main plugin configuration
174
+ - ### ..
215
175
 
216
- ```typescript
217
- interface ServerSPAPluginConfig {
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
- **Features:**
249
- - Schema.org compatible data
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
- **Features:**
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
- - #### `createSPARoute(pageConfig, baseConfig): RouteDefinition`
266
- > Creates CruxJS route definition
186
+ > When combined with [@cruxjs/client](https://github.com/cruxjs-org/client):
267
187
 
268
- - #### `createErrorHandler(errorPageMap, baseConfig): Function`
269
- > Creates error handler for CruxJS
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 l(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,o=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",a=e.description||t.defaultDescription||"A modern single-page application",r=(e.keywords||t.defaultKeywords||[]).join(", ");return `
2
- <!-- \u{1F50D} Core SEO Meta Tags -->
3
- <meta charset="UTF-8" />
4
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
- <meta name="description" content="${a}" />
6
- ${r?`<meta name="keywords" content="${r}" />`:""}
7
- <meta name="robots" content="${o}" />
8
- <meta name="language" content="en" />
9
- <meta http-equiv="content-language" content="en-us" />
10
-
11
- <!-- \u{1F465} E-E-A-T Signals for AI Search -->
12
- ${t.author?`<meta name="author" content="${t.author}" />`:""}
13
- ${e.expertise?`<meta name="expertise" content="${e.expertise}" />`:""}
14
- ${e.experience?`<meta name="experience" content="${e.experience}" />`:""}
15
- ${e.authority?`<meta name="authority" content="${e.authority}" />`:""}
16
-
17
- <!-- \u{1F4F1} Mobile & Performance -->
18
- <meta name="mobile-web-app-capable" content="yes" />
19
- <meta name="apple-mobile-web-app-capable" content="yes" />
20
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
21
- <meta name="theme-color" content="#000000" />
22
-
23
- <!-- \u{1F517} Canonical & Prefetch -->
24
- <link rel="canonical" href="${n}" />
25
- ${(e.clientScriptPath||t.clientScriptPath)?.map(s=>`<link rel="prefetch" href="${s}" />`).join(`
26
- `)}
27
-
28
- <!-- \u26A1 Performance & Security -->
29
- <meta name="format-detection" content="telephone=no" />
30
- <meta http-equiv="x-ua-compatible" content="IE=edge" />
31
- ${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
32
- <meta property="og:title" content="${e.title}" />
33
- <meta property="og:description" content="${a}" />
34
- <meta property="og:url" content="${n}" />`}function c(e,t,n="WebPage"){let o=e.canonical||`${t.baseUrl}${e.path}`,a={"@context":"https://schema.org","@type":n,name:e.title,url:o,description:e.description||t.defaultDescription,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(e.expertise||e.experience||e.authority)&&{creator:{"@type":"Person",name:t.author||"Unknown",...e.expertise&&{expertise:e.expertise},...e.experience&&{experience:e.experience},...e.authority&&{authority:e.authority}}}};return `<script type="application/ld+json">${JSON.stringify(a,null,2)}</script>`}function p(e,t){let n=e.clientScriptPath||t.clientScriptPath,o=e.clientStylePath||t.clientStylePath||[],a=n.map(s=>` <script type="module" src="${s}"></script>`).join(`
35
- `),r=o.map(s=>` <link rel="stylesheet" href="${s}" />`).join(`
36
- `);return `<!DOCTYPE html>
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
- ${l(e,t)}
40
- ${c(e,t,e.contentType==="article"?"Article":"WebPage")}
41
- ${r||""}
47
+ ${y(e,t)}
48
+ ${S(e,t,e.contentType==="article"?"Article":"WebPage")}
49
+ ${o}
50
+ ${s}
42
51
  </head>
43
52
  <body>
44
- <div id="app"></div>
45
- ${a}
53
+ <div id="app"></div>
54
+ ${i}
46
55
  </body>
47
- </html>`}function i(e,t){return {method:"GET",path:e.path,handler:n=>{let o=p(e,t);return n.html(o)}}}function P(e,t,n,o){if(o.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),r=p(a,n);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:r}}return {status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function u(e,t){return (n,o)=>{let a=P(n,e,t,o);return new Response(a.body,{status:a.status,headers:a.headers})}}function m(){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 $(e){let t=[],n=new Map;if(e.pages&&e.pages.length>0)for(let r of e.pages)t.push(i(r,e));if(e.errorPages&&e.errorPages.length>0)for(let r of e.errorPages)n.set(r.statusCode,r),t.push(i(r,e));if(e.enableAutoNotFound&&!n.has(404)){let r=m();n.set(404,r),t.push(i(r,e));}let o=u(n,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:t,__spaErrorHandler:o,onRegister:async r=>{if(console.log(`[SPA Plugin] Registered ${t.length} SPA routes`),n.size>0){let s=Array.from(n.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${s}`);}},onAwake:async r=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async r=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async r=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}exports.serverSPA=$;//# sourceMappingURL=index.cjs.map
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
@@ -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/min.js',
60
- * clientStylePath: '/static/dist/css/min.css',
61
- * enableAutoNotFound: true, // Auto-handle 404s
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/min.js',
60
- * clientStylePath: '/static/dist/css/min.css',
61
- * enableAutoNotFound: true, // Auto-handle 404s
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 l(e,t){let n=e.canonical||`${t.baseUrl}${e.path}`,o=e.robots||t.defaultRobots||"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1",a=e.description||t.defaultDescription||"A modern single-page application",r=(e.keywords||t.defaultKeywords||[]).join(", ");return `
2
- <!-- \u{1F50D} Core SEO Meta Tags -->
3
- <meta charset="UTF-8" />
4
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
- <meta name="description" content="${a}" />
6
- ${r?`<meta name="keywords" content="${r}" />`:""}
7
- <meta name="robots" content="${o}" />
8
- <meta name="language" content="en" />
9
- <meta http-equiv="content-language" content="en-us" />
10
-
11
- <!-- \u{1F465} E-E-A-T Signals for AI Search -->
12
- ${t.author?`<meta name="author" content="${t.author}" />`:""}
13
- ${e.expertise?`<meta name="expertise" content="${e.expertise}" />`:""}
14
- ${e.experience?`<meta name="experience" content="${e.experience}" />`:""}
15
- ${e.authority?`<meta name="authority" content="${e.authority}" />`:""}
16
-
17
- <!-- \u{1F4F1} Mobile & Performance -->
18
- <meta name="mobile-web-app-capable" content="yes" />
19
- <meta name="apple-mobile-web-app-capable" content="yes" />
20
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
21
- <meta name="theme-color" content="#000000" />
22
-
23
- <!-- \u{1F517} Canonical & Prefetch -->
24
- <link rel="canonical" href="${n}" />
25
- ${(e.clientScriptPath||t.clientScriptPath)?.map(s=>`<link rel="prefetch" href="${s}" />`).join(`
26
- `)}
27
-
28
- <!-- \u26A1 Performance & Security -->
29
- <meta name="format-detection" content="telephone=no" />
30
- <meta http-equiv="x-ua-compatible" content="IE=edge" />
31
- ${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
32
- <meta property="og:title" content="${e.title}" />
33
- <meta property="og:description" content="${a}" />
34
- <meta property="og:url" content="${n}" />`}function c(e,t,n="WebPage"){let o=e.canonical||`${t.baseUrl}${e.path}`,a={"@context":"https://schema.org","@type":n,name:e.title,url:o,description:e.description||t.defaultDescription,inLanguage:"en",...e.contentType&&{genre:e.contentType},...t.author&&{author:{"@type":"Person",name:t.author,...t.authorUrl&&{url:t.authorUrl}}},...(e.expertise||e.experience||e.authority)&&{creator:{"@type":"Person",name:t.author||"Unknown",...e.expertise&&{expertise:e.expertise},...e.experience&&{experience:e.experience},...e.authority&&{authority:e.authority}}}};return `<script type="application/ld+json">${JSON.stringify(a,null,2)}</script>`}function p(e,t){let n=e.clientScriptPath||t.clientScriptPath,o=e.clientStylePath||t.clientStylePath||[],a=n.map(s=>` <script type="module" src="${s}"></script>`).join(`
35
- `),r=o.map(s=>` <link rel="stylesheet" href="${s}" />`).join(`
36
- `);return `<!DOCTYPE html>
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
- ${l(e,t)}
40
- ${c(e,t,e.contentType==="article"?"Article":"WebPage")}
41
- ${r||""}
47
+ ${y(e,t)}
48
+ ${S(e,t,e.contentType==="article"?"Article":"WebPage")}
49
+ ${o}
50
+ ${s}
42
51
  </head>
43
52
  <body>
44
- <div id="app"></div>
45
- ${a}
53
+ <div id="app"></div>
54
+ ${i}
46
55
  </body>
47
- </html>`}function i(e,t){return {method:"GET",path:e.path,handler:n=>{let o=p(e,t);return n.html(o)}}}function P(e,t,n,o){if(o.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),r=p(a,n);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:r}}return {status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function u(e,t){return (n,o)=>{let a=P(n,e,t,o);return new Response(a.body,{status:a.status,headers:a.headers})}}function m(){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 $(e){let t=[],n=new Map;if(e.pages&&e.pages.length>0)for(let r of e.pages)t.push(i(r,e));if(e.errorPages&&e.errorPages.length>0)for(let r of e.errorPages)n.set(r.statusCode,r),t.push(i(r,e));if(e.enableAutoNotFound&&!n.has(404)){let r=m();n.set(404,r),t.push(i(r,e));}let o=u(n,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:t,__spaErrorHandler:o,onRegister:async r=>{if(console.log(`[SPA Plugin] Registered ${t.length} SPA routes`),n.size>0){let s=Array.from(n.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${s}`);}},onAwake:async r=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async r=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async r=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}export{$ as serverSPA};//# sourceMappingURL=index.js.map
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.7",
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.5",
50
+ "@cruxjs/base": "^0.0.7",
51
51
  "@minejs/db": "^0.0.3",
52
- "@minejs/server": "^0.0.5",
53
- "hmm": "^0.1.0",
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",