@uniweb/build 0.1.31 → 0.1.33

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 CHANGED
@@ -333,6 +333,34 @@ dist/
333
333
  └── default.webp
334
334
  ```
335
335
 
336
+ ### Schema.json Structure
337
+
338
+ The generated `schema.json` contains:
339
+
340
+ ```json
341
+ {
342
+ "_self": {
343
+ "name": "foundation",
344
+ "version": "0.1.0",
345
+ "description": "My foundation description",
346
+ "vars": { ... }
347
+ },
348
+ "Hero": { ... },
349
+ "Features": { ... }
350
+ }
351
+ ```
352
+
353
+ The `_self` object contains foundation-level metadata:
354
+
355
+ | Field | Source | Description |
356
+ |-------|--------|-------------|
357
+ | `name` | `package.json` | Foundation package name |
358
+ | `version` | `package.json` | Foundation version |
359
+ | `description` | `package.json` | Foundation description |
360
+ | `vars` | `foundation.js` | CSS custom properties sites can override |
361
+
362
+ Identity fields (`name`, `version`, `description`) come from the foundation's `package.json`. Configuration fields (`vars`, etc.) come from `src/foundation.js`.
363
+
336
364
  ## API Reference
337
365
 
338
366
  ### Schema Functions
@@ -341,7 +369,8 @@ dist/
341
369
  |----------|-------------|
342
370
  | `discoverComponents(srcDir)` | Discover all exposed components |
343
371
  | `loadComponentMeta(componentDir)` | Load meta file for a component |
344
- | `loadFoundationMeta(srcDir)` | Load foundation-level meta |
372
+ | `loadPackageJson(srcDir)` | Load identity from package.json |
373
+ | `loadFoundationConfig(srcDir)` | Load foundation.js configuration |
345
374
  | `buildSchema(srcDir)` | Build complete schema object |
346
375
 
347
376
  ### Entry Generation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "optionalDependencies": {
53
53
  "@uniweb/content-reader": "1.0.4",
54
- "@uniweb/runtime": "0.2.17"
54
+ "@uniweb/runtime": "0.2.19"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -60,7 +60,7 @@
60
60
  "@tailwindcss/vite": "^4.0.0",
61
61
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
62
62
  "vite-plugin-svgr": "^4.0.0",
63
- "@uniweb/core": "0.1.14"
63
+ "@uniweb/core": "0.1.16"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "vite": {
@@ -26,20 +26,41 @@ import { discoverComponents } from './schema.js'
26
26
  import { extractAllRuntimeSchemas } from './runtime-schema.js'
27
27
 
28
28
  /**
29
- * Detect foundation exports file (for custom Layout, props, etc.)
30
- * Looks for: src/exports.js, src/exports.jsx, src/exports/index.js, src/exports/index.jsx
29
+ * Detect foundation config/exports file (for custom Layout, props, vars, etc.)
30
+ *
31
+ * Looks for (in order of preference):
32
+ * 1. foundation.js - New consolidated format
33
+ * 2. exports.js - Legacy format (for backward compatibility)
34
+ *
35
+ * The file should export:
36
+ * - Layout (optional) - Custom page layout component
37
+ * - props (optional) - Foundation-wide props
38
+ * - vars (optional) - CSS custom properties (also read by schema builder)
31
39
  */
32
40
  function detectFoundationExports(srcDir) {
33
- const candidates = [
41
+ // Prefer foundation.js (new consolidated format)
42
+ const foundationCandidates = [
43
+ { path: 'foundation.js', ext: 'js' },
44
+ { path: 'foundation.jsx', ext: 'jsx' },
45
+ ]
46
+
47
+ for (const { path, ext } of foundationCandidates) {
48
+ if (existsSync(join(srcDir, path))) {
49
+ return { path: `./${path}`, ext, isFoundationJs: true }
50
+ }
51
+ }
52
+
53
+ // Fall back to exports.js (legacy format)
54
+ const legacyCandidates = [
34
55
  { path: 'exports.js', ext: 'js' },
35
56
  { path: 'exports.jsx', ext: 'jsx' },
36
57
  { path: 'exports/index.js', ext: 'js' },
37
58
  { path: 'exports/index.jsx', ext: 'jsx' },
38
59
  ]
39
60
 
40
- for (const { path, ext } of candidates) {
61
+ for (const { path, ext } of legacyCandidates) {
41
62
  if (existsSync(join(srcDir, path))) {
42
- return { path: `./${path.replace(/\/index\.(js|jsx)$/, '')}`, ext }
63
+ return { path: `./${path.replace(/\/index\.(js|jsx)$/, '')}`, ext, isFoundationJs: false }
43
64
  }
44
65
  }
45
66
  return null
package/src/index.js CHANGED
@@ -7,7 +7,8 @@
7
7
  // Schema discovery and loading
8
8
  export {
9
9
  loadComponentMeta,
10
- loadFoundationMeta,
10
+ loadFoundationConfig,
11
+ loadFoundationMeta, // @deprecated - use loadFoundationConfig
11
12
  discoverComponents,
12
13
  buildSchema,
13
14
  getExposedComponents,
package/src/prerender.js CHANGED
@@ -516,6 +516,14 @@ export async function prerenderSite(siteDir, options = {}) {
516
516
  function injectContent(shell, renderedContent, page, siteContent) {
517
517
  let html = shell
518
518
 
519
+ // Inject theme CSS if not already present
520
+ if (siteContent?.theme?.css && !html.includes('id="uniweb-theme"')) {
521
+ html = html.replace(
522
+ '</head>',
523
+ ` <style id="uniweb-theme">\n${siteContent.theme.css}\n </style>\n </head>`
524
+ )
525
+ }
526
+
519
527
  // Replace the empty root div with pre-rendered content
520
528
  html = html.replace(
521
529
  /<div id="root">[\s\S]*?<\/div>/,
@@ -548,7 +556,13 @@ function injectContent(shell, renderedContent, page, siteContent) {
548
556
 
549
557
  // Inject site content as JSON for hydration
550
558
  // Replace existing content if present, otherwise add it
551
- const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${JSON.stringify(siteContent)}</script>`
559
+ // Strip CSS from theme (it's already in a <style> tag)
560
+ const contentForJson = { ...siteContent }
561
+ if (contentForJson.theme?.css) {
562
+ contentForJson.theme = { ...contentForJson.theme }
563
+ delete contentForJson.theme.css
564
+ }
565
+ const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${JSON.stringify(contentForJson)}</script>`
552
566
  if (html.includes('__SITE_CONTENT__')) {
553
567
  // Replace existing site content with updated version (includes expanded dynamic routes)
554
568
  // Match script tag with attributes in any order
package/src/schema.js CHANGED
@@ -5,14 +5,17 @@
5
5
  * Schema data is for editor-time only, not runtime.
6
6
  */
7
7
 
8
- import { readdir } from 'node:fs/promises'
8
+ import { readdir, readFile } from 'node:fs/promises'
9
9
  import { existsSync } from 'node:fs'
10
- import { join, basename } from 'node:path'
10
+ import { join, dirname } from 'node:path'
11
11
  import { pathToFileURL } from 'node:url'
12
12
 
13
- // Meta file name (standardized to meta.js)
13
+ // Component meta file name
14
14
  const META_FILE_NAME = 'meta.js'
15
15
 
16
+ // Foundation config file name
17
+ const FOUNDATION_FILE_NAME = 'foundation.js'
18
+
16
19
  // Default component paths (relative to srcDir)
17
20
  const DEFAULT_COMPONENT_PATHS = ['components']
18
21
 
@@ -44,21 +47,71 @@ export async function loadComponentMeta(componentDir) {
44
47
  }
45
48
 
46
49
  /**
47
- * Load foundation-level meta file
50
+ * Load package.json from foundation root
51
+ * Extracts identity fields: name, version, description
52
+ *
53
+ * @param {string} srcDir - Source directory (e.g., 'src')
54
+ * @returns {Object} Identity fields from package.json
48
55
  */
49
- export async function loadFoundationMeta(srcDir) {
50
- const filePath = join(srcDir, META_FILE_NAME)
56
+ export async function loadPackageJson(srcDir) {
57
+ // package.json is in the foundation root (parent of srcDir)
58
+ const foundationRoot = dirname(srcDir)
59
+ const packagePath = join(foundationRoot, 'package.json')
60
+
61
+ if (!existsSync(packagePath)) {
62
+ return {}
63
+ }
64
+
65
+ try {
66
+ const content = await readFile(packagePath, 'utf-8')
67
+ const pkg = JSON.parse(content)
68
+
69
+ // Extract only identity fields for schema
70
+ return {
71
+ name: pkg.name,
72
+ version: pkg.version,
73
+ description: pkg.description,
74
+ }
75
+ } catch (error) {
76
+ console.warn(`Warning: Failed to load package.json:`, error.message)
77
+ return {}
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Load foundation-level config file (foundation.js)
83
+ *
84
+ * Contains foundation-wide configuration:
85
+ * - vars: CSS custom properties sites can override
86
+ * - Layout: Custom layout component
87
+ * - Future: providers, middleware, etc.
88
+ */
89
+ export async function loadFoundationConfig(srcDir) {
90
+ const filePath = join(srcDir, FOUNDATION_FILE_NAME)
51
91
  if (!existsSync(filePath)) {
52
92
  return {}
53
93
  }
54
94
  try {
55
- return await loadMetaFile(filePath)
95
+ const module = await import(pathToFileURL(filePath).href)
96
+ // Support both default export and named exports
97
+ return {
98
+ ...module.default,
99
+ vars: module.vars || module.default?.vars,
100
+ Layout: module.Layout || module.default?.Layout,
101
+ }
56
102
  } catch (error) {
57
- console.warn(`Warning: Failed to load foundation meta ${filePath}:`, error.message)
103
+ console.warn(`Warning: Failed to load foundation config ${filePath}:`, error.message)
58
104
  return {}
59
105
  }
60
106
  }
61
107
 
108
+ /**
109
+ * @deprecated Use loadFoundationConfig instead
110
+ */
111
+ export async function loadFoundationMeta(srcDir) {
112
+ return loadFoundationConfig(srcDir)
113
+ }
114
+
62
115
  /**
63
116
  * Discover components in a single path
64
117
  * @param {string} srcDir - Source directory (e.g., 'src')
@@ -127,17 +180,31 @@ export async function discoverComponents(srcDir, componentPaths = DEFAULT_COMPON
127
180
 
128
181
  /**
129
182
  * Build complete schema for a foundation
130
- * Returns { _self: foundationMeta, ComponentName: componentMeta, ... }
183
+ * Returns { _self: { identity + config }, ComponentName: componentMeta, ... }
184
+ *
185
+ * The _self object contains:
186
+ * - Identity from package.json (name, version, description)
187
+ * - Configuration from foundation.js (vars, Layout, etc.)
131
188
  *
132
189
  * @param {string} srcDir - Source directory
133
190
  * @param {string[]} [componentPaths] - Paths to search for components
134
191
  */
135
192
  export async function buildSchema(srcDir, componentPaths) {
136
- const foundationMeta = await loadFoundationMeta(srcDir)
193
+ // Load identity from package.json
194
+ const identity = await loadPackageJson(srcDir)
195
+
196
+ // Load configuration from foundation.js
197
+ const foundationConfig = await loadFoundationConfig(srcDir)
198
+
199
+ // Discover components
137
200
  const components = await discoverComponents(srcDir, componentPaths)
138
201
 
139
202
  return {
140
- _self: foundationMeta,
203
+ // Merge identity and config - identity fields take precedence
204
+ _self: {
205
+ ...foundationConfig,
206
+ ...identity,
207
+ },
141
208
  ...components,
142
209
  }
143
210
  }
@@ -63,6 +63,7 @@ try {
63
63
  * // Extended form
64
64
  * parseCollectionConfig('articles', {
65
65
  * path: 'library/articles',
66
+ * route: '/blog',
66
67
  * sort: 'date desc',
67
68
  * filter: 'published != false',
68
69
  * limit: 100
@@ -73,6 +74,7 @@ function parseCollectionConfig(name, config) {
73
74
  return {
74
75
  name,
75
76
  path: config,
77
+ route: null,
76
78
  sort: null,
77
79
  filter: null,
78
80
  limit: 0,
@@ -83,6 +85,7 @@ function parseCollectionConfig(name, config) {
83
85
  return {
84
86
  name,
85
87
  path: config.path,
88
+ route: config.route || null,
86
89
  sort: config.sort || null,
87
90
  filter: config.filter || null,
88
91
  limit: config.limit || 0,
@@ -274,6 +277,15 @@ async function collectItems(siteDir, config) {
274
277
  // Filter out nulls (unpublished items)
275
278
  items = items.filter(Boolean)
276
279
 
280
+ // Add routes to items if collection has a route configured
281
+ if (config.route) {
282
+ const baseRoute = config.route.replace(/\/$/, '') // Remove trailing slash
283
+ items = items.map(item => ({
284
+ ...item,
285
+ route: `${baseRoute}/${item.slug}`
286
+ }))
287
+ }
288
+
277
289
  // Apply custom filter
278
290
  if (config.filter) {
279
291
  items = applyFilter(items, config.filter)
@@ -235,7 +235,8 @@ export async function defineSiteConfig(options = {}) {
235
235
  inject: true,
236
236
  seo,
237
237
  assets,
238
- search
238
+ search,
239
+ foundationPath: foundationInfo.path // For loading foundation theme vars
239
240
  }),
240
241
 
241
242
  // Foundation dev server (only in runtime mode with local foundation)
@@ -28,6 +28,7 @@ import { existsSync } from 'node:fs'
28
28
  import yaml from 'js-yaml'
29
29
  import { collectSectionAssets, mergeAssetCollections } from './assets.js'
30
30
  import { parseFetchConfig, singularize } from './data-fetcher.js'
31
+ import { buildTheme, extractFoundationVars } from '../theme/index.js'
31
32
 
32
33
  // Try to import content-reader, fall back to simplified parser
33
34
  let markdownToProseMirror
@@ -161,11 +162,20 @@ async function processMarkdownFile(filePath, id, siteRoot) {
161
162
  }
162
163
  }
163
164
 
164
- const { type, component, preset, input, props, fetch, ...params } = frontMatter
165
+ const { type, component, preset, input, props, fetch, data, ...params } = frontMatter
165
166
 
166
167
  // Convert markdown to ProseMirror
167
168
  const proseMirrorContent = markdownToProseMirror(markdown)
168
169
 
170
+ // Support 'data:' shorthand for collection fetch
171
+ // data: team → fetch: { collection: team }
172
+ // data: [team, articles] → fetch: { collection: team } (first item, others via inheritData)
173
+ let resolvedFetch = fetch
174
+ if (!fetch && data) {
175
+ const collectionName = Array.isArray(data) ? data[0] : data
176
+ resolvedFetch = { collection: collectionName }
177
+ }
178
+
169
179
  const section = {
170
180
  id,
171
181
  component: type || component || 'Section',
@@ -173,7 +183,7 @@ async function processMarkdownFile(filePath, id, siteRoot) {
173
183
  input,
174
184
  params: { ...params, ...props },
175
185
  content: proseMirrorContent,
176
- fetch: parseFetchConfig(fetch),
186
+ fetch: parseFetchConfig(resolvedFetch),
177
187
  subsections: []
178
188
  }
179
189
 
@@ -438,7 +448,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
438
448
  },
439
449
 
440
450
  // Data fetching
441
- fetch: parseFetchConfig(pageConfig.fetch),
451
+ // Support 'data:' shorthand at page level
452
+ // data: team → fetch: { collection: team }
453
+ fetch: parseFetchConfig(
454
+ pageConfig.fetch || (pageConfig.data
455
+ ? { collection: Array.isArray(pageConfig.data) ? pageConfig.data[0] : pageConfig.data }
456
+ : undefined)
457
+ ),
442
458
 
443
459
  sections: hierarchicalSections
444
460
  },
@@ -488,7 +504,7 @@ function determineIndexPage(orderConfig, availableFolders) {
488
504
  * @param {string} siteRoot - Site root directory for asset resolution
489
505
  * @param {Object} orderConfig - { pages: [...], index: 'name' } from parent's config
490
506
  * @param {Object} parentFetch - Parent page's fetch config (for dynamic child routes)
491
- * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right }
507
+ * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right, notFound }
492
508
  */
493
509
  async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null) {
494
510
  const entries = await readdir(dirPath)
@@ -502,6 +518,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
502
518
  let footer = null
503
519
  let left = null
504
520
  let right = null
521
+ let notFound = null
505
522
 
506
523
  // First pass: discover all page folders and read their order values
507
524
  const pageFolders = []
@@ -545,7 +562,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
545
562
  const { page, assetCollection: pageAssets } = result
546
563
  assetCollection = mergeAssetCollections(assetCollection, pageAssets)
547
564
 
548
- // Handle special pages (layout areas) - only at root level
565
+ // Handle special pages (layout areas and 404) - only at root level
549
566
  if (parentRoute === '/') {
550
567
  if (entry === '@header') {
551
568
  header = page
@@ -555,6 +572,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
555
572
  left = page
556
573
  } else if (entry === '@right') {
557
574
  right = page
575
+ } else if (entry === '404') {
576
+ notFound = page
558
577
  } else {
559
578
  pages.push(page)
560
579
  }
@@ -575,27 +594,72 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
575
594
  }
576
595
  }
577
596
 
578
- return { pages, assetCollection, header, footer, left, right }
597
+ return { pages, assetCollection, header, footer, left, right, notFound }
598
+ }
599
+
600
+ /**
601
+ * Load foundation variables from schema.json
602
+ *
603
+ * @param {string} foundationPath - Path to foundation directory
604
+ * @returns {Promise<Object>} Foundation variables or empty object
605
+ */
606
+ async function loadFoundationVars(foundationPath) {
607
+ if (!foundationPath) return {}
608
+
609
+ // Try dist/schema.json first (built foundation), then src/schema.json
610
+ const distSchemaPath = join(foundationPath, 'dist', 'schema.json')
611
+ const srcSchemaPath = join(foundationPath, 'schema.json')
612
+
613
+ const schemaPath = existsSync(distSchemaPath) ? distSchemaPath : srcSchemaPath
614
+
615
+ if (!existsSync(schemaPath)) {
616
+ return {}
617
+ }
618
+
619
+ try {
620
+ const schemaContent = await readFile(schemaPath, 'utf8')
621
+ const schema = JSON.parse(schemaContent)
622
+ // Foundation config is in _self, support both 'vars' (new) and 'themeVars' (legacy)
623
+ return schema._self?.vars || schema._self?.themeVars || schema.themeVars || {}
624
+ } catch (err) {
625
+ console.warn('[content-collector] Failed to load foundation schema:', err.message)
626
+ return {}
627
+ }
579
628
  }
580
629
 
581
630
  /**
582
631
  * Collect all site content
583
632
  *
584
633
  * @param {string} sitePath - Path to site directory
634
+ * @param {Object} options - Collection options
635
+ * @param {string} options.foundationPath - Path to foundation directory (for theme vars)
585
636
  * @returns {Promise<Object>} Site content object with assets manifest
586
637
  */
587
- export async function collectSiteContent(sitePath) {
638
+ export async function collectSiteContent(sitePath, options = {}) {
639
+ const { foundationPath } = options
588
640
  const pagesPath = join(sitePath, 'pages')
589
641
 
590
- // Read site config
642
+ // Read site config and raw theme config
591
643
  const siteConfig = await readYamlFile(join(sitePath, 'site.yml'))
592
- const themeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
644
+ const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
645
+
646
+ // Load foundation vars and process theme
647
+ const foundationVars = await loadFoundationVars(foundationPath)
648
+ const { config: processedTheme, css: themeCSS, warnings } = buildTheme(rawThemeConfig, { foundationVars })
649
+
650
+ // Log theme warnings
651
+ if (warnings?.length > 0) {
652
+ warnings.forEach(w => console.warn(`[theme] ${w}`))
653
+ }
593
654
 
594
655
  // Check if pages directory exists
595
656
  if (!existsSync(pagesPath)) {
596
657
  return {
597
658
  config: siteConfig,
598
- theme: themeConfig,
659
+ theme: {
660
+ ...processedTheme,
661
+ css: themeCSS
662
+ },
599
663
  pages: [],
600
664
  assets: {}
601
665
  }
@@ -608,7 +672,7 @@ export async function collectSiteContent(sitePath) {
608
672
  }
609
673
 
610
674
  // Recursively collect all pages
611
- const { pages, assetCollection, header, footer, left, right } =
675
+ const { pages, assetCollection, header, footer, left, right, notFound } =
612
676
  await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
613
677
 
614
678
  // Sort pages by order
@@ -626,12 +690,16 @@ export async function collectSiteContent(sitePath) {
626
690
  ...siteConfig,
627
691
  fetch: parseFetchConfig(siteConfig.fetch),
628
692
  },
629
- theme: themeConfig,
693
+ theme: {
694
+ ...processedTheme,
695
+ css: themeCSS
696
+ },
630
697
  pages,
631
698
  header,
632
699
  footer,
633
700
  left,
634
701
  right,
702
+ notFound,
635
703
  assets: assetCollection.assets,
636
704
  hasExplicitPoster: assetCollection.hasExplicitPoster,
637
705
  hasExplicitPreview: assetCollection.hasExplicitPreview
@@ -305,6 +305,7 @@ function escapeHtml(str) {
305
305
  * @param {Object} [options.search] - Search index configuration
306
306
  * @param {boolean} [options.search.enabled=true] - Generate search index (uses site.yml config by default)
307
307
  * @param {string} [options.search.filename='search-index.json'] - Search index filename
308
+ * @param {string} [options.foundationPath] - Path to foundation directory (for loading theme vars)
308
309
  */
309
310
  export function siteContentPlugin(options = {}) {
310
311
  const {
@@ -316,7 +317,8 @@ export function siteContentPlugin(options = {}) {
316
317
  watch: shouldWatch = true,
317
318
  seo = {},
318
319
  assets: assetsConfig = {},
319
- search: searchPluginConfig = {}
320
+ search: searchPluginConfig = {},
321
+ foundationPath
320
322
  } = options
321
323
 
322
324
  // Extract asset processing options
@@ -416,7 +418,7 @@ export function siteContentPlugin(options = {}) {
416
418
  if (!isProduction) {
417
419
  try {
418
420
  // Do an early content collection to get the collections config
419
- const earlyContent = await collectSiteContent(resolvedSitePath)
421
+ const earlyContent = await collectSiteContent(resolvedSitePath, { foundationPath })
420
422
  collectionsConfig = earlyContent.config?.collections
421
423
 
422
424
  if (collectionsConfig) {
@@ -433,7 +435,7 @@ export function siteContentPlugin(options = {}) {
433
435
  async buildStart() {
434
436
  // Collect content at build start
435
437
  try {
436
- siteContent = await collectSiteContent(resolvedSitePath)
438
+ siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
437
439
  console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
438
440
 
439
441
  // Process content collections if defined in site.yml
@@ -480,7 +482,7 @@ export function siteContentPlugin(options = {}) {
480
482
  rebuildTimeout = setTimeout(async () => {
481
483
  console.log('[site-content] Content changed, rebuilding...')
482
484
  try {
483
- siteContent = await collectSiteContent(resolvedSitePath)
485
+ siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
484
486
  // Execute fetches for the updated content
485
487
  await executeDevFetches(siteContent, resolvedSitePath)
486
488
  console.log(`[site-content] Rebuilt ${siteContent.pages?.length || 0} pages`)
@@ -685,6 +687,11 @@ export function siteContentPlugin(options = {}) {
685
687
 
686
688
  let headInjection = ''
687
689
 
690
+ // Inject theme CSS
691
+ if (contentToInject.theme?.css) {
692
+ headInjection += ` <style id="uniweb-theme">\n${contentToInject.theme.css}\n </style>\n`
693
+ }
694
+
688
695
  // Inject SEO meta tags
689
696
  if (seoEnabled) {
690
697
  const metaTags = generateMetaTags(contentToInject, seoOptions)
@@ -791,6 +798,9 @@ export function siteContentPlugin(options = {}) {
791
798
  delete finalContent.hasExplicitPoster
792
799
  delete finalContent.hasExplicitPreview
793
800
 
801
+ // Note: theme.css is kept here so prerender can inject it into HTML
802
+ // Prerender will strip it from the JSON it injects into each page
803
+
794
804
  // Emit content as JSON file in production build
795
805
  this.emitFile({
796
806
  type: 'asset',