@cruxplug/spa 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Maysara Elshewehy (https://github.com/maysara-elshewehy)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ <!-- ╔══════════════════════════════ BEG ══════════════════════════════╗ -->
2
+
3
+ <br>
4
+ <div align="center">
5
+ <p>
6
+ <img src="./assets/img/logo.png" alt="logo" style="" height="60" />
7
+ </p>
8
+ </div>
9
+
10
+ <div align="center">
11
+ <img src="https://img.shields.io/badge/v-0.0.1-black"/>
12
+ <img src="https://img.shields.io/badge/🔥-@cruxplug-black"/>
13
+ <br>
14
+ <img src="https://img.shields.io/badge/coverage----%25-brightgreen" alt="Test Coverage" />
15
+ <img src="https://img.shields.io/github/issues/cruxplug-/spa?style=flat" alt="Github Repo Issues" />
16
+ <img src="https://img.shields.io/github/stars/cruxplug-/spa?style=social" alt="GitHub Repo stars" />
17
+ </div>
18
+ <br>
19
+
20
+ <!-- ╚═════════════════════════════════════════════════════════════════╝ -->
21
+
22
+
23
+
24
+ <!-- ╔══════════════════════════════ DOC ══════════════════════════════╗ -->
25
+
26
+ - ## Quick Start 🔥
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**.
29
+
30
+ > Advanced SEO metadata and AI Search optimization
31
+
32
+ > Mobile-first meta tags and web app capabilities
33
+
34
+ > Structured data for rich snippets and knowledge panels
35
+
36
+ > Automatic error page handling (404, 500, etc.)
37
+
38
+ > Performance optimization (canonical URLs, prefetching)
39
+
40
+ - ### Setup
41
+
42
+ > install [`hmm`](https://github.com/maysara-elshewehy/hmm) first.
43
+
44
+ ```bash
45
+ hmm i @cruxplug/spa
46
+ ```
47
+
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>
49
+
50
+ - ### Usage
51
+
52
+ ```ts
53
+ import { serverSPA } from '@cruxplug/spa';
54
+ ```
55
+
56
+ - #### 1. Basic Usage
57
+
58
+ ```typescript
59
+ const spaPlugin = serverSPA({
60
+ baseUrl: 'https://example.com',
61
+ clientEntry: './src/client/browser.tsx',
62
+ clientScriptPath: '/static/dist/js/browser.js',
63
+ enableAutoNotFound: true,
64
+ pages: [
65
+ {
66
+ title: 'Home',
67
+ path: '/',
68
+ description: 'Welcome to our platform',
69
+ keywords: ['home', 'landing']
70
+ },
71
+ {
72
+ title: 'About Us',
73
+ path: '/about',
74
+ description: 'Learn more about our company',
75
+ contentType: 'page'
76
+ }
77
+ ]
78
+ });
79
+
80
+ app.use(spaPlugin);
81
+ ```
82
+
83
+ - #### 2. With E-E-A-T Signals (for AI Search)
84
+
85
+ ```typescript
86
+ const spaPlugin = serverSPA({
87
+ baseUrl: 'https://example.com',
88
+ clientScriptPath: '/js/app.js',
89
+ clientEntry: './src/client/index.tsx',
90
+ author: 'Your Company Name',
91
+ authorUrl: 'https://example.com/about',
92
+ pages: [
93
+ {
94
+ title: 'Blog Post',
95
+ path: '/blog/seo-guide',
96
+ description: 'Complete SEO guide for 2026',
97
+ contentType: 'article',
98
+ expertise: 'SEO and digital marketing',
99
+ experience: '10+ years in the industry',
100
+ authority: 'Published in major tech blogs'
101
+ }
102
+ ]
103
+ });
104
+ ```
105
+
106
+ - ### 3. With Custom Error Pages
107
+
108
+ ```typescript
109
+ const spaPlugin = serverSPA({
110
+ baseUrl: 'https://example.com',
111
+ clientScriptPath: '/js/app.js',
112
+ clientEntry: './src/client/index.tsx',
113
+ enableAutoNotFound: true,
114
+ errorPages: [
115
+ {
116
+ statusCode: 404,
117
+ title: '404 - Page Not Found',
118
+ path: '/404',
119
+ description: 'The page you're looking for doesn\'t exist',
120
+ robots: 'noindex, nofollow'
121
+ },
122
+ {
123
+ statusCode: 500,
124
+ title: '500 - Server Error',
125
+ path: '/500',
126
+ description: 'Something went wrong on our end'
127
+ }
128
+ ]
129
+ });
130
+ ```
131
+
132
+ <br>
133
+
134
+ - ## API Reference 🔥
135
+
136
+ ### Core Plugin
137
+
138
+ - #### `serverSPA(config: ServerSPAPluginConfig): CruxPlugin`
139
+ > Creates and returns the SPA plugin with SEO support
140
+
141
+ **Parameters:**
142
+ - `baseUrl`: Base URL for canonical links and SEO (required)
143
+ - `clientScriptPath`: Path to client-side JS bundle (required)
144
+ - `clientEntry`: Path to client entry point (required)
145
+ - `pages`: Array of pages to serve as SPA (optional)
146
+ - `errorPages`: Array of error page configurations (optional)
147
+ - `author`: Author name for structured data (optional)
148
+ - `authorUrl`: Author profile URL (optional)
149
+ - `enableAutoNotFound`: Auto-generate 404 page if true (optional, default: false)
150
+ - `defaultDescription`: Default SEO description (optional)
151
+ - `defaultKeywords`: Default SEO keywords array (optional)
152
+ - `defaultRobots`: Default robots meta tag (optional)
153
+
154
+ **Returns:** CruxPlugin with SEO support
155
+
156
+ ```typescript
157
+ const plugin = serverSPA({
158
+ baseUrl: 'https://example.com',
159
+ clientScriptPath: '/js/app.js',
160
+ clientEntry: './src/client/index.tsx',
161
+ enableAutoNotFound: true,
162
+ pages: [
163
+ { title: 'Home', path: '/', description: 'Home page' }
164
+ ]
165
+ });
166
+ ```
167
+
168
+ ### Type Definitions
169
+
170
+ - #### `SPAPageConfig`
171
+ > Configuration for a single SPA page
172
+
173
+ ```typescript
174
+ interface SPAPageConfig {
175
+ // Required
176
+ title: string; // Page title
177
+ path: string; // Route path
178
+
179
+ // SEO
180
+ description?: string; // Meta description
181
+ keywords?: string[]; // Meta keywords array
182
+ ogImage?: string; // Open Graph image URL
183
+ canonical?: string; // Canonical URL
184
+ robots?: string; // Robots meta tag
185
+
186
+ // E-E-A-T Signals (Google AI Overviews)
187
+ expertise?: string; // Author's expertise
188
+ experience?: string; // Author's experience
189
+ authority?: string; // Author's authority
190
+
191
+ // Content
192
+ contentType?: 'article' | 'product' | 'service' | 'app' | 'workspace' | 'page';
193
+ clientScriptPath?: string; // Override client script path
194
+ clientEntry?: string; // Override client entry
195
+ }
196
+ ```
197
+
198
+ - #### `ErrorPageConfig`
199
+ > Configuration for error pages
200
+
201
+ ```typescript
202
+ interface ErrorPageConfig extends SPAPageConfig {
203
+ statusCode: number; // HTTP status code (404, 500, etc.)
204
+ }
205
+ ```
206
+
207
+ - #### `ServerSPAPluginConfig`
208
+ > Main plugin configuration
209
+
210
+ ```typescript
211
+ interface ServerSPAPluginConfig {
212
+ baseUrl: string;
213
+ pages?: SPAPageConfig[];
214
+ errorPages?: ErrorPageConfig[];
215
+ clientEntry: string;
216
+ clientScriptPath: string;
217
+ author?: string;
218
+ authorUrl?: string;
219
+ defaultDescription?: string;
220
+ defaultKeywords?: string[];
221
+ defaultRobots?: string;
222
+ enableAutoNotFound?: boolean;
223
+ }
224
+ ```
225
+
226
+ ### Utility Functions (Advanced)
227
+
228
+ - #### `generateSEOMetaTags(config, baseConfig): string`
229
+ > Generates SEO meta tags with E-E-A-T signals
230
+
231
+ **Features:**
232
+ - Core SEO metadata (charset, viewport, description, keywords)
233
+ - E-E-A-T signals for AI search optimization
234
+ - Mobile optimization (web app capable, status bar)
235
+ - Open Graph protocol tags
236
+ - Performance & security headers
237
+
238
+ - #### `generateStructuredData(pageConfig, baseConfig, contentType): string`
239
+ > Generates JSON-LD structured data
240
+
241
+ **Features:**
242
+ - Schema.org compatible data
243
+ - Support for multiple content types
244
+ - Rich snippets for search results
245
+ - Author and creator information
246
+ - AI overview optimization
247
+
248
+ - #### `generateSPAHTML(pageConfig, baseConfig): string`
249
+ > Generates complete HTML document
250
+
251
+ **Features:**
252
+ - Full HTML5 shell with doctype
253
+ - Integrated SEO and structured data
254
+ - App mount point (#app)
255
+ - Module script loading
256
+
257
+ - #### `createSPARoute(pageConfig, baseConfig): RouteDefinition`
258
+ > Creates CruxJS route definition
259
+
260
+ - #### `createErrorHandler(errorPageMap, baseConfig): Function`
261
+ > Creates error handler for CruxJS
262
+
263
+ **Features:**
264
+ - Differentiates API vs web requests
265
+ - JSON responses for `/api/*` routes
266
+ - Custom HTML pages for web requests
267
+ - Fallback error handling
268
+
269
+ <!-- ╚═════════════════════════════════════════════════════════════════╝ -->
270
+
271
+
272
+
273
+ <!-- ╔══════════════════════════════ END ══════════════════════════════╗ -->
274
+
275
+ <br>
276
+
277
+ ---
278
+
279
+ <div align="center">
280
+ <a href="https://github.com/maysara-elshewehy"><img src="https://img.shields.io/badge/by-Maysara-black"/></a>
281
+ </div>
282
+
283
+ <!-- ╚═════════════════════════════════════════════════════════════════╝ -->
package/dist/index.cjs ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';function p(e,t){let r=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",n=(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
+ ${n?`<meta name="keywords" content="${n}" />`:""}
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="${r}" />
25
+ <link rel="prefetch" href="${e.clientScriptPath||t.clientScriptPath}" />
26
+
27
+ <!-- \u26A1 Performance & Security -->
28
+ <meta name="format-detection" content="telephone=no" />
29
+ <meta http-equiv="x-ua-compatible" content="IE=edge" />
30
+ ${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
31
+ <meta property="og:title" content="${e.title}" />
32
+ <meta property="og:description" content="${a}" />
33
+ <meta property="og:url" content="${r}" />`}function l(e,t,r="WebPage"){let o=e.canonical||`${t.baseUrl}${e.path}`,a={"@context":"https://schema.org","@type":r,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 i(e,t){let r=e.clientScriptPath||t.clientScriptPath;return `<!DOCTYPE html>
34
+ <html lang="en">
35
+ <head>
36
+ ${p(e,t)}
37
+ ${l(e,t,e.contentType==="article"?"Article":"WebPage")}
38
+ </head>
39
+ <body>
40
+ <div id="app"></div>
41
+ <script type="module" src="${r}"></script>
42
+ </body>
43
+ </html>`}function s(e,t){return {method:"GET",path:e.path,handler:r=>{let o=i(e,t);return r.html(o)}}}function P(e,t,r,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),n=i(a,r);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:n}}return {status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function c(e,t){return (r,o)=>{let a=P(r,e,t,o);return new Response(a.body,{status:a.status,headers:a.headers})}}function u(){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 b(e){let t=[],r=new Map;if(e.pages&&e.pages.length>0)for(let n of e.pages)t.push(s(n,e));if(e.errorPages&&e.errorPages.length>0)for(let n of e.errorPages)r.set(n.statusCode,n),t.push(s(n,e));if(e.enableAutoNotFound&&!r.has(404)){let n=u();r.set(404,n),t.push(s(n,e));}let o=c(r,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:t,__spaErrorHandler:o,onRegister:async n=>{if(console.log(`[SPA Plugin] Registered ${t.length} SPA routes`),r.size>0){let m=Array.from(r.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${m}`);}},onAwake:async n=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async n=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async n=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}exports.serverSPA=b;//# sourceMappingURL=index.cjs.map
44
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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","generateStructuredData","pageConfig","contentType","schema","generateSPAHTML","clientScript","createSPARoute","c","html","buildErrorResponse","statusCode","errorPageMap","path","errorConfig","createErrorHandler","errorResponse","createDefault404Page","serverSPA","routes","errorPageConfig","defaultErrorPage","errorHandler","app","statusCodes","ctx"],"mappings":"aA0BW,SAASA,EACZC,CAAAA,CACAC,CAAAA,CACM,CACN,IAAMC,EAAeF,CAAAA,CAAO,SAAA,EAAa,CAAA,EAAGC,CAAAA,CAAW,OAAO,CAAA,EAAGD,CAAAA,CAAO,IAAI,CAAA,CAAA,CACtEG,EAASH,CAAAA,CAAO,MAAA,EAAUC,CAAAA,CAAW,aAAA,EAAiB,+EACtDG,CAAAA,CAAcJ,CAAAA,CAAO,WAAA,EAAeC,CAAAA,CAAW,oBAAsB,kCAAA,CACrEI,CAAAA,CAAAA,CAAYL,CAAAA,CAAO,QAAA,EAAYC,EAAW,eAAA,EAAmB,IAAI,IAAA,CAAK,IAAI,EAEhF,OAAO;AAAA;AAAA;AAAA;AAAA,0CAAA,EAI6BG,CAAW,CAAA;AAAA,QAAA,EAC7CC,CAAAA,CAAW,CAAA,+BAAA,EAAkCA,CAAQ,CAAA,IAAA,CAAA,CAAS,EAAE;AAAA,qCAAA,EACnCF,CAAM,CAAA;AAAA;AAAA;;AAAA;AAAA,QAAA,EAKnCF,EAAW,MAAA,CAAS,CAAA,6BAAA,EAAgCA,CAAAA,CAAW,MAAM,OAAS,EAAE;AAAA,QAAA,EAChFD,EAAO,SAAA,CAAY,CAAA,gCAAA,EAAmCA,CAAAA,CAAO,SAAS,OAAS,EAAE;AAAA,QAAA,EACjFA,EAAO,UAAA,CAAa,CAAA,iCAAA,EAAoCA,CAAAA,CAAO,UAAU,OAAS,EAAE;AAAA,QAAA,EACpFA,EAAO,SAAA,CAAY,CAAA,gCAAA,EAAmCA,CAAAA,CAAO,SAAS,OAAS,EAAE;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA,oCAAA,EASrDE,CAAY,CAAA;AAAA,mCAAA,EACbF,CAAAA,CAAO,gBAAA,EAAoBC,CAAAA,CAAW,gBAAgB,CAAA;;AAAA;AAAA;AAAA;AAAA,QAAA,EAKjFD,EAAO,OAAA,CAAU,CAAA,mCAAA,EAAsCA,CAAAA,CAAO,OAAO,OAAS,EAAE;AAAA,2CAAA,EAC7CA,EAAO,KAAK,CAAA;AAAA,iDAAA,EACNI,CAAW,CAAA;AAAA,yCAAA,EACnBF,CAAY,CAAA,IAAA,CACnD,CAYO,SAASI,CAAAA,CACZC,CAAAA,CACAN,CAAAA,CACAO,CAAAA,CAAsB,SAAA,CAChB,CACN,IAAMN,CAAAA,CAAeK,EAAW,SAAA,EAAa,CAAA,EAAGN,CAAAA,CAAW,OAAO,CAAA,EAAGM,CAAAA,CAAW,IAAI,CAAA,CAAA,CAE9EE,CAAAA,CAAS,CACX,UAAA,CAAY,oBAAA,CACZ,OAAA,CAASD,CAAAA,CACT,IAAA,CAAQD,CAAAA,CAAW,MACnB,GAAA,CAAOL,CAAAA,CACP,WAAA,CAAeK,CAAAA,CAAW,WAAA,EAAeN,CAAAA,CAAW,kBAAA,CACpD,UAAA,CAAc,IAAA,CACd,GAAIM,CAAAA,CAAW,WAAA,EAAe,CAAE,KAAA,CAASA,CAAAA,CAAW,WAAY,EAChE,GAAIN,CAAAA,CAAW,MAAA,EAAU,CACrB,MAAA,CAAU,CACN,OAAA,CAAS,QAAA,CACT,IAAA,CAAQA,CAAAA,CAAW,MAAA,CACnB,GAAIA,CAAAA,CAAW,SAAA,EAAa,CAAE,GAAA,CAAOA,EAAW,SAAU,CAC9D,CACJ,CAAA,CACA,GAAA,CAAIM,CAAAA,CAAW,SAAA,EAAaA,CAAAA,CAAW,UAAA,EAAcA,CAAAA,CAAW,SAAA,GAAc,CAC1E,OAAA,CAAW,CACP,OAAA,CAAS,QAAA,CACT,KAAQN,CAAAA,CAAW,MAAA,EAAU,SAAA,CAC7B,GAAIM,CAAAA,CAAW,SAAA,EAAa,CAAE,SAAA,CAAaA,CAAAA,CAAW,SAAU,CAAA,CAChE,GAAIA,CAAAA,CAAW,UAAA,EAAc,CAAE,UAAA,CAAcA,EAAW,UAAW,CAAA,CACnE,GAAIA,CAAAA,CAAW,SAAA,EAAa,CAAE,SAAA,CAAaA,CAAAA,CAAW,SAAU,CACpE,CACJ,CACJ,CAAA,CAEA,OAAO,CAAA,mCAAA,EAAsC,IAAA,CAAK,UAAUE,CAAAA,CAAQ,IAAA,CAAM,CAAC,CAAC,CAAA,SAAA,CAChF,CCvFO,SAASC,CAAAA,CACZH,CAAAA,CACAN,CAAAA,CACM,CACN,IAAMU,CAAAA,CAAeJ,CAAAA,CAAW,gBAAA,EAAoBN,CAAAA,CAAW,iBAE/D,OAAO,CAAA;AAAA;AAAA;AAAA,IAAA,EAGTF,CAAAA,CAAoBQ,CAAAA,CAAYN,CAAU,CAAC;AAAA,IAAA,EAC3CK,CAAAA,CAAuBC,EAAYN,CAAAA,CAAYM,CAAAA,CAAW,cAAgB,SAAA,CAAY,SAAA,CAAY,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA,+BAAA,EAIjFI,CAAY,CAAA;AAAA;AAAA,OAAA,CAGzC,CAQO,SAASC,CAAAA,CACZL,CAAAA,CACAN,CAAAA,CACe,CACf,OAAO,CACH,MAAA,CAAQ,KAAA,CACR,KAAMM,CAAAA,CAAW,IAAA,CACjB,OAAA,CAAUM,CAAAA,EAAkB,CACxB,IAAMC,CAAAA,CAAOJ,CAAAA,CAAgBH,CAAAA,CAAYN,CAAU,CAAA,CACnD,OAAOY,CAAAA,CAAE,IAAA,CAAKC,CAAI,CACtB,CACJ,CACJ,CC3BO,SAASC,CAAAA,CACZC,CAAAA,CACAC,CAAAA,CACAhB,CAAAA,CACAiB,EACa,CAEb,GAAIA,CAAAA,CAAK,UAAA,CAAW,OAAO,CAAA,CACvB,OAAO,CACH,MAAA,CAAQF,EACR,OAAA,CAAS,CAAE,cAAA,CAAgB,kBAAmB,EAC9C,IAAA,CAAM,IAAA,CAAK,SAAA,CAAU,CAAE,MAAO,CAAA,MAAA,EAASA,CAAU,CAAA,CAAG,CAAC,CACzD,CAAA,CAIJ,GAAIC,CAAAA,CAAa,GAAA,CAAID,CAAU,CAAA,CAAG,CAC9B,IAAMG,CAAAA,CAAcF,EAAa,GAAA,CAAID,CAAU,EACzCF,CAAAA,CAAOJ,CAAAA,CAAgBS,EAAalB,CAAU,CAAA,CACpD,OAAO,CACH,OAAQe,CAAAA,CACR,OAAA,CAAS,CAAE,cAAA,CAAgB,0BAA2B,CAAA,CACtD,IAAA,CAAMF,CACV,CACJ,CAGA,OAAO,CACH,MAAA,CAAQE,CAAAA,CACR,QAAS,CAAE,cAAA,CAAgB,YAAa,CAAA,CACxC,KAAM,CAAA,MAAA,EAASA,CAAU,CAAA,CAC7B,CACJ,CAWO,SAASI,CAAAA,CACZH,CAAAA,CACAhB,CAAAA,CAC8C,CAC9C,OAAO,CAACe,CAAAA,CAAoBE,CAAAA,GAAiB,CACzC,IAAMG,CAAAA,CAAgBN,CAAAA,CAAmBC,CAAAA,CAAYC,EAAchB,CAAAA,CAAYiB,CAAI,CAAA,CACnF,OAAO,IAAI,QAAA,CAASG,CAAAA,CAAc,IAAA,CAAM,CACpC,OAAQA,CAAAA,CAAc,MAAA,CACtB,OAAA,CAASA,CAAAA,CAAc,OAC3B,CAAC,CACL,CACJ,CAKO,SAASC,CAAAA,EAAwC,CACpD,OAAO,CACH,WAAY,GAAA,CACZ,KAAA,CAAO,sBAAA,CACP,IAAA,CAAM,OACN,WAAA,CAAa,kDAAA,CACb,SAAU,CAAC,KAAA,CAAO,YAAa,OAAO,CAAA,CACtC,MAAA,CAAQ,mBACZ,CACJ,CCjDO,SAASC,CAAAA,CAAUvB,CAAAA,CAA+E,CACrG,IAAMwB,CAAAA,CAAS,EAAC,CACVP,EAAe,IAAI,GAAA,CAGzB,GAAIjB,CAAAA,CAAO,OAASA,CAAAA,CAAO,KAAA,CAAM,MAAA,CAAS,CAAA,CACtC,QAAWO,CAAAA,IAAcP,CAAAA,CAAO,KAAA,CAC5BwB,CAAAA,CAAO,KAAKZ,CAAAA,CAAeL,CAAAA,CAAYP,CAAM,CAAC,EAKtD,GAAIA,CAAAA,CAAO,UAAA,EAAcA,CAAAA,CAAO,WAAW,MAAA,CAAS,CAAA,CAChD,IAAA,IAAWyB,CAAAA,IAAmBzB,EAAO,UAAA,CACjCiB,CAAAA,CAAa,GAAA,CAAIQ,CAAAA,CAAgB,WAAYA,CAAe,CAAA,CAE5DD,CAAAA,CAAO,IAAA,CAAKZ,EAAea,CAAAA,CAAiBzB,CAAM,CAAC,CAAA,CAK3D,GAAIA,CAAAA,CAAO,kBAAA,EAAsB,CAACiB,CAAAA,CAAa,IAAI,GAAG,CAAA,CAAG,CACrD,IAAMS,EAAmBJ,CAAAA,EAAqB,CAC9CL,CAAAA,CAAa,GAAA,CAAI,IAAKS,CAAgB,CAAA,CAEtCF,EAAO,IAAA,CAAKZ,CAAAA,CAAec,EAAkB1B,CAAM,CAAC,EACxD,CAGA,IAAM2B,CAAAA,CAAeP,CAAAA,CAAmBH,CAAAA,CAAcjB,CAAM,EAgC5D,OA9ByD,CACrD,IAAA,CAAM,eAAA,CACN,QAAS,OAAA,CAET,MAAA,CAAAwB,CAAAA,CAGA,iBAAA,CAAmBG,EAEnB,UAAA,CAAY,MAAOC,CAAAA,EAAqB,CAEpC,GADA,OAAA,CAAQ,GAAA,CAAI,CAAA,wBAAA,EAA2BJ,CAAAA,CAAO,MAAM,CAAA,WAAA,CAAa,CAAA,CAC7DP,CAAAA,CAAa,IAAA,CAAO,EAAG,CACvB,IAAMY,EAAc,KAAA,CAAM,IAAA,CAAKZ,EAAa,IAAA,EAAM,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA,CAC7D,OAAA,CAAQ,GAAA,CAAI,CAAA,yCAAA,EAA4CY,CAAW,CAAA,CAAE,EACzE,CACJ,CAAA,CAEA,QAAS,MAAOC,CAAAA,EAAa,CACzB,OAAA,CAAQ,IAAI,6CAA6C,EAC7D,CAAA,CAEA,OAAA,CAAS,MAAOA,CAAAA,EAAa,CACzB,OAAA,CAAQ,GAAA,CAAI,wCAAwC,EACxD,CAAA,CAEA,OAAA,CAAS,MAAOA,GAAa,CACzB,OAAA,CAAQ,IAAI,qDAAqD,EACrE,CACJ,CAGJ","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 <link rel=\"prefetch\" href=\"${config.clientScriptPath || baseConfig.clientScriptPath}\" />\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 entry point\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const clientScript = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\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</head>\r\n<body>\r\n <div id=\"app\"></div>\r\n <script type=\"module\" src=\"${clientScript}\"></script>\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 { generateSPAHTML, createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page } from './utils/errors';\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/browser.js',\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// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
@@ -0,0 +1,81 @@
1
+ import { CruxPlugin } from '@cruxjs/base';
2
+
3
+ /**
4
+ * SEO Configuration for SPA routes
5
+ * Supports modern E-E-A-T signals and AI Search optimization
6
+ */
7
+ interface SPAPageConfig {
8
+ title: string;
9
+ path: string;
10
+ description?: string;
11
+ keywords?: string[];
12
+ expertise?: string;
13
+ experience?: string;
14
+ authority?: string;
15
+ contentType?: 'article' | 'product' | 'service' | 'app' | 'workspace' | 'page';
16
+ ogImage?: string;
17
+ canonical?: string;
18
+ robots?: string;
19
+ clientEntry?: string;
20
+ clientScriptPath?: string;
21
+ }
22
+ /**
23
+ * Error Page Configuration
24
+ * Define custom error pages (404, 500, etc.)
25
+ */
26
+ interface ErrorPageConfig extends SPAPageConfig {
27
+ statusCode: number;
28
+ }
29
+ /**
30
+ * Server SPA Plugin Configuration
31
+ */
32
+ interface ServerSPAPluginConfig {
33
+ baseUrl: string;
34
+ pages?: SPAPageConfig[];
35
+ errorPages?: ErrorPageConfig[];
36
+ clientEntry: string;
37
+ clientScriptPath: string;
38
+ author?: string;
39
+ authorUrl?: string;
40
+ defaultDescription?: string;
41
+ defaultKeywords?: string[];
42
+ defaultRobots?: string;
43
+ enableAutoNotFound?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Create Server SPA Plugin
48
+ *
49
+ * Generates SPA routes with SEO/CEO metadata and structured data
50
+ * Error pages are handled via CruxJS error hooks
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const spaPlugin = serverSPA({
55
+ * baseUrl: 'https://example.com',
56
+ * clientEntry: './src/client/browser.tsx',
57
+ * clientScriptPath: '/static/dist/js/browser.js',
58
+ * enableAutoNotFound: true, // Auto-handle 404s
59
+ * pages: [
60
+ * {
61
+ * title: 'Home',
62
+ * path: '/',
63
+ * description: 'Welcome to our platform'
64
+ * }
65
+ * ],
66
+ * errorPages: [
67
+ * {
68
+ * statusCode: 404,
69
+ * title: '404 - Not Found',
70
+ * path: '/404',
71
+ * description: 'Page not found'
72
+ * }
73
+ * ]
74
+ * });
75
+ * ```
76
+ */
77
+ declare function serverSPA(config: ServerSPAPluginConfig): CruxPlugin & {
78
+ __spaErrorHandler?: any;
79
+ };
80
+
81
+ export { serverSPA };
@@ -0,0 +1,81 @@
1
+ import { CruxPlugin } from '@cruxjs/base';
2
+
3
+ /**
4
+ * SEO Configuration for SPA routes
5
+ * Supports modern E-E-A-T signals and AI Search optimization
6
+ */
7
+ interface SPAPageConfig {
8
+ title: string;
9
+ path: string;
10
+ description?: string;
11
+ keywords?: string[];
12
+ expertise?: string;
13
+ experience?: string;
14
+ authority?: string;
15
+ contentType?: 'article' | 'product' | 'service' | 'app' | 'workspace' | 'page';
16
+ ogImage?: string;
17
+ canonical?: string;
18
+ robots?: string;
19
+ clientEntry?: string;
20
+ clientScriptPath?: string;
21
+ }
22
+ /**
23
+ * Error Page Configuration
24
+ * Define custom error pages (404, 500, etc.)
25
+ */
26
+ interface ErrorPageConfig extends SPAPageConfig {
27
+ statusCode: number;
28
+ }
29
+ /**
30
+ * Server SPA Plugin Configuration
31
+ */
32
+ interface ServerSPAPluginConfig {
33
+ baseUrl: string;
34
+ pages?: SPAPageConfig[];
35
+ errorPages?: ErrorPageConfig[];
36
+ clientEntry: string;
37
+ clientScriptPath: string;
38
+ author?: string;
39
+ authorUrl?: string;
40
+ defaultDescription?: string;
41
+ defaultKeywords?: string[];
42
+ defaultRobots?: string;
43
+ enableAutoNotFound?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Create Server SPA Plugin
48
+ *
49
+ * Generates SPA routes with SEO/CEO metadata and structured data
50
+ * Error pages are handled via CruxJS error hooks
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const spaPlugin = serverSPA({
55
+ * baseUrl: 'https://example.com',
56
+ * clientEntry: './src/client/browser.tsx',
57
+ * clientScriptPath: '/static/dist/js/browser.js',
58
+ * enableAutoNotFound: true, // Auto-handle 404s
59
+ * pages: [
60
+ * {
61
+ * title: 'Home',
62
+ * path: '/',
63
+ * description: 'Welcome to our platform'
64
+ * }
65
+ * ],
66
+ * errorPages: [
67
+ * {
68
+ * statusCode: 404,
69
+ * title: '404 - Not Found',
70
+ * path: '/404',
71
+ * description: 'Page not found'
72
+ * }
73
+ * ]
74
+ * });
75
+ * ```
76
+ */
77
+ declare function serverSPA(config: ServerSPAPluginConfig): CruxPlugin & {
78
+ __spaErrorHandler?: any;
79
+ };
80
+
81
+ export { serverSPA };
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ function p(e,t){let r=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",n=(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
+ ${n?`<meta name="keywords" content="${n}" />`:""}
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="${r}" />
25
+ <link rel="prefetch" href="${e.clientScriptPath||t.clientScriptPath}" />
26
+
27
+ <!-- \u26A1 Performance & Security -->
28
+ <meta name="format-detection" content="telephone=no" />
29
+ <meta http-equiv="x-ua-compatible" content="IE=edge" />
30
+ ${e.ogImage?`<meta property="og:image" content="${e.ogImage}" />`:""}
31
+ <meta property="og:title" content="${e.title}" />
32
+ <meta property="og:description" content="${a}" />
33
+ <meta property="og:url" content="${r}" />`}function l(e,t,r="WebPage"){let o=e.canonical||`${t.baseUrl}${e.path}`,a={"@context":"https://schema.org","@type":r,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 i(e,t){let r=e.clientScriptPath||t.clientScriptPath;return `<!DOCTYPE html>
34
+ <html lang="en">
35
+ <head>
36
+ ${p(e,t)}
37
+ ${l(e,t,e.contentType==="article"?"Article":"WebPage")}
38
+ </head>
39
+ <body>
40
+ <div id="app"></div>
41
+ <script type="module" src="${r}"></script>
42
+ </body>
43
+ </html>`}function s(e,t){return {method:"GET",path:e.path,handler:r=>{let o=i(e,t);return r.html(o)}}}function P(e,t,r,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),n=i(a,r);return {status:e,headers:{"Content-Type":"text/html; charset=utf-8"},body:n}}return {status:e,headers:{"Content-Type":"text/plain"},body:`Error ${e}`}}function c(e,t){return (r,o)=>{let a=P(r,e,t,o);return new Response(a.body,{status:a.status,headers:a.headers})}}function u(){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 b(e){let t=[],r=new Map;if(e.pages&&e.pages.length>0)for(let n of e.pages)t.push(s(n,e));if(e.errorPages&&e.errorPages.length>0)for(let n of e.errorPages)r.set(n.statusCode,n),t.push(s(n,e));if(e.enableAutoNotFound&&!r.has(404)){let n=u();r.set(404,n),t.push(s(n,e));}let o=c(r,e);return {name:"@cruxplug/SPA",version:"0.1.0",routes:t,__spaErrorHandler:o,onRegister:async n=>{if(console.log(`[SPA Plugin] Registered ${t.length} SPA routes`),r.size>0){let m=Array.from(r.keys()).join(", ");console.log(`[SPA Plugin] Error pages configured for: ${m}`);}},onAwake:async n=>{console.log("[SPA Plugin] Awake phase - SPA routes ready");},onStart:async n=>{console.log("[SPA Plugin] Start phase - serving SPA");},onReady:async n=>{console.log("[SPA Plugin] Ready phase - SPA is fully operational");}}}export{b as serverSPA};//# sourceMappingURL=index.js.map
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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","generateStructuredData","pageConfig","contentType","schema","generateSPAHTML","clientScript","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 <link rel=\"prefetch\" href=\"${config.clientScriptPath || baseConfig.clientScriptPath}\" />\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 entry point\r\n */\r\n export function generateSPAHTML(\r\n pageConfig: SPAPageConfig,\r\n baseConfig: ServerSPAPluginConfig\r\n ): string {\r\n const clientScript = pageConfig.clientScriptPath || baseConfig.clientScriptPath;\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</head>\r\n<body>\r\n <div id=\"app\"></div>\r\n <script type=\"module\" src=\"${clientScript}\"></script>\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 { generateSPAHTML, createSPARoute } from './utils/spa';\r\n import { createErrorHandler, createDefault404Page } from './utils/errors';\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/browser.js',\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// ╚══════════════════════════════════════════════════════════════════════════════════════╝"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@cruxplug/spa",
3
+ "version": "0.0.1",
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
+ "keywords": [
6
+ "cruxjs",
7
+ "plugins",
8
+ "single",
9
+ "page",
10
+ "application",
11
+ "spa"
12
+ ],
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/cruxplug-org/spa#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/cruxplug-org/spa/issues"
17
+ },
18
+ "author": {
19
+ "name": "Maysara",
20
+ "email": "maysara.elshewehy@gmail.com",
21
+ "url": "https://github.com/maysara-elshewehy"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/cruxplug-org/spa.git"
26
+ },
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "files": ["dist"],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "lint": "eslint src --ext .ts",
41
+ "test": "bun test"
42
+ },
43
+ "engines": {
44
+ "bun": ">=1.3.3"
45
+ },
46
+ "peerDependencies": {
47
+ "bun": "^1.3.3"
48
+ },
49
+ "dependencies": {
50
+ "@cruxjs/base": "^0.0.3"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "^9.39.2",
54
+ "@stylistic/eslint-plugin": "^5.6.1",
55
+ "@types/bun": "^1.3.5",
56
+ "@types/node": "^20.19.27",
57
+ "bun-plugin-dts": "^0.3.0",
58
+ "bun-types": "^1.3.5",
59
+ "ts-node": "^10.9.2",
60
+ "tsup": "^8.5.1",
61
+ "typescript": "^5.9.3",
62
+ "typescript-eslint": "^8.52.0"
63
+ }
64
+ }