@uniweb/build 0.6.3 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,9 +50,9 @@
50
50
  "sharp": "^0.33.2"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@uniweb/content-reader": "1.1.2",
54
- "@uniweb/runtime": "0.5.13",
55
- "@uniweb/schemas": "0.2.1"
53
+ "@uniweb/schemas": "0.2.1",
54
+ "@uniweb/runtime": "0.5.14",
55
+ "@uniweb/content-reader": "1.1.2"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -61,7 +61,7 @@
61
61
  "@tailwindcss/vite": "^4.0.0",
62
62
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
63
63
  "vite-plugin-svgr": "^4.0.0",
64
- "@uniweb/core": "0.4.1"
64
+ "@uniweb/core": "0.4.3"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "vite": {
@@ -41,7 +41,7 @@ export function extractTranslatableContent(siteContent) {
41
41
  for (const layoutKey of ['header', 'footer', 'left', 'right']) {
42
42
  const layoutPage = siteContent[layoutKey]
43
43
  if (layoutPage?.sections) {
44
- const pageRoute = layoutPage.route || `/@${layoutKey}`
44
+ const pageRoute = layoutPage.route || `/layout/${layoutKey}`
45
45
  for (const section of layoutPage.sections) {
46
46
  extractFromSection(section, pageRoute, units)
47
47
  }
package/src/i18n/merge.js CHANGED
@@ -80,7 +80,7 @@ function mergeTranslationsSync(siteContent, translations, fallbackToSource) {
80
80
  for (const layoutKey of ['header', 'footer', 'left', 'right']) {
81
81
  const layoutPage = translated[layoutKey]
82
82
  if (layoutPage?.sections) {
83
- const pageRoute = layoutPage.route || `/@${layoutKey}`
83
+ const pageRoute = layoutPage.route || `/layout/${layoutKey}`
84
84
  for (const section of layoutPage.sections) {
85
85
  translateSectionSync(section, pageRoute, translations, fallbackToSource)
86
86
  }
@@ -132,8 +132,8 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
132
132
  for (const layoutKey of ['header', 'footer', 'left', 'right']) {
133
133
  const layoutPage = translated[layoutKey]
134
134
  if (layoutPage?.sections) {
135
- // Ensure route is set for context matching (extract uses /@header, etc.)
136
- if (!layoutPage.route) layoutPage.route = `/@${layoutKey}`
135
+ // Ensure route is set for context matching
136
+ if (!layoutPage.route) layoutPage.route = `/layout/${layoutKey}`
137
137
  for (const section of layoutPage.sections) {
138
138
  await translateSectionAsync(section, layoutPage, translations, {
139
139
  fallbackToSource,
package/src/prerender.js CHANGED
@@ -26,19 +26,41 @@ let preparePropsSSR, getComponentMetaSSR
26
26
  * @param {Object} siteContent - The site content from site-content.json
27
27
  * @param {string} siteDir - Path to the site directory
28
28
  * @param {function} onProgress - Progress callback
29
+ * @param {Object} [localeInfo] - Locale info for collection data localization
30
+ * @param {string} [localeInfo.locale] - Active locale code
31
+ * @param {string} [localeInfo.defaultLocale] - Default locale code
32
+ * @param {string} [localeInfo.distDir] - Path to dist directory (where locale-specific data lives)
29
33
  * @returns {Object} { pageFetchedData, fetchedData } - Fetched data for dynamic route expansion and DataStore pre-population
30
34
  */
31
- async function executeAllFetches(siteContent, siteDir, onProgress) {
35
+ async function executeAllFetches(siteContent, siteDir, onProgress, localeInfo) {
32
36
  const fetchOptions = { siteRoot: siteDir, publicDir: 'public' }
33
37
  const fetchedData = [] // Collected for DataStore pre-population
34
38
 
39
+ // For non-default locales, translated collection data lives in dist/{locale}/data/
40
+ // instead of public/data/. Create a localized fetch helper.
41
+ const isNonDefaultLocale = localeInfo &&
42
+ localeInfo.locale !== localeInfo.defaultLocale &&
43
+ localeInfo.distDir
44
+
45
+ function localizeFetch(config) {
46
+ if (!isNonDefaultLocale || !config.path?.startsWith('/data/')) return config
47
+ return { ...config, path: `/${localeInfo.locale}${config.path}` }
48
+ }
49
+
50
+ // Fetch options pointing to dist/ for localized data
51
+ const localizedFetchOptions = isNonDefaultLocale
52
+ ? { siteRoot: localeInfo.distDir, publicDir: '.' }
53
+ : fetchOptions
54
+
35
55
  // 1. Site-level fetch
36
56
  const siteFetch = siteContent.config?.fetch
37
57
  if (siteFetch && siteFetch.prerender !== false) {
38
- onProgress(` Fetching site data: ${siteFetch.path || siteFetch.url}`)
39
- const result = await executeFetch(siteFetch, fetchOptions)
58
+ const cfg = localizeFetch(siteFetch)
59
+ const opts = cfg !== siteFetch ? localizedFetchOptions : fetchOptions
60
+ onProgress(` Fetching site data: ${cfg.path || cfg.url}`)
61
+ const result = await executeFetch(cfg, opts)
40
62
  if (result.data && !result.error) {
41
- fetchedData.push({ config: siteFetch, data: result.data })
63
+ fetchedData.push({ config: cfg, data: result.data })
42
64
  }
43
65
  }
44
66
 
@@ -49,10 +71,12 @@ async function executeAllFetches(siteContent, siteDir, onProgress) {
49
71
  // Page-level fetch
50
72
  const pageFetch = page.fetch
51
73
  if (pageFetch && pageFetch.prerender !== false) {
52
- onProgress(` Fetching page data for ${page.route}: ${pageFetch.path || pageFetch.url}`)
53
- const result = await executeFetch(pageFetch, fetchOptions)
74
+ const cfg = localizeFetch(pageFetch)
75
+ const opts = cfg !== pageFetch ? localizedFetchOptions : fetchOptions
76
+ onProgress(` Fetching page data for ${page.route}: ${cfg.path || cfg.url}`)
77
+ const result = await executeFetch(cfg, opts)
54
78
  if (result.data && !result.error) {
55
- fetchedData.push({ config: pageFetch, data: result.data })
79
+ fetchedData.push({ config: cfg, data: result.data })
56
80
  // Store for dynamic route expansion
57
81
  pageFetchedData.set(page.route, {
58
82
  schema: pageFetch.schema,
@@ -656,8 +680,13 @@ export async function prerenderSite(siteDir, options = {}) {
656
680
  siteContent.config.activeLocale = locale
657
681
 
658
682
  // Execute data fetches (site, page, section levels)
683
+ // For non-default locales, collection data is read from dist/{locale}/data/
659
684
  onProgress('Executing data fetches...')
660
- const { pageFetchedData, fetchedData } = await executeAllFetches(siteContent, siteDir, onProgress)
685
+ const defaultLocale = defaultSiteContent.config?.defaultLanguage || 'en'
686
+ const { pageFetchedData, fetchedData } = await executeAllFetches(
687
+ siteContent, siteDir, onProgress,
688
+ { locale, defaultLocale, distDir }
689
+ )
661
690
 
662
691
  // Store fetchedData on siteContent for runtime DataStore pre-population
663
692
  siteContent.fetchedData = fetchedData
@@ -41,11 +41,6 @@ export function extractSearchContent(siteContent, options = {}) {
41
41
  continue
42
42
  }
43
43
 
44
- // Skip special pages (header, footer, etc.)
45
- if (pageRoute.startsWith('/@')) {
46
- continue
47
- }
48
-
49
44
  // Skip pages marked as noindex
50
45
  if (page.seo?.noindex) {
51
46
  continue
@@ -17,7 +17,7 @@
17
17
  * // site.yml
18
18
  * collections:
19
19
  * articles:
20
- * path: library/articles
20
+ * path: collections/articles
21
21
  * sort: date desc
22
22
  *
23
23
  * // Usage
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  import { readFile, readdir, stat, writeFile, mkdir, copyFile } from 'node:fs/promises'
29
- import { join, basename, extname, dirname, relative } from 'node:path'
29
+ import { join, basename, extname, dirname, relative, resolve } from 'node:path'
30
30
  import { existsSync } from 'node:fs'
31
31
  import yaml from 'js-yaml'
32
32
  import { applyFilter, applySort } from './data-fetcher.js'
@@ -59,11 +59,11 @@ try {
59
59
  *
60
60
  * @example
61
61
  * // Simple form
62
- * parseCollectionConfig('articles', 'library/articles')
62
+ * parseCollectionConfig('articles', 'collections/articles')
63
63
  *
64
64
  * // Extended form
65
65
  * parseCollectionConfig('articles', {
66
- * path: 'library/articles',
66
+ * path: 'collections/articles',
67
67
  * route: '/blog',
68
68
  * sort: 'date desc',
69
69
  * filter: 'published != false',
@@ -213,7 +213,7 @@ function isExternalUrl(src) {
213
213
  /**
214
214
  * Process assets in collection content
215
215
  * - Resolves relative paths to site-root-relative paths
216
- * - Copies co-located assets to public/library/<collection>/
216
+ * - Copies co-located assets to public/collections/<collection>/
217
217
  * - Updates paths in the content in place
218
218
  *
219
219
  * @param {Object} content - ProseMirror document
@@ -226,7 +226,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
226
226
  const assets = {}
227
227
  const itemDir = dirname(itemPath)
228
228
  const publicDir = join(siteRoot, 'public')
229
- const targetDir = join(publicDir, 'library', collectionName)
229
+ const targetDir = join(publicDir, 'collections', collectionName)
230
230
 
231
231
  // Walk content and collect asset paths
232
232
  const assetNodes = []
@@ -248,7 +248,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
248
248
  if (src.startsWith('./') || src.startsWith('../')) {
249
249
  // Check if file exists at resolved location
250
250
  if (existsSync(result.resolved)) {
251
- // Copy to public/library/<collection>/
251
+ // Copy to public/collections/<collection>/
252
252
  const assetFilename = basename(result.resolved)
253
253
  const targetPath = join(targetDir, assetFilename)
254
254
 
@@ -259,7 +259,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
259
259
  await copyFile(result.resolved, targetPath)
260
260
 
261
261
  // Update path to site-root-relative
262
- finalPath = `/library/${collectionName}/${assetFilename}`
262
+ finalPath = `/collections/${collectionName}/${assetFilename}`
263
263
 
264
264
  assets[src] = {
265
265
  original: src,
@@ -294,7 +294,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
294
294
  const posterTarget = join(targetDir, posterFilename)
295
295
  await mkdir(targetDir, { recursive: true })
296
296
  await copyFile(posterResult.resolved, posterTarget)
297
- node.attrs.poster = `/library/${collectionName}/${posterFilename}`
297
+ node.attrs.poster = `/collections/${collectionName}/${posterFilename}`
298
298
  }
299
299
  }
300
300
 
@@ -305,7 +305,7 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
305
305
  const previewTarget = join(targetDir, previewFilename)
306
306
  await mkdir(targetDir, { recursive: true })
307
307
  await copyFile(previewResult.resolved, previewTarget)
308
- node.attrs.preview = `/library/${collectionName}/${previewFilename}`
308
+ node.attrs.preview = `/collections/${collectionName}/${previewFilename}`
309
309
  }
310
310
  }
311
311
  }
@@ -421,8 +421,9 @@ async function processContentItem(dir, filename, config, siteRoot) {
421
421
  * @param {Object} config - Parsed collection config
422
422
  * @returns {Promise<Array>} Array of processed items
423
423
  */
424
- async function collectItems(siteDir, config) {
425
- const collectionDir = join(siteDir, config.path)
424
+ async function collectItems(siteDir, config, collectionsBase) {
425
+ const base = collectionsBase || siteDir
426
+ const collectionDir = resolve(base, config.path)
426
427
 
427
428
  // Check if collection directory exists
428
429
  if (!existsSync(collectionDir)) {
@@ -491,12 +492,12 @@ async function collectItems(siteDir, config) {
491
492
  *
492
493
  * @example
493
494
  * const collections = await processCollections('/path/to/site', {
494
- * articles: { path: 'library/articles', sort: 'date desc' },
495
- * products: 'library/products'
495
+ * articles: { path: 'collections/articles', sort: 'date desc' },
496
+ * products: 'collections/products'
496
497
  * })
497
498
  * // { articles: [...], products: [...] }
498
499
  */
499
- export async function processCollections(siteDir, collectionsConfig) {
500
+ export async function processCollections(siteDir, collectionsConfig, collectionsBase) {
500
501
  if (!collectionsConfig || typeof collectionsConfig !== 'object') {
501
502
  return {}
502
503
  }
@@ -505,7 +506,7 @@ export async function processCollections(siteDir, collectionsConfig) {
505
506
 
506
507
  for (const [name, config] of Object.entries(collectionsConfig)) {
507
508
  const parsed = parseCollectionConfig(name, config)
508
- const items = await collectItems(siteDir, parsed)
509
+ const items = await collectItems(siteDir, parsed, collectionsBase)
509
510
  results[name] = items
510
511
  console.log(`[collection-processor] Processed ${name}: ${items.length} items`)
511
512
  }
@@ -328,7 +328,21 @@ export async function defineSiteConfig(options = {}) {
328
328
  server: {
329
329
  fs: {
330
330
  // Allow parent directory for foundation sibling access
331
- allow: ['..']
331
+ // Plus any external content paths from site.yml paths: group
332
+ allow: (() => {
333
+ const allowed = ['..']
334
+ const parentDir = resolve(siteRoot, '..')
335
+ const paths = siteConfig.paths || {}
336
+ for (const key of ['pages', 'layout', 'collections']) {
337
+ if (paths[key]) {
338
+ const resolved = resolve(siteRoot, paths[key])
339
+ if (!resolved.startsWith(parentDir)) {
340
+ allowed.push(resolved)
341
+ }
342
+ }
343
+ }
344
+ return allowed
345
+ })()
332
346
  },
333
347
  ...(siteConfig.build?.port && { port: siteConfig.build.port }),
334
348
  ...serverOverrides
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Content Collector
3
3
  *
4
- * Collects site content from a pages/ directory structure:
4
+ * Collects site content from a site directory structure:
5
5
  * - site.yml: Site configuration
6
6
  * - pages/: Directory of page folders
7
7
  * - page.yml: Page metadata
8
8
  * - *.md: Section content with YAML frontmatter
9
+ * - layout/: Layout panel folders (header, footer, left, right)
9
10
  *
10
11
  * Section frontmatter reserved properties:
11
12
  * - type: Component type (e.g., "Hero", "Features")
@@ -23,7 +24,7 @@
23
24
  */
24
25
 
25
26
  import { readFile, readdir, stat } from 'node:fs/promises'
26
- import { join, parse } from 'node:path'
27
+ import { join, parse, resolve } from 'node:path'
27
28
  import { existsSync } from 'node:fs'
28
29
  import yaml from 'js-yaml'
29
30
  import { collectSectionAssets, mergeAssetCollections } from './assets.js'
@@ -502,10 +503,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
502
503
 
503
504
  // First, calculate the folder-based route (what the route would be without index handling)
504
505
  let folderRoute
505
- if (pageName.startsWith('@')) {
506
- // Special pages (layout areas) keep their @ prefix
507
- folderRoute = parentRoute === '/' ? `/@${pageName.slice(1)}` : `${parentRoute}/@${pageName.slice(1)}`
508
- } else if (isDynamic) {
506
+ if (isDynamic) {
509
507
  // Dynamic routes: /blog/[slug] → /blog/:slug (for route matching)
510
508
  folderRoute = parentRoute === '/' ? `/:${paramName}` : `${parentRoute}/:${paramName}`
511
509
  } else {
@@ -648,10 +646,6 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
648
646
  icons: new Set(),
649
647
  bySource: new Map()
650
648
  }
651
- let header = null
652
- let footer = null
653
- let left = null
654
- let right = null
655
649
  let notFound = null
656
650
  const versionedScopes = new Map() // scope route → versionMeta
657
651
 
@@ -737,8 +731,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
737
731
  for (const [scope, meta] of subResult.versionedScopes) {
738
732
  versionedScopes.set(scope, meta)
739
733
  }
740
- } else if (!entry.startsWith('@')) {
741
- // Non-version, non-special folders in a versioned section
734
+ } else {
735
+ // Non-version folders in a versioned section
742
736
  // These could be shared across versions - process normally
743
737
  const result = await processPage(entryPath, entry, siteRoot, {
744
738
  isIndex: false,
@@ -755,14 +749,14 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
755
749
  }
756
750
 
757
751
  // Return early - we've handled all children
758
- return { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes }
752
+ return { pages, assetCollection, iconCollection, notFound, versionedScopes }
759
753
  }
760
754
 
761
755
  // Determine which page is the index for this level
762
756
  // A directory with its own .md content is a real page, not a container —
763
757
  // never promote a child as index, even if explicit config says so
764
758
  // (that config is likely a leftover from before the directory had content)
765
- const regularFolders = pageFolders.filter(f => !f.name.startsWith('@'))
759
+ const regularFolders = pageFolders
766
760
  const hasExplicitOrder = orderConfig?.index || (Array.isArray(orderConfig?.pages) && orderConfig.pages.length > 0)
767
761
  const hasMdContent = entries.some(e => isMarkdownFile(e))
768
762
  const indexPageName = hasMdContent ? null : determineIndexPage(orderConfig, regularFolders)
@@ -771,12 +765,11 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
771
765
  for (const folder of pageFolders) {
772
766
  const { name: entry, path: entryPath, childOrderConfig } = folder
773
767
  const isIndex = entry === indexPageName
774
- const isSpecial = entry.startsWith('@')
775
768
 
776
769
  // Process this directory as a page
777
770
  // Pass parentFetch so dynamic routes can inherit parent's data schema
778
771
  const result = await processPage(entryPath, entry, siteRoot, {
779
- isIndex: isIndex && !isSpecial,
772
+ isIndex,
780
773
  parentRoute,
781
774
  parentFetch,
782
775
  versionContext
@@ -787,27 +780,15 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
787
780
  assetCollection = mergeAssetCollections(assetCollection, pageAssets)
788
781
  iconCollection = mergeIconCollections(iconCollection, pageIcons)
789
782
 
790
- // Handle special pages (layout areas and 404) - only at root level
791
- if (parentRoute === '/') {
792
- if (entry === '@header') {
793
- header = page
794
- } else if (entry === '@footer') {
795
- footer = page
796
- } else if (entry === '@left') {
797
- left = page
798
- } else if (entry === '@right') {
799
- right = page
800
- } else if (entry === '404') {
801
- notFound = page
802
- } else {
803
- pages.push(page)
804
- }
783
+ // Handle 404 page - only at root level
784
+ if (parentRoute === '/' && entry === '404') {
785
+ notFound = page
805
786
  } else {
806
787
  pages.push(page)
807
788
  }
808
789
 
809
- // Recursively process subdirectories (but not special @ directories)
810
- if (!isSpecial) {
790
+ // Recursively process subdirectories
791
+ {
811
792
  // The child route depends on whether this page is the index
812
793
  // For explicit index (from site.yml `index:` or `pages:`), children use parentRoute
813
794
  // since that's a true structural promotion. For auto-detected index, children use
@@ -830,7 +811,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
830
811
  }
831
812
  }
832
813
 
833
- return { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes }
814
+ return { pages, assetCollection, iconCollection, notFound, versionedScopes }
834
815
  }
835
816
 
836
817
  /**
@@ -863,6 +844,43 @@ async function loadFoundationVars(foundationPath) {
863
844
  }
864
845
  }
865
846
 
847
+ /**
848
+ * Collect layout panels from the layout/ directory
849
+ *
850
+ * Layout panels (header, footer, left, right) are persistent regions
851
+ * that appear on every page. They live in layout/ parallel to pages/.
852
+ *
853
+ * @param {string} layoutDir - Path to layout directory
854
+ * @param {string} siteRoot - Path to site root
855
+ * @returns {Promise<Object>} { header, footer, left, right }
856
+ */
857
+ async function collectLayoutPanels(layoutDir, siteRoot) {
858
+ const result = { header: null, footer: null, left: null, right: null }
859
+
860
+ if (!existsSync(layoutDir)) return result
861
+
862
+ const knownPanels = ['header', 'footer', 'left', 'right']
863
+ const entries = await readdir(layoutDir)
864
+
865
+ for (const entry of entries) {
866
+ if (!knownPanels.includes(entry)) continue
867
+ const entryPath = join(layoutDir, entry)
868
+ const stats = await stat(entryPath)
869
+ if (!stats.isDirectory()) continue
870
+
871
+ const pageResult = await processPage(entryPath, entry, siteRoot, {
872
+ isIndex: false,
873
+ parentRoute: '/layout'
874
+ })
875
+
876
+ if (pageResult) {
877
+ result[entry] = pageResult.page
878
+ }
879
+ }
880
+
881
+ return result
882
+ }
883
+
866
884
  /**
867
885
  * Collect all site content
868
886
  *
@@ -873,10 +891,18 @@ async function loadFoundationVars(foundationPath) {
873
891
  */
874
892
  export async function collectSiteContent(sitePath, options = {}) {
875
893
  const { foundationPath } = options
876
- const pagesPath = join(sitePath, 'pages')
877
894
 
878
895
  // Read site config and raw theme config
879
896
  const siteConfig = await readYamlFile(join(sitePath, 'site.yml'))
897
+
898
+ // Resolve content paths from site.yml paths: group, defaulting to standard locations
899
+ const pagesPath = siteConfig.paths?.pages
900
+ ? resolve(sitePath, siteConfig.paths.pages)
901
+ : join(sitePath, 'pages')
902
+
903
+ const layoutPath = siteConfig.paths?.layout
904
+ ? resolve(sitePath, siteConfig.paths.layout)
905
+ : join(sitePath, 'layout')
880
906
  const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
881
907
 
882
908
  // Load foundation vars and process theme
@@ -907,8 +933,11 @@ export async function collectSiteContent(sitePath, options = {}) {
907
933
  index: siteConfig.index
908
934
  }
909
935
 
936
+ // Collect layout panels from layout/ directory
937
+ const { header, footer, left, right } = await collectLayoutPanels(layoutPath, sitePath)
938
+
910
939
  // Recursively collect all pages
911
- const { pages, assetCollection, iconCollection, header, footer, left, right, notFound, versionedScopes } =
940
+ const { pages, assetCollection, iconCollection, notFound, versionedScopes } =
912
941
  await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
913
942
 
914
943
  // Deduplicate: remove content-less container pages whose route duplicates
@@ -376,6 +376,9 @@ export function siteContentPlugin(options = {}) {
376
376
  let collectionTranslations = {} // Cache: { locale: collection translations }
377
377
  let localesDir = 'locales' // Default, updated from site config
378
378
  let collectionsConfig = null // Cached for watcher setup
379
+ let resolvedPagesPath = null // Resolved from site.yml pagesDir or default
380
+ let resolvedLayoutPath = null // Resolved from site.yml layoutDir or default
381
+ let resolvedCollectionsBase = null // Resolved from site.yml collectionsDir
379
382
 
380
383
  /**
381
384
  * Load translations for a specific locale
@@ -486,15 +489,43 @@ export function siteContentPlugin(options = {}) {
486
489
  const earlyContent = await collectSiteContent(resolvedSitePath, { foundationPath })
487
490
  collectionsConfig = earlyContent.config?.collections
488
491
 
492
+ // Resolve content directory paths from site.yml paths: group
493
+ const paths = earlyContent?.config?.paths || {}
494
+ resolvedPagesPath = paths.pages
495
+ ? resolve(resolvedSitePath, paths.pages)
496
+ : resolve(resolvedSitePath, pagesDir)
497
+ resolvedLayoutPath = paths.layout
498
+ ? resolve(resolvedSitePath, paths.layout)
499
+ : resolve(resolvedSitePath, 'layout')
500
+ resolvedCollectionsBase = paths.collections
501
+ ? resolve(resolvedSitePath, paths.collections)
502
+ : null
503
+
489
504
  if (collectionsConfig) {
490
505
  console.log('[site-content] Processing content collections...')
491
- const collections = await processCollections(resolvedSitePath, collectionsConfig)
506
+ const collections = await processCollections(resolvedSitePath, collectionsConfig, resolvedCollectionsBase)
492
507
  await writeCollectionFiles(resolvedSitePath, collections)
493
508
  }
494
509
  } catch (err) {
495
510
  console.warn('[site-content] Early collection processing failed:', err.message)
496
511
  }
497
512
  }
513
+
514
+ // In production, resolve content paths from site.yml directly
515
+ if (isProduction || !resolvedPagesPath) {
516
+ const { readSiteConfig } = await import('./config.js')
517
+ const cfg = readSiteConfig(resolvedSitePath)
518
+ const paths = cfg.paths || {}
519
+ resolvedPagesPath = paths.pages
520
+ ? resolve(resolvedSitePath, paths.pages)
521
+ : resolve(resolvedSitePath, pagesDir)
522
+ resolvedLayoutPath = paths.layout
523
+ ? resolve(resolvedSitePath, paths.layout)
524
+ : resolve(resolvedSitePath, 'layout')
525
+ resolvedCollectionsBase = paths.collections
526
+ ? resolve(resolvedSitePath, paths.collections)
527
+ : null
528
+ }
498
529
  },
499
530
 
500
531
  async buildStart() {
@@ -508,7 +539,7 @@ export function siteContentPlugin(options = {}) {
508
539
  // In production, do it here
509
540
  if (isProduction && siteContent.config?.collections) {
510
541
  console.log('[site-content] Processing content collections...')
511
- const collections = await processCollections(resolvedSitePath, siteContent.config.collections)
542
+ const collections = await processCollections(resolvedSitePath, siteContent.config.collections, resolvedCollectionsBase)
512
543
  await writeCollectionFiles(resolvedSitePath, collections)
513
544
  }
514
545
 
@@ -537,7 +568,6 @@ export function siteContentPlugin(options = {}) {
537
568
 
538
569
  // Watch for content changes in dev mode
539
570
  if (shouldWatch) {
540
- const pagesPath = resolve(resolvedSitePath, pagesDir)
541
571
  const siteYmlPath = resolve(resolvedSitePath, 'site.yml')
542
572
  const themeYmlPath = resolve(resolvedSitePath, 'theme.yml')
543
573
 
@@ -571,7 +601,7 @@ export function siteContentPlugin(options = {}) {
571
601
  // Use collectionsConfig (cached from configResolved) or siteContent
572
602
  const collections = collectionsConfig || siteContent?.config?.collections
573
603
  if (collections) {
574
- const processed = await processCollections(resolvedSitePath, collections)
604
+ const processed = await processCollections(resolvedSitePath, collections, resolvedCollectionsBase)
575
605
  await writeCollectionFiles(resolvedSitePath, processed)
576
606
  }
577
607
  // Send full reload to client
@@ -585,12 +615,24 @@ export function siteContentPlugin(options = {}) {
585
615
  // Track all watchers for cleanup
586
616
  const watchers = []
587
617
 
588
- // Watch pages directory
589
- try {
590
- watchers.push(watch(pagesPath, { recursive: true }, scheduleRebuild))
591
- console.log(`[site-content] Watching ${pagesPath}`)
592
- } catch (err) {
593
- console.warn('[site-content] Could not watch pages directory:', err.message)
618
+ // Watch pages directory (resolved from site.yml pagesDir or default)
619
+ if (existsSync(resolvedPagesPath)) {
620
+ try {
621
+ watchers.push(watch(resolvedPagesPath, { recursive: true }, scheduleRebuild))
622
+ console.log(`[site-content] Watching ${resolvedPagesPath}`)
623
+ } catch (err) {
624
+ console.warn('[site-content] Could not watch pages directory:', err.message)
625
+ }
626
+ }
627
+
628
+ // Watch layout directory (resolved from site.yml layoutDir or default)
629
+ if (existsSync(resolvedLayoutPath)) {
630
+ try {
631
+ watchers.push(watch(resolvedLayoutPath, { recursive: true }, scheduleRebuild))
632
+ console.log(`[site-content] Watching ${resolvedLayoutPath}`)
633
+ } catch (err) {
634
+ console.warn('[site-content] Could not watch layout directory:', err.message)
635
+ }
594
636
  }
595
637
 
596
638
  // Watch site.yml
@@ -611,10 +653,11 @@ export function siteContentPlugin(options = {}) {
611
653
  // Use collectionsConfig cached from configResolved (siteContent may be null here)
612
654
  if (collectionsConfig) {
613
655
  const contentPaths = new Set()
656
+ const collectionBase = resolvedCollectionsBase || resolvedSitePath
614
657
  for (const config of Object.values(collectionsConfig)) {
615
658
  const collectionPath = typeof config === 'string' ? config : config.path
616
659
  if (collectionPath) {
617
- contentPaths.add(resolve(resolvedSitePath, collectionPath))
660
+ contentPaths.add(resolve(collectionBase, collectionPath))
618
661
  }
619
662
  }
620
663