frontend-hamroun 1.2.24 → 1.2.25

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.
@@ -1,577 +1,392 @@
1
- import { server } from 'frontend-hamroun';
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
- async function startServer() {
10
- try {
11
- // Import server components dynamically
12
- const { Server, AuthService, Database, renderToString } = await server.getServer();
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
- // Create the server instance
15
- const app = new Server({
16
- port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
17
- apiDir: './api',
18
- staticDir: './public',
19
-
20
- // Enable CORS for API endpoints
21
- enableCors: true,
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
- // Get the Express app instance to add custom routes
39
- const expressApp = app.getExpressApp();
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
- // Get the auth service
42
- const auth = app.getAuth();
58
+ if (headingMatch) {
59
+ return headingMatch[1].trim();
60
+ }
43
61
 
44
- // Add authentication routes
45
- expressApp.post('/api/auth/login', (req, res) => {
46
- // Mock users for demonstration
47
- const users = [
48
- { id: 1, username: 'admin', password: '$2a$10$eBMQ3PFvX0G5a.U/zOA8Y.3JHZ5Ktq0hTjh4n2mjvXcOt6CvTxAZ2', roles: ['admin'] }, // password: admin123
49
- { id: 2, username: 'user', password: '$2a$10$h6y7r0WSRO.BT9WBnTJj1udTI5OtMBdHcQ3/xNW7UQ1WXzMpYvNHe', roles: ['user'] } // password: user123
50
- ];
51
-
52
- const { username, password } = req.body;
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
- // Protected API route example
96
- expressApp.get('/api/protected', auth.requireAuth(), (req, res) => {
97
- res.json({
98
- message: 'This is protected data',
99
- user: req.user,
100
- timestamp: new Date().toISOString()
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
- // Admin-only route example
105
- expressApp.get('/api/admin', auth.requireRoles(['admin']), (req, res) => {
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
- // Override the default route handler with our SSR implementation
114
- expressApp.get('/', async (req, res) => {
127
+ // Try the endpoint
128
+ for (const url of endpoints) {
115
129
  try {
116
- // Create a virtual DOM tree with more complex structure
117
- const vnode = {
118
- type: 'div',
119
- props: {
120
- id: 'app',
121
- children: [
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
- type: 'footer',
266
- props: {
267
- children: [
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
- // Send complete HTML document
283
- res.send(`
284
- <!DOCTYPE html>
285
- <html>
286
- <head>
287
- <meta charset="UTF-8">
288
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
289
- <title>Frontend Hamroun Full-Stack</title>
290
- <style>
291
- body {
292
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
293
- line-height: 1.6;
294
- color: #333;
295
- max-width: 1200px;
296
- margin: 0 auto;
297
- padding: 0 1rem;
298
- }
299
- .header {
300
- display: flex;
301
- justify-content: space-between;
302
- align-items: center;
303
- padding: 1rem 0;
304
- border-bottom: 1px solid #eee;
305
- }
306
- .hero {
307
- padding: 2rem 0;
308
- }
309
- .btn {
310
- background-color: #4CAF50;
311
- border: none;
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
- // Start the server
556
- await app.start();
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
- console.log(`Server running at http://localhost:${app.config.port}`);
559
- console.log('Available routes:');
560
- console.log('- / (Home page with SSR)');
561
- console.log('- /api/auth/login (POST: Login endpoint)');
562
- console.log('- /api/protected (GET: Protected data, requires authentication)');
563
- console.log('- /api/admin (GET: Admin data, requires admin role)');
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
- // Handle graceful shutdown
566
- process.on('SIGINT', async () => {
567
- console.log('Shutting down server...');
568
- await app.stop();
569
- process.exit(0);
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('Failed to start server:', error);
573
- process.exit(1);
205
+ console.error('Error generating meta tags with Gemini:', error);
206
+ throw error;
574
207
  }
575
208
  }
576
209
 
577
- startServer();
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
+ });