@uniweb/build 0.1.2 → 0.1.5
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 +189 -11
- package/package.json +24 -4
- package/src/dev/index.js +9 -0
- package/src/dev/plugin.js +206 -0
- package/src/docs.js +217 -0
- package/src/images.js +5 -3
- package/src/index.js +11 -0
- package/src/prerender.js +310 -0
- package/src/site/advanced-processors.js +393 -0
- package/src/site/asset-processor.js +281 -0
- package/src/site/assets.js +247 -0
- package/src/site/content-collector.js +344 -0
- package/src/site/index.js +32 -0
- package/src/site/plugin.js +497 -0
- package/src/vite-foundation-plugin.js +7 -3
package/src/docs.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates markdown documentation from foundation schema.json
|
|
5
|
+
* or directly from component meta files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { buildSchema } from './schema.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate markdown documentation for a single component
|
|
15
|
+
*
|
|
16
|
+
* @param {string} name - Component name
|
|
17
|
+
* @param {Object} meta - Component metadata
|
|
18
|
+
* @returns {string} Markdown content
|
|
19
|
+
*/
|
|
20
|
+
function generateComponentDocs(name, meta) {
|
|
21
|
+
const lines = []
|
|
22
|
+
|
|
23
|
+
// Component header
|
|
24
|
+
lines.push(`## ${name}`)
|
|
25
|
+
lines.push('')
|
|
26
|
+
|
|
27
|
+
// Description
|
|
28
|
+
if (meta.description) {
|
|
29
|
+
lines.push(meta.description)
|
|
30
|
+
lines.push('')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Category badge
|
|
34
|
+
if (meta.category) {
|
|
35
|
+
lines.push(`**Category:** ${meta.category}`)
|
|
36
|
+
lines.push('')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Content Elements
|
|
40
|
+
if (meta.elements && Object.keys(meta.elements).length > 0) {
|
|
41
|
+
lines.push('### Content Elements')
|
|
42
|
+
lines.push('')
|
|
43
|
+
lines.push('| Element | Label | Required | Description |')
|
|
44
|
+
lines.push('|---------|-------|----------|-------------|')
|
|
45
|
+
|
|
46
|
+
for (const [key, element] of Object.entries(meta.elements)) {
|
|
47
|
+
const label = element.label || key
|
|
48
|
+
const required = element.required ? 'Yes' : ''
|
|
49
|
+
const description = element.description || ''
|
|
50
|
+
lines.push(`| \`${key}\` | ${label} | ${required} | ${description} |`)
|
|
51
|
+
}
|
|
52
|
+
lines.push('')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parameters/Properties
|
|
56
|
+
if (meta.properties && Object.keys(meta.properties).length > 0) {
|
|
57
|
+
lines.push('### Parameters')
|
|
58
|
+
lines.push('')
|
|
59
|
+
lines.push('| Parameter | Type | Default | Description |')
|
|
60
|
+
lines.push('|-----------|------|---------|-------------|')
|
|
61
|
+
|
|
62
|
+
for (const [key, prop] of Object.entries(meta.properties)) {
|
|
63
|
+
const type = prop.type || 'string'
|
|
64
|
+
const defaultVal = prop.default !== undefined ? `\`${prop.default}\`` : ''
|
|
65
|
+
let description = prop.label || ''
|
|
66
|
+
|
|
67
|
+
// Add options for select type
|
|
68
|
+
if (prop.type === 'select' && prop.options) {
|
|
69
|
+
const optionValues = prop.options.map(o =>
|
|
70
|
+
typeof o === 'object' ? o.value : o
|
|
71
|
+
).join(', ')
|
|
72
|
+
description += description ? ` (${optionValues})` : optionValues
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lines.push(`| \`${key}\` | ${type} | ${defaultVal} | ${description} |`)
|
|
76
|
+
}
|
|
77
|
+
lines.push('')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Presets
|
|
81
|
+
if (meta.presets && meta.presets.length > 0) {
|
|
82
|
+
lines.push('### Presets')
|
|
83
|
+
lines.push('')
|
|
84
|
+
|
|
85
|
+
for (const preset of meta.presets) {
|
|
86
|
+
const settings = preset.settings
|
|
87
|
+
? Object.entries(preset.settings)
|
|
88
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
89
|
+
.join(', ')
|
|
90
|
+
: ''
|
|
91
|
+
lines.push(`- **${preset.name}** - ${preset.label || ''} ${settings ? `(${settings})` : ''}`)
|
|
92
|
+
}
|
|
93
|
+
lines.push('')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return lines.join('\n')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate full markdown documentation for a foundation
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} schema - Foundation schema object
|
|
103
|
+
* @param {Object} options - Generation options
|
|
104
|
+
* @param {string} [options.title] - Document title
|
|
105
|
+
* @returns {string} Complete markdown documentation
|
|
106
|
+
*/
|
|
107
|
+
export function generateDocsFromSchema(schema, options = {}) {
|
|
108
|
+
const { title = 'Foundation Components' } = options
|
|
109
|
+
const lines = []
|
|
110
|
+
|
|
111
|
+
// Header
|
|
112
|
+
lines.push(`# ${title}`)
|
|
113
|
+
lines.push('')
|
|
114
|
+
|
|
115
|
+
// Foundation info
|
|
116
|
+
const foundationMeta = schema._self
|
|
117
|
+
if (foundationMeta) {
|
|
118
|
+
if (foundationMeta.name) {
|
|
119
|
+
lines.push(`**${foundationMeta.name}**`)
|
|
120
|
+
lines.push('')
|
|
121
|
+
}
|
|
122
|
+
if (foundationMeta.description) {
|
|
123
|
+
lines.push(foundationMeta.description)
|
|
124
|
+
lines.push('')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push('---')
|
|
129
|
+
lines.push('')
|
|
130
|
+
|
|
131
|
+
// Table of contents
|
|
132
|
+
const componentNames = Object.keys(schema).filter(k => k !== '_self')
|
|
133
|
+
|
|
134
|
+
if (componentNames.length > 0) {
|
|
135
|
+
lines.push('## Components')
|
|
136
|
+
lines.push('')
|
|
137
|
+
for (const name of componentNames) {
|
|
138
|
+
const meta = schema[name]
|
|
139
|
+
const title = meta.title || name
|
|
140
|
+
lines.push(`- [${title}](#${name.toLowerCase()}) - ${meta.description || ''}`)
|
|
141
|
+
}
|
|
142
|
+
lines.push('')
|
|
143
|
+
lines.push('---')
|
|
144
|
+
lines.push('')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Component documentation
|
|
148
|
+
for (const name of componentNames) {
|
|
149
|
+
const meta = schema[name]
|
|
150
|
+
lines.push(generateComponentDocs(name, meta))
|
|
151
|
+
lines.push('---')
|
|
152
|
+
lines.push('')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Footer
|
|
156
|
+
lines.push('*Generated from foundation schema*')
|
|
157
|
+
|
|
158
|
+
return lines.join('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate documentation for a foundation directory
|
|
163
|
+
*
|
|
164
|
+
* Can read from existing schema.json or build schema from source.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} foundationDir - Path to foundation directory
|
|
167
|
+
* @param {Object} options - Options
|
|
168
|
+
* @param {string} [options.output] - Output file path (default: COMPONENTS.md)
|
|
169
|
+
* @param {boolean} [options.fromSource] - Build schema from source instead of dist
|
|
170
|
+
* @returns {Promise<{outputPath: string, componentCount: number}>}
|
|
171
|
+
*/
|
|
172
|
+
export async function generateDocs(foundationDir, options = {}) {
|
|
173
|
+
const {
|
|
174
|
+
output = 'COMPONENTS.md',
|
|
175
|
+
fromSource = false,
|
|
176
|
+
} = options
|
|
177
|
+
|
|
178
|
+
let schema
|
|
179
|
+
|
|
180
|
+
// Try to load schema.json from dist
|
|
181
|
+
const schemaPath = join(foundationDir, 'dist', 'schema.json')
|
|
182
|
+
|
|
183
|
+
if (!fromSource && existsSync(schemaPath)) {
|
|
184
|
+
// Load from existing schema.json
|
|
185
|
+
const schemaContent = await readFile(schemaPath, 'utf-8')
|
|
186
|
+
schema = JSON.parse(schemaContent)
|
|
187
|
+
} else {
|
|
188
|
+
// Build schema from source
|
|
189
|
+
const srcDir = join(foundationDir, 'src')
|
|
190
|
+
if (!existsSync(srcDir)) {
|
|
191
|
+
throw new Error(`Source directory not found: ${srcDir}`)
|
|
192
|
+
}
|
|
193
|
+
schema = await buildSchema(srcDir)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get foundation name for title
|
|
197
|
+
const pkgPath = join(foundationDir, 'package.json')
|
|
198
|
+
let title = 'Foundation Components'
|
|
199
|
+
if (existsSync(pkgPath)) {
|
|
200
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
201
|
+
if (pkg.name) {
|
|
202
|
+
title = `${pkg.name} Components`
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Generate markdown
|
|
207
|
+
const markdown = generateDocsFromSchema(schema, { title })
|
|
208
|
+
|
|
209
|
+
// Write output
|
|
210
|
+
const outputPath = join(foundationDir, output)
|
|
211
|
+
await writeFile(outputPath, markdown)
|
|
212
|
+
|
|
213
|
+
// Count components
|
|
214
|
+
const componentCount = Object.keys(schema).filter(k => k !== '_self').length
|
|
215
|
+
|
|
216
|
+
return { outputPath, componentCount }
|
|
217
|
+
}
|
package/src/images.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Image Processing Utilities
|
|
3
3
|
*
|
|
4
4
|
* Handles preview image discovery, conversion to webp, and metadata extraction.
|
|
5
|
+
* Preview images are editor metadata (for preset visualization) and are output
|
|
6
|
+
* to dist/meta/previews/ to keep them separate from runtime assets.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { readdir, mkdir, copyFile } from 'node:fs/promises'
|
|
@@ -71,8 +73,8 @@ export async function processComponentPreviews(componentDir, componentName, outp
|
|
|
71
73
|
return previews
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
// Create output directory
|
|
75
|
-
const componentOutputDir = join(outputDir, '
|
|
76
|
+
// Create output directory for preview images (editor metadata)
|
|
77
|
+
const componentOutputDir = join(outputDir, 'meta', 'previews', componentName)
|
|
76
78
|
await mkdir(componentOutputDir, { recursive: true })
|
|
77
79
|
|
|
78
80
|
// Get all image files
|
|
@@ -115,7 +117,7 @@ export async function processComponentPreviews(componentDir, componentName, outp
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
previews[presetName] = {
|
|
118
|
-
path: `
|
|
120
|
+
path: `meta/previews/${componentName}/${outputFilename}`,
|
|
119
121
|
width: metadata.width,
|
|
120
122
|
height: metadata.height,
|
|
121
123
|
type: finalFormat,
|
package/src/index.js
CHANGED
|
@@ -35,5 +35,16 @@ export {
|
|
|
35
35
|
foundationDevPlugin,
|
|
36
36
|
} from './vite-foundation-plugin.js'
|
|
37
37
|
|
|
38
|
+
// SSG Prerendering
|
|
39
|
+
export {
|
|
40
|
+
prerenderSite,
|
|
41
|
+
} from './prerender.js'
|
|
42
|
+
|
|
43
|
+
// Documentation generation
|
|
44
|
+
export {
|
|
45
|
+
generateDocs,
|
|
46
|
+
generateDocsFromSchema,
|
|
47
|
+
} from './docs.js'
|
|
48
|
+
|
|
38
49
|
// Default export is the combined Vite plugin
|
|
39
50
|
export { default } from './vite-foundation-plugin.js'
|
package/src/prerender.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSG Prerendering for Uniweb Sites
|
|
3
|
+
*
|
|
4
|
+
* Renders each page to static HTML at build time.
|
|
5
|
+
* The output includes full HTML with hydration support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { join, dirname } from 'node:path'
|
|
11
|
+
import { pathToFileURL } from 'node:url'
|
|
12
|
+
|
|
13
|
+
// Lazily loaded dependencies (ESM with React)
|
|
14
|
+
let React, renderToString, createUniweb
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load dependencies dynamically
|
|
18
|
+
* These are ESM modules that may not be available at import time
|
|
19
|
+
*/
|
|
20
|
+
async function loadDependencies() {
|
|
21
|
+
if (React) return // Already loaded
|
|
22
|
+
|
|
23
|
+
const [reactMod, serverMod, coreMod] = await Promise.all([
|
|
24
|
+
import('react'),
|
|
25
|
+
import('react-dom/server'),
|
|
26
|
+
import('@uniweb/core')
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
React = reactMod.default || reactMod
|
|
30
|
+
renderToString = serverMod.renderToString
|
|
31
|
+
createUniweb = coreMod.createUniweb
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pre-render all pages in a built site to static HTML
|
|
36
|
+
*
|
|
37
|
+
* @param {string} siteDir - Path to the site directory
|
|
38
|
+
* @param {Object} options
|
|
39
|
+
* @param {string} options.foundationDir - Path to foundation directory (default: ../foundation)
|
|
40
|
+
* @param {function} options.onProgress - Progress callback
|
|
41
|
+
* @returns {Promise<{pages: number, files: string[]}>}
|
|
42
|
+
*/
|
|
43
|
+
export async function prerenderSite(siteDir, options = {}) {
|
|
44
|
+
const {
|
|
45
|
+
foundationDir = join(siteDir, '..', 'foundation'),
|
|
46
|
+
onProgress = () => {}
|
|
47
|
+
} = options
|
|
48
|
+
|
|
49
|
+
const distDir = join(siteDir, 'dist')
|
|
50
|
+
|
|
51
|
+
// Verify build exists
|
|
52
|
+
if (!existsSync(distDir)) {
|
|
53
|
+
throw new Error(`Site must be built first. No dist directory found at: ${distDir}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load dependencies
|
|
57
|
+
onProgress('Loading dependencies...')
|
|
58
|
+
await loadDependencies()
|
|
59
|
+
|
|
60
|
+
// Load site content
|
|
61
|
+
onProgress('Loading site content...')
|
|
62
|
+
const contentPath = join(distDir, 'site-content.json')
|
|
63
|
+
if (!existsSync(contentPath)) {
|
|
64
|
+
throw new Error(`site-content.json not found at: ${contentPath}`)
|
|
65
|
+
}
|
|
66
|
+
const siteContent = JSON.parse(await readFile(contentPath, 'utf8'))
|
|
67
|
+
|
|
68
|
+
// Load the HTML shell
|
|
69
|
+
onProgress('Loading HTML shell...')
|
|
70
|
+
const shellPath = join(distDir, 'index.html')
|
|
71
|
+
if (!existsSync(shellPath)) {
|
|
72
|
+
throw new Error(`index.html not found at: ${shellPath}`)
|
|
73
|
+
}
|
|
74
|
+
const htmlShell = await readFile(shellPath, 'utf8')
|
|
75
|
+
|
|
76
|
+
// Load the foundation module
|
|
77
|
+
onProgress('Loading foundation...')
|
|
78
|
+
const foundationPath = join(foundationDir, 'dist', 'foundation.js')
|
|
79
|
+
if (!existsSync(foundationPath)) {
|
|
80
|
+
throw new Error(`Foundation not found at: ${foundationPath}. Build foundation first.`)
|
|
81
|
+
}
|
|
82
|
+
const foundationUrl = pathToFileURL(foundationPath).href
|
|
83
|
+
const foundation = await import(foundationUrl)
|
|
84
|
+
|
|
85
|
+
// Initialize the Uniweb runtime (this sets globalThis.uniweb)
|
|
86
|
+
onProgress('Initializing runtime...')
|
|
87
|
+
const uniweb = createUniweb(siteContent)
|
|
88
|
+
uniweb.setFoundation(foundation)
|
|
89
|
+
|
|
90
|
+
if (foundation.config || foundation.site) {
|
|
91
|
+
uniweb.setFoundationConfig(foundation.config || foundation.site)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pre-render each page
|
|
95
|
+
const renderedFiles = []
|
|
96
|
+
const pages = uniweb.activeWebsite.pages
|
|
97
|
+
|
|
98
|
+
for (const page of pages) {
|
|
99
|
+
const route = page.route
|
|
100
|
+
onProgress(`Rendering ${route}...`)
|
|
101
|
+
|
|
102
|
+
// Set this as the active page
|
|
103
|
+
uniweb.activeWebsite.setActivePage(route)
|
|
104
|
+
|
|
105
|
+
// Create the page element
|
|
106
|
+
// Note: We don't need StaticRouter for SSG since we're just rendering
|
|
107
|
+
// components to strings. The routing context isn't needed for static HTML.
|
|
108
|
+
const element = React.createElement(PageRenderer, { page, foundation })
|
|
109
|
+
|
|
110
|
+
// Render to HTML string
|
|
111
|
+
let renderedContent
|
|
112
|
+
try {
|
|
113
|
+
renderedContent = renderToString(element)
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(`Warning: Failed to render ${route}: ${err.message}`)
|
|
116
|
+
if (process.env.DEBUG) {
|
|
117
|
+
console.error(err.stack)
|
|
118
|
+
}
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Inject into shell
|
|
123
|
+
const html = injectContent(htmlShell, renderedContent, page, siteContent)
|
|
124
|
+
|
|
125
|
+
// Determine output path
|
|
126
|
+
const outputPath = getOutputPath(distDir, route)
|
|
127
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
128
|
+
await writeFile(outputPath, html)
|
|
129
|
+
|
|
130
|
+
renderedFiles.push(outputPath)
|
|
131
|
+
onProgress(` → ${outputPath.replace(distDir, 'dist')}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onProgress(`Pre-rendered ${renderedFiles.length} pages`)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
pages: renderedFiles.length,
|
|
138
|
+
files: renderedFiles
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Minimal page renderer for SSG
|
|
144
|
+
* Renders blocks using foundation components
|
|
145
|
+
*/
|
|
146
|
+
function PageRenderer({ page, foundation }) {
|
|
147
|
+
const blocks = page.getPageBlocks()
|
|
148
|
+
|
|
149
|
+
return React.createElement(
|
|
150
|
+
'main',
|
|
151
|
+
null,
|
|
152
|
+
blocks.map((block, index) =>
|
|
153
|
+
React.createElement(BlockRenderer, {
|
|
154
|
+
key: block.id || index,
|
|
155
|
+
block,
|
|
156
|
+
foundation
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Block renderer - maps block to foundation component
|
|
164
|
+
*/
|
|
165
|
+
function BlockRenderer({ block, foundation }) {
|
|
166
|
+
// Get component from foundation
|
|
167
|
+
const componentName = block.component
|
|
168
|
+
let Component = null
|
|
169
|
+
|
|
170
|
+
if (typeof foundation.getComponent === 'function') {
|
|
171
|
+
Component = foundation.getComponent(componentName)
|
|
172
|
+
} else if (foundation[componentName]) {
|
|
173
|
+
Component = foundation[componentName]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!Component) {
|
|
177
|
+
// Return placeholder for unknown components
|
|
178
|
+
return React.createElement(
|
|
179
|
+
'div',
|
|
180
|
+
{
|
|
181
|
+
className: 'block-placeholder',
|
|
182
|
+
'data-component': componentName,
|
|
183
|
+
style: { display: 'none' }
|
|
184
|
+
},
|
|
185
|
+
`Component: ${componentName}`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Build content object (same as runtime's BlockRenderer)
|
|
190
|
+
let content
|
|
191
|
+
if (block.parsedContent?.raw) {
|
|
192
|
+
content = block.parsedContent.raw
|
|
193
|
+
} else {
|
|
194
|
+
content = {
|
|
195
|
+
...block.parsedContent,
|
|
196
|
+
...block.properties,
|
|
197
|
+
_prosemirror: block.parsedContent
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build wrapper props
|
|
202
|
+
const theme = block.themeName
|
|
203
|
+
const className = theme || ''
|
|
204
|
+
const wrapperProps = {
|
|
205
|
+
id: `Section${block.id}`,
|
|
206
|
+
className
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Component props
|
|
210
|
+
const componentProps = {
|
|
211
|
+
content,
|
|
212
|
+
params: block.properties,
|
|
213
|
+
block,
|
|
214
|
+
page: globalThis.uniweb?.activeWebsite?.activePage,
|
|
215
|
+
website: globalThis.uniweb?.activeWebsite,
|
|
216
|
+
input: block.input
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return React.createElement(
|
|
220
|
+
'div',
|
|
221
|
+
wrapperProps,
|
|
222
|
+
React.createElement(Component, componentProps)
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Inject rendered content into HTML shell
|
|
228
|
+
*/
|
|
229
|
+
function injectContent(shell, renderedContent, page, siteContent) {
|
|
230
|
+
let html = shell
|
|
231
|
+
|
|
232
|
+
// Replace the empty root div with pre-rendered content
|
|
233
|
+
// Handle various formats of root div
|
|
234
|
+
html = html.replace(
|
|
235
|
+
/<div id="root">[\s\S]*?<\/div>/,
|
|
236
|
+
`<div id="root">${renderedContent}</div>`
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Update page title
|
|
240
|
+
if (page.title) {
|
|
241
|
+
html = html.replace(
|
|
242
|
+
/<title>.*?<\/title>/,
|
|
243
|
+
`<title>${escapeHtml(page.title)}</title>`
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add meta description if available
|
|
248
|
+
if (page.description) {
|
|
249
|
+
const metaDesc = `<meta name="description" content="${escapeHtml(page.description)}">`
|
|
250
|
+
if (html.includes('<meta name="description"')) {
|
|
251
|
+
html = html.replace(
|
|
252
|
+
/<meta name="description"[^>]*>/,
|
|
253
|
+
metaDesc
|
|
254
|
+
)
|
|
255
|
+
} else {
|
|
256
|
+
html = html.replace(
|
|
257
|
+
'</head>',
|
|
258
|
+
` ${metaDesc}\n </head>`
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Inject site content as JSON for hydration
|
|
264
|
+
// This allows the client-side React to hydrate with the same data
|
|
265
|
+
const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${JSON.stringify(siteContent)}</script>`
|
|
266
|
+
if (!html.includes('__SITE_CONTENT__')) {
|
|
267
|
+
html = html.replace(
|
|
268
|
+
'</head>',
|
|
269
|
+
` ${contentScript}\n </head>`
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return html
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get output path for a route
|
|
278
|
+
*/
|
|
279
|
+
function getOutputPath(distDir, route) {
|
|
280
|
+
// Normalize route
|
|
281
|
+
let normalizedRoute = route
|
|
282
|
+
|
|
283
|
+
// Handle root route
|
|
284
|
+
if (normalizedRoute === '/' || normalizedRoute === '') {
|
|
285
|
+
return join(distDir, 'index.html')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Remove leading slash
|
|
289
|
+
if (normalizedRoute.startsWith('/')) {
|
|
290
|
+
normalizedRoute = normalizedRoute.slice(1)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Create directory structure: /about -> /about/index.html
|
|
294
|
+
return join(distDir, normalizedRoute, 'index.html')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Escape HTML special characters
|
|
299
|
+
*/
|
|
300
|
+
function escapeHtml(str) {
|
|
301
|
+
if (!str) return ''
|
|
302
|
+
return String(str)
|
|
303
|
+
.replace(/&/g, '&')
|
|
304
|
+
.replace(/</g, '<')
|
|
305
|
+
.replace(/>/g, '>')
|
|
306
|
+
.replace(/"/g, '"')
|
|
307
|
+
.replace(/'/g, ''')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default prerenderSite
|