@uniweb/core 0.5.22 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.5.22",
3
+ "version": "0.6.0",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
package/src/block.js CHANGED
@@ -215,6 +215,21 @@ export default class Block {
215
215
  return content.groups
216
216
  }
217
217
 
218
+ // Foundation content handler hook
219
+ // Runs BEFORE semantic parsing. Foundations can declare a handler
220
+ // in foundation.js `handlers.content` to transform raw ProseMirror
221
+ // content — typically for template engine instantiation of
222
+ // {placeholder} expressions against profile/report data.
223
+ const contentHandler = globalThis.uniweb?.foundationConfig?.handlers?.content
224
+ if (contentHandler && typeof contentHandler === 'function') {
225
+ try {
226
+ const transformed = contentHandler(content, this)
227
+ if (transformed !== undefined) content = transformed
228
+ } catch (err) {
229
+ console.error('Foundation content handler failed:', err)
230
+ }
231
+ }
232
+
218
233
  // ProseMirror document - use semantic-parser
219
234
  if (content?.type === 'doc') {
220
235
  return this.extractFromProseMirror(content)
package/src/page.js CHANGED
@@ -73,13 +73,25 @@ export default class Page {
73
73
  this.versionMeta = pageData.versionMeta || null // { versions, latestId }
74
74
  this.versionScope = pageData.versionScope || null // The route where versioning starts
75
75
 
76
+ // Build-time flag: does this page have renderable content?
77
+ // Distinct from "are sections loaded?" — content-less containers
78
+ // (folders with page.yml but no markdown) are always false.
79
+ // Falls back to checking sections for backward compat (non-split mode).
80
+ this._hasContent = pageData.hasContent ?? (pageData.sections?.length > 0)
81
+
76
82
  // Store raw section data for lazy block building
77
83
  // Blocks are created on first access (when page is rendered), not during Website init
78
84
  // This ensures foundationConfig is available for getDefaultBlockType()
79
85
  // Layout panels (header, footer, left, right) are shared at Website level
86
+ // undefined = not yet loaded (split mode, non-current page)
87
+ // [] = loaded but empty (content-less container)
88
+ // [...] = loaded with content
80
89
  this._bodySections = pageData.sections
81
90
  this._bodyBlocks = null
82
91
 
92
+ // Guard against concurrent loadContent() calls
93
+ this._loadingContent = null
94
+
83
95
  Object.seal(this)
84
96
  }
85
97
 
@@ -89,6 +101,9 @@ export default class Page {
89
101
  */
90
102
  get bodyBlocks() {
91
103
  if (!this._bodyBlocks) {
104
+ // If sections haven't been loaded yet (split mode), return empty array.
105
+ // PageRenderer will call loadContent() before rendering.
106
+ if (this._bodySections === undefined) return []
92
107
  this._bodyBlocks = (this._bodySections || []).map(
93
108
  (section, index) => new Block(section, index, this)
94
109
  )
@@ -360,11 +375,58 @@ export default class Page {
360
375
  }
361
376
 
362
377
  /**
363
- * Check if page has body content (sections)
378
+ * Check if page has body content (sections).
379
+ * Uses a build-time flag — always reflects whether the page has markdown,
380
+ * regardless of whether section content has been loaded yet (split mode).
364
381
  * @returns {boolean}
365
382
  */
366
383
  hasContent() {
367
- return this.bodyBlocks.length > 0
384
+ return this._hasContent
385
+ }
386
+
387
+ /**
388
+ * Check if section content has been loaded.
389
+ * In non-split mode, always true (sections are always present).
390
+ * In split mode, true for the pre-embedded current page and any page
391
+ * whose content has been fetched via loadContent().
392
+ * @returns {boolean}
393
+ */
394
+ isContentLoaded() {
395
+ return this._bodySections !== undefined
396
+ }
397
+
398
+ /**
399
+ * Fetch and store section content from the server.
400
+ * Deduplicates concurrent calls (e.g., rapid navigation).
401
+ * No-op if content is already loaded or embedded.
402
+ * @returns {Promise<void>}
403
+ */
404
+ async loadContent() {
405
+ if (this._bodySections !== undefined) return // already loaded or embedded
406
+ if (this._loadingContent) return this._loadingContent // deduplicate
407
+
408
+ this._loadingContent = (async () => {
409
+ try {
410
+ const base = this.website.basePath || ''
411
+ // Locale-aware URL: non-default locale pages live under /{locale}/_pages/
412
+ const localePrefix = this.website.activeLocale !== this.website.defaultLocale
413
+ ? `/${this.website.activeLocale}` : ''
414
+ const routePath = this.route === '/' ? '/index' : this.route
415
+ const res = await fetch(`${base}${localePrefix}/_pages${routePath}.json`)
416
+ if (!res.ok) {
417
+ console.warn(`[Page] Failed to load content for ${this.route}: ${res.status}`)
418
+ this._bodySections = [] // Mark as loaded (empty) to prevent retries
419
+ return
420
+ }
421
+ const data = await res.json()
422
+ this._bodySections = data.sections || []
423
+ this._bodyBlocks = null // Reset lazy cache so getter rebuilds
424
+ } finally {
425
+ this._loadingContent = null
426
+ }
427
+ })()
428
+
429
+ return this._loadingContent
368
430
  }
369
431
 
370
432
  // ─────────────────────────────────────────────────────────────────
package/src/website.js CHANGED
@@ -605,6 +605,17 @@ export default class Website {
605
605
  return globalThis.uniweb?.foundationConfig?.layoutMeta?.[layoutName] || null
606
606
  }
607
607
 
608
+ /**
609
+ * Whether view transitions are enabled for SPA navigation.
610
+ * Defaults to true — the browser's default crossfade is progressive
611
+ * enhancement with no downside. Foundations can set viewTransitions: false
612
+ * in foundation.js to disable.
613
+ * @type {boolean}
614
+ */
615
+ get viewTransitions() {
616
+ return globalThis.uniweb.foundationConfig?.viewTransitions !== false
617
+ }
618
+
608
619
  /**
609
620
  * Get remote props from foundation config
610
621
  */