@uniweb/build 0.5.0 → 0.6.0
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 +4 -3
- package/src/docs.js +3 -3
- package/src/i18n/collections.js +479 -126
- package/src/i18n/index.js +2 -0
- package/src/prerender.js +59 -73
- package/src/runtime-schema.js +36 -7
- package/src/site/collection-processor.js +73 -9
- package/src/site/data-fetcher.js +3 -1
- package/src/site/plugin.js +93 -15
- package/src/theme/css-generator.js +21 -1
- package/src/theme/processor.js +8 -0
package/src/i18n/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
extractCollectionContent,
|
|
21
21
|
buildLocalizedCollections,
|
|
22
22
|
getCollectionLocales,
|
|
23
|
+
translateCollectionData,
|
|
23
24
|
COLLECTIONS_DIR
|
|
24
25
|
} from './collections.js'
|
|
25
26
|
import { generateSearchIndex, isSearchEnabled } from '../search/index.js'
|
|
@@ -69,6 +70,7 @@ export {
|
|
|
69
70
|
extractCollectionContent,
|
|
70
71
|
buildLocalizedCollections,
|
|
71
72
|
getCollectionLocales,
|
|
73
|
+
translateCollectionData,
|
|
72
74
|
COLLECTIONS_DIR,
|
|
73
75
|
|
|
74
76
|
// Locale resolution
|
package/src/prerender.js
CHANGED
|
@@ -26,19 +26,19 @@ 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
|
-
* @returns {Object} {
|
|
29
|
+
* @returns {Object} { pageFetchedData, fetchedData } - Fetched data for dynamic route expansion and DataStore pre-population
|
|
30
30
|
*/
|
|
31
31
|
async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
32
32
|
const fetchOptions = { siteRoot: siteDir, publicDir: 'public' }
|
|
33
|
+
const fetchedData = [] // Collected for DataStore pre-population
|
|
33
34
|
|
|
34
|
-
// 1. Site-level fetch
|
|
35
|
-
let siteCascadedData = {}
|
|
35
|
+
// 1. Site-level fetch
|
|
36
36
|
const siteFetch = siteContent.config?.fetch
|
|
37
37
|
if (siteFetch && siteFetch.prerender !== false) {
|
|
38
38
|
onProgress(` Fetching site data: ${siteFetch.path || siteFetch.url}`)
|
|
39
39
|
const result = await executeFetch(siteFetch, fetchOptions)
|
|
40
40
|
if (result.data && !result.error) {
|
|
41
|
-
|
|
41
|
+
fetchedData.push({ config: siteFetch, data: result.data })
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -46,14 +46,13 @@ async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
|
46
46
|
const pageFetchedData = new Map()
|
|
47
47
|
|
|
48
48
|
for (const page of siteContent.pages || []) {
|
|
49
|
-
// Page-level fetch
|
|
50
|
-
let pageCascadedData = { ...siteCascadedData }
|
|
49
|
+
// Page-level fetch
|
|
51
50
|
const pageFetch = page.fetch
|
|
52
51
|
if (pageFetch && pageFetch.prerender !== false) {
|
|
53
52
|
onProgress(` Fetching page data for ${page.route}: ${pageFetch.path || pageFetch.url}`)
|
|
54
53
|
const result = await executeFetch(pageFetch, fetchOptions)
|
|
55
54
|
if (result.data && !result.error) {
|
|
56
|
-
|
|
55
|
+
fetchedData.push({ config: pageFetch, data: result.data })
|
|
57
56
|
// Store for dynamic route expansion
|
|
58
57
|
pageFetchedData.set(page.route, {
|
|
59
58
|
schema: pageFetch.schema,
|
|
@@ -62,11 +61,11 @@ async function executeAllFetches(siteContent, siteDir, onProgress) {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
// Process
|
|
66
|
-
await processSectionFetches(page.sections,
|
|
64
|
+
// Process section-level fetches (own fetch → parsedContent.data, not cascaded)
|
|
65
|
+
await processSectionFetches(page.sections, fetchOptions, onProgress)
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
return {
|
|
68
|
+
return { pageFetchedData, fetchedData }
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
@@ -142,12 +141,6 @@ function expandDynamicPages(pages, pageFetchedData, onProgress) {
|
|
|
142
141
|
allItems: items, // All items from parent
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
// Also inject into sections' cascadedData for components with inheritData
|
|
146
|
-
injectDynamicData(concretePage.sections, {
|
|
147
|
-
[singularSchema]: item, // Current item as singular
|
|
148
|
-
[schema]: items, // All items as plural
|
|
149
|
-
})
|
|
150
|
-
|
|
151
144
|
// Use item data for page metadata if available
|
|
152
145
|
if (item.title) concretePage.title = item.title
|
|
153
146
|
if (item.description || item.excerpt) concretePage.description = item.description || item.excerpt
|
|
@@ -159,38 +152,15 @@ function expandDynamicPages(pages, pageFetchedData, onProgress) {
|
|
|
159
152
|
return expandedPages
|
|
160
153
|
}
|
|
161
154
|
|
|
162
|
-
/**
|
|
163
|
-
* Inject dynamic route data into section cascadedData
|
|
164
|
-
* This ensures components with inheritData receive the current item
|
|
165
|
-
*
|
|
166
|
-
* @param {Array} sections - Sections to update
|
|
167
|
-
* @param {Object} data - Data to inject { article: {...}, articles: [...] }
|
|
168
|
-
*/
|
|
169
|
-
function injectDynamicData(sections, data) {
|
|
170
|
-
if (!sections || !Array.isArray(sections)) return
|
|
171
|
-
|
|
172
|
-
for (const section of sections) {
|
|
173
|
-
section.cascadedData = {
|
|
174
|
-
...(section.cascadedData || {}),
|
|
175
|
-
...data,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Recurse into subsections
|
|
179
|
-
if (section.subsections && section.subsections.length > 0) {
|
|
180
|
-
injectDynamicData(section.subsections, data)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
155
|
/**
|
|
186
156
|
* Process fetch configs for sections (and subsections recursively)
|
|
157
|
+
* Section-level fetches merge data into parsedContent.data (not cascaded).
|
|
187
158
|
*
|
|
188
159
|
* @param {Array} sections - Array of section objects
|
|
189
|
-
* @param {Object} cascadedData - Data cascaded from site/page level
|
|
190
160
|
* @param {Object} fetchOptions - Options for executeFetch
|
|
191
161
|
* @param {function} onProgress - Progress callback
|
|
192
162
|
*/
|
|
193
|
-
async function processSectionFetches(sections,
|
|
163
|
+
async function processSectionFetches(sections, fetchOptions, onProgress) {
|
|
194
164
|
if (!sections || !Array.isArray(sections)) return
|
|
195
165
|
|
|
196
166
|
for (const section of sections) {
|
|
@@ -210,12 +180,9 @@ async function processSectionFetches(sections, cascadedData, fetchOptions, onPro
|
|
|
210
180
|
}
|
|
211
181
|
}
|
|
212
182
|
|
|
213
|
-
// Attach cascaded data for components with inheritData
|
|
214
|
-
section.cascadedData = cascadedData
|
|
215
|
-
|
|
216
183
|
// Process subsections recursively
|
|
217
184
|
if (section.subsections && section.subsections.length > 0) {
|
|
218
|
-
await processSectionFetches(section.subsections,
|
|
185
|
+
await processSectionFetches(section.subsections, fetchOptions, onProgress)
|
|
219
186
|
}
|
|
220
187
|
}
|
|
221
188
|
}
|
|
@@ -329,6 +296,13 @@ function getWrapperProps(block) {
|
|
|
329
296
|
style.position = 'relative'
|
|
330
297
|
}
|
|
331
298
|
|
|
299
|
+
// Apply context overrides as inline CSS custom properties (mirrors BlockRenderer.jsx)
|
|
300
|
+
if (block.contextOverrides) {
|
|
301
|
+
for (const [key, value] of Object.entries(block.contextOverrides)) {
|
|
302
|
+
style[`--${key}`] = value
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
332
306
|
const sectionId = block.stableId || block.id
|
|
333
307
|
return { id: `section-${sectionId}`, style, className, background }
|
|
334
308
|
}
|
|
@@ -372,18 +346,16 @@ function renderBackground(background) {
|
|
|
372
346
|
|
|
373
347
|
if (background.mode === 'gradient' && background.gradient) {
|
|
374
348
|
const g = background.gradient
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
const startPos = g.startPosition || 0
|
|
379
|
-
const endPos = g.endPosition || 100
|
|
349
|
+
// Raw CSS gradient string (e.g., "linear-gradient(to bottom, #000, #333)")
|
|
350
|
+
const bgValue = typeof g === 'string' ? g
|
|
351
|
+
: `linear-gradient(${g.angle || 0}deg, ${g.start || 'transparent'} ${g.startPosition || 0}%, ${g.end || 'transparent'} ${g.endPosition || 100}%)`
|
|
380
352
|
children.push(
|
|
381
353
|
React.createElement('div', {
|
|
382
354
|
key: 'bg-gradient',
|
|
383
355
|
className: 'background-gradient',
|
|
384
356
|
style: {
|
|
385
357
|
position: 'absolute', inset: '0',
|
|
386
|
-
background:
|
|
358
|
+
background: bgValue
|
|
387
359
|
},
|
|
388
360
|
'aria-hidden': 'true'
|
|
389
361
|
})
|
|
@@ -449,7 +421,8 @@ function renderBackground(background) {
|
|
|
449
421
|
|
|
450
422
|
/**
|
|
451
423
|
* Render a single block for SSR
|
|
452
|
-
* Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching in SSR)
|
|
424
|
+
* Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching in SSR).
|
|
425
|
+
* block.dataLoading is always false at prerender time — runtime fetches only happen client-side.
|
|
453
426
|
*/
|
|
454
427
|
function renderBlock(block) {
|
|
455
428
|
const Component = block.initComponent()
|
|
@@ -462,35 +435,37 @@ function renderBlock(block) {
|
|
|
462
435
|
}
|
|
463
436
|
|
|
464
437
|
// Build content and params with runtime guarantees
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const meta = getComponentMetaSSR(block.type)
|
|
472
|
-
const prepared = preparePropsSSR(block, meta)
|
|
473
|
-
params = prepared.params
|
|
474
|
-
content = {
|
|
475
|
-
...prepared.content,
|
|
476
|
-
...block.properties,
|
|
477
|
-
_prosemirror: block.parsedContent
|
|
478
|
-
}
|
|
438
|
+
const meta = getComponentMetaSSR(block.type)
|
|
439
|
+
const prepared = preparePropsSSR(block, meta)
|
|
440
|
+
let params = prepared.params
|
|
441
|
+
let content = {
|
|
442
|
+
...prepared.content,
|
|
443
|
+
...block.properties,
|
|
479
444
|
}
|
|
480
445
|
|
|
481
446
|
// Background handling (mirrors BlockRenderer.jsx)
|
|
482
447
|
const { background, ...wrapperProps } = getWrapperProps(block)
|
|
483
|
-
const meta = getComponentMetaSSR(block.type)
|
|
484
|
-
const hasBackground = background?.mode && meta?.background !== 'self'
|
|
485
448
|
|
|
486
|
-
|
|
487
|
-
|
|
449
|
+
// Merge Component.className (static classes declared on the component function)
|
|
450
|
+
// Order: context-{theme} + block.state.className + Component.className
|
|
451
|
+
const componentClassName = Component.className
|
|
452
|
+
if (componentClassName) {
|
|
453
|
+
wrapperProps.className = wrapperProps.className
|
|
454
|
+
? `${wrapperProps.className} ${componentClassName}`
|
|
455
|
+
: componentClassName
|
|
488
456
|
}
|
|
489
457
|
|
|
458
|
+
const hasBackground = background?.mode && meta?.background !== 'self'
|
|
459
|
+
|
|
460
|
+
block.hasBackground = hasBackground
|
|
461
|
+
|
|
462
|
+
// Use Component.as as the wrapper tag (default: 'section')
|
|
463
|
+
const wrapperTag = Component.as || 'section'
|
|
464
|
+
|
|
490
465
|
const componentProps = { content, params, block }
|
|
491
466
|
|
|
492
467
|
if (hasBackground) {
|
|
493
|
-
return React.createElement(
|
|
468
|
+
return React.createElement(wrapperTag, wrapperProps,
|
|
494
469
|
renderBackground(background),
|
|
495
470
|
React.createElement('div', { className: 'relative z-10' },
|
|
496
471
|
React.createElement(Component, componentProps)
|
|
@@ -498,7 +473,7 @@ function renderBlock(block) {
|
|
|
498
473
|
)
|
|
499
474
|
}
|
|
500
475
|
|
|
501
|
-
return React.createElement(
|
|
476
|
+
return React.createElement(wrapperTag, wrapperProps,
|
|
502
477
|
React.createElement(Component, componentProps)
|
|
503
478
|
)
|
|
504
479
|
}
|
|
@@ -678,7 +653,10 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
678
653
|
|
|
679
654
|
// Execute data fetches (site, page, section levels)
|
|
680
655
|
onProgress('Executing data fetches...')
|
|
681
|
-
const {
|
|
656
|
+
const { pageFetchedData, fetchedData } = await executeAllFetches(siteContent, siteDir, onProgress)
|
|
657
|
+
|
|
658
|
+
// Store fetchedData on siteContent for runtime DataStore pre-population
|
|
659
|
+
siteContent.fetchedData = fetchedData
|
|
682
660
|
|
|
683
661
|
// Expand dynamic pages (e.g., /blog/:slug → /blog/post-1, /blog/post-2)
|
|
684
662
|
if (siteContent.pages?.some(p => p.isDynamic)) {
|
|
@@ -693,6 +671,14 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
693
671
|
// Initialize the Uniweb runtime for this locale
|
|
694
672
|
onProgress('Initializing runtime...')
|
|
695
673
|
const uniweb = createUniweb(siteContent)
|
|
674
|
+
|
|
675
|
+
// Pre-populate DataStore so EntityStore can resolve data during prerender
|
|
676
|
+
if (fetchedData.length > 0 && uniweb.activeWebsite?.dataStore) {
|
|
677
|
+
for (const entry of fetchedData) {
|
|
678
|
+
uniweb.activeWebsite.dataStore.set(entry.config, entry.data)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
696
682
|
uniweb.setFoundation(foundation)
|
|
697
683
|
|
|
698
684
|
// Set base path from site config so components can access it during SSR
|
package/src/runtime-schema.js
CHANGED
|
@@ -197,15 +197,36 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// Data binding (CMS entities)
|
|
200
|
+
// Supports both old format (data: 'person:6') and new consolidated format
|
|
201
|
+
// (data: { entity: 'person:6', schemas: {...}, inherit: [...] })
|
|
200
202
|
if (fullMeta.data) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
if (typeof fullMeta.data === 'string') {
|
|
204
|
+
// Old format: data: 'person:6'
|
|
205
|
+
const parsed = parseDataString(fullMeta.data)
|
|
206
|
+
if (parsed) {
|
|
207
|
+
runtime.data = parsed
|
|
208
|
+
}
|
|
209
|
+
} else if (typeof fullMeta.data === 'object') {
|
|
210
|
+
// New format: data: { entity, schemas, inherit }
|
|
211
|
+
if (fullMeta.data.entity) {
|
|
212
|
+
const parsed = parseDataString(fullMeta.data.entity)
|
|
213
|
+
if (parsed) {
|
|
214
|
+
runtime.data = parsed
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (fullMeta.data.schemas) {
|
|
218
|
+
const schemas = extractSchemas(fullMeta.data.schemas)
|
|
219
|
+
if (schemas) {
|
|
220
|
+
runtime.schemas = schemas
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (fullMeta.data.inherit !== undefined) {
|
|
224
|
+
runtime.inheritData = fullMeta.data.inherit
|
|
225
|
+
}
|
|
204
226
|
}
|
|
205
227
|
}
|
|
206
228
|
|
|
207
|
-
|
|
208
|
-
const paramsObj = fullMeta.params || fullMeta.properties
|
|
229
|
+
const paramsObj = fullMeta.params
|
|
209
230
|
const defaults = extractParamDefaults(paramsObj)
|
|
210
231
|
if (defaults) {
|
|
211
232
|
runtime.defaults = defaults
|
|
@@ -225,7 +246,8 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
225
246
|
|
|
226
247
|
// Schemas - lean version for runtime validation/defaults
|
|
227
248
|
// Strips editor-only fields (label, hint, description)
|
|
228
|
-
|
|
249
|
+
// Top-level schemas supported for backwards compat (lower priority than data.schemas)
|
|
250
|
+
if (fullMeta.schemas && !runtime.schemas) {
|
|
229
251
|
const schemas = extractSchemas(fullMeta.schemas)
|
|
230
252
|
if (schemas) {
|
|
231
253
|
runtime.schemas = schemas
|
|
@@ -234,10 +256,17 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
234
256
|
|
|
235
257
|
// Data inheritance - component receives cascaded data from page/site level fetches
|
|
236
258
|
// Can be: true (inherit all), false (inherit none), or ['schema1', 'schema2'] (selective)
|
|
237
|
-
|
|
259
|
+
// Top-level inheritData supported for backwards compat (lower priority than data.inherit)
|
|
260
|
+
if (fullMeta.inheritData !== undefined && runtime.inheritData === undefined) {
|
|
238
261
|
runtime.inheritData = fullMeta.inheritData
|
|
239
262
|
}
|
|
240
263
|
|
|
264
|
+
// Auto-derive inheritData from entity type when no explicit inherit is set.
|
|
265
|
+
// data: { entity: 'articles' } implies inheritData: ['articles']
|
|
266
|
+
if (runtime.data && runtime.inheritData === undefined) {
|
|
267
|
+
runtime.inheritData = [runtime.data.type]
|
|
268
|
+
}
|
|
269
|
+
|
|
241
270
|
return Object.keys(runtime).length > 0 ? runtime : null
|
|
242
271
|
}
|
|
243
272
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection Processor
|
|
3
3
|
*
|
|
4
|
-
* Processes content collections from markdown files into JSON data.
|
|
4
|
+
* Processes content collections from markdown and YAML files into JSON data.
|
|
5
5
|
* Collections are defined in site.yml and processed at build time.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
|
-
* - Discovers markdown files in collection folders
|
|
9
|
-
* - Parses frontmatter for metadata
|
|
8
|
+
* - Discovers markdown (.md), data (.yml/.yaml), and JSON (.json) files in collection folders
|
|
9
|
+
* - Parses frontmatter for metadata (markdown), full YAML (data items), or JSON (data items)
|
|
10
10
|
* - Converts markdown body to ProseMirror JSON
|
|
11
11
|
* - Supports filtering, sorting, and limiting
|
|
12
|
-
* - Auto-generates excerpts and extracts first images
|
|
12
|
+
* - Auto-generates excerpts and extracts first images (markdown items only)
|
|
13
13
|
*
|
|
14
14
|
* @module @uniweb/build/site/collection-processor
|
|
15
15
|
*
|
|
@@ -315,6 +315,53 @@ async function processCollectionAssets(content, itemPath, siteRoot, collectionNa
|
|
|
315
315
|
|
|
316
316
|
// Filter and sort utilities are imported from data-fetcher.js
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Process a single data item from a YAML file
|
|
320
|
+
*
|
|
321
|
+
* YAML items are pure data — no ProseMirror conversion, no body, no excerpt,
|
|
322
|
+
* no image extraction, no lastModified. The output is slug + YAML fields.
|
|
323
|
+
*
|
|
324
|
+
* @param {string} dir - Collection directory path
|
|
325
|
+
* @param {string} filename - YAML filename (.yml or .yaml)
|
|
326
|
+
* @returns {Promise<Object|null>} Processed item or null if unpublished
|
|
327
|
+
*/
|
|
328
|
+
async function processDataItem(dir, filename) {
|
|
329
|
+
const filepath = join(dir, filename)
|
|
330
|
+
const raw = await readFile(filepath, 'utf-8')
|
|
331
|
+
const slug = basename(filename, extname(filename))
|
|
332
|
+
const data = yaml.load(raw) || {}
|
|
333
|
+
|
|
334
|
+
// Skip unpublished items
|
|
335
|
+
if (data.published === false) return null
|
|
336
|
+
|
|
337
|
+
return { slug, ...data }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Process a single data item from a JSON file
|
|
342
|
+
*
|
|
343
|
+
* JSON items are pure data — like YAML items, no ProseMirror conversion.
|
|
344
|
+
* A JSON file containing an array returns all items (single-file collection).
|
|
345
|
+
* A JSON file containing an object returns a single item with slug from filename.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} dir - Collection directory path
|
|
348
|
+
* @param {string} filename - JSON filename
|
|
349
|
+
* @returns {Promise<Object|Array|null>} Processed item(s) or null if unpublished
|
|
350
|
+
*/
|
|
351
|
+
async function processJsonItem(dir, filename) {
|
|
352
|
+
const filepath = join(dir, filename)
|
|
353
|
+
const raw = await readFile(filepath, 'utf-8')
|
|
354
|
+
const slug = basename(filename, '.json')
|
|
355
|
+
const data = JSON.parse(raw)
|
|
356
|
+
|
|
357
|
+
// Array → multiple items (single-file collection)
|
|
358
|
+
if (Array.isArray(data)) return data
|
|
359
|
+
|
|
360
|
+
// Object → single item
|
|
361
|
+
if (data.published === false) return null
|
|
362
|
+
return { slug, ...data }
|
|
363
|
+
}
|
|
364
|
+
|
|
318
365
|
/**
|
|
319
366
|
* Process a single content item from a markdown file
|
|
320
367
|
*
|
|
@@ -384,13 +431,27 @@ async function collectItems(siteDir, config) {
|
|
|
384
431
|
}
|
|
385
432
|
|
|
386
433
|
const files = await readdir(collectionDir)
|
|
387
|
-
const
|
|
434
|
+
const itemFiles = files.filter(f =>
|
|
435
|
+
!f.startsWith('_') &&
|
|
436
|
+
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json'))
|
|
437
|
+
)
|
|
388
438
|
|
|
389
|
-
// Process all markdown
|
|
439
|
+
// Process all collection files (markdown → content items, YAML/JSON → data items)
|
|
390
440
|
let items = await Promise.all(
|
|
391
|
-
|
|
441
|
+
itemFiles.map(file => {
|
|
442
|
+
if (file.endsWith('.json')) {
|
|
443
|
+
return processJsonItem(collectionDir, file)
|
|
444
|
+
}
|
|
445
|
+
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
446
|
+
return processDataItem(collectionDir, file)
|
|
447
|
+
}
|
|
448
|
+
return processContentItem(collectionDir, file, config, siteDir)
|
|
449
|
+
})
|
|
392
450
|
)
|
|
393
451
|
|
|
452
|
+
// Flatten arrays from JSON files that contain multiple items
|
|
453
|
+
items = items.flat()
|
|
454
|
+
|
|
394
455
|
// Filter out nulls (unpublished items)
|
|
395
456
|
items = items.filter(Boolean)
|
|
396
457
|
|
|
@@ -496,11 +557,14 @@ export async function getCollectionLastModified(siteDir, config) {
|
|
|
496
557
|
}
|
|
497
558
|
|
|
498
559
|
const files = await readdir(collectionDir)
|
|
499
|
-
const
|
|
560
|
+
const itemFiles = files.filter(f =>
|
|
561
|
+
!f.startsWith('_') &&
|
|
562
|
+
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json'))
|
|
563
|
+
)
|
|
500
564
|
|
|
501
565
|
let lastModified = null
|
|
502
566
|
|
|
503
|
-
for (const file of
|
|
567
|
+
for (const file of itemFiles) {
|
|
504
568
|
const fileStat = await stat(join(collectionDir, file))
|
|
505
569
|
if (!lastModified || fileStat.mtime > lastModified) {
|
|
506
570
|
lastModified = fileStat.mtime
|
package/src/site/data-fetcher.js
CHANGED
|
@@ -245,9 +245,10 @@ export function parseFetchConfig(fetch) {
|
|
|
245
245
|
path,
|
|
246
246
|
url,
|
|
247
247
|
schema,
|
|
248
|
-
prerender = true,
|
|
248
|
+
prerender = url ? false : true,
|
|
249
249
|
merge = false,
|
|
250
250
|
transform,
|
|
251
|
+
detail,
|
|
251
252
|
// Post-processing options (also supported for path/url fetches)
|
|
252
253
|
limit,
|
|
253
254
|
sort,
|
|
@@ -264,6 +265,7 @@ export function parseFetchConfig(fetch) {
|
|
|
264
265
|
prerender,
|
|
265
266
|
merge,
|
|
266
267
|
transform,
|
|
268
|
+
detail,
|
|
267
269
|
// Post-processing options
|
|
268
270
|
limit,
|
|
269
271
|
sort,
|
package/src/site/plugin.js
CHANGED
|
@@ -41,50 +41,51 @@ import { executeFetch, mergeDataIntoContent } from './data-fetcher.js'
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Execute all fetches for site content (used in dev mode)
|
|
44
|
-
*
|
|
44
|
+
* Collects fetchedData for DataStore pre-population at runtime
|
|
45
45
|
*
|
|
46
46
|
* @param {Object} siteContent - The collected site content
|
|
47
47
|
* @param {string} siteDir - Path to site directory
|
|
48
48
|
*/
|
|
49
49
|
async function executeDevFetches(siteContent, siteDir) {
|
|
50
50
|
const fetchOptions = { siteRoot: siteDir, publicDir: 'public' }
|
|
51
|
+
const fetchedData = []
|
|
51
52
|
|
|
52
53
|
// Site-level fetch
|
|
53
|
-
let siteCascadedData = {}
|
|
54
54
|
const siteFetch = siteContent.config?.fetch
|
|
55
55
|
if (siteFetch) {
|
|
56
56
|
const result = await executeFetch(siteFetch, fetchOptions)
|
|
57
57
|
if (result.data && !result.error) {
|
|
58
|
-
|
|
58
|
+
fetchedData.push({ config: siteFetch, data: result.data })
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Process each page
|
|
63
63
|
for (const page of siteContent.pages || []) {
|
|
64
|
-
let pageCascadedData = { ...siteCascadedData }
|
|
65
|
-
|
|
66
64
|
// Page-level fetch
|
|
67
65
|
const pageFetch = page.fetch
|
|
68
66
|
if (pageFetch) {
|
|
69
67
|
const result = await executeFetch(pageFetch, fetchOptions)
|
|
70
68
|
if (result.data && !result.error) {
|
|
71
|
-
|
|
69
|
+
fetchedData.push({ config: pageFetch, data: result.data })
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
// Process
|
|
76
|
-
await processDevSectionFetches(page.sections,
|
|
73
|
+
// Process section-level fetches (own fetch → parsedContent.data)
|
|
74
|
+
await processDevSectionFetches(page.sections, fetchOptions)
|
|
77
75
|
}
|
|
76
|
+
|
|
77
|
+
// Store on siteContent for runtime DataStore pre-population
|
|
78
|
+
siteContent.fetchedData = fetchedData
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* Process fetches for sections recursively
|
|
83
|
+
* Section-level fetches merge data into parsedContent.data (not cascaded).
|
|
82
84
|
*
|
|
83
85
|
* @param {Array} sections - Sections to process
|
|
84
|
-
* @param {Object} cascadedData - Data from parent levels
|
|
85
86
|
* @param {Object} fetchOptions - Options for executeFetch
|
|
86
87
|
*/
|
|
87
|
-
async function processDevSectionFetches(sections,
|
|
88
|
+
async function processDevSectionFetches(sections, fetchOptions) {
|
|
88
89
|
if (!sections || !Array.isArray(sections)) return
|
|
89
90
|
|
|
90
91
|
for (const section of sections) {
|
|
@@ -104,13 +105,9 @@ async function processDevSectionFetches(sections, cascadedData, fetchOptions) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
// Attach cascaded data for components with inheritData
|
|
108
|
-
// Note: cascadedData is from page/site level only, not section's own fetch
|
|
109
|
-
section.cascadedData = cascadedData
|
|
110
|
-
|
|
111
108
|
// Process subsections recursively
|
|
112
109
|
if (section.subsections && section.subsections.length > 0) {
|
|
113
|
-
await processDevSectionFetches(section.subsections,
|
|
110
|
+
await processDevSectionFetches(section.subsections, fetchOptions)
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
113
|
}
|
|
@@ -376,6 +373,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
376
373
|
let watcher = null
|
|
377
374
|
let server = null
|
|
378
375
|
let localeTranslations = {} // Cache: { locale: translations }
|
|
376
|
+
let collectionTranslations = {} // Cache: { locale: collection translations }
|
|
379
377
|
let localesDir = 'locales' // Default, updated from site config
|
|
380
378
|
let collectionsConfig = null // Cached for watcher setup
|
|
381
379
|
|
|
@@ -402,6 +400,29 @@ export function siteContentPlugin(options = {}) {
|
|
|
402
400
|
}
|
|
403
401
|
}
|
|
404
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Load collection translations for a specific locale
|
|
405
|
+
*/
|
|
406
|
+
async function loadCollectionTranslations(locale) {
|
|
407
|
+
if (collectionTranslations[locale]) {
|
|
408
|
+
return collectionTranslations[locale]
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const localePath = join(resolvedSitePath, localesDir, 'collections', `${locale}.json`)
|
|
412
|
+
if (!existsSync(localePath)) {
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const content = await readFile(localePath, 'utf-8')
|
|
418
|
+
const translations = JSON.parse(content)
|
|
419
|
+
collectionTranslations[locale] = translations
|
|
420
|
+
return translations
|
|
421
|
+
} catch {
|
|
422
|
+
return null
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
405
426
|
/**
|
|
406
427
|
* Get available locales from locales directory
|
|
407
428
|
*/
|
|
@@ -504,6 +525,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
504
525
|
|
|
505
526
|
// Clear translation cache on rebuild
|
|
506
527
|
localeTranslations = {}
|
|
528
|
+
collectionTranslations = {}
|
|
507
529
|
} catch (err) {
|
|
508
530
|
console.error('[site-content] Failed to collect content:', err.message)
|
|
509
531
|
siteContent = { config: {}, theme: {}, pages: [] }
|
|
@@ -621,6 +643,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
621
643
|
const localeWatcher = watch(localesPath, { recursive: false }, () => {
|
|
622
644
|
console.log('[site-content] Translation files changed, clearing cache...')
|
|
623
645
|
localeTranslations = {}
|
|
646
|
+
collectionTranslations = {}
|
|
624
647
|
server.ws.send({ type: 'full-reload' })
|
|
625
648
|
})
|
|
626
649
|
additionalWatchers.push(localeWatcher)
|
|
@@ -643,6 +666,22 @@ export function siteContentPlugin(options = {}) {
|
|
|
643
666
|
// freeform dir may not exist, that's ok
|
|
644
667
|
}
|
|
645
668
|
}
|
|
669
|
+
|
|
670
|
+
// Watch collection translations directory
|
|
671
|
+
const collectionsLocalesPath = resolve(localesPath, 'collections')
|
|
672
|
+
if (existsSync(collectionsLocalesPath)) {
|
|
673
|
+
try {
|
|
674
|
+
const collWatcher = watch(collectionsLocalesPath, { recursive: false }, () => {
|
|
675
|
+
console.log('[site-content] Collection translations changed, clearing cache...')
|
|
676
|
+
collectionTranslations = {}
|
|
677
|
+
server.ws.send({ type: 'full-reload' })
|
|
678
|
+
})
|
|
679
|
+
additionalWatchers.push(collWatcher)
|
|
680
|
+
console.log(`[site-content] Watching ${collectionsLocalesPath} for collection translation changes`)
|
|
681
|
+
} catch (err) {
|
|
682
|
+
// collections locales dir may not exist, that's ok
|
|
683
|
+
}
|
|
684
|
+
}
|
|
646
685
|
}
|
|
647
686
|
|
|
648
687
|
// Add additional watchers to cleanup
|
|
@@ -721,6 +760,45 @@ export function siteContentPlugin(options = {}) {
|
|
|
721
760
|
}
|
|
722
761
|
}
|
|
723
762
|
|
|
763
|
+
// Handle localized collection data (e.g., /fr/data/articles.json)
|
|
764
|
+
const localeDataMatch = req.url.match(/^\/([a-z]{2})\/data\/(.+\.json)$/)
|
|
765
|
+
if (localeDataMatch) {
|
|
766
|
+
const locale = localeDataMatch[1]
|
|
767
|
+
const filename = localeDataMatch[2]
|
|
768
|
+
const collectionName = filename.replace('.json', '')
|
|
769
|
+
const sourcePath = join(resolvedSitePath, 'public', 'data', filename)
|
|
770
|
+
|
|
771
|
+
if (existsSync(sourcePath)) {
|
|
772
|
+
try {
|
|
773
|
+
const raw = await readFile(sourcePath, 'utf-8')
|
|
774
|
+
const items = JSON.parse(raw)
|
|
775
|
+
|
|
776
|
+
// Load collection translations for this locale
|
|
777
|
+
const translations = await loadCollectionTranslations(locale) || {}
|
|
778
|
+
|
|
779
|
+
// Check for free-form translations
|
|
780
|
+
const freeformDir = join(resolvedSitePath, localesDir, 'freeform', locale)
|
|
781
|
+
const hasFreeform = existsSync(freeformDir)
|
|
782
|
+
|
|
783
|
+
// Translate using the collections module
|
|
784
|
+
const { translateCollectionData } = await import('../i18n/collections.js')
|
|
785
|
+
const translated = await translateCollectionData(items, collectionName, resolvedSitePath, {
|
|
786
|
+
locale,
|
|
787
|
+
localesDir: join(resolvedSitePath, localesDir),
|
|
788
|
+
translations,
|
|
789
|
+
freeformEnabled: hasFreeform
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
res.setHeader('Content-Type', 'application/json')
|
|
793
|
+
res.end(JSON.stringify(translated, null, 2))
|
|
794
|
+
return
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.warn(`[site-content] Failed to serve localized collection ${filename}: ${err.message}`)
|
|
797
|
+
// Fall through to Vite's static server
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
724
802
|
next()
|
|
725
803
|
})
|
|
726
804
|
},
|