frontend-hamroun 1.2.24 → 1.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +6 -1
- package/package.json +1 -1
- package/templates/fullstack-app/api/hello.js +11 -0
- package/templates/fullstack-app/index.html +13 -0
- package/templates/fullstack-app/package-lock.json +3130 -0
- package/templates/fullstack-app/package.json +8 -19
- package/templates/fullstack-app/server.js +210 -0
- package/templates/fullstack-app/server.ts +70 -31
- package/templates/fullstack-app/src/client.js +20 -0
- package/templates/fullstack-app/src/main.tsx +9 -0
- package/templates/fullstack-app/src/pages/index.tsx +40 -36
- package/templates/fullstack-app/vite.config.js +40 -0
- package/templates/ssr-template/package-lock.json +95 -1161
- package/templates/ssr-template/package.json +6 -4
- package/templates/ssr-template/readme.md +112 -37
- package/templates/ssr-template/server.js +364 -549
- package/templates/ssr-template/server.ts +166 -14
- package/templates/ssr-template/tsconfig.json +2 -3
@@ -1,577 +1,392 @@
|
|
1
|
-
import
|
1
|
+
import express from 'express';
|
2
2
|
import path from 'path';
|
3
3
|
import { fileURLToPath } from 'url';
|
4
|
+
import fetch from 'node-fetch';
|
5
|
+
import dotenv from 'dotenv';
|
6
|
+
|
7
|
+
// Load environment variables
|
8
|
+
dotenv.config();
|
4
9
|
|
5
10
|
// Get __dirname equivalent in ESM
|
6
11
|
const __filename = fileURLToPath(import.meta.url);
|
7
12
|
const __dirname = path.dirname(__filename);
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
// Initialize express app
|
15
|
+
const app = express();
|
16
|
+
const PORT = process.env.PORT || 3000;
|
17
|
+
|
18
|
+
// Simple API endpoint
|
19
|
+
app.get('/api/hello', (req, res) => {
|
20
|
+
res.json({
|
21
|
+
message: 'Hello from Server-Side API',
|
22
|
+
time: new Date().toISOString()
|
23
|
+
});
|
24
|
+
});
|
25
|
+
|
26
|
+
// Function to generate meta tags locally without requiring OpenAI
|
27
|
+
function generateMetaTagsLocally(pageContent) {
|
28
|
+
// Simple keyword extraction - get common meaningful words
|
29
|
+
const keywordExtraction = (text) => {
|
30
|
+
// Remove common words and extract potential keywords
|
31
|
+
const commonWords = ['a', 'an', 'the', 'and', 'or', 'but', 'is', 'are', 'was', 'were',
|
32
|
+
'has', 'have', 'had', 'be', 'been', 'being', 'to', 'of', 'for', 'with', 'about', 'at'];
|
13
33
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
// Setup authentication
|
24
|
-
auth: {
|
25
|
-
secret: process.env.JWT_SECRET || 'your-development-secret-key',
|
26
|
-
expiresIn: '7d'
|
27
|
-
},
|
28
|
-
|
29
|
-
// Optional database configuration
|
30
|
-
/*
|
31
|
-
db: {
|
32
|
-
url: process.env.DATABASE_URL || 'mongodb://localhost:27017/my_app',
|
33
|
-
type: 'mongodb'
|
34
|
-
}
|
35
|
-
*/
|
34
|
+
const words = text.toLowerCase()
|
35
|
+
.replace(/[^\w\s]/g, '') // Remove punctuation
|
36
|
+
.split(/\s+/) // Split by whitespace
|
37
|
+
.filter(word => word.length > 3 && !commonWords.includes(word)); // Filter short and common words
|
38
|
+
|
39
|
+
// Count word frequency
|
40
|
+
const wordCount = {};
|
41
|
+
words.forEach(word => {
|
42
|
+
wordCount[word] = (wordCount[word] || 0) + 1;
|
36
43
|
});
|
37
44
|
|
38
|
-
//
|
39
|
-
|
45
|
+
// Sort by frequency and get top keywords
|
46
|
+
return Object.entries(wordCount)
|
47
|
+
.sort((a, b) => b[1] - a[1])
|
48
|
+
.slice(0, 5)
|
49
|
+
.map(entry => entry[0])
|
50
|
+
.join(', ');
|
51
|
+
};
|
52
|
+
|
53
|
+
// Extract main topic (first heading or first sentence)
|
54
|
+
const getMainTopic = (text) => {
|
55
|
+
const headingMatch = text.match(/<h1[^>]*>(.*?)<\/h1>/i) ||
|
56
|
+
text.match(/<h2[^>]*>(.*?)<\/h2>/i);
|
40
57
|
|
41
|
-
|
42
|
-
|
58
|
+
if (headingMatch) {
|
59
|
+
return headingMatch[1].trim();
|
60
|
+
}
|
43
61
|
|
44
|
-
//
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
if (!username || !password) {
|
55
|
-
return res.status(400).json({ message: 'Username and password are required' });
|
56
|
-
}
|
57
|
-
|
58
|
-
// Find user
|
59
|
-
const user = users.find(u => u.username === username);
|
60
|
-
|
61
|
-
if (!user) {
|
62
|
-
return res.status(401).json({ message: 'Invalid credentials' });
|
63
|
-
}
|
64
|
-
|
65
|
-
// Authenticate user
|
66
|
-
auth.comparePasswords(password, user.password)
|
67
|
-
.then(isMatch => {
|
68
|
-
if (!isMatch) {
|
69
|
-
return res.status(401).json({ message: 'Invalid credentials' });
|
70
|
-
}
|
71
|
-
|
72
|
-
// Generate token
|
73
|
-
const token = auth.generateToken({
|
74
|
-
id: user.id,
|
75
|
-
username: user.username,
|
76
|
-
roles: user.roles
|
77
|
-
});
|
78
|
-
|
79
|
-
// Return token
|
80
|
-
res.json({
|
81
|
-
token,
|
82
|
-
user: {
|
83
|
-
id: user.id,
|
84
|
-
username: user.username,
|
85
|
-
roles: user.roles
|
86
|
-
}
|
87
|
-
});
|
88
|
-
})
|
89
|
-
.catch(err => {
|
90
|
-
console.error('Auth error:', err);
|
91
|
-
res.status(500).json({ message: 'Authentication error' });
|
92
|
-
});
|
93
|
-
});
|
62
|
+
// If no heading, use first sentence
|
63
|
+
const firstSentence = text.split(/[.!?]/).filter(s => s.trim().length > 0)[0];
|
64
|
+
return firstSentence ? firstSentence.trim() : "Frontend Hamroun SSR Page";
|
65
|
+
};
|
66
|
+
|
67
|
+
// Generate description (first few sentences, truncated)
|
68
|
+
const getDescription = (text) => {
|
69
|
+
const plainText = text.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
70
|
+
const sentences = plainText.split(/[.!?]/).filter(s => s.trim().length > 0);
|
71
|
+
const description = sentences.slice(0, 2).join('. ');
|
94
72
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
73
|
+
return description.length > 160
|
74
|
+
? description.substring(0, 157) + '...'
|
75
|
+
: description;
|
76
|
+
};
|
77
|
+
|
78
|
+
// Generate the meta tags
|
79
|
+
const title = getMainTopic(pageContent);
|
80
|
+
const description = getDescription(pageContent);
|
81
|
+
const keywords = keywordExtraction(pageContent);
|
82
|
+
|
83
|
+
return {
|
84
|
+
title: title || 'Frontend Hamroun SSR App',
|
85
|
+
description: description || 'A server-side rendered application using Frontend Hamroun framework',
|
86
|
+
keywords: keywords || 'ssr, javascript, frontend, hamroun, web development'
|
87
|
+
};
|
88
|
+
}
|
89
|
+
|
90
|
+
// Function to generate meta tags - using Gemini with local fallback
|
91
|
+
async function generateMetaTags(pageContent) {
|
92
|
+
// Remove forcing local generation
|
93
|
+
process.env.USE_LOCAL_GENERATION = 'false';
|
94
|
+
|
95
|
+
// Check if local generation is forced
|
96
|
+
if (process.env.USE_LOCAL_GENERATION === 'true') {
|
97
|
+
console.log('Using local meta tag generation (forced by config)');
|
98
|
+
return generateMetaTagsLocally(pageContent);
|
99
|
+
}
|
100
|
+
|
101
|
+
try {
|
102
|
+
console.log('Attempting to generate meta tags with Gemini AI...');
|
103
|
+
return await generateMetaTagsWithGemini(pageContent);
|
104
|
+
} catch (error) {
|
105
|
+
console.error('Error generating meta tags with Gemini, falling back to local generation:', error);
|
106
|
+
return generateMetaTagsLocally(pageContent);
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
// Function to generate meta tags using Google's Gemini API
|
111
|
+
async function generateMetaTagsWithGemini(pageContent) {
|
112
|
+
if (!process.env.GEMINI_API_KEY) {
|
113
|
+
console.log('Gemini API key not found. Using local meta tag generation.');
|
114
|
+
return generateMetaTagsLocally(pageContent);
|
115
|
+
}
|
116
|
+
|
117
|
+
try {
|
118
|
+
console.log('Connecting to Gemini AI...');
|
119
|
+
// Use the correct model as indicated in the working curl example
|
120
|
+
const endpoints = [
|
121
|
+
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'
|
122
|
+
];
|
103
123
|
|
104
|
-
|
105
|
-
|
106
|
-
res.json({
|
107
|
-
message: 'Admin-only data',
|
108
|
-
user: req.user,
|
109
|
-
timestamp: new Date().toISOString()
|
110
|
-
});
|
111
|
-
});
|
124
|
+
let response = null;
|
125
|
+
let responseData = null;
|
112
126
|
|
113
|
-
//
|
114
|
-
|
127
|
+
// Try the endpoint
|
128
|
+
for (const url of endpoints) {
|
115
129
|
try {
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
{
|
123
|
-
type: 'header',
|
124
|
-
props: {
|
125
|
-
className: 'header',
|
126
|
-
children: [
|
127
|
-
{
|
128
|
-
type: 'h1',
|
129
|
-
props: { children: 'Frontend Hamroun Full-Stack App' }
|
130
|
-
},
|
131
|
-
{
|
132
|
-
type: 'nav',
|
133
|
-
props: {
|
134
|
-
children: [
|
135
|
-
{ type: 'a', props: { href: '/', children: 'Home' } },
|
136
|
-
{ type: 'span', props: { children: ' | ' } },
|
137
|
-
{ type: 'a', props: { href: '/about', children: 'About' } },
|
138
|
-
{ type: 'span', props: { children: ' | ' } },
|
139
|
-
{ type: 'a', props: { href: '#login', id: 'login-link', children: 'Login' } }
|
140
|
-
]
|
141
|
-
}
|
142
|
-
}
|
143
|
-
]
|
144
|
-
}
|
145
|
-
},
|
146
|
-
{
|
147
|
-
type: 'main',
|
148
|
-
props: {
|
149
|
-
children: [
|
150
|
-
{
|
151
|
-
type: 'section',
|
152
|
-
props: {
|
153
|
-
className: 'hero',
|
154
|
-
children: [
|
155
|
-
{
|
156
|
-
type: 'h2',
|
157
|
-
props: { children: 'Server-Side Rendering with Authentication' }
|
158
|
-
},
|
159
|
-
{
|
160
|
-
type: 'p',
|
161
|
-
props: { children: `This page was rendered on the server at ${new Date().toISOString()}` }
|
162
|
-
}
|
163
|
-
]
|
164
|
-
}
|
165
|
-
},
|
166
|
-
{
|
167
|
-
type: 'section',
|
168
|
-
props: {
|
169
|
-
id: 'login-form',
|
170
|
-
className: 'login-form hidden',
|
171
|
-
children: [
|
172
|
-
{
|
173
|
-
type: 'h3',
|
174
|
-
props: { children: 'Login' }
|
175
|
-
},
|
176
|
-
{
|
177
|
-
type: 'div',
|
178
|
-
props: {
|
179
|
-
className: 'form-group',
|
180
|
-
children: [
|
181
|
-
{
|
182
|
-
type: 'label',
|
183
|
-
props: { htmlFor: 'username', children: 'Username:' }
|
184
|
-
},
|
185
|
-
{
|
186
|
-
type: 'input',
|
187
|
-
props: { id: 'username', type: 'text', placeholder: 'admin or user' }
|
188
|
-
}
|
189
|
-
]
|
190
|
-
}
|
191
|
-
},
|
192
|
-
{
|
193
|
-
type: 'div',
|
194
|
-
props: {
|
195
|
-
className: 'form-group',
|
196
|
-
children: [
|
197
|
-
{
|
198
|
-
type: 'label',
|
199
|
-
props: { htmlFor: 'password', children: 'Password:' }
|
200
|
-
},
|
201
|
-
{
|
202
|
-
type: 'input',
|
203
|
-
props: { id: 'password', type: 'password', placeholder: 'admin123 or user123' }
|
204
|
-
}
|
205
|
-
]
|
206
|
-
}
|
207
|
-
},
|
208
|
-
{
|
209
|
-
type: 'button',
|
210
|
-
props: { id: 'login-button', className: 'btn', children: 'Login' }
|
211
|
-
},
|
212
|
-
{
|
213
|
-
type: 'div',
|
214
|
-
props: { id: 'login-message', className: 'message' }
|
215
|
-
}
|
216
|
-
]
|
217
|
-
}
|
218
|
-
},
|
219
|
-
{
|
220
|
-
type: 'section',
|
221
|
-
props: {
|
222
|
-
id: 'protected-content',
|
223
|
-
className: 'protected-content hidden',
|
224
|
-
children: [
|
225
|
-
{
|
226
|
-
type: 'h3',
|
227
|
-
props: { children: 'Protected Content' }
|
228
|
-
},
|
229
|
-
{
|
230
|
-
type: 'div',
|
231
|
-
props: { id: 'protected-data', className: 'data-container' }
|
232
|
-
},
|
233
|
-
{
|
234
|
-
type: 'button',
|
235
|
-
props: { id: 'load-protected', className: 'btn', children: 'Load Protected Data' }
|
236
|
-
}
|
237
|
-
]
|
238
|
-
}
|
239
|
-
},
|
240
|
-
{
|
241
|
-
type: 'section',
|
242
|
-
props: {
|
243
|
-
id: 'admin-content',
|
244
|
-
className: 'admin-content hidden',
|
245
|
-
children: [
|
246
|
-
{
|
247
|
-
type: 'h3',
|
248
|
-
props: { children: 'Admin Content' }
|
249
|
-
},
|
250
|
-
{
|
251
|
-
type: 'div',
|
252
|
-
props: { id: 'admin-data', className: 'data-container' }
|
253
|
-
},
|
254
|
-
{
|
255
|
-
type: 'button',
|
256
|
-
props: { id: 'load-admin', className: 'btn', children: 'Load Admin Data' }
|
257
|
-
}
|
258
|
-
]
|
259
|
-
}
|
260
|
-
}
|
261
|
-
]
|
262
|
-
}
|
263
|
-
},
|
130
|
+
console.log(`Trying Gemini endpoint: ${url}`);
|
131
|
+
|
132
|
+
const requestBody = {
|
133
|
+
contents: [{
|
134
|
+
parts: [{
|
135
|
+
text: `Generate SEO-friendly meta tags as JSON with the following format:
|
264
136
|
{
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
{
|
269
|
-
type: 'p',
|
270
|
-
props: { children: '© 2023 Frontend Hamroun' }
|
271
|
-
}
|
272
|
-
]
|
273
|
-
}
|
137
|
+
"title": "A concise and engaging title",
|
138
|
+
"description": "A compelling description under 160 characters",
|
139
|
+
"keywords": "keyword1, keyword2, keyword3, keyword4, keyword5"
|
274
140
|
}
|
275
|
-
|
276
|
-
|
141
|
+
|
142
|
+
The meta tags should be based on this content: ${pageContent}`
|
143
|
+
}]
|
144
|
+
}]
|
277
145
|
};
|
278
|
-
|
279
|
-
// Generate HTML from our virtual node
|
280
|
-
const content = await renderToString(vnode);
|
281
146
|
|
282
|
-
//
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
color: white;
|
313
|
-
padding: 10px 20px;
|
314
|
-
cursor: pointer;
|
315
|
-
border-radius: 4px;
|
316
|
-
margin-top: 1rem;
|
317
|
-
}
|
318
|
-
.hidden {
|
319
|
-
display: none;
|
320
|
-
}
|
321
|
-
.form-group {
|
322
|
-
margin-bottom: 1rem;
|
323
|
-
}
|
324
|
-
.form-group label {
|
325
|
-
display: block;
|
326
|
-
margin-bottom: 0.5rem;
|
327
|
-
}
|
328
|
-
.form-group input {
|
329
|
-
padding: 0.5rem;
|
330
|
-
width: 100%;
|
331
|
-
max-width: 300px;
|
332
|
-
border: 1px solid #ddd;
|
333
|
-
border-radius: 4px;
|
334
|
-
}
|
335
|
-
.message {
|
336
|
-
margin-top: 1rem;
|
337
|
-
padding: 0.5rem;
|
338
|
-
}
|
339
|
-
.success {
|
340
|
-
color: green;
|
341
|
-
background-color: #e8f5e9;
|
342
|
-
}
|
343
|
-
.error {
|
344
|
-
color: red;
|
345
|
-
background-color: #ffebee;
|
346
|
-
}
|
347
|
-
.data-container {
|
348
|
-
background-color: #f5f5f5;
|
349
|
-
padding: 1rem;
|
350
|
-
border-radius: 4px;
|
351
|
-
margin: 1rem 0;
|
352
|
-
min-height: 100px;
|
353
|
-
}
|
354
|
-
footer {
|
355
|
-
margin-top: 2rem;
|
356
|
-
padding: 1rem 0;
|
357
|
-
border-top: 1px solid #eee;
|
358
|
-
text-align: center;
|
359
|
-
}
|
360
|
-
</style>
|
361
|
-
<script>
|
362
|
-
// Client-side JavaScript for interactivity
|
363
|
-
document.addEventListener('DOMContentLoaded', () => {
|
364
|
-
let token = localStorage.getItem('token');
|
365
|
-
const user = JSON.parse(localStorage.getItem('user') || 'null');
|
366
|
-
|
367
|
-
// Elements
|
368
|
-
const loginLink = document.getElementById('login-link');
|
369
|
-
const loginForm = document.getElementById('login-form');
|
370
|
-
const loginButton = document.getElementById('login-button');
|
371
|
-
const loginMessage = document.getElementById('login-message');
|
372
|
-
const usernameInput = document.getElementById('username');
|
373
|
-
const passwordInput = document.getElementById('password');
|
374
|
-
const protectedContent = document.getElementById('protected-content');
|
375
|
-
const adminContent = document.getElementById('admin-content');
|
376
|
-
const loadProtectedBtn = document.getElementById('load-protected');
|
377
|
-
const loadAdminBtn = document.getElementById('load-admin');
|
378
|
-
const protectedData = document.getElementById('protected-data');
|
379
|
-
const adminData = document.getElementById('admin-data');
|
380
|
-
|
381
|
-
// Check if user is logged in
|
382
|
-
if (token && user) {
|
383
|
-
loginLink.textContent = \`Logout (\${user.username})\`;
|
384
|
-
protectedContent.classList.remove('hidden');
|
385
|
-
|
386
|
-
// Show admin content if user has admin role
|
387
|
-
if (user.roles && user.roles.includes('admin')) {
|
388
|
-
adminContent.classList.remove('hidden');
|
389
|
-
}
|
390
|
-
}
|
391
|
-
|
392
|
-
// Toggle login form
|
393
|
-
loginLink.addEventListener('click', (e) => {
|
394
|
-
e.preventDefault();
|
395
|
-
|
396
|
-
if (token) {
|
397
|
-
// Logout
|
398
|
-
localStorage.removeItem('token');
|
399
|
-
localStorage.removeItem('user');
|
400
|
-
token = null;
|
401
|
-
loginLink.textContent = 'Login';
|
402
|
-
protectedContent.classList.add('hidden');
|
403
|
-
adminContent.classList.add('hidden');
|
404
|
-
loginMessage.textContent = '';
|
405
|
-
loginMessage.className = 'message';
|
406
|
-
} else {
|
407
|
-
// Show login form
|
408
|
-
loginForm.classList.toggle('hidden');
|
409
|
-
}
|
410
|
-
});
|
411
|
-
|
412
|
-
// Login form submission
|
413
|
-
loginButton.addEventListener('click', async () => {
|
414
|
-
const username = usernameInput.value.trim();
|
415
|
-
const password = passwordInput.value.trim();
|
416
|
-
|
417
|
-
if (!username || !password) {
|
418
|
-
loginMessage.textContent = 'Please enter both username and password';
|
419
|
-
loginMessage.className = 'message error';
|
420
|
-
return;
|
421
|
-
}
|
422
|
-
|
423
|
-
try {
|
424
|
-
const response = await fetch('/api/auth/login', {
|
425
|
-
method: 'POST',
|
426
|
-
headers: {
|
427
|
-
'Content-Type': 'application/json'
|
428
|
-
},
|
429
|
-
body: JSON.stringify({ username, password })
|
430
|
-
});
|
431
|
-
|
432
|
-
const data = await response.json();
|
433
|
-
|
434
|
-
if (response.ok) {
|
435
|
-
token = data.token;
|
436
|
-
localStorage.setItem('token', token);
|
437
|
-
localStorage.setItem('user', JSON.stringify(data.user));
|
438
|
-
|
439
|
-
loginMessage.textContent = 'Login successful';
|
440
|
-
loginMessage.className = 'message success';
|
441
|
-
|
442
|
-
// Update UI
|
443
|
-
loginLink.textContent = \`Logout (\${data.user.username})\`;
|
444
|
-
protectedContent.classList.remove('hidden');
|
445
|
-
|
446
|
-
// Show admin content if user has admin role
|
447
|
-
if (data.user.roles && data.user.roles.includes('admin')) {
|
448
|
-
adminContent.classList.remove('hidden');
|
449
|
-
}
|
450
|
-
|
451
|
-
// Clear form
|
452
|
-
usernameInput.value = '';
|
453
|
-
passwordInput.value = '';
|
454
|
-
|
455
|
-
// Hide login form after successful login
|
456
|
-
setTimeout(() => {
|
457
|
-
loginForm.classList.add('hidden');
|
458
|
-
}, 1500);
|
459
|
-
} else {
|
460
|
-
loginMessage.textContent = data.message || 'Login failed';
|
461
|
-
loginMessage.className = 'message error';
|
462
|
-
}
|
463
|
-
} catch (error) {
|
464
|
-
console.error('Login error:', error);
|
465
|
-
loginMessage.textContent = 'An error occurred while logging in';
|
466
|
-
loginMessage.className = 'message error';
|
467
|
-
}
|
468
|
-
});
|
469
|
-
|
470
|
-
// Load protected data
|
471
|
-
loadProtectedBtn.addEventListener('click', async () => {
|
472
|
-
if (!token) {
|
473
|
-
protectedData.textContent = 'You must be logged in to view this data';
|
474
|
-
return;
|
475
|
-
}
|
476
|
-
|
477
|
-
try {
|
478
|
-
const response = await fetch('/api/protected', {
|
479
|
-
headers: {
|
480
|
-
'Authorization': \`Bearer \${token}\`
|
481
|
-
}
|
482
|
-
});
|
483
|
-
|
484
|
-
if (response.ok) {
|
485
|
-
const data = await response.json();
|
486
|
-
protectedData.textContent = JSON.stringify(data, null, 2);
|
487
|
-
} else {
|
488
|
-
protectedData.textContent = 'Failed to load protected data. Your session may have expired.';
|
489
|
-
|
490
|
-
if (response.status === 401) {
|
491
|
-
// Session expired, clear localStorage
|
492
|
-
localStorage.removeItem('token');
|
493
|
-
localStorage.removeItem('user');
|
494
|
-
token = null;
|
495
|
-
}
|
496
|
-
}
|
497
|
-
} catch (error) {
|
498
|
-
console.error('API error:', error);
|
499
|
-
protectedData.textContent = 'An error occurred while fetching data';
|
500
|
-
}
|
501
|
-
});
|
502
|
-
|
503
|
-
// Load admin data
|
504
|
-
loadAdminBtn.addEventListener('click', async () => {
|
505
|
-
if (!token) {
|
506
|
-
adminData.textContent = 'You must be logged in to view this data';
|
507
|
-
return;
|
508
|
-
}
|
509
|
-
|
510
|
-
try {
|
511
|
-
const response = await fetch('/api/admin', {
|
512
|
-
headers: {
|
513
|
-
'Authorization': \`Bearer \${token}\`
|
514
|
-
}
|
515
|
-
});
|
516
|
-
|
517
|
-
if (response.ok) {
|
518
|
-
const data = await response.json();
|
519
|
-
adminData.textContent = JSON.stringify(data, null, 2);
|
520
|
-
} else if (response.status === 403) {
|
521
|
-
adminData.textContent = 'You do not have permission to access this data. Admin role required.';
|
522
|
-
} else {
|
523
|
-
adminData.textContent = 'Failed to load admin data. Your session may have expired.';
|
524
|
-
}
|
525
|
-
} catch (error) {
|
526
|
-
console.error('API error:', error);
|
527
|
-
adminData.textContent = 'An error occurred while fetching data';
|
528
|
-
}
|
529
|
-
});
|
530
|
-
});
|
531
|
-
</script>
|
532
|
-
</head>
|
533
|
-
<body>${content}</body>
|
534
|
-
</html>
|
535
|
-
`);
|
536
|
-
} catch (error) {
|
537
|
-
console.error('SSR Error:', error);
|
538
|
-
res.status(500).send(`
|
539
|
-
<!DOCTYPE html>
|
540
|
-
<html>
|
541
|
-
<head>
|
542
|
-
<title>Server Error</title>
|
543
|
-
<style>body { font-family: sans-serif; padding: 2rem; }</style>
|
544
|
-
</head>
|
545
|
-
<body>
|
546
|
-
<h1>Server Error</h1>
|
547
|
-
<p>An error occurred while rendering the page.</p>
|
548
|
-
<pre>${error.stack}</pre>
|
549
|
-
</body>
|
550
|
-
</html>
|
551
|
-
`);
|
147
|
+
// Log the exact request for debugging
|
148
|
+
console.log('Sending request to Gemini:', JSON.stringify(requestBody, null, 2).substring(0, 150) + '...');
|
149
|
+
|
150
|
+
response = await fetch(`${url}?key=${process.env.GEMINI_API_KEY}`, {
|
151
|
+
method: 'POST',
|
152
|
+
headers: {
|
153
|
+
'Content-Type': 'application/json'
|
154
|
+
},
|
155
|
+
body: JSON.stringify(requestBody)
|
156
|
+
});
|
157
|
+
|
158
|
+
// Get the response data
|
159
|
+
responseData = await response.text();
|
160
|
+
|
161
|
+
// Try to parse it as JSON
|
162
|
+
try {
|
163
|
+
responseData = JSON.parse(responseData);
|
164
|
+
|
165
|
+
if (response.ok) {
|
166
|
+
console.log('Successfully received response from Gemini');
|
167
|
+
break;
|
168
|
+
} else {
|
169
|
+
console.error('Gemini API error response:', responseData);
|
170
|
+
}
|
171
|
+
} catch (parseError) {
|
172
|
+
console.error('Failed to parse Gemini response as JSON:', responseData.substring(0, 150));
|
173
|
+
throw parseError;
|
174
|
+
}
|
175
|
+
} catch (err) {
|
176
|
+
console.log(`Endpoint ${url} failed:`, err.message);
|
552
177
|
}
|
553
|
-
}
|
178
|
+
}
|
554
179
|
|
555
|
-
|
556
|
-
|
180
|
+
if (!response || !response.ok) {
|
181
|
+
throw new Error('Failed to get valid response from Gemini API');
|
182
|
+
}
|
183
|
+
|
184
|
+
// Extract the text response - using the correct response format for gemini-2.0-flash
|
185
|
+
const textResponse = responseData.candidates[0].content.parts[0].text;
|
557
186
|
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
187
|
+
// Extract the JSON object from the text response
|
188
|
+
const jsonMatch = textResponse.match(/\{[\s\S]*\}/);
|
189
|
+
if (!jsonMatch) {
|
190
|
+
console.error('Could not find JSON in response:', textResponse);
|
191
|
+
throw new Error('Could not parse JSON from Gemini response');
|
192
|
+
}
|
564
193
|
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
194
|
+
try {
|
195
|
+
const metaTagsJson = jsonMatch[0];
|
196
|
+
const metaTags = JSON.parse(metaTagsJson);
|
197
|
+
console.log('Generated meta tags using Gemini:', metaTags);
|
198
|
+
|
199
|
+
return metaTags;
|
200
|
+
} catch (jsonError) {
|
201
|
+
console.error('Failed to parse extracted JSON:', jsonMatch[0]);
|
202
|
+
throw jsonError;
|
203
|
+
}
|
571
204
|
} catch (error) {
|
572
|
-
console.error('
|
573
|
-
|
205
|
+
console.error('Error generating meta tags with Gemini:', error);
|
206
|
+
throw error;
|
574
207
|
}
|
575
208
|
}
|
576
209
|
|
577
|
-
|
210
|
+
// Basic server-side rendering implementation
|
211
|
+
app.get('/', async (req, res) => {
|
212
|
+
console.log('Handling root route for SSR');
|
213
|
+
try {
|
214
|
+
// Import the renderToString function
|
215
|
+
const frontendLib = await import('frontend-hamroun');
|
216
|
+
|
217
|
+
// Create a simple virtual DOM tree
|
218
|
+
const vnode = {
|
219
|
+
type: 'div',
|
220
|
+
props: {
|
221
|
+
id: 'app',
|
222
|
+
children: [
|
223
|
+
{
|
224
|
+
type: 'h1',
|
225
|
+
props: {
|
226
|
+
children: 'Hello from Server-Side Rendering!'
|
227
|
+
}
|
228
|
+
},
|
229
|
+
{
|
230
|
+
type: 'p',
|
231
|
+
props: {
|
232
|
+
children: `This page was rendered at ${new Date().toISOString()}`
|
233
|
+
}
|
234
|
+
},
|
235
|
+
{
|
236
|
+
type: 'button',
|
237
|
+
props: {
|
238
|
+
id: 'counter-btn',
|
239
|
+
className: 'btn',
|
240
|
+
children: 'Click me (0)'
|
241
|
+
}
|
242
|
+
}
|
243
|
+
]
|
244
|
+
}
|
245
|
+
};
|
246
|
+
|
247
|
+
// Generate content for meta tag creation
|
248
|
+
const contentForMetaTags = 'Server-side rendered page using Frontend Hamroun framework. ' +
|
249
|
+
'This demonstrates SSR capabilities with dynamic content generation and client-side hydration.';
|
250
|
+
|
251
|
+
console.log('Fetching meta tags for the page using Gemini 2.0 Flash...');
|
252
|
+
const metaTags = await generateMetaTags(contentForMetaTags);
|
253
|
+
|
254
|
+
// The renderToString function exists and works properly
|
255
|
+
console.log('renderToString type:', typeof frontendLib.renderToString);
|
256
|
+
|
257
|
+
// Generate HTML from our virtual node
|
258
|
+
let content;
|
259
|
+
if (typeof frontendLib.renderToString === 'function') {
|
260
|
+
// Call the function
|
261
|
+
const renderResult = frontendLib.renderToString(vnode);
|
262
|
+
|
263
|
+
// If it returns a promise, await it
|
264
|
+
if (renderResult instanceof Promise) {
|
265
|
+
console.log('renderToString returned a Promise, awaiting it');
|
266
|
+
content = await renderResult;
|
267
|
+
} else {
|
268
|
+
console.log('renderToString returned directly:', typeof renderResult);
|
269
|
+
content = renderResult;
|
270
|
+
}
|
271
|
+
} else {
|
272
|
+
throw new Error('renderToString is not a function');
|
273
|
+
}
|
274
|
+
|
275
|
+
// Log the content to verify it's not empty or still a Promise
|
276
|
+
console.log('Final content type:', typeof content);
|
277
|
+
console.log('Content preview:', content ? content.substring(0, 100) : 'empty');
|
278
|
+
|
279
|
+
// Send complete HTML document with explicit content type
|
280
|
+
res.setHeader('Content-Type', 'text/html');
|
281
|
+
res.send(`
|
282
|
+
<!DOCTYPE html>
|
283
|
+
<html>
|
284
|
+
<head>
|
285
|
+
<meta charset="UTF-8">
|
286
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
287
|
+
|
288
|
+
<!-- AI Generated Meta Tags -->
|
289
|
+
<title>${metaTags.title}</title>
|
290
|
+
<meta name="description" content="${metaTags.description}">
|
291
|
+
<meta name="keywords" content="${metaTags.keywords}">
|
292
|
+
|
293
|
+
<!-- Open Graph tags -->
|
294
|
+
<meta property="og:title" content="${metaTags.title}">
|
295
|
+
<meta property="og:description" content="${metaTags.description}">
|
296
|
+
<meta property="og:type" content="website">
|
297
|
+
<meta property="og:url" content="${req.protocol}://${req.get('host')}${req.originalUrl}">
|
298
|
+
|
299
|
+
<!-- Twitter Card tags -->
|
300
|
+
<meta name="twitter:card" content="summary_large_image">
|
301
|
+
<meta name="twitter:title" content="${metaTags.title}">
|
302
|
+
<meta name="twitter:description" content="${metaTags.description}">
|
303
|
+
|
304
|
+
<style>
|
305
|
+
body {
|
306
|
+
font-family: sans-serif;
|
307
|
+
max-width: 800px;
|
308
|
+
margin: 0 auto;
|
309
|
+
padding: 2rem;
|
310
|
+
}
|
311
|
+
.btn {
|
312
|
+
background-color: #4CAF50;
|
313
|
+
border: none;
|
314
|
+
color: white;
|
315
|
+
padding: 10px 20px;
|
316
|
+
cursor: pointer;
|
317
|
+
border-radius: 4px;
|
318
|
+
margin-top: 1rem;
|
319
|
+
}
|
320
|
+
</style>
|
321
|
+
<script>
|
322
|
+
// Simple client-side interactivity
|
323
|
+
document.addEventListener('DOMContentLoaded', () => {
|
324
|
+
const btn = document.getElementById('counter-btn');
|
325
|
+
if (btn) {
|
326
|
+
let count = 0;
|
327
|
+
btn.addEventListener('click', () => {
|
328
|
+
count++;
|
329
|
+
btn.textContent = \`Click me (\${count})\`;
|
330
|
+
});
|
331
|
+
console.log('Button click handler attached');
|
332
|
+
}
|
333
|
+
});
|
334
|
+
</script>
|
335
|
+
</head>
|
336
|
+
<body>${content || '<div>Error: No content generated</div>'}</body>
|
337
|
+
</html>
|
338
|
+
`);
|
339
|
+
} catch (error) {
|
340
|
+
console.error('SSR Error:', error);
|
341
|
+
|
342
|
+
// Fallback HTML with error details
|
343
|
+
res.status(500).send(`
|
344
|
+
<!DOCTYPE html>
|
345
|
+
<html>
|
346
|
+
<head>
|
347
|
+
<title>SSR Error</title>
|
348
|
+
<style>
|
349
|
+
body { font-family: sans-serif; padding: 2rem; }
|
350
|
+
pre { background: #f5f5f5; padding: 1rem; overflow: auto; }
|
351
|
+
</style>
|
352
|
+
</head>
|
353
|
+
<body>
|
354
|
+
<h1>Server-Side Rendering Error</h1>
|
355
|
+
<p>There was a problem rendering the page.</p>
|
356
|
+
<pre>${error.stack}</pre>
|
357
|
+
<p>Try refreshing the page or contact the administrator if the problem persists.</p>
|
358
|
+
</body>
|
359
|
+
</html>
|
360
|
+
`);
|
361
|
+
}
|
362
|
+
});
|
363
|
+
|
364
|
+
// Serve static files AFTER routes that need SSR
|
365
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
366
|
+
|
367
|
+
// A catch-all route for any other requests
|
368
|
+
app.get('*', (req, res) => {
|
369
|
+
console.log(`Handling catch-all route: ${req.path}`);
|
370
|
+
res.status(404).send(`
|
371
|
+
<!DOCTYPE html>
|
372
|
+
<html>
|
373
|
+
<head>
|
374
|
+
<title>Page Not Found</title>
|
375
|
+
<style>
|
376
|
+
body { font-family: sans-serif; padding: 2rem; }
|
377
|
+
</style>
|
378
|
+
</head>
|
379
|
+
<body>
|
380
|
+
<h1>Page Not Found</h1>
|
381
|
+
<p>The page you requested does not exist.</p>
|
382
|
+
<p><a href="/">Go to home page</a></p>
|
383
|
+
</body>
|
384
|
+
</html>
|
385
|
+
`);
|
386
|
+
});
|
387
|
+
|
388
|
+
// Start the server
|
389
|
+
app.listen(PORT, () => {
|
390
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
391
|
+
console.log(`Open your browser and navigate to http://localhost:${PORT}`);
|
392
|
+
});
|