@eeacms/volto-cca-policy 0.3.121 → 0.3.123
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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
### [0.3.123](https://github.com/eea/volto-cca-policy/compare/0.3.122...0.3.123) - 11 May 2026
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: improve error logging [kreafox - [`58bfd70`](https://github.com/eea/volto-cca-policy/commit/58bfd70ce2155de6f8a773b8bcf9482ceb704211)]
|
|
12
|
+
- fix: add error logging in RSS middleware [kreafox - [`048d715`](https://github.com/eea/volto-cca-policy/commit/048d7154a118367a1ce4c6660c71517f8f049d55)]
|
|
13
|
+
- fix: improve image URL resolution [kreafox - [`f3e2e32`](https://github.com/eea/volto-cca-policy/commit/f3e2e323b8d9c9a77e1656c1f2f8a09592733b86)]
|
|
14
|
+
|
|
15
|
+
### [0.3.122](https://github.com/eea/volto-cca-policy/compare/0.3.121...0.3.122) - 8 May 2026
|
|
16
|
+
|
|
17
|
+
#### :bug: Bug Fixes
|
|
18
|
+
|
|
19
|
+
- fix: add image fallback & improvements in RSS middleware [kreafox - [`6e6091a`](https://github.com/eea/volto-cca-policy/commit/6e6091a2e72c53fd37489fc959f5c1083e99c84e)]
|
|
20
|
+
|
|
21
|
+
#### :house: Internal changes
|
|
22
|
+
|
|
23
|
+
- style: Automated code fix [eea-jenkins - [`278f8b4`](https://github.com/eea/volto-cca-policy/commit/278f8b49ae0c56ce7a705a2eea8a6ff3373e6dff)]
|
|
24
|
+
|
|
25
|
+
#### :house: Documentation changes
|
|
26
|
+
|
|
27
|
+
- docs: update README.md [kreafox - [`5d810e1`](https://github.com/eea/volto-cca-policy/commit/5d810e1385c22d481fd14977c8c69dcecd16f70e)]
|
|
28
|
+
|
|
7
29
|
### [0.3.121](https://github.com/eea/volto-cca-policy/compare/0.3.120...0.3.121) - 7 May 2026
|
|
8
30
|
|
|
9
31
|
#### :rocket: New Features
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
## RSS
|
|
1
|
+
## RSS middleware customization
|
|
2
2
|
|
|
3
3
|
This customization patches the `volto-rss-provider` middleware to correctly handle reversed sorting in RSS Listing block queries.
|
|
4
|
+
|
|
5
|
+
It also fixes image handling so items with small lead images or missing `preview` scales do not break the entire feed; those items remain in the RSS output, but their image enclosure is skipped.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import superagent from 'superagent';
|
|
3
3
|
import RSS from 'rss';
|
|
4
|
-
import { findBlocks
|
|
4
|
+
import { findBlocks } from '@plone/volto/helpers';
|
|
5
5
|
import config from '@plone/volto/registry';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -26,75 +26,83 @@ import config from '@plone/volto/registry';
|
|
|
26
26
|
*
|
|
27
27
|
* @function getRssFeedData
|
|
28
28
|
* @param {string} apiPath - The base path for the API requests.
|
|
29
|
-
* @param {string}
|
|
29
|
+
* @param {string} APISUFFIX - The suffix added to the API path depending on the environment.
|
|
30
30
|
* @param {Object} req - The incoming Express request object.
|
|
31
31
|
* @param {Object} settings - Configuration settings for the application.
|
|
32
32
|
* @return {Object} An object containing the query data, language, description, and title of the rss_feed.
|
|
33
33
|
* @throws Will throw an error if no query data is found in the listing block or if the request fails.
|
|
34
34
|
*/
|
|
35
|
-
async function getRssFeedData(apiPath,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'/rss.xml',
|
|
41
|
-
'',
|
|
42
|
-
)}`,
|
|
43
|
-
)
|
|
44
|
-
.accept('json');
|
|
45
|
-
|
|
46
|
-
const authToken = req.universalCookies.get('auth_token');
|
|
47
|
-
if (authToken) {
|
|
48
|
-
request.set('Authorization', `Bearer ${authToken}`);
|
|
49
|
-
}
|
|
35
|
+
async function getRssFeedData(apiPath, APISUFFIX, req, settings) {
|
|
36
|
+
const url = `${apiPath}${__DEVELOPMENT__ ? '' : APISUFFIX}${req.path.replace(
|
|
37
|
+
'/rss.xml',
|
|
38
|
+
'',
|
|
39
|
+
)}`;
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const listingBlock = findBlocks(json.blocks, 'listing');
|
|
54
|
-
let queryData = json.blocks[listingBlock]?.querystring;
|
|
55
|
-
let language = json.language.token ?? 'en';
|
|
56
|
-
let description = json.description ?? 'A Volto RSS Feed';
|
|
57
|
-
let title = json.title;
|
|
58
|
-
let subjects = json.subjects;
|
|
59
|
-
let date = json.effective;
|
|
60
|
-
let max_description_length = json.max_description_length;
|
|
61
|
-
let max_title_length = json.max_title_length;
|
|
62
|
-
if (!queryData) {
|
|
63
|
-
throw new Error('No query data found in listing block');
|
|
64
|
-
}
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.error('[RSS] Fetching feed data from:', url);
|
|
65
43
|
|
|
66
|
-
|
|
67
|
-
if (typeof queryData.sort_order === 'boolean') {
|
|
68
|
-
queryData.sort_order = queryData.sort_order ? 'reverse' : 'ascending';
|
|
69
|
-
}
|
|
44
|
+
const request = superagent.get(url).accept('json');
|
|
70
45
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
46
|
+
const authToken = req.universalCookies?.get?.('auth_token');
|
|
47
|
+
if (authToken) {
|
|
48
|
+
request.set('Authorization', `Bearer ${authToken}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await request;
|
|
52
|
+
const json = response.body;
|
|
53
|
+
const listingBlocks = findBlocks(json.blocks, 'listing');
|
|
54
|
+
const listingBlockId = Array.isArray(listingBlocks)
|
|
55
|
+
? listingBlocks[0]
|
|
56
|
+
: listingBlocks;
|
|
57
|
+
|
|
58
|
+
const queryData = listingBlockId
|
|
59
|
+
? json.blocks?.[listingBlockId]?.querystring
|
|
60
|
+
: null;
|
|
61
|
+
const language = json.language?.token ?? 'en';
|
|
62
|
+
const description = json.description ?? 'A Volto RSS Feed';
|
|
63
|
+
const title = json.title;
|
|
64
|
+
const subjects = json.subjects;
|
|
65
|
+
const date = json.effective;
|
|
66
|
+
const max_description_length = json.max_description_length;
|
|
67
|
+
const max_title_length = json.max_title_length;
|
|
68
|
+
if (!queryData) {
|
|
69
|
+
throw new Error('No query data found in listing block');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const normalizedQueryData = { ...queryData };
|
|
73
|
+
|
|
74
|
+
if (normalizedQueryData.sort_order != null) {
|
|
75
|
+
if (typeof normalizedQueryData.sort_order === 'boolean') {
|
|
76
|
+
normalizedQueryData.sort_order = normalizedQueryData.sort_order
|
|
77
|
+
? 'reverse'
|
|
78
|
+
: 'ascending';
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
b_size: settings.defaultPageSize,
|
|
80
|
-
}),
|
|
81
|
-
query: queryData?.query,
|
|
82
|
-
metadata_fields: '_all',
|
|
83
|
-
b_start: 0,
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
query,
|
|
87
|
-
language,
|
|
88
|
-
description,
|
|
89
|
-
title,
|
|
90
|
-
subjects,
|
|
91
|
-
date,
|
|
92
|
-
max_description_length,
|
|
93
|
-
max_title_length,
|
|
94
|
-
};
|
|
95
|
-
} catch (err) {
|
|
96
|
-
throw err;
|
|
81
|
+
if (normalizedQueryData.sort_order === 'descending') {
|
|
82
|
+
normalizedQueryData.sort_order = 'reverse';
|
|
83
|
+
}
|
|
97
84
|
}
|
|
85
|
+
|
|
86
|
+
const query = {
|
|
87
|
+
...normalizedQueryData,
|
|
88
|
+
...(!normalizedQueryData.b_size && {
|
|
89
|
+
b_size: settings.defaultPageSize,
|
|
90
|
+
}),
|
|
91
|
+
query: normalizedQueryData?.query,
|
|
92
|
+
metadata_fields: '_all',
|
|
93
|
+
b_start: 0,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
query,
|
|
98
|
+
language,
|
|
99
|
+
description,
|
|
100
|
+
title,
|
|
101
|
+
subjects,
|
|
102
|
+
date,
|
|
103
|
+
max_description_length,
|
|
104
|
+
max_title_length,
|
|
105
|
+
};
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
/**
|
|
@@ -107,26 +115,44 @@ async function getRssFeedData(apiPath, APISUFIX, req, settings) {
|
|
|
107
115
|
* @function fetchListingItems
|
|
108
116
|
* @param {Object} query - The query data used for fetching items.
|
|
109
117
|
* @param {string} apiPath - The base path for the API requests.
|
|
110
|
-
* @param {string}
|
|
118
|
+
* @param {string} APISUFFIX - The suffix added to the API path depending on the environment.
|
|
111
119
|
* @param {string} authToken - The authentication token for authorized requests.
|
|
112
120
|
* @return {Array} An array of items that match the query criteria.
|
|
113
121
|
* @throws Will throw an error if the request fails.
|
|
114
122
|
*/
|
|
115
|
-
async function fetchListingItems(query, apiPath,
|
|
123
|
+
async function fetchListingItems(query, apiPath, APISUFFIX, authToken) {
|
|
124
|
+
const request = superagent
|
|
125
|
+
.post(`${apiPath}${__DEVELOPMENT__ ? '' : APISUFFIX}/@querystring-search`)
|
|
126
|
+
.send(query)
|
|
127
|
+
.accept('json');
|
|
128
|
+
|
|
129
|
+
if (authToken) {
|
|
130
|
+
request.set('Authorization', `Bearer ${authToken}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const response = await request;
|
|
134
|
+
return response.body.items || [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeFeedURL(url, publicURL, apiPath) {
|
|
138
|
+
if (!url) return publicURL;
|
|
139
|
+
|
|
116
140
|
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
const parsedUrl = new URL(url, publicURL);
|
|
142
|
+
|
|
143
|
+
if (apiPath) {
|
|
144
|
+
const apiOrigin = new URL(apiPath).origin;
|
|
145
|
+
const publicOrigin = new URL(publicURL).origin;
|
|
121
146
|
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
if (parsedUrl.origin === apiOrigin) {
|
|
148
|
+
parsedUrl.protocol = new URL(publicOrigin).protocol;
|
|
149
|
+
parsedUrl.host = new URL(publicOrigin).host;
|
|
150
|
+
}
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
throw err;
|
|
153
|
+
return parsedUrl.toString();
|
|
154
|
+
} catch {
|
|
155
|
+
return url;
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
|
|
@@ -137,8 +163,31 @@ async function fetchListingItems(query, apiPath, APISUFIX, authToken) {
|
|
|
137
163
|
* @param {number} maxLength - The maximum length of the text.
|
|
138
164
|
* @return {string} The truncated text.
|
|
139
165
|
*/
|
|
140
|
-
function truncateText(text, maxLength) {
|
|
141
|
-
|
|
166
|
+
function truncateText(text = '', maxLength) {
|
|
167
|
+
if (!maxLength || typeof text !== 'string') return text || '';
|
|
168
|
+
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function safeDate(value) {
|
|
172
|
+
if (!value) return undefined;
|
|
173
|
+
const d = new Date(value);
|
|
174
|
+
if (Number.isNaN(d.getTime()) || d.getTime() <= 0) return undefined;
|
|
175
|
+
return d;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveImageUrl(download, itemUrl, publicURL, apiPath) {
|
|
179
|
+
if (!download || !itemUrl) return undefined;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const base = itemUrl.endsWith('/') ? itemUrl : `${itemUrl}/`;
|
|
183
|
+
return normalizeFeedURL(
|
|
184
|
+
new URL(download, base).toString(),
|
|
185
|
+
publicURL,
|
|
186
|
+
apiPath,
|
|
187
|
+
);
|
|
188
|
+
} catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
142
191
|
}
|
|
143
192
|
|
|
144
193
|
/**
|
|
@@ -153,7 +202,7 @@ function truncateText(text, maxLength) {
|
|
|
153
202
|
*/
|
|
154
203
|
function make_rssMiddleware() {
|
|
155
204
|
const { settings } = config;
|
|
156
|
-
const
|
|
205
|
+
const APISUFFIX = settings.legacyTraverse ? '' : '/++api++';
|
|
157
206
|
let apiPath = '';
|
|
158
207
|
const host = process.env.HOST || 'localhost';
|
|
159
208
|
const port = process.env.PORT || '3000';
|
|
@@ -180,69 +229,80 @@ function make_rssMiddleware() {
|
|
|
180
229
|
date,
|
|
181
230
|
max_description_length,
|
|
182
231
|
max_title_length,
|
|
183
|
-
} = await getRssFeedData(apiPath,
|
|
232
|
+
} = await getRssFeedData(apiPath, APISUFFIX, req, settings);
|
|
184
233
|
const items = await fetchListingItems(
|
|
185
234
|
query,
|
|
186
235
|
apiPath,
|
|
187
|
-
|
|
188
|
-
req.universalCookies
|
|
236
|
+
APISUFFIX,
|
|
237
|
+
req.universalCookies?.get?.('auth_token'),
|
|
189
238
|
);
|
|
190
239
|
const feedOptions = {
|
|
191
240
|
title: truncateText(title, max_title_length),
|
|
192
241
|
description: truncateText(description, max_description_length),
|
|
193
|
-
feed_url:
|
|
242
|
+
feed_url: normalizeFeedURL(req.path, settings.publicURL, apiPath),
|
|
194
243
|
site_url: settings.publicURL,
|
|
195
244
|
generator: 'RSS Feed Generator',
|
|
196
245
|
language: language,
|
|
197
|
-
pubDate:
|
|
246
|
+
pubDate: safeDate(date),
|
|
247
|
+
categories: subjects || [],
|
|
198
248
|
};
|
|
199
|
-
|
|
200
|
-
for (let i = 0; i < subjects.length; i++) {
|
|
201
|
-
feedOptions.categories = subjects;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
249
|
+
|
|
204
250
|
const feed = new RSS(feedOptions);
|
|
205
251
|
|
|
206
252
|
items.forEach((item) => {
|
|
207
|
-
|
|
253
|
+
const link = normalizeFeedURL(item.getURL, settings.publicURL, apiPath);
|
|
208
254
|
let enclosure = undefined;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
255
|
+
|
|
256
|
+
const imageData = item.image_scales?.[item.image_field]?.[0];
|
|
257
|
+
const previewScale = imageData?.scales?.preview;
|
|
258
|
+
const imageDownload = previewScale?.download || imageData?.download;
|
|
259
|
+
const imageSize = previewScale?.size || imageData?.size;
|
|
260
|
+
const imageType =
|
|
261
|
+
previewScale?.['content-type'] || imageData?.['content-type'];
|
|
262
|
+
|
|
263
|
+
const imageUrl = resolveImageUrl(
|
|
264
|
+
imageDownload,
|
|
265
|
+
link,
|
|
266
|
+
settings.publicURL,
|
|
267
|
+
apiPath,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const numericImageSize = Number(imageSize);
|
|
271
|
+
|
|
272
|
+
if (imageUrl && Number.isFinite(numericImageSize) && imageType) {
|
|
222
273
|
enclosure = {
|
|
223
274
|
url: imageUrl,
|
|
224
|
-
type:
|
|
225
|
-
size:
|
|
275
|
+
type: imageType,
|
|
276
|
+
size: numericImageSize,
|
|
226
277
|
};
|
|
227
278
|
}
|
|
228
279
|
feed.item({
|
|
229
280
|
title: truncateText(item.title, max_title_length),
|
|
230
|
-
description: truncateText(
|
|
281
|
+
description: truncateText(
|
|
282
|
+
item.description || item.Description || '',
|
|
283
|
+
max_description_length,
|
|
284
|
+
),
|
|
231
285
|
url: link,
|
|
232
286
|
guid: item.UID,
|
|
233
|
-
date:
|
|
234
|
-
author: item
|
|
235
|
-
? item['listCreators'].map((creator) => creator).join(', ')
|
|
236
|
-
: undefined,
|
|
287
|
+
date: safeDate(item.effective || item.created || item.modified),
|
|
288
|
+
author: item.listCreators?.join(', '),
|
|
237
289
|
categories: item.Subject ? item.Subject : [],
|
|
238
290
|
enclosure: enclosure || undefined,
|
|
239
291
|
});
|
|
240
292
|
});
|
|
241
293
|
|
|
242
294
|
const xml = feed.xml({ indent: true });
|
|
243
|
-
res.setHeader('
|
|
295
|
+
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
|
|
244
296
|
res.send(xml);
|
|
245
297
|
} catch (err) {
|
|
298
|
+
// eslint-disable-next-line no-console
|
|
299
|
+
console.error('[RSS] Failed', {
|
|
300
|
+
path: req.path,
|
|
301
|
+
apiPath,
|
|
302
|
+
APISUFFIX,
|
|
303
|
+
message: err.message,
|
|
304
|
+
stack: err.stack,
|
|
305
|
+
});
|
|
246
306
|
if (err.response && err.response.status === 401) {
|
|
247
307
|
// Handle unauthorized errors
|
|
248
308
|
res.status(401).json({
|
|
@@ -275,7 +335,7 @@ function make_rssMiddleware() {
|
|
|
275
335
|
export default function makeMiddlewares() {
|
|
276
336
|
const middleware = express.Router();
|
|
277
337
|
middleware.use(express.urlencoded({ extended: true }));
|
|
278
|
-
middleware.
|
|
338
|
+
middleware.get('**/rss.xml', make_rssMiddleware());
|
|
279
339
|
|
|
280
340
|
middleware.id = 'rss-middleware';
|
|
281
341
|
|