bertui 0.3.5 → 0.3.6
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/package.json +1 -1
- package/src/build.js +93 -55
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -32,15 +32,16 @@ export async function buildProduction(options = {}) {
|
|
|
32
32
|
logger.info('Step 2: Building CSS with Lightning CSS...');
|
|
33
33
|
await buildAllCSS(root, outDir);
|
|
34
34
|
|
|
35
|
-
|
|
36
35
|
const publicDir = join(root, 'public');
|
|
37
36
|
if (existsSync(publicDir)) {
|
|
38
37
|
logger.info('Step 3: Copying public assets...');
|
|
39
38
|
const publicFiles = readdirSync(publicDir);
|
|
40
39
|
for (const file of publicFiles) {
|
|
41
40
|
const srcFile = join(publicDir, file);
|
|
42
|
-
const destFile = join(outDir, file);
|
|
43
|
-
|
|
41
|
+
const destFile = join(outDir, file);
|
|
42
|
+
if (statSync(srcFile).isFile()) {
|
|
43
|
+
cpSync(srcFile, destFile);
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
46
|
logger.success('Public assets copied');
|
|
46
47
|
} else {
|
|
@@ -78,7 +79,7 @@ export async function buildProduction(options = {}) {
|
|
|
78
79
|
|
|
79
80
|
logger.success('JavaScript bundled with tree-shaking');
|
|
80
81
|
|
|
81
|
-
logger.info('Step 5: Generating HTML files
|
|
82
|
+
logger.info('Step 5: Generating SEO-optimized HTML files...');
|
|
82
83
|
await generateProductionHTML(root, outDir, result, routes);
|
|
83
84
|
|
|
84
85
|
rmSync(buildDir, { recursive: true });
|
|
@@ -447,80 +448,64 @@ function fixRelativeImports(code) {
|
|
|
447
448
|
return code;
|
|
448
449
|
}
|
|
449
450
|
|
|
450
|
-
//
|
|
451
|
-
|
|
451
|
+
// IMPROVED: Extract meta using regex (works on raw source code)
|
|
452
|
+
function extractMetaFromSource(code) {
|
|
452
453
|
try {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
// Match: export const meta = { ... }
|
|
456
|
-
const metaRegex = /export\s+const\s+meta\s*=\s*(\{[\s\S]*?\});/;
|
|
454
|
+
// Match: export const meta = { ... };
|
|
455
|
+
const metaRegex = /export\s+const\s+meta\s*=\s*\{([^}]+)\}/s;
|
|
457
456
|
const match = code.match(metaRegex);
|
|
458
457
|
|
|
459
458
|
if (!match) return null;
|
|
460
459
|
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
460
|
+
const metaContent = match[1];
|
|
461
|
+
const meta = {};
|
|
462
|
+
|
|
463
|
+
// Extract key-value pairs
|
|
464
|
+
const pairs = metaContent.match(/(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g);
|
|
465
|
+
|
|
466
|
+
if (!pairs) return null;
|
|
467
|
+
|
|
468
|
+
pairs.forEach(pair => {
|
|
469
|
+
const [key, value] = pair.split(':').map(s => s.trim());
|
|
470
|
+
meta[key] = value.replace(/['"]/g, '');
|
|
471
|
+
});
|
|
464
472
|
|
|
465
473
|
return meta;
|
|
466
474
|
} catch (error) {
|
|
467
|
-
logger.warn(`Failed to extract meta from ${pagePath}: ${error.message}`);
|
|
468
475
|
return null;
|
|
469
476
|
}
|
|
470
477
|
}
|
|
471
478
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (!mainBundle) {
|
|
478
|
-
throw new Error('Could not find main bundle in build output');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const bundlePath = mainBundle.path.replace(outDir, '').replace(/^\//, '');
|
|
479
|
+
// IMPROVED: Render component to static HTML using JSDOM-like approach
|
|
480
|
+
async function renderComponentToHTML(route, bundlePath, userStylesheets, meta) {
|
|
481
|
+
// For now, we'll create a basic shell with meta tags
|
|
482
|
+
// Full SSR would require running React server-side
|
|
482
483
|
|
|
483
|
-
const
|
|
484
|
-
let userStylesheets = '';
|
|
485
|
-
|
|
486
|
-
if (existsSync(srcStylesDir)) {
|
|
487
|
-
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
488
|
-
userStylesheets = cssFiles.map(f =>
|
|
489
|
-
` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
|
|
490
|
-
).join('\n');
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Generate HTML for each route
|
|
494
|
-
logger.info('Generating HTML files for each route...');
|
|
495
|
-
|
|
496
|
-
for (const route of routes) {
|
|
497
|
-
if (route.type === 'dynamic') continue; // Skip dynamic routes
|
|
498
|
-
|
|
499
|
-
// Extract meta from page component
|
|
500
|
-
const pageMeta = await extractMetaFromPage(route.path);
|
|
501
|
-
|
|
502
|
-
const meta = pageMeta || {
|
|
503
|
-
title: 'BertUI App',
|
|
504
|
-
description: 'Built with BertUI - Lightning fast React development'
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
const html = `<!DOCTYPE html>
|
|
484
|
+
const html = `<!DOCTYPE html>
|
|
508
485
|
<html lang="en">
|
|
509
486
|
<head>
|
|
510
487
|
<meta charset="UTF-8">
|
|
511
488
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
512
|
-
<title>${meta.title}</title>
|
|
489
|
+
<title>${meta.title || 'BertUI App'}</title>
|
|
513
490
|
|
|
514
|
-
<meta name="description" content="${meta.description || ''}">
|
|
491
|
+
<meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
|
|
515
492
|
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
516
493
|
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
517
494
|
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
518
495
|
|
|
519
|
-
|
|
520
|
-
|
|
496
|
+
<meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
497
|
+
<meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
521
498
|
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
499
|
+
<meta property="og:type" content="website">
|
|
500
|
+
<meta property="og:url" content="${route}">
|
|
501
|
+
|
|
502
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
503
|
+
<meta name="twitter:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
504
|
+
<meta name="twitter:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
505
|
+
${meta.ogImage ? `<meta name="twitter:image" content="${meta.ogImage}">` : ''}
|
|
522
506
|
|
|
523
507
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
508
|
+
<link rel="canonical" href="${route}">
|
|
524
509
|
|
|
525
510
|
${userStylesheets}
|
|
526
511
|
|
|
@@ -534,12 +519,65 @@ ${userStylesheets}
|
|
|
534
519
|
}
|
|
535
520
|
}
|
|
536
521
|
</script>
|
|
522
|
+
|
|
523
|
+
<!-- SEO Preload Hints -->
|
|
524
|
+
<link rel="preconnect" href="https://esm.sh">
|
|
525
|
+
<link rel="dns-prefetch" href="https://esm.sh">
|
|
537
526
|
</head>
|
|
538
527
|
<body>
|
|
539
|
-
<div id="root"
|
|
528
|
+
<div id="root">
|
|
529
|
+
<!-- App shell - JavaScript will hydrate this -->
|
|
530
|
+
<div style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:system-ui">
|
|
531
|
+
<div style="text-align:center">
|
|
532
|
+
<h1 style="font-size:2rem;margin-bottom:1rem">${meta.title || 'Loading...'}</h1>
|
|
533
|
+
<p style="color:#666">${meta.description || 'Please wait while we load your content'}</p>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
540
537
|
<script type="module" src="/${bundlePath}"></script>
|
|
541
538
|
</body>
|
|
542
539
|
</html>`;
|
|
540
|
+
|
|
541
|
+
return html;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function generateProductionHTML(root, outDir, buildResult, routes) {
|
|
545
|
+
const mainBundle = buildResult.outputs.find(o =>
|
|
546
|
+
o.path.includes('main') && o.kind === 'entry-point'
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (!mainBundle) {
|
|
550
|
+
throw new Error('Could not find main bundle in build output');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const bundlePath = mainBundle.path.replace(outDir, '').replace(/^[\/\\]/, '');
|
|
554
|
+
|
|
555
|
+
const srcStylesDir = join(root, 'src', 'styles');
|
|
556
|
+
let userStylesheets = '';
|
|
557
|
+
|
|
558
|
+
if (existsSync(srcStylesDir)) {
|
|
559
|
+
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
560
|
+
userStylesheets = cssFiles.map(f =>
|
|
561
|
+
` <link rel="stylesheet" href="/styles/${f.replace('.css', '.min.css')}">`
|
|
562
|
+
).join('\n');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
logger.info('Generating SEO-optimized HTML files...');
|
|
566
|
+
|
|
567
|
+
for (const route of routes) {
|
|
568
|
+
if (route.type === 'dynamic') {
|
|
569
|
+
logger.info(`Skipping dynamic route: ${route.route}`);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Read source file and extract meta
|
|
574
|
+
const sourceCode = await Bun.file(route.path).text();
|
|
575
|
+
const meta = extractMetaFromSource(sourceCode) || {};
|
|
576
|
+
|
|
577
|
+
logger.info(`Extracting meta for ${route.route}: ${JSON.stringify(meta)}`);
|
|
578
|
+
|
|
579
|
+
// Generate HTML with meta tags and app shell
|
|
580
|
+
const html = await renderComponentToHTML(route.route, bundlePath, userStylesheets, meta);
|
|
543
581
|
|
|
544
582
|
// Determine output path
|
|
545
583
|
let htmlPath;
|
|
@@ -552,6 +590,6 @@ ${userStylesheets}
|
|
|
552
590
|
}
|
|
553
591
|
|
|
554
592
|
await Bun.write(htmlPath, html);
|
|
555
|
-
logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'}`);
|
|
593
|
+
logger.success(`Generated ${route.route === '/' ? 'index.html' : route.route + '/index.html'} with meta: ${meta.title || 'default'}`);
|
|
556
594
|
}
|
|
557
595
|
}
|