coralite-plugin-aggregation 0.10.0 → 0.11.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/README.md +39 -23
- package/lib/components/coralite-pagination.html +1 -1
- package/lib/index.js +186 -186
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,10 +33,24 @@ Create a template for individual items (e.g., `templates/coralite-post.html`):
|
|
|
33
33
|
```html
|
|
34
34
|
<template id="coralite-post">
|
|
35
35
|
<article class="post">
|
|
36
|
-
<h2><a href="{{
|
|
37
|
-
<p>{{
|
|
36
|
+
<h2><a href="{{ url }}">{{ title }}</a></h2>
|
|
37
|
+
<p>{{ description }}</p>
|
|
38
38
|
</article>
|
|
39
39
|
</template>
|
|
40
|
+
|
|
41
|
+
<script type="module">
|
|
42
|
+
import { defineComponent } from 'coralite'
|
|
43
|
+
|
|
44
|
+
export default defineComponent({
|
|
45
|
+
data: ({ page }) => {
|
|
46
|
+
return {
|
|
47
|
+
url: page.url.pathname,
|
|
48
|
+
title: page.meta.title,
|
|
49
|
+
description: page.meta.description
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
</script>
|
|
40
54
|
```
|
|
41
55
|
|
|
42
56
|
Create a component to list them (e.g., `templates/blog-list.html`):
|
|
@@ -50,28 +64,30 @@ Create a component to list them (e.g., `templates/blog-list.html`):
|
|
|
50
64
|
|
|
51
65
|
<script type="module">
|
|
52
66
|
import { defineComponent } from 'coralite'
|
|
53
|
-
import {
|
|
67
|
+
import { aggregate } from 'aggregation'
|
|
54
68
|
|
|
55
69
|
export default defineComponent({
|
|
56
|
-
|
|
57
|
-
posts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
data: async () => {
|
|
71
|
+
const posts = await aggregate({
|
|
72
|
+
// Path to aggregate pages from (relative to pages directory)
|
|
73
|
+
path: ['blog'],
|
|
74
|
+
// Template ID to render for each item
|
|
75
|
+
template: 'coralite-post',
|
|
76
|
+
// Sort by date descending (assuming meta.date exists)
|
|
77
|
+
sort: (a, b) => new Date(b.meta.date) - new Date(a.meta.date),
|
|
78
|
+
// Limit items per page
|
|
79
|
+
limit: 10,
|
|
80
|
+
// Enable pagination
|
|
81
|
+
pagination: {
|
|
82
|
+
segment: 'page', // URL segment: /blog/page/1
|
|
83
|
+
maxVisible: 5, // Max pagination links to show
|
|
84
|
+
ariaLabel: 'Blog Pagination',
|
|
85
|
+
ellipsis: '...'
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
posts
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
})
|
|
@@ -114,4 +130,4 @@ When `pagination` is enabled and `limit` is set:
|
|
|
114
130
|
|
|
115
131
|
## License
|
|
116
132
|
|
|
117
|
-
|
|
133
|
+
MPL-2.0
|
package/lib/index.js
CHANGED
|
@@ -3,237 +3,237 @@ import path from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
export const aggregation = definePlugin({
|
|
5
5
|
name: 'aggregation',
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
6
|
+
exports: {
|
|
7
|
+
aggregate: (context) => async (options) => {
|
|
8
|
+
const { state = {}, page: currentPageContext, app, renderContext: currentRenderContext } = context;
|
|
9
|
+
const {
|
|
10
|
+
path: paths = [],
|
|
11
|
+
template: component,
|
|
12
|
+
pagination,
|
|
13
|
+
filter,
|
|
14
|
+
sort,
|
|
15
|
+
limit,
|
|
16
|
+
offset = 0,
|
|
17
|
+
recursive = false,
|
|
18
|
+
transformState
|
|
19
|
+
} = options
|
|
20
|
+
const pagesRoot = app.options.pages
|
|
21
|
+
|
|
22
|
+
// Collect pages
|
|
23
|
+
let allPages = []
|
|
24
|
+
const uniquePaths = new Set()
|
|
25
|
+
|
|
26
|
+
for (const relativePath of paths) {
|
|
27
|
+
const targetPath = path.join(pagesRoot, relativePath)
|
|
28
|
+
|
|
29
|
+
if (!recursive) {
|
|
30
|
+
const pagesInDir = app.pages.getListByPath(targetPath)
|
|
31
|
+
|
|
32
|
+
if (pagesInDir) {
|
|
33
|
+
for (const item of pagesInDir) {
|
|
34
|
+
const itemPath = item.path.pathname
|
|
35
|
+
|
|
36
|
+
if (!uniquePaths.has(itemPath)) {
|
|
37
|
+
uniquePaths.add(itemPath)
|
|
38
|
+
allPages.push(item)
|
|
39
|
+
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const dirname = page.path.dirname
|
|
42
|
+
} else {
|
|
43
|
+
// Recursive search
|
|
44
|
+
for (const item of app.pages.list) {
|
|
45
|
+
const dirname = item.path.dirname
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (dirname === targetPath || dirname.startsWith(targetPath + path.sep)) {
|
|
48
|
+
const itemPath = item.path.pathname
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
if (!uniquePaths.has(itemPath)) {
|
|
51
|
+
uniquePaths.add(itemPath)
|
|
52
|
+
allPages.push(item)
|
|
53
|
+
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Filter
|
|
61
|
-
if (typeof filter === 'function') {
|
|
62
|
-
allPages = allPages.filter(page => {
|
|
63
|
-
return filter(page.result.state)
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Pagination
|
|
77
|
-
let startIndex = offset
|
|
78
|
-
let endIndex = allPages.length
|
|
79
|
-
|
|
80
|
-
let currentPage = 1
|
|
81
|
-
let totalPages = 1
|
|
82
|
-
|
|
83
|
-
const currentRenderContext = this.renderContext
|
|
84
|
-
const buildId = currentRenderContext && currentRenderContext.buildId
|
|
59
|
+
// Filter
|
|
60
|
+
if (typeof filter === 'function') {
|
|
61
|
+
allPages = allPages.filter(item => {
|
|
62
|
+
const itemState = (item.result && item.result.state) ? item.result.state : item.state
|
|
63
|
+
return filter(itemState)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
85
66
|
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
67
|
+
// Sort
|
|
68
|
+
if (typeof sort === 'function') {
|
|
69
|
+
allPages.sort((a, b) => {
|
|
70
|
+
const propsA = (a.result && a.result.state) ? a.result.state : a.state
|
|
71
|
+
const propsB = (b.result && b.result.state) ? b.result.state : b.state
|
|
72
|
+
return sort(propsA, propsB)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
90
75
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
76
|
+
// Pagination
|
|
77
|
+
let startIndex = offset
|
|
78
|
+
let endIndex = allPages.length
|
|
94
79
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
80
|
+
let currentPage = 1
|
|
81
|
+
let totalPages = 1
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
endIndex = startIndex + limit
|
|
101
|
-
totalPages = Math.ceil(allPages.length / limit)
|
|
83
|
+
const buildId = currentRenderContext && currentRenderContext.buildId
|
|
102
84
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
85
|
+
if (limit) {
|
|
86
|
+
if (pagination) {
|
|
87
|
+
const segment = pagination.segment || 'page'
|
|
88
|
+
const urlPathname = currentPageContext.url.pathname
|
|
107
89
|
|
|
108
|
-
|
|
109
|
-
|
|
90
|
+
const escapedSegment = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
91
|
+
const segmentRegex = new RegExp(`/${escapedSegment}/(\\d+)`)
|
|
92
|
+
const match = urlPathname.match(segmentRegex)
|
|
110
93
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
urlPrefixBase = path.dirname(urlPathname)
|
|
114
|
-
} else {
|
|
115
|
-
const basename = path.basename(currentFilename, path.extname(currentFilename))
|
|
116
|
-
targetDir = path.join(currentDirname, basename)
|
|
117
|
-
urlPrefixBase = urlPathname.replace(path.extname(currentFilename), '')
|
|
94
|
+
if (match) {
|
|
95
|
+
currentPage = parseInt(match[1], 10)
|
|
118
96
|
}
|
|
119
97
|
|
|
120
|
-
|
|
98
|
+
startIndex = offset + (currentPage - 1) * limit
|
|
99
|
+
endIndex = startIndex + limit
|
|
100
|
+
totalPages = Math.ceil(allPages.length / limit)
|
|
101
|
+
|
|
102
|
+
if (!match && currentPage === 1 && totalPages > 1 && buildId) {
|
|
103
|
+
const currentPathname = currentPageContext.file.pathname
|
|
104
|
+
const currentFilename = currentPageContext.file.filename
|
|
105
|
+
const currentDirname = currentPageContext.file.dirname
|
|
106
|
+
|
|
107
|
+
let targetDir = currentDirname
|
|
108
|
+
let urlPrefixBase = ''
|
|
109
|
+
|
|
110
|
+
if (currentFilename === 'index.html') {
|
|
111
|
+
targetDir = currentDirname
|
|
112
|
+
urlPrefixBase = path.dirname(urlPathname)
|
|
113
|
+
} else {
|
|
114
|
+
const basename = path.basename(currentFilename, path.extname(currentFilename))
|
|
115
|
+
targetDir = path.join(currentDirname, basename)
|
|
116
|
+
urlPrefixBase = urlPathname.replace(path.extname(currentFilename), '')
|
|
117
|
+
}
|
|
121
118
|
|
|
122
|
-
if (currentFilename === 'index.html') {
|
|
123
|
-
urlPrefixBase = path.dirname(urlPathname)
|
|
124
119
|
if (!urlPrefixBase.endsWith('/')) urlPrefixBase += '/'
|
|
125
|
-
}
|
|
126
120
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const newPathname = path.join(targetDir, segment, `${i}.html`)
|
|
131
|
-
|
|
132
|
-
const virtualItem = {
|
|
133
|
-
content: currentItem ? currentItem.content : '',
|
|
134
|
-
path: {
|
|
135
|
-
pathname: newPathname,
|
|
136
|
-
dirname: path.dirname(newPathname),
|
|
137
|
-
filename: path.basename(newPathname)
|
|
138
|
-
},
|
|
139
|
-
state: {
|
|
140
|
-
paginationBaseUrl: urlPathname,
|
|
141
|
-
paginationUrlPrefix: urlPrefixBase
|
|
142
|
-
},
|
|
143
|
-
type: 'page'
|
|
121
|
+
if (currentFilename === 'index.html') {
|
|
122
|
+
urlPrefixBase = path.dirname(urlPathname)
|
|
123
|
+
if (!urlPrefixBase.endsWith('/')) urlPrefixBase += '/'
|
|
144
124
|
}
|
|
145
125
|
|
|
146
|
-
|
|
126
|
+
const currentItem = app.pages.getItem(currentPathname)
|
|
127
|
+
|
|
128
|
+
for (let i = 2; i <= totalPages; i++) {
|
|
129
|
+
const newPathname = path.join(targetDir, segment, `${i}.html`)
|
|
130
|
+
|
|
131
|
+
const virtualItem = {
|
|
132
|
+
content: currentItem ? currentItem.content : '',
|
|
133
|
+
path: {
|
|
134
|
+
pathname: newPathname,
|
|
135
|
+
dirname: path.dirname(newPathname),
|
|
136
|
+
filename: path.basename(newPathname)
|
|
137
|
+
},
|
|
138
|
+
state: {
|
|
139
|
+
paginationBaseUrl: urlPathname,
|
|
140
|
+
paginationUrlPrefix: urlPrefixBase
|
|
141
|
+
},
|
|
142
|
+
type: 'page'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await app.addRenderQueue(virtualItem, buildId)
|
|
146
|
+
}
|
|
147
147
|
}
|
|
148
|
+
} else {
|
|
149
|
+
endIndex = Math.min(startIndex + limit, allPages.length)
|
|
148
150
|
}
|
|
149
|
-
} else {
|
|
150
|
-
endIndex = Math.min(startIndex + limit, allPages.length)
|
|
151
151
|
}
|
|
152
|
-
}
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
153
|
+
const paginatedPages = allPages.slice(startIndex, endIndex)
|
|
154
|
+
const resultNodes = []
|
|
155
|
+
|
|
156
|
+
for (const item of paginatedPages) {
|
|
157
|
+
const itemState = (item.result && item.result.state) ? item.result.state : item.state
|
|
158
|
+
let itemProps = { ...itemState }
|
|
159
|
+
|
|
160
|
+
// Apply properties transformations
|
|
161
|
+
if (transformState && typeof transformState === 'object') {
|
|
162
|
+
for (const key in transformState) {
|
|
163
|
+
if (Object.prototype.hasOwnProperty.call(transformState, key)) {
|
|
164
|
+
const transform = transformState[key]
|
|
165
|
+
if (typeof transform === 'string') {
|
|
166
|
+
itemProps[key] = itemState[transform]
|
|
167
|
+
} else if (typeof transform === 'function') {
|
|
168
|
+
itemProps[key] = transform(itemState)
|
|
169
|
+
}
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
}
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
if (component) {
|
|
175
|
+
const componentElement = await app.createComponentElement({
|
|
176
|
+
id: component,
|
|
177
|
+
state: itemProps,
|
|
178
|
+
page: item.result?.page || item.state?.page,
|
|
179
|
+
renderContext: currentRenderContext
|
|
180
|
+
})
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
if (componentElement && componentElement.children) {
|
|
183
|
+
resultNodes.push(...componentElement.children)
|
|
184
|
+
}
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
-
}
|
|
188
187
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
if (pagination) {
|
|
189
|
+
const paginationComponentId = pagination.component || 'coralite-pagination'
|
|
190
|
+
const urlPathname = currentPageContext.url.pathname
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
let baseUrl = urlPathname
|
|
193
|
+
let urlPrefix = ''
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
if (state.paginationBaseUrl) {
|
|
196
|
+
baseUrl = state.paginationBaseUrl
|
|
197
|
+
}
|
|
199
198
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} else {
|
|
203
|
-
if (baseUrl.endsWith('/index.html') || baseUrl.endsWith('/')) {
|
|
204
|
-
urlPrefix = path.dirname(baseUrl)
|
|
199
|
+
if (state.paginationUrlPrefix) {
|
|
200
|
+
urlPrefix = state.paginationUrlPrefix
|
|
205
201
|
} else {
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
if (baseUrl.endsWith('/index.html') || baseUrl.endsWith('/')) {
|
|
203
|
+
urlPrefix = path.dirname(baseUrl)
|
|
204
|
+
} else {
|
|
205
|
+
const basename = path.basename(baseUrl, '.html')
|
|
206
|
+
urlPrefix = path.join(path.dirname(baseUrl), basename)
|
|
207
|
+
}
|
|
208
208
|
}
|
|
209
|
-
}
|
|
210
209
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
210
|
+
if (!urlPrefix.endsWith('/')) urlPrefix += '/'
|
|
211
|
+
|
|
212
|
+
const paginationProps = {
|
|
213
|
+
'current-page': String(currentPage),
|
|
214
|
+
'total-pages': String(totalPages),
|
|
215
|
+
'base-url': baseUrl,
|
|
216
|
+
'url-prefix': urlPrefix,
|
|
217
|
+
segment: pagination.segment || 'page',
|
|
218
|
+
'max-visible': String(pagination.maxVisible || 5),
|
|
219
|
+
'aria-label': pagination.ariaLabel || 'Pagination',
|
|
220
|
+
ellipsis: pagination.ellipsis || '...'
|
|
221
|
+
}
|
|
223
222
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
const componentElement = await app.createComponentElement({
|
|
224
|
+
id: paginationComponentId,
|
|
225
|
+
state: paginationProps,
|
|
226
|
+
page: currentPageContext,
|
|
227
|
+
renderContext: currentRenderContext
|
|
228
|
+
})
|
|
230
229
|
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
if (componentElement && componentElement.children) {
|
|
231
|
+
resultNodes.push(...componentElement.children)
|
|
232
|
+
}
|
|
233
233
|
}
|
|
234
|
-
}
|
|
235
234
|
|
|
236
|
-
|
|
235
|
+
return app.transform(resultNodes)
|
|
236
|
+
}
|
|
237
237
|
},
|
|
238
238
|
components: [
|
|
239
239
|
// @ts-ignore
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coralite-plugin-aggregation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A Coralite plugin for dynamically collecting, filtering, sorting, and displaying content across multiple sources. Build database-free Coralite websites with automated content aggregation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@stylistic/eslint-plugin-js": "^4.2.0",
|
|
45
45
|
"@stylistic/eslint-plugin-plus": "^4.2.0",
|
|
46
|
-
"coralite": "^0.
|
|
46
|
+
"coralite": "^0.35.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"test": "node --test ./tests/index.spec.js"
|