@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 +21 -0
- package/README.md +283 -0
- package/dist/index.cjs +44 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
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":"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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 <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/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|