@eeacms/volto-cca-policy 0.3.121 → 0.3.122
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,20 @@ 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.122](https://github.com/eea/volto-cca-policy/compare/0.3.121...0.3.122) - 8 May 2026
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: add image fallback & improvements in RSS middleware [kreafox - [`6e6091a`](https://github.com/eea/volto-cca-policy/commit/6e6091a2e72c53fd37489fc959f5c1083e99c84e)]
|
|
12
|
+
|
|
13
|
+
#### :house: Internal changes
|
|
14
|
+
|
|
15
|
+
- style: Automated code fix [eea-jenkins - [`278f8b4`](https://github.com/eea/volto-cca-policy/commit/278f8b49ae0c56ce7a705a2eea8a6ff3373e6dff)]
|
|
16
|
+
|
|
17
|
+
#### :house: Documentation changes
|
|
18
|
+
|
|
19
|
+
- docs: update README.md [kreafox - [`5d810e1`](https://github.com/eea/volto-cca-policy/commit/5d810e1385c22d481fd14977c8c69dcecd16f70e)]
|
|
20
|
+
|
|
7
21
|
### [0.3.121](https://github.com/eea/volto-cca-policy/compare/0.3.120...0.3.121) - 7 May 2026
|
|
8
22
|
|
|
9
23
|
#### :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,82 @@ 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.accept('json');
|
|
35
|
+
async function getRssFeedData(apiPath, APISUFFIX, req, settings) {
|
|
36
|
+
const request = superagent
|
|
37
|
+
.get(
|
|
38
|
+
`${apiPath}${__DEVELOPMENT__ ? '' : APISUFFIX}${req.path.replace(
|
|
39
|
+
'/rss.xml',
|
|
40
|
+
'',
|
|
41
|
+
)}`,
|
|
42
|
+
)
|
|
43
|
+
.accept('json');
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const authToken = req.universalCookies.get('auth_token');
|
|
46
|
+
if (authToken) {
|
|
47
|
+
request.set('Authorization', `Bearer ${authToken}`);
|
|
48
|
+
}
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
}
|
|
50
|
+
const response = await request;
|
|
51
|
+
const json = response.body;
|
|
52
|
+
const listingBlocks = findBlocks(json.blocks, 'listing');
|
|
53
|
+
const listingBlockId = Array.isArray(listingBlocks)
|
|
54
|
+
? listingBlocks[0]
|
|
55
|
+
: listingBlocks;
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const queryData = listingBlockId
|
|
58
|
+
? json.blocks?.[listingBlockId]?.querystring
|
|
59
|
+
: null;
|
|
60
|
+
const language = json.language?.token ?? 'en';
|
|
61
|
+
const description = json.description ?? 'A Volto RSS Feed';
|
|
62
|
+
const title = json.title;
|
|
63
|
+
const subjects = json.subjects;
|
|
64
|
+
const date = json.effective;
|
|
65
|
+
const max_description_length = json.max_description_length;
|
|
66
|
+
const max_title_length = json.max_title_length;
|
|
67
|
+
if (!queryData) {
|
|
68
|
+
throw new Error('No query data found in listing block');
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const normalizedQueryData = { ...queryData };
|
|
72
|
+
|
|
73
|
+
if (normalizedQueryData.sort_order != null) {
|
|
74
|
+
if (typeof normalizedQueryData.sort_order === 'boolean') {
|
|
75
|
+
normalizedQueryData.sort_order = normalizedQueryData.sort_order
|
|
76
|
+
? 'reverse'
|
|
77
|
+
: 'ascending';
|
|
74
78
|
}
|
|
75
79
|
|
|
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;
|
|
80
|
+
if (normalizedQueryData.sort_order === 'descending') {
|
|
81
|
+
normalizedQueryData.sort_order = 'reverse';
|
|
82
|
+
}
|
|
97
83
|
}
|
|
84
|
+
|
|
85
|
+
const query = {
|
|
86
|
+
...normalizedQueryData,
|
|
87
|
+
...(!normalizedQueryData.b_size && {
|
|
88
|
+
b_size: settings.defaultPageSize,
|
|
89
|
+
}),
|
|
90
|
+
query: normalizedQueryData?.query,
|
|
91
|
+
metadata_fields: '_all',
|
|
92
|
+
b_start: 0,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
query,
|
|
97
|
+
language,
|
|
98
|
+
description,
|
|
99
|
+
title,
|
|
100
|
+
subjects,
|
|
101
|
+
date,
|
|
102
|
+
max_description_length,
|
|
103
|
+
max_title_length,
|
|
104
|
+
};
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/**
|
|
@@ -107,26 +114,44 @@ async function getRssFeedData(apiPath, APISUFIX, req, settings) {
|
|
|
107
114
|
* @function fetchListingItems
|
|
108
115
|
* @param {Object} query - The query data used for fetching items.
|
|
109
116
|
* @param {string} apiPath - The base path for the API requests.
|
|
110
|
-
* @param {string}
|
|
117
|
+
* @param {string} APISUFFIX - The suffix added to the API path depending on the environment.
|
|
111
118
|
* @param {string} authToken - The authentication token for authorized requests.
|
|
112
119
|
* @return {Array} An array of items that match the query criteria.
|
|
113
120
|
* @throws Will throw an error if the request fails.
|
|
114
121
|
*/
|
|
115
|
-
async function fetchListingItems(query, apiPath,
|
|
122
|
+
async function fetchListingItems(query, apiPath, APISUFFIX, authToken) {
|
|
123
|
+
const request = superagent
|
|
124
|
+
.post(`${apiPath}${__DEVELOPMENT__ ? '' : APISUFFIX}/@querystring-search`)
|
|
125
|
+
.send(query)
|
|
126
|
+
.accept('json');
|
|
127
|
+
|
|
128
|
+
if (authToken) {
|
|
129
|
+
request.set('Authorization', `Bearer ${authToken}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = await request;
|
|
133
|
+
return response.body.items || [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeFeedURL(url, publicURL, apiPath) {
|
|
137
|
+
if (!url) return publicURL;
|
|
138
|
+
|
|
116
139
|
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
const parsedUrl = new URL(url, publicURL);
|
|
141
|
+
|
|
142
|
+
if (apiPath) {
|
|
143
|
+
const apiOrigin = new URL(apiPath).origin;
|
|
144
|
+
const publicOrigin = new URL(publicURL).origin;
|
|
121
145
|
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
if (parsedUrl.origin === apiOrigin) {
|
|
147
|
+
parsedUrl.protocol = new URL(publicOrigin).protocol;
|
|
148
|
+
parsedUrl.host = new URL(publicOrigin).host;
|
|
149
|
+
}
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
throw err;
|
|
152
|
+
return parsedUrl.toString();
|
|
153
|
+
} catch {
|
|
154
|
+
return url;
|
|
130
155
|
}
|
|
131
156
|
}
|
|
132
157
|
|
|
@@ -137,8 +162,16 @@ async function fetchListingItems(query, apiPath, APISUFIX, authToken) {
|
|
|
137
162
|
* @param {number} maxLength - The maximum length of the text.
|
|
138
163
|
* @return {string} The truncated text.
|
|
139
164
|
*/
|
|
140
|
-
function truncateText(text, maxLength) {
|
|
141
|
-
|
|
165
|
+
function truncateText(text = '', maxLength) {
|
|
166
|
+
if (!maxLength || typeof text !== 'string') return text || '';
|
|
167
|
+
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function safeDate(value) {
|
|
171
|
+
if (!value) return undefined;
|
|
172
|
+
const d = new Date(value);
|
|
173
|
+
if (Number.isNaN(d.getTime()) || d.getTime() <= 0) return undefined;
|
|
174
|
+
return d;
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
/**
|
|
@@ -153,7 +186,7 @@ function truncateText(text, maxLength) {
|
|
|
153
186
|
*/
|
|
154
187
|
function make_rssMiddleware() {
|
|
155
188
|
const { settings } = config;
|
|
156
|
-
const
|
|
189
|
+
const APISUFFIX = settings.legacyTraverse ? '' : '/++api++';
|
|
157
190
|
let apiPath = '';
|
|
158
191
|
const host = process.env.HOST || 'localhost';
|
|
159
192
|
const port = process.env.PORT || '3000';
|
|
@@ -180,67 +213,62 @@ function make_rssMiddleware() {
|
|
|
180
213
|
date,
|
|
181
214
|
max_description_length,
|
|
182
215
|
max_title_length,
|
|
183
|
-
} = await getRssFeedData(apiPath,
|
|
216
|
+
} = await getRssFeedData(apiPath, APISUFFIX, req, settings);
|
|
184
217
|
const items = await fetchListingItems(
|
|
185
218
|
query,
|
|
186
219
|
apiPath,
|
|
187
|
-
|
|
220
|
+
APISUFFIX,
|
|
188
221
|
req.universalCookies.get('auth_token'),
|
|
189
222
|
);
|
|
190
223
|
const feedOptions = {
|
|
191
224
|
title: truncateText(title, max_title_length),
|
|
192
225
|
description: truncateText(description, max_description_length),
|
|
193
|
-
feed_url:
|
|
226
|
+
feed_url: normalizeFeedURL(req.path, settings.publicURL, apiPath),
|
|
194
227
|
site_url: settings.publicURL,
|
|
195
228
|
generator: 'RSS Feed Generator',
|
|
196
229
|
language: language,
|
|
197
|
-
pubDate:
|
|
230
|
+
pubDate: safeDate(date),
|
|
231
|
+
categories: subjects || [],
|
|
198
232
|
};
|
|
199
|
-
|
|
200
|
-
for (let i = 0; i < subjects.length; i++) {
|
|
201
|
-
feedOptions.categories = subjects;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
233
|
+
|
|
204
234
|
const feed = new RSS(feedOptions);
|
|
205
235
|
|
|
206
236
|
items.forEach((item) => {
|
|
207
|
-
|
|
237
|
+
const link = normalizeFeedURL(item.getURL, settings.publicURL, apiPath);
|
|
208
238
|
let enclosure = undefined;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
) {
|
|
214
|
-
const imageData = item.image_scales[item.image_field][0];
|
|
215
|
-
const imageUrl = `${link
|
|
216
|
-
.concat('/')
|
|
217
|
-
.concat(
|
|
218
|
-
item.image_scales[item.image_field][0].scales.preview.download,
|
|
219
|
-
)}`;
|
|
220
|
-
const mimeType = item['content-type'];
|
|
221
|
-
const originalSize = imageData.size;
|
|
239
|
+
|
|
240
|
+
const imageData = item.image_scales?.[item.image_field]?.[0];
|
|
241
|
+
const previewDownload = imageData?.scales?.preview?.download;
|
|
242
|
+
|
|
243
|
+
if (previewDownload && imageData.size && imageData['content-type']) {
|
|
222
244
|
enclosure = {
|
|
223
|
-
url:
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
url: normalizeFeedURL(
|
|
246
|
+
new URL(previewDownload, link).toString(),
|
|
247
|
+
settings.publicURL,
|
|
248
|
+
apiPath,
|
|
249
|
+
),
|
|
250
|
+
type: imageData['content-type'],
|
|
251
|
+
size: imageData.size,
|
|
226
252
|
};
|
|
227
253
|
}
|
|
254
|
+
|
|
228
255
|
feed.item({
|
|
229
256
|
title: truncateText(item.title, max_title_length),
|
|
230
|
-
description: truncateText(
|
|
257
|
+
description: truncateText(
|
|
258
|
+
item.description || item.Description || '',
|
|
259
|
+
max_description_length,
|
|
260
|
+
),
|
|
231
261
|
url: link,
|
|
232
262
|
guid: item.UID,
|
|
233
|
-
date:
|
|
234
|
-
author: item
|
|
235
|
-
? item['listCreators'].map((creator) => creator).join(', ')
|
|
236
|
-
: undefined,
|
|
263
|
+
date: safeDate(item.effective || item.created || item.modified),
|
|
264
|
+
author: item.listCreators?.join(', '),
|
|
237
265
|
categories: item.Subject ? item.Subject : [],
|
|
238
266
|
enclosure: enclosure || undefined,
|
|
239
267
|
});
|
|
240
268
|
});
|
|
241
269
|
|
|
242
270
|
const xml = feed.xml({ indent: true });
|
|
243
|
-
res.setHeader('
|
|
271
|
+
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
|
|
244
272
|
res.send(xml);
|
|
245
273
|
} catch (err) {
|
|
246
274
|
if (err.response && err.response.status === 401) {
|
|
@@ -275,7 +303,7 @@ function make_rssMiddleware() {
|
|
|
275
303
|
export default function makeMiddlewares() {
|
|
276
304
|
const middleware = express.Router();
|
|
277
305
|
middleware.use(express.urlencoded({ extended: true }));
|
|
278
|
-
middleware.
|
|
306
|
+
middleware.get('**/rss.xml', make_rssMiddleware());
|
|
279
307
|
|
|
280
308
|
middleware.id = 'rss-middleware';
|
|
281
309
|
|