underpost 2.90.1 → 2.90.4

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/src/cli/static.js CHANGED
@@ -1,84 +1,820 @@
1
1
  /**
2
- * Static site generation module
2
+ * Static site generation module with enhanced customization capabilities
3
3
  * @module src/cli/static.js
4
4
  * @namespace UnderpostStatic
5
+ *
6
+ * @example
7
+ * // Basic usage - generate a simple page
8
+ * import UnderpostStatic from './static.js';
9
+ *
10
+ * await UnderpostStatic.API.callback({
11
+ * page: './src/client/ssr/body/DefaultSplashScreen.js',
12
+ * title: 'My App',
13
+ * outputPath: './dist/index.html'
14
+ * });
15
+ *
16
+ * @example
17
+ * // Advanced usage - full customization
18
+ * await UnderpostStatic.API.callback({
19
+ * page: './src/client/ssr/body/CustomPage.js',
20
+ * outputPath: './dist/custom.html',
21
+ * metadata: {
22
+ * title: 'My Custom Page',
23
+ * description: 'A fully customized static page',
24
+ * keywords: ['static', 'generator', 'custom'],
25
+ * author: 'John Doe',
26
+ * themeColor: '#007bff',
27
+ * canonicalURL: 'https://example.com/custom',
28
+ * thumbnail: 'https://example.com/thumb.png'
29
+ * },
30
+ * scripts: {
31
+ * head: [
32
+ * { src: '/vendor/library.js', async: true },
33
+ * { content: 'console.log("Inline script");', type: 'module' }
34
+ * ],
35
+ * body: [
36
+ * { src: '/app.js', defer: true }
37
+ * ]
38
+ * },
39
+ * styles: [
40
+ * { href: '/custom.css' },
41
+ * { content: 'body { margin: 0; }' }
42
+ * ],
43
+ * headComponents: [
44
+ * './src/client/ssr/head/Seo.js',
45
+ * './src/client/ssr/head/Pwa.js'
46
+ * ],
47
+ * icons: {
48
+ * favicon: '/custom-favicon.ico',
49
+ * appleTouchIcon: '/apple-touch-icon.png'
50
+ * },
51
+ * env: 'production',
52
+ * minify: true
53
+ * });
5
54
  */
6
55
 
7
56
  import fs from 'fs-extra';
57
+ import path from 'path';
8
58
  import { ssrFactory } from '../server/ssr.js';
9
59
  import { shellExec } from '../server/process.js';
10
60
  import Underpost from '../index.js';
11
61
  import { JSONweb } from '../server/client-formatted.js';
62
+ import { loggerFactory } from '../server/logger.js';
63
+
64
+ const logger = loggerFactory(import.meta);
65
+
66
+ /**
67
+ * @typedef {Object} MetadataOptions
68
+ * @memberof UnderpostStatic
69
+ * @property {string} [title='Home'] - Page title
70
+ * @property {string} [description=''] - Page description for SEO
71
+ * @property {string[]} [keywords=[]] - Keywords for SEO
72
+ * @property {string} [author=''] - Page author
73
+ * @property {string} [themeColor='#ffffff'] - Theme color for mobile browsers
74
+ * @property {string} [canonicalURL=''] - Canonical URL for SEO
75
+ * @property {string} [thumbnail=''] - Open Graph thumbnail image URL
76
+ * @property {string} [locale='en-US'] - Page locale
77
+ * @property {string} [siteName=''] - Site name for Open Graph
78
+ * @property {Object} [openGraph={}] - Additional Open Graph metadata
79
+ * @property {Object} [twitter={}] - Twitter card metadata
80
+ */
81
+
82
+ /**
83
+ * @typedef {Object} ScriptOptions
84
+ * @memberof UnderpostStatic
85
+ * @property {string} [src] - External script source URL
86
+ * @property {string} [content] - Inline script content
87
+ * @property {string} [type='text/javascript'] - Script type
88
+ * @property {boolean} [async=false] - Async loading flag
89
+ * @property {boolean} [defer=false] - Defer loading flag
90
+ * @property {string} [integrity] - Subresource integrity hash
91
+ * @property {string} [crossorigin] - CORS settings
92
+ * @property {Object} [attributes={}] - Additional HTML attributes
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} StyleOptions
97
+ * @memberof UnderpostStatic
98
+ * @property {string} [href] - External stylesheet URL
99
+ * @property {string} [content] - Inline style content
100
+ * @property {string} [media='all'] - Media query
101
+ * @property {string} [integrity] - Subresource integrity hash
102
+ * @property {string} [crossorigin] - CORS settings
103
+ */
104
+
105
+ /**
106
+ * @typedef {Object} IconOptions
107
+ * @memberof UnderpostStatic
108
+ * @property {string} [favicon] - Favicon path
109
+ * @property {string} [appleTouchIcon] - Apple touch icon path
110
+ * @property {string} [manifest] - Web manifest path
111
+ * @property {Object[]} [additional=[]] - Additional icon definitions
112
+ */
113
+
114
+ /**
115
+ * @typedef {Object} StaticGenerationOptions
116
+ * @memberof UnderpostStatic
117
+ * @property {string} [page=''] - SSR component path to render
118
+ * @property {string} [title='Home'] - Page title (deprecated: use metadata.title)
119
+ * @property {string} [outputPath='.'] - Output file path
120
+ * @property {string} [deployId=''] - Deployment identifier
121
+ * @property {string} [buildHost=''] - Build host URL
122
+ * @property {string} [buildPath='/'] - Build path
123
+ * @property {string} [env='production'] - Environment (development/production)
124
+ * @property {boolean} [build=false] - Whether to trigger build
125
+ * @property {boolean} [dev=false] - Development mode flag
126
+ * @property {boolean} [minify=true] - Minify HTML output
127
+ * @property {MetadataOptions} [metadata={}] - Comprehensive metadata options
128
+ * @property {Object} [scripts={}] - Script injection options
129
+ * @property {ScriptOptions[]} [scripts.head=[]] - Scripts for head section
130
+ * @property {ScriptOptions[]} [scripts.body=[]] - Scripts for body section
131
+ * @property {StyleOptions[]} [styles=[]] - Stylesheet options
132
+ * @property {string[]} [headComponents=[]] - Array of SSR head component paths
133
+ * @property {string[]} [bodyComponents=[]] - Array of SSR body component paths
134
+ * @property {IconOptions} [icons={}] - Icon configuration
135
+ * @property {Object} [customPayload={}] - Custom data to inject into renderPayload
136
+ * @property {Object} [templateHelpers={}] - Custom helper functions for templates
137
+ * @property {string} [configFile=''] - Path to JSON config file
138
+ * @property {string} [lang='en'] - HTML lang attribute
139
+ * @property {string} [dir='ltr'] - HTML dir attribute
140
+ * @property {Object} [microdata=[]] - Structured data (JSON-LD)
141
+ */
142
+
143
+ /**
144
+ * Template helper functions for common SSR patterns
145
+ * @namespace TemplateHelpers
146
+ */
147
+ const TemplateHelpers = {
148
+ /**
149
+ * Generates a script tag from options
150
+ * @param {ScriptOptions} options - Script options
151
+ * @returns {string} HTML script tag
152
+ * @memberof TemplateHelpers
153
+ *
154
+ * @example
155
+ * // External script with async
156
+ * TemplateHelpers.createScriptTag({ src: '/app.js', async: true })
157
+ * // Returns: <script async src="/app.js"></script>
158
+ *
159
+ * @example
160
+ * // Inline module script
161
+ * TemplateHelpers.createScriptTag({
162
+ * content: 'console.log("Hello");',
163
+ * type: 'module'
164
+ * })
165
+ * // Returns: <script type="module">console.log("Hello");</script>
166
+ */
167
+ createScriptTag(options) {
168
+ const attrs = [];
169
+
170
+ if (options.type && options.type !== 'text/javascript') {
171
+ attrs.push(`type="${options.type}"`);
172
+ }
173
+ if (options.async) attrs.push('async');
174
+ if (options.defer) attrs.push('defer');
175
+ if (options.src) attrs.push(`src="${options.src}"`);
176
+ if (options.integrity) attrs.push(`integrity="${options.integrity}"`);
177
+ if (options.crossorigin) attrs.push(`crossorigin="${options.crossorigin}"`);
178
+
179
+ // Add custom attributes
180
+ if (options.attributes) {
181
+ Object.entries(options.attributes).forEach(([key, value]) => {
182
+ attrs.push(`${key}="${value}"`);
183
+ });
184
+ }
185
+
186
+ const attrString = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
187
+ const content = options.content || '';
188
+
189
+ return `<script${attrString}>${content}</script>`;
190
+ },
191
+
192
+ /**
193
+ * Generates a link/style tag from options
194
+ * @param {StyleOptions} options - Style options
195
+ * @returns {string} HTML link or style tag
196
+ * @memberof TemplateHelpers
197
+ *
198
+ * @example
199
+ * // External stylesheet
200
+ * TemplateHelpers.createStyleTag({ href: '/styles.css' })
201
+ * // Returns: <link rel="stylesheet" href="/styles.css" media="all">
202
+ *
203
+ * @example
204
+ * // Inline styles
205
+ * TemplateHelpers.createStyleTag({ content: 'body { margin: 0; }' })
206
+ * // Returns: <style>body { margin: 0; }</style>
207
+ */
208
+ createStyleTag(options) {
209
+ if (options.content) {
210
+ return `<style>${options.content}</style>`;
211
+ }
212
+
213
+ const attrs = [`rel="stylesheet"`];
214
+ if (options.href) attrs.push(`href="${options.href}"`);
215
+ if (options.media) attrs.push(`media="${options.media}"`);
216
+ if (options.integrity) attrs.push(`integrity="${options.integrity}"`);
217
+ if (options.crossorigin) attrs.push(`crossorigin="${options.crossorigin}"`);
218
+
219
+ return `<link ${attrs.join(' ')}>`;
220
+ },
221
+
222
+ /**
223
+ * Generates icon link tags
224
+ * @param {IconOptions} icons - Icon options
225
+ * @returns {string} HTML icon link tags
226
+ * @memberof TemplateHelpers
227
+ *
228
+ * @example
229
+ * TemplateHelpers.createIconTags({
230
+ * favicon: '/favicon.ico',
231
+ * appleTouchIcon: '/apple-touch-icon.png',
232
+ * manifest: '/manifest.json'
233
+ * })
234
+ */
235
+ createIconTags(icons) {
236
+ const tags = [];
237
+
238
+ if (icons.favicon) {
239
+ tags.push(`<link rel="icon" type="image/x-icon" href="${icons.favicon}">`);
240
+ }
241
+ if (icons.appleTouchIcon) {
242
+ tags.push(`<link rel="apple-touch-icon" href="${icons.appleTouchIcon}">`);
243
+ }
244
+ if (icons.manifest) {
245
+ tags.push(`<link rel="manifest" href="${icons.manifest}">`);
246
+ }
247
+ if (icons.additional && Array.isArray(icons.additional)) {
248
+ icons.additional.forEach((icon) => {
249
+ const attrs = Object.entries(icon)
250
+ .map(([key, value]) => `${key}="${value}"`)
251
+ .join(' ');
252
+ tags.push(`<link ${attrs}>`);
253
+ });
254
+ }
255
+
256
+ return tags.join('\n');
257
+ },
258
+
259
+ /**
260
+ * Generates meta tags from metadata object
261
+ * @param {MetadataOptions} metadata - Metadata options
262
+ * @returns {string} HTML meta tags
263
+ * @memberof TemplateHelpers
264
+ *
265
+ * @example
266
+ * TemplateHelpers.createMetaTags({
267
+ * description: 'My page description',
268
+ * keywords: ['web', 'app'],
269
+ * author: 'John Doe'
270
+ * })
271
+ */
272
+ createMetaTags(metadata) {
273
+ const tags = [];
274
+
275
+ if (metadata.description) {
276
+ tags.push(`<meta name="description" content="${metadata.description}">`);
277
+ }
278
+ if (metadata.keywords && metadata.keywords.length > 0) {
279
+ tags.push(`<meta name="keywords" content="${metadata.keywords.join(', ')}">`);
280
+ }
281
+ if (metadata.author) {
282
+ tags.push(`<meta name="author" content="${metadata.author}">`);
283
+ }
284
+ if (metadata.themeColor) {
285
+ tags.push(`<meta name="theme-color" content="${metadata.themeColor}">`);
286
+ }
287
+
288
+ // Open Graph
289
+ if (metadata.title) {
290
+ tags.push(`<meta property="og:title" content="${metadata.title}">`);
291
+ }
292
+ if (metadata.description) {
293
+ tags.push(`<meta property="og:description" content="${metadata.description}">`);
294
+ }
295
+ if (metadata.thumbnail) {
296
+ tags.push(`<meta property="og:image" content="${metadata.thumbnail}">`);
297
+ }
298
+ if (metadata.canonicalURL) {
299
+ tags.push(`<meta property="og:url" content="${metadata.canonicalURL}">`);
300
+ tags.push(`<link rel="canonical" href="${metadata.canonicalURL}">`);
301
+ }
302
+ if (metadata.siteName) {
303
+ tags.push(`<meta property="og:site_name" content="${metadata.siteName}">`);
304
+ }
305
+ if (metadata.locale) {
306
+ tags.push(`<meta property="og:locale" content="${metadata.locale}">`);
307
+ }
308
+
309
+ // Twitter Card
310
+ tags.push(`<meta name="twitter:card" content="summary_large_image">`);
311
+ if (metadata.twitter) {
312
+ Object.entries(metadata.twitter).forEach(([key, value]) => {
313
+ tags.push(`<meta name="twitter:${key}" content="${value}">`);
314
+ });
315
+ }
316
+
317
+ // Additional Open Graph
318
+ if (metadata.openGraph) {
319
+ Object.entries(metadata.openGraph).forEach(([key, value]) => {
320
+ tags.push(`<meta property="og:${key}" content="${value}">`);
321
+ });
322
+ }
323
+
324
+ return tags.join('\n');
325
+ },
326
+
327
+ /**
328
+ * Generates JSON-LD structured data script tags
329
+ * @param {Object[]} microdata - Array of structured data objects
330
+ * @returns {string} HTML script tags with JSON-LD
331
+ * @memberof TemplateHelpers
332
+ *
333
+ * @example
334
+ * TemplateHelpers.createMicrodataTags([
335
+ * {
336
+ * '@context': 'https://schema.org',
337
+ * '@type': 'WebSite',
338
+ * 'name': 'My Site',
339
+ * 'url': 'https://example.com'
340
+ * }
341
+ * ])
342
+ */
343
+ createMicrodataTags(microdata) {
344
+ if (!microdata || !Array.isArray(microdata) || microdata.length === 0) {
345
+ return '';
346
+ }
347
+
348
+ return microdata
349
+ .map((data) => `<script type="application/ld+json">\n${JSON.stringify(data, null, 2)}\n</script>`)
350
+ .join('\n');
351
+ },
352
+ };
353
+
354
+ /**
355
+ * Configuration validator
356
+ * @namespace ConfigValidator
357
+ */
358
+ const ConfigValidator = {
359
+ /**
360
+ * Validates static generation options
361
+ * @param {StaticGenerationOptions} options - Options to validate
362
+ * @returns {Object} Validation result with isValid flag and errors array
363
+ * @memberof ConfigValidator
364
+ */
365
+ validate(options) {
366
+ const errors = [];
367
+
368
+ // Validate page path
369
+ if (options.page && !fs.existsSync(options.page)) {
370
+ errors.push(`Page component does not exist: ${options.page}`);
371
+ }
372
+
373
+ // Validate head components
374
+ if (options.headComponents && Array.isArray(options.headComponents)) {
375
+ options.headComponents.forEach((comp) => {
376
+ if (!fs.existsSync(comp)) {
377
+ errors.push(`Head component does not exist: ${comp}`);
378
+ }
379
+ });
380
+ }
381
+
382
+ // Validate body components
383
+ if (options.bodyComponents && Array.isArray(options.bodyComponents)) {
384
+ options.bodyComponents.forEach((comp) => {
385
+ if (!fs.existsSync(comp)) {
386
+ errors.push(`Body component does not exist: ${comp}`);
387
+ }
388
+ });
389
+ }
390
+
391
+ // Validate environment
392
+ if (options.env && !['development', 'production'].includes(options.env)) {
393
+ logger.warn(`Invalid environment: ${options.env}. Using 'production' as default.`);
394
+ }
395
+
396
+ return {
397
+ isValid: errors.length === 0,
398
+ errors,
399
+ };
400
+ },
401
+ };
402
+
403
+ /**
404
+ * Configuration file loader
405
+ * @namespace ConfigLoader
406
+ */
407
+ const ConfigLoader = {
408
+ /**
409
+ * Loads configuration from a JSON file
410
+ * @param {string} configPath - Path to config file
411
+ * @returns {Object} Configuration object
412
+ * @memberof ConfigLoader
413
+ *
414
+ * @example
415
+ * // static-config.json
416
+ * {
417
+ * "metadata": {
418
+ * "title": "My App",
419
+ * "description": "My application description"
420
+ * },
421
+ * "env": "production"
422
+ * }
423
+ *
424
+ * // Usage
425
+ * const config = ConfigLoader.load('./static-config.json');
426
+ */
427
+ load(configPath) {
428
+ try {
429
+ if (!fs.existsSync(configPath)) {
430
+ logger.error(`Config file not found: ${configPath}`);
431
+ return {};
432
+ }
433
+
434
+ const content = fs.readFileSync(configPath, 'utf8');
435
+ return JSON.parse(content);
436
+ } catch (error) {
437
+ logger.error(`Error loading config file: ${error.message}`);
438
+ return {};
439
+ }
440
+ },
441
+
442
+ /**
443
+ * Saves configuration to a JSON file
444
+ * @param {string} configPath - Path to save config
445
+ * @param {Object} config - Configuration object
446
+ * @memberof ConfigLoader
447
+ */
448
+ save(configPath, config) {
449
+ try {
450
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
451
+ logger.info(`Config saved to: ${configPath}`);
452
+ } catch (error) {
453
+ logger.error(`Error saving config file: ${error.message}`);
454
+ }
455
+ },
456
+ };
12
457
 
13
458
  /**
14
459
  * @class UnderpostStatic
15
- * @description Static site generation class
460
+ * @description Enhanced static site generation class with comprehensive customization
16
461
  * @memberof UnderpostStatic
17
462
  */
18
463
  class UnderpostStatic {
19
464
  static API = {
20
465
  /**
21
- * Generate static HTML file
22
- * @param {Object} options - Options for static generation
23
- * @param {string} options.page - Page identifier
24
- * @param {string} options.title - Page title
25
- * @param {string} options.outputPath - Output file path
26
- * @param {string} options.deployId - Deployment identifier
27
- * @param {string} options.buildHost - Build host
28
- * @param {string} options.buildPath - Build path
29
- * @param {string} options.env - Environment (development/production)
30
- * @param {boolean} options.build - Whether to trigger build
31
- * @param {boolean} options.dev - Development mode flag
32
- * @memberof UnderpostStatic
466
+ * Generate static HTML file with enhanced customization options
467
+ *
468
+ * @param {StaticGenerationOptions} options - Options for static generation
33
469
  * @returns {Promise<void>}
470
+ * @memberof UnderpostStatic
471
+ *
472
+ * @example
473
+ * // Minimal usage
474
+ * await UnderpostStatic.API.callback({
475
+ * page: './src/client/ssr/body/DefaultSplashScreen.js',
476
+ * outputPath: './dist/index.html'
477
+ * });
478
+ *
479
+ * @example
480
+ * // Full customization with metadata and scripts
481
+ * await UnderpostStatic.API.callback({
482
+ * page: './src/client/ssr/body/CustomPage.js',
483
+ * outputPath: './dist/page.html',
484
+ * metadata: {
485
+ * title: 'My Custom Page',
486
+ * description: 'A fully customized page',
487
+ * keywords: ['custom', 'static', 'page'],
488
+ * author: 'Jane Developer',
489
+ * themeColor: '#4CAF50',
490
+ * canonicalURL: 'https://example.com/page',
491
+ * thumbnail: 'https://example.com/images/thumbnail.png',
492
+ * locale: 'en-US',
493
+ * siteName: 'My Website'
494
+ * },
495
+ * scripts: {
496
+ * head: [
497
+ * { src: 'https://cdn.example.com/analytics.js', async: true },
498
+ * { content: 'window.config = { apiUrl: "https://api.example.com" };' }
499
+ * ],
500
+ * body: [
501
+ * { src: '/app.js', type: 'module', defer: true }
502
+ * ]
503
+ * },
504
+ * styles: [
505
+ * { href: '/main.css' },
506
+ * { content: 'body { font-family: sans-serif; }' }
507
+ * ],
508
+ * icons: {
509
+ * favicon: '/favicon.ico',
510
+ * appleTouchIcon: '/apple-touch-icon.png',
511
+ * manifest: '/manifest.json'
512
+ * },
513
+ * headComponents: [
514
+ * './src/client/ssr/head/Seo.js',
515
+ * './src/client/ssr/head/Pwa.js'
516
+ * ],
517
+ * microdata: [
518
+ * {
519
+ * '@context': 'https://schema.org',
520
+ * '@type': 'WebPage',
521
+ * 'name': 'My Custom Page',
522
+ * 'url': 'https://example.com/page'
523
+ * }
524
+ * ],
525
+ * customPayload: {
526
+ * apiEndpoint: 'https://api.example.com',
527
+ * features: ['feature1', 'feature2']
528
+ * },
529
+ * env: 'production',
530
+ * minify: true
531
+ * });
532
+ *
533
+ * @example
534
+ * // Using a config file
535
+ * await UnderpostStatic.API.callback({
536
+ * configFile: './static-config.json',
537
+ * outputPath: './dist/index.html'
538
+ * });
539
+ *
540
+ * @example
541
+ * // Generate with build trigger
542
+ * await UnderpostStatic.API.callback({
543
+ * page: './src/client/ssr/body/DefaultSplashScreen.js',
544
+ * outputPath: './public/index.html',
545
+ * deployId: 'production-v1',
546
+ * buildHost: 'example.com',
547
+ * buildPath: '/',
548
+ * build: true,
549
+ * env: 'production'
550
+ * });
34
551
  */
35
- async callback(
36
- options = {
37
- page: '',
38
- title: '',
39
- outputPath: '',
40
- deployId: '',
41
- buildHost: '',
42
- buildPath: '',
43
- env: '',
44
- build: false,
45
- dev: false,
46
- },
47
- ) {
552
+ async callback(options = {}) {
553
+ // Load config from file if specified
554
+ if (options.configFile) {
555
+ const fileConfig = ConfigLoader.load(options.configFile);
556
+ options = { ...fileConfig, ...options }; // CLI options override file config
557
+ }
558
+
559
+ // Set defaults
48
560
  if (!options.outputPath) options.outputPath = '.';
49
561
  if (!options.buildPath) options.buildPath = '/';
50
562
  if (!options.env) options.env = 'production';
563
+ if (options.minify === undefined) options.minify = options.env === 'production';
564
+ if (!options.metadata) options.metadata = {};
565
+ if (!options.lang) options.lang = 'en';
566
+ if (!options.dir) options.dir = 'ltr';
567
+
568
+ // Merge title for backwards compatibility
569
+ if (options.title && !options.metadata.title) {
570
+ options.metadata.title = options.title;
571
+ }
572
+ if (!options.metadata.title) {
573
+ options.metadata.title = 'Home';
574
+ }
575
+
576
+ // Validate options
577
+ const validation = ConfigValidator.validate(options);
578
+ if (!validation.isValid) {
579
+ logger.error('Validation errors:');
580
+ validation.errors.forEach((err) => logger.error(` - ${err}`));
581
+ if (validation.errors.some((err) => err.includes('does not exist'))) {
582
+ return; // Exit if critical path errors
583
+ }
584
+ }
51
585
 
586
+ // Generate page HTML
52
587
  if (options.page) {
53
- const Render = await ssrFactory();
54
- const SsrComponent = await ssrFactory(options.page);
55
- const htmlSrc = Render({
56
- title: options.title || 'Home',
57
- ssrPath: '/',
58
- ssrHeadComponents: '',
59
- ssrBodyComponents: SsrComponent(),
60
- // buildId: options.deployId || 'local',
61
- renderPayload: {
62
- // apiBaseProxyPath,
63
- // apiBaseHost,
64
- // apiBasePath: process.env.BASE_API,
588
+ try {
589
+ logger.info(`Generating static page: ${options.page}`);
590
+
591
+ const Render = await ssrFactory();
592
+ const SsrComponent = await ssrFactory(options.page);
593
+
594
+ // Build head components
595
+ let ssrHeadComponents = '';
596
+
597
+ // Add custom meta tags
598
+ if (options.metadata) {
599
+ ssrHeadComponents += TemplateHelpers.createMetaTags(options.metadata);
600
+ }
601
+
602
+ // Add custom icons
603
+ if (options.icons) {
604
+ ssrHeadComponents += '\n' + TemplateHelpers.createIconTags(options.icons);
605
+ }
606
+
607
+ // Add custom styles
608
+ if (options.styles && Array.isArray(options.styles)) {
609
+ ssrHeadComponents += '\n' + options.styles.map((style) => TemplateHelpers.createStyleTag(style)).join('\n');
610
+ }
611
+
612
+ // Add custom head scripts
613
+ if (options.scripts?.head && Array.isArray(options.scripts.head)) {
614
+ ssrHeadComponents +=
615
+ '\n' + options.scripts.head.map((script) => TemplateHelpers.createScriptTag(script)).join('\n');
616
+ }
617
+
618
+ // Add microdata/structured data
619
+ if (options.microdata && Array.isArray(options.microdata)) {
620
+ ssrHeadComponents += '\n' + TemplateHelpers.createMicrodataTags(options.microdata);
621
+ }
622
+
623
+ // Load additional head components
624
+ if (options.headComponents && Array.isArray(options.headComponents)) {
625
+ for (const compPath of options.headComponents) {
626
+ try {
627
+ const HeadComponent = await ssrFactory(compPath);
628
+ // Pass metadata and other options to component
629
+ const componentData = {
630
+ ...options.metadata,
631
+ ssrPath: options.buildPath === '/' ? '/' : `${options.buildPath}/`,
632
+ microdata: options.microdata || [],
633
+ };
634
+ ssrHeadComponents += '\n' + HeadComponent(componentData);
635
+ } catch (error) {
636
+ logger.error(`Error loading head component ${compPath}: ${error.message}`);
637
+ }
638
+ }
639
+ }
640
+
641
+ // Build body components
642
+ let ssrBodyComponents = SsrComponent();
643
+
644
+ // Load additional body components
645
+ if (options.bodyComponents && Array.isArray(options.bodyComponents)) {
646
+ for (const compPath of options.bodyComponents) {
647
+ try {
648
+ const BodyComponent = await ssrFactory(compPath);
649
+ ssrBodyComponents += '\n' + BodyComponent();
650
+ } catch (error) {
651
+ logger.error(`Error loading body component ${compPath}: ${error.message}`);
652
+ }
653
+ }
654
+ }
655
+
656
+ // Add custom body scripts
657
+ if (options.scripts?.body && Array.isArray(options.scripts.body)) {
658
+ ssrBodyComponents +=
659
+ '\n' + options.scripts.body.map((script) => TemplateHelpers.createScriptTag(script)).join('\n');
660
+ }
661
+
662
+ // Build render payload
663
+ const renderPayload = {
65
664
  version: Underpost.version,
66
665
  ...(options.env === 'development' ? { dev: true } : undefined),
67
- },
68
- renderApi: {
69
- JSONweb,
70
- },
71
- });
72
- fs.writeFileSync(options.outputPath, htmlSrc, 'utf8');
666
+ ...options.customPayload,
667
+ };
668
+
669
+ // Generate HTML
670
+ const htmlSrc = Render({
671
+ title: options.metadata.title,
672
+ ssrPath: options.buildPath === '/' ? '/' : `${options.buildPath}/`,
673
+ ssrHeadComponents,
674
+ ssrBodyComponents,
675
+ renderPayload,
676
+ renderApi: {
677
+ JSONweb,
678
+ },
679
+ });
680
+
681
+ // Write output file
682
+ const outputDir = path.dirname(options.outputPath);
683
+ if (!fs.existsSync(outputDir)) {
684
+ fs.mkdirSync(outputDir, { recursive: true });
685
+ }
686
+
687
+ fs.writeFileSync(options.outputPath, htmlSrc, 'utf8');
688
+ logger.info(`Static page generated: ${options.outputPath}`);
689
+ } catch (error) {
690
+ logger.error(`Error generating static page: ${error.message}`);
691
+ logger.error(error.stack);
692
+ throw error;
693
+ }
73
694
  }
695
+
696
+ // Trigger build if requested
74
697
  if (options.deployId && options.build) {
75
- shellExec(`underpost env ${options.deployId} ${options.env}`);
76
- shellExec(
77
- `npm run build ${options.deployId}${options.buildHost ? ` ${options.buildHost} ${options.buildPath}` : ``}`,
78
- );
698
+ try {
699
+ logger.info(`Triggering build for deployment: ${options.deployId}`);
700
+
701
+ shellExec(`underpost env ${options.deployId} ${options.env}`);
702
+ shellExec(
703
+ `npm run build ${options.deployId}${options.buildHost ? ` ${options.buildHost} ${options.buildPath}` : ``}`,
704
+ );
705
+
706
+ logger.info('Build completed successfully');
707
+ } catch (error) {
708
+ logger.error(`Build error: ${error.message}`);
709
+ throw error;
710
+ }
79
711
  }
80
712
  },
713
+
714
+ /**
715
+ * Helper method to generate a config template file
716
+ *
717
+ * @param {string} outputPath - Where to save the template config
718
+ * @returns {void}
719
+ * @memberof UnderpostStatic
720
+ *
721
+ * @example
722
+ * // Generate a template configuration file
723
+ * UnderpostStatic.API.generateConfigTemplate('./my-static-config.json');
724
+ */
725
+ generateConfigTemplate(outputPath = './static-config.json') {
726
+ const template = {
727
+ page: './src/client/ssr/body/DefaultSplashScreen.js',
728
+ outputPath: './dist/index.html',
729
+ env: 'production',
730
+ minify: true,
731
+ lang: 'en',
732
+ dir: 'ltr',
733
+ metadata: {
734
+ title: 'My Application',
735
+ description: 'A description of my application',
736
+ keywords: ['keyword1', 'keyword2', 'keyword3'],
737
+ author: 'Your Name',
738
+ themeColor: '#ffffff',
739
+ canonicalURL: 'https://example.com',
740
+ thumbnail: 'https://example.com/images/thumbnail.png',
741
+ locale: 'en-US',
742
+ siteName: 'My Site',
743
+ },
744
+ scripts: {
745
+ head: [
746
+ {
747
+ src: 'https://example.com/analytics.js',
748
+ async: true,
749
+ comment: 'Analytics script',
750
+ },
751
+ {
752
+ content: 'window.config = { apiUrl: "https://api.example.com" };',
753
+ comment: 'App configuration',
754
+ },
755
+ ],
756
+ body: [
757
+ {
758
+ src: '/app.js',
759
+ type: 'module',
760
+ defer: true,
761
+ },
762
+ ],
763
+ },
764
+ styles: [
765
+ {
766
+ href: '/styles/main.css',
767
+ },
768
+ {
769
+ content: 'body { margin: 0; padding: 0; }',
770
+ },
771
+ ],
772
+ icons: {
773
+ favicon: '/favicon.ico',
774
+ appleTouchIcon: '/apple-touch-icon.png',
775
+ manifest: '/manifest.json',
776
+ },
777
+ headComponents: ['./src/client/ssr/head/Seo.js', './src/client/ssr/head/Pwa.js'],
778
+ microdata: [
779
+ {
780
+ '@context': 'https://schema.org',
781
+ '@type': 'WebSite',
782
+ name: 'My Site',
783
+ url: 'https://example.com',
784
+ },
785
+ ],
786
+ customPayload: {
787
+ apiEndpoint: 'https://api.example.com',
788
+ customFeature: true,
789
+ },
790
+ };
791
+
792
+ ConfigLoader.save(outputPath, template);
793
+ logger.info(`Config template generated: ${outputPath}`);
794
+ },
81
795
  };
796
+
797
+ /**
798
+ * Export template helpers for external use
799
+ * @static
800
+ * @memberof UnderpostStatic
801
+ */
802
+ static TemplateHelpers = TemplateHelpers;
803
+
804
+ /**
805
+ * Export config validator for external use
806
+ * @static
807
+ * @memberof UnderpostStatic
808
+ */
809
+ static ConfigValidator = ConfigValidator;
810
+
811
+ /**
812
+ * Export config loader for external use
813
+ * @static
814
+ * @memberof UnderpostStatic
815
+ */
816
+ static ConfigLoader = ConfigLoader;
82
817
  }
83
818
 
84
819
  export default UnderpostStatic;
820
+ export { TemplateHelpers, ConfigValidator, ConfigLoader };