@uniweb/core 0.1.4 → 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 +112 -4
- package/package.json +2 -2
- package/src/analytics.js +232 -0
- package/src/index.js +1 -0
- package/src/page.js +246 -22
- package/src/uniweb.js +4 -0
- package/src/website.js +268 -11
package/README.md
CHANGED
|
@@ -56,22 +56,130 @@ uniweb.setFoundation(module) // Set the foundation module
|
|
|
56
56
|
Manages pages, theme, and localization.
|
|
57
57
|
|
|
58
58
|
```js
|
|
59
|
+
// Page navigation
|
|
59
60
|
website.getPage(route) // Get page by route
|
|
60
61
|
website.setActivePage(route) // Navigate to page
|
|
61
|
-
website.
|
|
62
|
-
website.
|
|
63
|
-
website.
|
|
62
|
+
website.activePage // Current active page
|
|
63
|
+
website.pages // All pages
|
|
64
|
+
website.pageRoutes // Array of route strings
|
|
65
|
+
|
|
66
|
+
// Page Hierarchy API (for navbars, footers, sitemaps)
|
|
67
|
+
website.getPageHierarchy(options) // Get pages for navigation
|
|
68
|
+
website.getHeaderPages() // Convenience: pages for header nav
|
|
69
|
+
website.getFooterPages() // Convenience: pages for footer nav
|
|
70
|
+
website.getAllPages() // Get flat list of all pages
|
|
71
|
+
|
|
72
|
+
// Locale API
|
|
73
|
+
website.getLocales() // Get all locales: [{code, label, isDefault}]
|
|
74
|
+
website.getActiveLocale() // Get current locale code
|
|
75
|
+
website.getDefaultLocale() // Get default locale code
|
|
76
|
+
website.hasMultipleLocales() // Check if site has multiple locales
|
|
77
|
+
website.getLocaleUrl(code, route) // Build URL for a locale
|
|
78
|
+
website.setActiveLocale(code) // Set active locale
|
|
79
|
+
website.getLocale(code) // Get locale info by code
|
|
80
|
+
|
|
81
|
+
// Content localization (for multilingual values)
|
|
82
|
+
website.localize(value) // Localize {en: "Hello", es: "Hola"} to active lang
|
|
64
83
|
website.makeHref(href) // Transform href for routing
|
|
84
|
+
|
|
85
|
+
// Deprecated (use Locale API instead)
|
|
86
|
+
website.getLanguage() // Use getActiveLocale()
|
|
87
|
+
website.getLanguages() // Use getLocales()
|
|
65
88
|
```
|
|
66
89
|
|
|
90
|
+
**Page Hierarchy API**
|
|
91
|
+
|
|
92
|
+
The `getPageHierarchy()` method returns pages filtered and formatted for navigation:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// Get pages for header navigation
|
|
96
|
+
const headerPages = website.getPageHierarchy({ for: 'header' })
|
|
97
|
+
// Returns: [{ id, route, title, label, description, order, hasContent, children }]
|
|
98
|
+
|
|
99
|
+
// Get flat list of all pages (for sitemaps)
|
|
100
|
+
const allPages = website.getPageHierarchy({ nested: false, includeHidden: true })
|
|
101
|
+
|
|
102
|
+
// Custom filtering and sorting
|
|
103
|
+
const topLevel = website.getPageHierarchy({
|
|
104
|
+
filter: (page) => page.order < 10,
|
|
105
|
+
sort: (a, b) => a.title.localeCompare(b.title)
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
- `nested` (default: true) - Return with nested children or flat list
|
|
111
|
+
- `for` - Filter for 'header', 'footer', or undefined (all)
|
|
112
|
+
- `includeHidden` (default: false) - Include hidden pages
|
|
113
|
+
- `filter` - Custom filter function: `(page) => boolean`
|
|
114
|
+
- `sort` - Custom sort function: `(a, b) => number`
|
|
115
|
+
|
|
67
116
|
#### Page
|
|
68
117
|
|
|
69
118
|
Represents a page with its sections.
|
|
70
119
|
|
|
71
120
|
```js
|
|
121
|
+
// Basic properties
|
|
72
122
|
page.route // Page route path
|
|
73
123
|
page.title // Page title
|
|
74
|
-
page.
|
|
124
|
+
page.description // Page description
|
|
125
|
+
page.label // Short navigation label (or null)
|
|
126
|
+
page.order // Sort order
|
|
127
|
+
page.children // Child pages (for nested hierarchy)
|
|
128
|
+
page.website // Back-reference to parent Website
|
|
129
|
+
page.site // Alias for page.website
|
|
130
|
+
|
|
131
|
+
// Navigation visibility
|
|
132
|
+
page.hidden // Hidden from all navigation
|
|
133
|
+
page.hideInHeader // Hidden from header nav only
|
|
134
|
+
page.hideInFooter // Hidden from footer nav only
|
|
135
|
+
page.isHidden() // Check if hidden from navigation
|
|
136
|
+
page.showInHeader() // Should appear in header nav?
|
|
137
|
+
page.showInFooter() // Should appear in footer nav?
|
|
138
|
+
page.getLabel() // Get navigation label (falls back to title)
|
|
139
|
+
|
|
140
|
+
// Layout options (per-page overrides)
|
|
141
|
+
page.layout.header // Show header on this page?
|
|
142
|
+
page.layout.footer // Show footer on this page?
|
|
143
|
+
page.layout.leftPanel // Show left panel?
|
|
144
|
+
page.layout.rightPanel // Show right panel?
|
|
145
|
+
page.hasHeader() // Convenience: page.layout.header
|
|
146
|
+
page.hasFooter() // Convenience: page.layout.footer
|
|
147
|
+
page.hasLeftPanel() // Convenience: page.layout.leftPanel
|
|
148
|
+
page.hasRightPanel() // Convenience: page.layout.rightPanel
|
|
149
|
+
|
|
150
|
+
// Content
|
|
151
|
+
page.getPageBlocks() // Get header + body + footer blocks
|
|
152
|
+
page.getBodyBlocks() // Get just body blocks
|
|
153
|
+
page.getHeader() // Get header block
|
|
154
|
+
page.getFooter() // Get footer block
|
|
155
|
+
page.hasChildren() // Has child pages?
|
|
156
|
+
page.getHeadMeta() // Get SEO meta tags
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Page Configuration (page.yml)**
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
title: About Us
|
|
163
|
+
description: Learn about our company
|
|
164
|
+
label: About # Short nav label (optional)
|
|
165
|
+
order: 2
|
|
166
|
+
|
|
167
|
+
# Navigation visibility
|
|
168
|
+
hidden: true # Hide from all navigation
|
|
169
|
+
hideInHeader: true # Hide from header nav only
|
|
170
|
+
hideInFooter: true # Hide from footer nav only
|
|
171
|
+
|
|
172
|
+
# Layout overrides (default: all true)
|
|
173
|
+
layout:
|
|
174
|
+
header: false # Don't show header on this page
|
|
175
|
+
footer: false # Don't show footer on this page
|
|
176
|
+
leftPanel: false # Don't show left panel
|
|
177
|
+
rightPanel: false # Don't show right panel
|
|
178
|
+
|
|
179
|
+
# SEO (optional)
|
|
180
|
+
seo:
|
|
181
|
+
noindex: false
|
|
182
|
+
image: /about-og.png
|
|
75
183
|
```
|
|
76
184
|
|
|
77
185
|
#### Block
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
"node": ">=20.19"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@uniweb/semantic-parser": "1.0.
|
|
30
|
+
"@uniweb/semantic-parser": "1.0.7"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/src/analytics.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics
|
|
3
|
+
*
|
|
4
|
+
* Lightweight analytics class for tracking page views, events, and scroll depth.
|
|
5
|
+
* Uses batched sending with sendBeacon for reliable delivery.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Batched event queue with periodic flush
|
|
9
|
+
* - Page view tracking
|
|
10
|
+
* - Custom event tracking
|
|
11
|
+
* - Scroll depth tracking (25%, 50%, 75%, 100%)
|
|
12
|
+
* - sendBeacon for reliable unload delivery
|
|
13
|
+
* - Optional - silently ignores if not configured
|
|
14
|
+
*
|
|
15
|
+
* Usage via uniweb singleton:
|
|
16
|
+
* ```js
|
|
17
|
+
* // Track events (no-op if analytics not configured)
|
|
18
|
+
* uniweb.analytics.trackPageView('/about', 'About Us')
|
|
19
|
+
* uniweb.analytics.trackEvent('button_click', { buttonId: 'cta' })
|
|
20
|
+
* uniweb.analytics.trackScrollDepth(50)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Check if running in browser environment
|
|
25
|
+
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
26
|
+
|
|
27
|
+
export default class Analytics {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} options
|
|
30
|
+
* @param {string} options.endpoint - Analytics endpoint URL (required to enable)
|
|
31
|
+
* @param {number} options.flushInterval - Interval to flush queue in ms (default: 5000)
|
|
32
|
+
* @param {number} options.maxQueueSize - Max events before auto-flush (default: 10)
|
|
33
|
+
* @param {boolean} options.debug - Enable debug logging (default: false)
|
|
34
|
+
*/
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.endpoint = options.endpoint || null
|
|
37
|
+
this.flushInterval = options.flushInterval || 5000
|
|
38
|
+
this.maxQueueSize = options.maxQueueSize || 10
|
|
39
|
+
this.debug = options.debug || false
|
|
40
|
+
|
|
41
|
+
// Event queue
|
|
42
|
+
this.queue = []
|
|
43
|
+
|
|
44
|
+
// Track scroll depth milestones already sent (to avoid duplicates)
|
|
45
|
+
this.scrollMilestones = new Set()
|
|
46
|
+
|
|
47
|
+
// Session info
|
|
48
|
+
this.sessionId = this.generateSessionId()
|
|
49
|
+
this.sessionStart = Date.now()
|
|
50
|
+
|
|
51
|
+
// Only set up browser handlers if in browser and configured
|
|
52
|
+
if (isBrowser && this.isEnabled()) {
|
|
53
|
+
this.setupFlushInterval()
|
|
54
|
+
this.setupUnloadHandler()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if analytics is enabled
|
|
60
|
+
* @returns {boolean}
|
|
61
|
+
*/
|
|
62
|
+
isEnabled() {
|
|
63
|
+
return !!this.endpoint
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate a simple session ID
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
generateSessionId() {
|
|
71
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set up periodic flush interval
|
|
76
|
+
*/
|
|
77
|
+
setupFlushInterval() {
|
|
78
|
+
this.flushIntervalId = setInterval(() => {
|
|
79
|
+
this.flush()
|
|
80
|
+
}, this.flushInterval)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set up unload handler for final flush
|
|
85
|
+
*/
|
|
86
|
+
setupUnloadHandler() {
|
|
87
|
+
const handleUnload = () => {
|
|
88
|
+
this.flush(true) // Force beacon
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
window.addEventListener('visibilitychange', () => {
|
|
92
|
+
if (document.visibilityState === 'hidden') {
|
|
93
|
+
handleUnload()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
window.addEventListener('pagehide', handleUnload)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Add event to queue
|
|
102
|
+
* @param {string} type - Event type
|
|
103
|
+
* @param {Object} data - Event data
|
|
104
|
+
*/
|
|
105
|
+
addToQueue(type, data) {
|
|
106
|
+
if (!this.isEnabled() || !isBrowser) return
|
|
107
|
+
|
|
108
|
+
const event = {
|
|
109
|
+
type,
|
|
110
|
+
data,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
sessionId: this.sessionId,
|
|
113
|
+
url: window.location.href,
|
|
114
|
+
referrer: document.referrer || null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.queue.push(event)
|
|
118
|
+
|
|
119
|
+
if (this.debug) {
|
|
120
|
+
console.log('[Analytics] Event queued:', event)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Auto-flush if queue is full
|
|
124
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
125
|
+
this.flush()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Track a page view
|
|
131
|
+
* @param {string} path - Page path
|
|
132
|
+
* @param {string} title - Page title
|
|
133
|
+
* @param {Object} meta - Additional metadata
|
|
134
|
+
*/
|
|
135
|
+
trackPageView(path, title, meta = {}) {
|
|
136
|
+
// Reset scroll milestones for new page
|
|
137
|
+
this.scrollMilestones.clear()
|
|
138
|
+
|
|
139
|
+
this.addToQueue('pageview', {
|
|
140
|
+
path,
|
|
141
|
+
title,
|
|
142
|
+
...meta
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Track a custom event
|
|
148
|
+
* @param {string} name - Event name
|
|
149
|
+
* @param {Object} data - Event data
|
|
150
|
+
*/
|
|
151
|
+
trackEvent(name, data = {}) {
|
|
152
|
+
this.addToQueue('event', {
|
|
153
|
+
name,
|
|
154
|
+
...data
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Track scroll depth milestone
|
|
160
|
+
* @param {number} percentage - Scroll depth percentage (25, 50, 75, 100)
|
|
161
|
+
*/
|
|
162
|
+
trackScrollDepth(percentage) {
|
|
163
|
+
// Only track standard milestones
|
|
164
|
+
const milestones = [25, 50, 75, 100]
|
|
165
|
+
if (!milestones.includes(percentage)) return
|
|
166
|
+
|
|
167
|
+
// Don't track the same milestone twice per page
|
|
168
|
+
if (this.scrollMilestones.has(percentage)) return
|
|
169
|
+
|
|
170
|
+
this.scrollMilestones.add(percentage)
|
|
171
|
+
|
|
172
|
+
this.addToQueue('scroll_depth', {
|
|
173
|
+
depth: percentage
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Flush the event queue
|
|
179
|
+
* @param {boolean} useBeacon - Force use of sendBeacon (for unload)
|
|
180
|
+
*/
|
|
181
|
+
flush(useBeacon = false) {
|
|
182
|
+
if (!this.isEnabled() || !isBrowser || this.queue.length === 0) return
|
|
183
|
+
|
|
184
|
+
const events = [...this.queue]
|
|
185
|
+
this.queue = []
|
|
186
|
+
|
|
187
|
+
const payload = JSON.stringify({
|
|
188
|
+
events,
|
|
189
|
+
sessionId: this.sessionId,
|
|
190
|
+
sessionDuration: Date.now() - this.sessionStart
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
if (this.debug) {
|
|
194
|
+
console.log('[Analytics] Flushing', events.length, 'events')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Use sendBeacon for reliable delivery on page unload
|
|
198
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
199
|
+
const blob = new Blob([payload], { type: 'application/json' })
|
|
200
|
+
const sent = navigator.sendBeacon(this.endpoint, blob)
|
|
201
|
+
|
|
202
|
+
if (!sent && this.debug) {
|
|
203
|
+
console.warn('[Analytics] sendBeacon failed, events may be lost')
|
|
204
|
+
}
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Use fetch for normal flush
|
|
209
|
+
fetch(this.endpoint, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
body: payload,
|
|
213
|
+
keepalive: true // Allows request to outlive page
|
|
214
|
+
}).catch((error) => {
|
|
215
|
+
if (this.debug) {
|
|
216
|
+
console.warn('[Analytics] Flush failed:', error)
|
|
217
|
+
}
|
|
218
|
+
// Put events back in queue for retry
|
|
219
|
+
this.queue.unshift(...events)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Clean up (stop interval, flush remaining events)
|
|
225
|
+
*/
|
|
226
|
+
destroy() {
|
|
227
|
+
if (this.flushIntervalId) {
|
|
228
|
+
clearInterval(this.flushIntervalId)
|
|
229
|
+
}
|
|
230
|
+
this.flush(true)
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { default as Website } from './website.js'
|
|
|
13
13
|
export { default as Page } from './page.js'
|
|
14
14
|
export { default as Block } from './block.js'
|
|
15
15
|
export { default as Input } from './input.js'
|
|
16
|
+
export { default as Analytics } from './analytics.js'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The singleton Uniweb instance.
|
package/src/page.js
CHANGED
|
@@ -1,52 +1,143 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Page
|
|
3
3
|
*
|
|
4
|
-
* Represents a single page with header, body
|
|
4
|
+
* Represents a single page with header, body, footer, and panel sections.
|
|
5
|
+
* Each layout area can have multiple sections/blocks.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import Block from './block.js'
|
|
8
9
|
|
|
9
10
|
export default class Page {
|
|
10
|
-
constructor(pageData, id, pageHeader, pageFooter) {
|
|
11
|
+
constructor(pageData, id, pageHeader, pageFooter, pageLeft, pageRight) {
|
|
11
12
|
this.id = id
|
|
12
13
|
this.route = pageData.route
|
|
13
14
|
this.title = pageData.title || ''
|
|
14
15
|
this.description = pageData.description || ''
|
|
16
|
+
this.label = pageData.label || null // Short label for navigation (null = use title)
|
|
17
|
+
this.keywords = pageData.keywords || null
|
|
18
|
+
this.order = pageData.order ?? 0
|
|
19
|
+
this.lastModified = pageData.lastModified || null
|
|
15
20
|
|
|
21
|
+
// Navigation visibility options
|
|
22
|
+
this.hidden = pageData.hidden || false
|
|
23
|
+
this.hideInHeader = pageData.hideInHeader || false
|
|
24
|
+
this.hideInFooter = pageData.hideInFooter || false
|
|
25
|
+
|
|
26
|
+
// Layout options (per-page overrides for header/footer/panels)
|
|
27
|
+
this.layout = {
|
|
28
|
+
header: pageData.layout?.header !== false,
|
|
29
|
+
footer: pageData.layout?.footer !== false,
|
|
30
|
+
left: pageData.layout?.left !== false,
|
|
31
|
+
right: pageData.layout?.right !== false,
|
|
32
|
+
// Aliases for backwards compatibility
|
|
33
|
+
leftPanel: pageData.layout?.left !== false,
|
|
34
|
+
rightPanel: pageData.layout?.right !== false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// SEO configuration
|
|
38
|
+
this.seo = {
|
|
39
|
+
noindex: pageData.seo?.noindex || false,
|
|
40
|
+
image: pageData.seo?.image || null,
|
|
41
|
+
ogTitle: pageData.seo?.ogTitle || null,
|
|
42
|
+
ogDescription: pageData.seo?.ogDescription || null,
|
|
43
|
+
canonical: pageData.seo?.canonical || null,
|
|
44
|
+
changefreq: pageData.seo?.changefreq || null,
|
|
45
|
+
priority: pageData.seo?.priority || null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Child pages (for nested hierarchy) - populated by Website
|
|
49
|
+
this.children = []
|
|
50
|
+
|
|
51
|
+
// Back-reference to website (set by Website constructor)
|
|
52
|
+
this.website = null
|
|
53
|
+
this.site = null // Alias
|
|
54
|
+
|
|
55
|
+
// Scroll position memory (for navigation restoration)
|
|
56
|
+
this.scrollY = 0
|
|
57
|
+
|
|
58
|
+
// Build block groups for all layout areas
|
|
16
59
|
this.pageBlocks = this.buildPageBlocks(
|
|
17
60
|
pageData.sections,
|
|
18
61
|
pageHeader?.sections,
|
|
19
|
-
pageFooter?.sections
|
|
62
|
+
pageFooter?.sections,
|
|
63
|
+
pageLeft?.sections,
|
|
64
|
+
pageRight?.sections
|
|
20
65
|
)
|
|
21
66
|
}
|
|
22
67
|
|
|
23
68
|
/**
|
|
24
|
-
*
|
|
69
|
+
* Get metadata for head tags
|
|
70
|
+
* @returns {Object} Head metadata
|
|
25
71
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
72
|
+
getHeadMeta() {
|
|
73
|
+
return {
|
|
74
|
+
title: this.title,
|
|
75
|
+
description: this.description,
|
|
76
|
+
keywords: this.keywords,
|
|
77
|
+
canonical: this.seo.canonical,
|
|
78
|
+
robots: this.seo.noindex ? 'noindex, nofollow' : null,
|
|
79
|
+
og: {
|
|
80
|
+
title: this.seo.ogTitle || this.title,
|
|
81
|
+
description: this.seo.ogDescription || this.description,
|
|
82
|
+
image: this.seo.image,
|
|
83
|
+
url: this.route
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the page block structure for all layout areas.
|
|
90
|
+
* Each area can have multiple sections/blocks.
|
|
91
|
+
*
|
|
92
|
+
* @param {Array} body - Body sections from page content
|
|
93
|
+
* @param {Array} header - Header sections from @header page
|
|
94
|
+
* @param {Array} footer - Footer sections from @footer page
|
|
95
|
+
* @param {Array} left - Left panel sections from @left page
|
|
96
|
+
* @param {Array} right - Right panel sections from @right page
|
|
97
|
+
* @returns {Object} Block groups for each layout area
|
|
98
|
+
*/
|
|
99
|
+
buildPageBlocks(body, header, footer, left, right) {
|
|
100
|
+
const buildBlocks = (sections, prefix) => {
|
|
101
|
+
if (!sections || sections.length === 0) return null
|
|
102
|
+
return sections.map((section, index) => new Block(section, `${prefix}-${index}`))
|
|
103
|
+
}
|
|
30
104
|
|
|
31
105
|
return {
|
|
32
|
-
header:
|
|
33
|
-
body:
|
|
34
|
-
footer:
|
|
35
|
-
|
|
36
|
-
|
|
106
|
+
header: buildBlocks(header, 'header'),
|
|
107
|
+
body: (body || []).map((section, index) => new Block(section, index)),
|
|
108
|
+
footer: buildBlocks(footer, 'footer'),
|
|
109
|
+
left: buildBlocks(left, 'left'),
|
|
110
|
+
right: buildBlocks(right, 'right')
|
|
37
111
|
}
|
|
38
112
|
}
|
|
39
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Get all block groups (for Layout component)
|
|
116
|
+
* @returns {Object} { header, body, footer, left, right }
|
|
117
|
+
*/
|
|
118
|
+
getBlockGroups() {
|
|
119
|
+
return this.pageBlocks
|
|
120
|
+
}
|
|
121
|
+
|
|
40
122
|
/**
|
|
41
123
|
* Get all blocks (header, body, footer) as flat array
|
|
124
|
+
* Respects page layout preferences (hasHeader, hasFooter, etc.)
|
|
42
125
|
* @returns {Block[]}
|
|
43
126
|
*/
|
|
44
127
|
getPageBlocks() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.pageBlocks.
|
|
49
|
-
|
|
128
|
+
const blocks = []
|
|
129
|
+
|
|
130
|
+
if (this.hasHeader() && this.pageBlocks.header) {
|
|
131
|
+
blocks.push(...this.pageBlocks.header)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
blocks.push(...this.pageBlocks.body)
|
|
135
|
+
|
|
136
|
+
if (this.hasFooter() && this.pageBlocks.footer) {
|
|
137
|
+
blocks.push(...this.pageBlocks.footer)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return blocks
|
|
50
141
|
}
|
|
51
142
|
|
|
52
143
|
/**
|
|
@@ -58,18 +149,151 @@ export default class Page {
|
|
|
58
149
|
}
|
|
59
150
|
|
|
60
151
|
/**
|
|
61
|
-
* Get header
|
|
152
|
+
* Get header blocks (respects layout preference)
|
|
153
|
+
* @returns {Block[]|null}
|
|
154
|
+
*/
|
|
155
|
+
getHeaderBlocks() {
|
|
156
|
+
if (!this.hasHeader()) return null
|
|
157
|
+
return this.pageBlocks.header
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get footer blocks (respects layout preference)
|
|
162
|
+
* @returns {Block[]|null}
|
|
163
|
+
*/
|
|
164
|
+
getFooterBlocks() {
|
|
165
|
+
if (!this.hasFooter()) return null
|
|
166
|
+
return this.pageBlocks.footer
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get left panel blocks (respects layout preference)
|
|
171
|
+
* @returns {Block[]|null}
|
|
172
|
+
*/
|
|
173
|
+
getLeftBlocks() {
|
|
174
|
+
if (!this.hasLeftPanel()) return null
|
|
175
|
+
return this.pageBlocks.left
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get right panel blocks (respects layout preference)
|
|
180
|
+
* @returns {Block[]|null}
|
|
181
|
+
*/
|
|
182
|
+
getRightBlocks() {
|
|
183
|
+
if (!this.hasRightPanel()) return null
|
|
184
|
+
return this.pageBlocks.right
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get header block (legacy - returns first block)
|
|
62
189
|
* @returns {Block|null}
|
|
190
|
+
* @deprecated Use getHeaderBlocks() instead
|
|
63
191
|
*/
|
|
64
192
|
getHeader() {
|
|
65
|
-
return this.pageBlocks.header
|
|
193
|
+
return this.pageBlocks.header?.[0] || null
|
|
66
194
|
}
|
|
67
195
|
|
|
68
196
|
/**
|
|
69
|
-
* Get footer block
|
|
197
|
+
* Get footer block (legacy - returns first block)
|
|
70
198
|
* @returns {Block|null}
|
|
199
|
+
* @deprecated Use getFooterBlocks() instead
|
|
71
200
|
*/
|
|
72
201
|
getFooter() {
|
|
73
|
-
return this.pageBlocks.footer
|
|
202
|
+
return this.pageBlocks.footer?.[0] || null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Reset block states (for scroll restoration)
|
|
207
|
+
*/
|
|
208
|
+
resetBlockStates() {
|
|
209
|
+
const allBlocks = [
|
|
210
|
+
...(this.pageBlocks.header || []),
|
|
211
|
+
...this.pageBlocks.body,
|
|
212
|
+
...(this.pageBlocks.footer || []),
|
|
213
|
+
...(this.pageBlocks.left || []),
|
|
214
|
+
...(this.pageBlocks.right || [])
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
for (const block of allBlocks) {
|
|
218
|
+
if (typeof block.initState === 'function') {
|
|
219
|
+
block.initState()
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────
|
|
225
|
+
// Navigation and Layout Helpers
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get display label for navigation (short form of title)
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
getLabel() {
|
|
233
|
+
return this.label || this.title
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if page should be hidden from navigation
|
|
238
|
+
* @returns {boolean}
|
|
239
|
+
*/
|
|
240
|
+
isHidden() {
|
|
241
|
+
return this.hidden
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if page should appear in header navigation
|
|
246
|
+
* @returns {boolean}
|
|
247
|
+
*/
|
|
248
|
+
showInHeader() {
|
|
249
|
+
return !this.hidden && !this.hideInHeader
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if page should appear in footer navigation
|
|
254
|
+
* @returns {boolean}
|
|
255
|
+
*/
|
|
256
|
+
showInFooter() {
|
|
257
|
+
return !this.hidden && !this.hideInFooter
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if header should be rendered on this page
|
|
262
|
+
* @returns {boolean}
|
|
263
|
+
*/
|
|
264
|
+
hasHeader() {
|
|
265
|
+
return this.layout.header
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if footer should be rendered on this page
|
|
270
|
+
* @returns {boolean}
|
|
271
|
+
*/
|
|
272
|
+
hasFooter() {
|
|
273
|
+
return this.layout.footer
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if left panel should be rendered on this page
|
|
278
|
+
* @returns {boolean}
|
|
279
|
+
*/
|
|
280
|
+
hasLeftPanel() {
|
|
281
|
+
return this.layout.leftPanel
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Check if right panel should be rendered on this page
|
|
286
|
+
* @returns {boolean}
|
|
287
|
+
*/
|
|
288
|
+
hasRightPanel() {
|
|
289
|
+
return this.layout.rightPanel
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if page has child pages
|
|
294
|
+
* @returns {boolean}
|
|
295
|
+
*/
|
|
296
|
+
hasChildren() {
|
|
297
|
+
return this.children.length > 0
|
|
74
298
|
}
|
|
75
299
|
}
|
package/src/uniweb.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import Website from './website.js'
|
|
9
|
+
import Analytics from './analytics.js'
|
|
9
10
|
|
|
10
11
|
export default class Uniweb {
|
|
11
12
|
constructor(configData) {
|
|
@@ -15,6 +16,9 @@ export default class Uniweb {
|
|
|
15
16
|
this.foundation = null // The loaded foundation module
|
|
16
17
|
this.foundationConfig = {} // Configuration from foundation
|
|
17
18
|
this.language = 'en'
|
|
19
|
+
|
|
20
|
+
// Initialize analytics (disabled by default, configure via site config)
|
|
21
|
+
this.analytics = new Analytics(configData.analytics || {})
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
package/src/website.js
CHANGED
|
@@ -6,21 +6,55 @@
|
|
|
6
6
|
|
|
7
7
|
import Page from './page.js'
|
|
8
8
|
|
|
9
|
+
// Common locale display names
|
|
10
|
+
const LOCALE_NAMES = {
|
|
11
|
+
en: 'English',
|
|
12
|
+
es: 'Español',
|
|
13
|
+
fr: 'Français',
|
|
14
|
+
de: 'Deutsch',
|
|
15
|
+
it: 'Italiano',
|
|
16
|
+
pt: 'Português',
|
|
17
|
+
nl: 'Nederlands',
|
|
18
|
+
pl: 'Polski',
|
|
19
|
+
ru: 'Русский',
|
|
20
|
+
ja: '日本語',
|
|
21
|
+
ko: '한국어',
|
|
22
|
+
zh: '中文',
|
|
23
|
+
ar: 'العربية'
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
export default class Website {
|
|
10
27
|
constructor(websiteData) {
|
|
11
|
-
const { pages = [], theme = {}, config = {} } = websiteData
|
|
28
|
+
const { pages = [], theme = {}, config = {}, header, footer, left, right } = websiteData
|
|
29
|
+
|
|
30
|
+
// Site metadata
|
|
31
|
+
this.name = config.name || ''
|
|
32
|
+
this.description = config.description || ''
|
|
33
|
+
this.url = config.url || ''
|
|
12
34
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
35
|
+
// Store special pages (layout areas)
|
|
36
|
+
// These come from top-level properties set by content-collector
|
|
37
|
+
// Fallback to searching pages array for backwards compatibility
|
|
38
|
+
this.headerPage = header || pages.find((p) => p.route === '/@header') || null
|
|
39
|
+
this.footerPage = footer || pages.find((p) => p.route === '/@footer') || null
|
|
40
|
+
this.leftPage = left || pages.find((p) => p.route === '/@left') || null
|
|
41
|
+
this.rightPage = right || pages.find((p) => p.route === '/@right') || null
|
|
16
42
|
|
|
43
|
+
// Filter out special pages from regular pages array
|
|
44
|
+
const specialRoutes = ['/@header', '/@footer', '/@left', '/@right']
|
|
17
45
|
this.pages = pages
|
|
18
|
-
.filter((page) =>
|
|
46
|
+
.filter((page) => !specialRoutes.includes(page.route))
|
|
19
47
|
.map(
|
|
20
48
|
(page, index) =>
|
|
21
|
-
new Page(page, index, this.headerPage, this.footerPage)
|
|
49
|
+
new Page(page, index, this.headerPage, this.footerPage, this.leftPage, this.rightPage)
|
|
22
50
|
)
|
|
23
51
|
|
|
52
|
+
// Set reference from pages back to website
|
|
53
|
+
for (const page of this.pages) {
|
|
54
|
+
page.website = this
|
|
55
|
+
page.site = this // Alias
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
this.activePage =
|
|
25
59
|
this.pages.find((page) => page.route === '/' || page.route === '/index') ||
|
|
26
60
|
this.pages[0]
|
|
@@ -28,11 +62,46 @@ export default class Website {
|
|
|
28
62
|
this.pageRoutes = this.pages.map((page) => page.route)
|
|
29
63
|
this.themeData = theme
|
|
30
64
|
this.config = config
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
|
|
66
|
+
// Locale configuration
|
|
67
|
+
this.defaultLocale = config.defaultLanguage || 'en'
|
|
68
|
+
this.activeLocale = config.activeLocale || this.defaultLocale
|
|
69
|
+
|
|
70
|
+
// Build locales list from i18n config
|
|
71
|
+
this.locales = this.buildLocalesList(config)
|
|
72
|
+
|
|
73
|
+
// Legacy language support (for editor multilingual)
|
|
74
|
+
this.activeLang = this.activeLocale
|
|
75
|
+
this.langs = config.languages || this.locales.map(l => ({
|
|
76
|
+
label: l.label,
|
|
77
|
+
value: l.code
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build locales list from config
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
buildLocalesList(config) {
|
|
86
|
+
const defaultLocale = config.defaultLanguage || 'en'
|
|
87
|
+
const i18nLocales = config.i18n?.locales || []
|
|
88
|
+
|
|
89
|
+
// Start with default locale
|
|
90
|
+
const allLocaleCodes = [defaultLocale]
|
|
91
|
+
|
|
92
|
+
// Add translated locales (avoiding duplicates)
|
|
93
|
+
for (const locale of i18nLocales) {
|
|
94
|
+
if (!allLocaleCodes.includes(locale)) {
|
|
95
|
+
allLocaleCodes.push(locale)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Build full locale objects
|
|
100
|
+
return allLocaleCodes.map(code => ({
|
|
101
|
+
code,
|
|
102
|
+
label: LOCALE_NAMES[code] || code.toUpperCase(),
|
|
103
|
+
isDefault: code === defaultLocale
|
|
104
|
+
}))
|
|
36
105
|
}
|
|
37
106
|
|
|
38
107
|
/**
|
|
@@ -88,6 +157,7 @@ export default class Website {
|
|
|
88
157
|
|
|
89
158
|
/**
|
|
90
159
|
* Get available languages
|
|
160
|
+
* @deprecated Use getLocales() instead
|
|
91
161
|
*/
|
|
92
162
|
getLanguages() {
|
|
93
163
|
return this.langs
|
|
@@ -95,11 +165,91 @@ export default class Website {
|
|
|
95
165
|
|
|
96
166
|
/**
|
|
97
167
|
* Get current language
|
|
168
|
+
* @deprecated Use getActiveLocale() instead
|
|
98
169
|
*/
|
|
99
170
|
getLanguage() {
|
|
100
171
|
return this.activeLang
|
|
101
172
|
}
|
|
102
173
|
|
|
174
|
+
// ─────────────────────────────────────────────────────────────────
|
|
175
|
+
// Locale API (new)
|
|
176
|
+
// ─────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get all available locales
|
|
180
|
+
* @returns {Array<{code: string, label: string, isDefault: boolean}>}
|
|
181
|
+
*/
|
|
182
|
+
getLocales() {
|
|
183
|
+
return this.locales
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get currently active locale code
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
getActiveLocale() {
|
|
191
|
+
return this.activeLocale
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the default locale code
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
getDefaultLocale() {
|
|
199
|
+
return this.defaultLocale
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check if site has multiple locales (useful for showing language switcher)
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
hasMultipleLocales() {
|
|
207
|
+
return this.locales.length > 1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set the active locale
|
|
212
|
+
* @param {string} localeCode - Locale code to activate
|
|
213
|
+
*/
|
|
214
|
+
setActiveLocale(localeCode) {
|
|
215
|
+
const locale = this.locales.find(l => l.code === localeCode)
|
|
216
|
+
if (locale) {
|
|
217
|
+
this.activeLocale = localeCode
|
|
218
|
+
this.activeLang = localeCode // Keep legacy in sync
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build URL for a specific locale
|
|
224
|
+
* @param {string} localeCode - Target locale code
|
|
225
|
+
* @param {string} route - Page route (default: current page route)
|
|
226
|
+
* @returns {string}
|
|
227
|
+
*/
|
|
228
|
+
getLocaleUrl(localeCode, route = null) {
|
|
229
|
+
const targetRoute = route || this.activePage?.route || '/'
|
|
230
|
+
|
|
231
|
+
// Default locale uses root path (no prefix)
|
|
232
|
+
if (localeCode === this.defaultLocale) {
|
|
233
|
+
return targetRoute
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Other locales use /locale/ prefix
|
|
237
|
+
if (targetRoute === '/') {
|
|
238
|
+
return `/${localeCode}/`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return `/${localeCode}${targetRoute}`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get locale info by code
|
|
246
|
+
* @param {string} localeCode - Locale code
|
|
247
|
+
* @returns {Object|undefined} Locale object or undefined
|
|
248
|
+
*/
|
|
249
|
+
getLocale(localeCode) {
|
|
250
|
+
return this.locales.find(l => l.code === localeCode)
|
|
251
|
+
}
|
|
252
|
+
|
|
103
253
|
/**
|
|
104
254
|
* Localize a value
|
|
105
255
|
* @param {any} val - Value to localize (object with lang keys, or string)
|
|
@@ -154,4 +304,111 @@ export default class Website {
|
|
|
154
304
|
.join('\n')
|
|
155
305
|
}))
|
|
156
306
|
}
|
|
307
|
+
|
|
308
|
+
// ─────────────────────────────────────────────────────────────────
|
|
309
|
+
// Page Hierarchy API (for navigation components)
|
|
310
|
+
// ─────────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get page hierarchy for building navigation (navbar, footer, sitemap)
|
|
314
|
+
*
|
|
315
|
+
* This is the primary API for navigation components. It returns pages
|
|
316
|
+
* filtered and formatted for navigation use.
|
|
317
|
+
*
|
|
318
|
+
* @param {Object} options - Configuration options
|
|
319
|
+
* @param {boolean} [options.nested=true] - Return nested hierarchy (with children) or flat list
|
|
320
|
+
* @param {string} [options.for] - Filter for specific navigation: 'header', 'footer', or undefined (all)
|
|
321
|
+
* @param {boolean} [options.includeHidden=false] - Include hidden pages
|
|
322
|
+
* @param {function} [options.filter] - Custom filter function (page) => boolean
|
|
323
|
+
* @param {function} [options.sort] - Custom sort function (a, b) => number
|
|
324
|
+
* @returns {Array<Object>} Array of page info objects for navigation
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* // Get pages for header navigation
|
|
328
|
+
* const headerPages = website.getPageHierarchy({ for: 'header' })
|
|
329
|
+
*
|
|
330
|
+
* // Get flat list of all pages
|
|
331
|
+
* const allPages = website.getPageHierarchy({ nested: false, includeHidden: true })
|
|
332
|
+
*
|
|
333
|
+
* // Custom filtering
|
|
334
|
+
* const topLevel = website.getPageHierarchy({
|
|
335
|
+
* filter: (page) => page.order < 10
|
|
336
|
+
* })
|
|
337
|
+
*/
|
|
338
|
+
getPageHierarchy(options = {}) {
|
|
339
|
+
const {
|
|
340
|
+
nested = true,
|
|
341
|
+
for: navType,
|
|
342
|
+
includeHidden = false,
|
|
343
|
+
filter: customFilter,
|
|
344
|
+
sort: customSort
|
|
345
|
+
} = options
|
|
346
|
+
|
|
347
|
+
// Filter pages based on navigation type and visibility
|
|
348
|
+
let filteredPages = this.pages.filter(page => {
|
|
349
|
+
// Always exclude special pages (header/footer are already separated)
|
|
350
|
+
if (page.route.startsWith('/@')) return false
|
|
351
|
+
|
|
352
|
+
// Check visibility based on navigation type
|
|
353
|
+
if (!includeHidden) {
|
|
354
|
+
if (page.hidden) return false
|
|
355
|
+
if (navType === 'header' && page.hideInHeader) return false
|
|
356
|
+
if (navType === 'footer' && page.hideInFooter) return false
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Apply custom filter if provided
|
|
360
|
+
if (customFilter && !customFilter(page)) return false
|
|
361
|
+
|
|
362
|
+
return true
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// Apply custom sort or default to order
|
|
366
|
+
if (customSort) {
|
|
367
|
+
filteredPages.sort(customSort)
|
|
368
|
+
}
|
|
369
|
+
// Already sorted by order in constructor, so no need to re-sort
|
|
370
|
+
|
|
371
|
+
// Build page info objects
|
|
372
|
+
const buildPageInfo = (page) => ({
|
|
373
|
+
id: page.id,
|
|
374
|
+
route: page.route === '/' ? '' : page.route,
|
|
375
|
+
title: page.title,
|
|
376
|
+
label: page.getLabel(),
|
|
377
|
+
description: page.description,
|
|
378
|
+
order: page.order,
|
|
379
|
+
hasContent: page.getBodyBlocks().length > 0,
|
|
380
|
+
children: nested && page.hasChildren()
|
|
381
|
+
? page.children.map(buildPageInfo)
|
|
382
|
+
: []
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
return filteredPages.map(buildPageInfo)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get pages for header navigation
|
|
390
|
+
* Convenience method equivalent to getPageHierarchy({ for: 'header' })
|
|
391
|
+
* @returns {Array<Object>}
|
|
392
|
+
*/
|
|
393
|
+
getHeaderPages() {
|
|
394
|
+
return this.getPageHierarchy({ for: 'header' })
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get pages for footer navigation
|
|
399
|
+
* Convenience method equivalent to getPageHierarchy({ for: 'footer' })
|
|
400
|
+
* @returns {Array<Object>}
|
|
401
|
+
*/
|
|
402
|
+
getFooterPages() {
|
|
403
|
+
return this.getPageHierarchy({ for: 'footer' })
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get flat list of all pages (for sitemaps, search, etc.)
|
|
408
|
+
* @param {boolean} includeHidden - Include hidden pages
|
|
409
|
+
* @returns {Array<Object>}
|
|
410
|
+
*/
|
|
411
|
+
getAllPages(includeHidden = false) {
|
|
412
|
+
return this.getPageHierarchy({ nested: false, includeHidden })
|
|
413
|
+
}
|
|
157
414
|
}
|