@eeacms/volto-cca-policy 0.3.120 → 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 +33 -0
- package/package.json +2 -1
- package/src/customizations/@plone-collective/volto-rss-provider/README.md +5 -0
- package/src/customizations/@plone-collective/volto-rss-provider/components/RSSFeedView.jsx +192 -0
- package/src/customizations/@plone-collective/volto-rss-provider/express-middleware.js +311 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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
|
+
|
|
21
|
+
### [0.3.121](https://github.com/eea/volto-cca-policy/compare/0.3.120...0.3.121) - 7 May 2026
|
|
22
|
+
|
|
23
|
+
#### :rocket: New Features
|
|
24
|
+
|
|
25
|
+
- feat: improve RSSFeedView [kreafox - [`a425682`](https://github.com/eea/volto-cca-policy/commit/a4256825ede073eda95707f9e09916177b426716)]
|
|
26
|
+
- feat: add original file of RSSFeedView.jsx [kreafox - [`581a27f`](https://github.com/eea/volto-cca-policy/commit/581a27fd79191d6df70f94163cb511397b4445d9)]
|
|
27
|
+
- feat: add volto-rss-provider dependency to package.json [kreafox - [`218c1da`](https://github.com/eea/volto-cca-policy/commit/218c1da54e019bee346535c893131e07b0bcba64)]
|
|
28
|
+
- feat: sort_order handling in volto-rss-provider middleware [kreafox - [`bc6cf86`](https://github.com/eea/volto-cca-policy/commit/bc6cf86696b4798b9939e0615f3025dfe1ac742a)]
|
|
29
|
+
- feat: add original file of volto-rss-provider middleware [kreafox - [`ddf4e22`](https://github.com/eea/volto-cca-policy/commit/ddf4e2256f8e75a8944fa416e07722ecd963907b)]
|
|
30
|
+
|
|
31
|
+
#### :bug: Bug Fixes
|
|
32
|
+
|
|
33
|
+
- fix(eslint): remove unused import and clean up useClipboard [kreafox - [`9abda41`](https://github.com/eea/volto-cca-policy/commit/9abda419ff91c3344b113dd2a8289d23f3512520)]
|
|
34
|
+
- fix: remove unused import [kreafox - [`597075d`](https://github.com/eea/volto-cca-policy/commit/597075d0dde7dfe2b8ec9fe9edb0de08503e1ad6)]
|
|
35
|
+
|
|
36
|
+
#### :nail_care: Enhancements
|
|
37
|
+
|
|
38
|
+
- refactor: RSSFeedView [kreafox - [`a32e7ff`](https://github.com/eea/volto-cca-policy/commit/a32e7ff7c8dba96a8ab7b87f219b6f3988072b5e)]
|
|
39
|
+
|
|
7
40
|
### [0.3.120](https://github.com/eea/volto-cca-policy/compare/0.3.119...0.3.120) - 30 April 2026
|
|
8
41
|
|
|
9
42
|
#### :bug: Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eeacms/volto-cca-policy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.122",
|
|
4
4
|
"description": "@eeacms/volto-cca-policy: Volto add-on",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"author": "European Environment Agency: IDM2 A-Team",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@eeacms/volto-tabs-block": "^9.0.3",
|
|
42
42
|
"@elastic/search-ui": "1.21.2",
|
|
43
43
|
"@plone-collective/volto-authomatic": "2.0.1",
|
|
44
|
+
"@plone-collective/volto-rss-provider": "1.0.1",
|
|
44
45
|
"@tanstack/react-table": "8.19.3",
|
|
45
46
|
"d3-array": "^2.12.1",
|
|
46
47
|
"jotai": "^1.6.0",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
## RSS middleware customization
|
|
2
|
+
|
|
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.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document view component.
|
|
3
|
+
* @module components/theme/View/DefaultView
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import PropTypes from 'prop-types';
|
|
8
|
+
import { isEqual } from 'lodash';
|
|
9
|
+
import { compose } from 'redux';
|
|
10
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
11
|
+
import { defineMessages, injectIntl } from 'react-intl';
|
|
12
|
+
import {
|
|
13
|
+
Container as SemanticContainer,
|
|
14
|
+
Segment,
|
|
15
|
+
Button,
|
|
16
|
+
Grid,
|
|
17
|
+
Label,
|
|
18
|
+
Icon,
|
|
19
|
+
} from 'semantic-ui-react';
|
|
20
|
+
import config from '@plone/volto/registry';
|
|
21
|
+
import { getSchema } from '@plone/volto/actions';
|
|
22
|
+
import { getWidget } from '@plone/volto/helpers/Widget/utils';
|
|
23
|
+
import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks';
|
|
24
|
+
import useClipboard from '@plone/volto/hooks/clipboard/useClipboard';
|
|
25
|
+
import {
|
|
26
|
+
hasBlocksData,
|
|
27
|
+
getBaseUrl,
|
|
28
|
+
addAppURL,
|
|
29
|
+
Helmet,
|
|
30
|
+
} from '@plone/volto/helpers';
|
|
31
|
+
|
|
32
|
+
const messages = defineMessages({
|
|
33
|
+
rssFeed: {
|
|
34
|
+
id: 'rssFeed',
|
|
35
|
+
defaultMessage: 'RSS Feed',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Component to display the default view.
|
|
41
|
+
* @function DefaultView
|
|
42
|
+
* @param {Object} content Content object.
|
|
43
|
+
* @returns {string} Markup of the component.
|
|
44
|
+
*/
|
|
45
|
+
const DefaultView = (props) => {
|
|
46
|
+
const dispatch = useDispatch();
|
|
47
|
+
const { content, location } = props;
|
|
48
|
+
const { views } = config.widgets;
|
|
49
|
+
const path = getBaseUrl(location?.pathname || '');
|
|
50
|
+
const contentSchema = useSelector((state) => state.schema?.schema);
|
|
51
|
+
const [visible, setVisible] = React.useState(false);
|
|
52
|
+
const [, copy] = useClipboard(`${addAppURL(path)}/rss.xml`);
|
|
53
|
+
const fieldsetsToExclude = [
|
|
54
|
+
'categorization',
|
|
55
|
+
'dates',
|
|
56
|
+
'ownership',
|
|
57
|
+
'settings',
|
|
58
|
+
];
|
|
59
|
+
const fieldsets = contentSchema?.fieldsets.filter(
|
|
60
|
+
(fs) => !fieldsetsToExclude.includes(fs.id),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// TL;DR: There is a flash of the non block-based view because of the reset
|
|
64
|
+
// of the content on route change. Subscribing to the content change at this
|
|
65
|
+
// level has nasty implications, so we can't watch the Redux state for loaded
|
|
66
|
+
// content flag here (because it forces an additional component update)
|
|
67
|
+
// Instead, we can watch if the content is "empty", but this has a drawback
|
|
68
|
+
// since the locking mechanism inserts a `lock` key before the content is there.
|
|
69
|
+
// So "empty" means `content` is present, but only with a `lock` key, thus the next
|
|
70
|
+
// ugly condition comes to life
|
|
71
|
+
const contentLoaded = content && !isEqual(Object.keys(content), ['lock']);
|
|
72
|
+
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
content?.['@type'] &&
|
|
75
|
+
!hasBlocksData(content) &&
|
|
76
|
+
dispatch(getSchema(content['@type'], location.pathname));
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const Container =
|
|
81
|
+
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
82
|
+
|
|
83
|
+
// If the content is not yet loaded, then do not show anything
|
|
84
|
+
return contentLoaded ? (
|
|
85
|
+
hasBlocksData(content) ? (
|
|
86
|
+
<Container id="page-document">
|
|
87
|
+
<Helmet
|
|
88
|
+
link={[
|
|
89
|
+
{
|
|
90
|
+
rel: 'alternate',
|
|
91
|
+
title: props.intl.formatMessage(messages.rssFeed),
|
|
92
|
+
href: `${addAppURL(path)}/rss.xml`,
|
|
93
|
+
type: 'application/rss+xml',
|
|
94
|
+
},
|
|
95
|
+
]}
|
|
96
|
+
/>
|
|
97
|
+
<Segment>
|
|
98
|
+
<Button
|
|
99
|
+
primary
|
|
100
|
+
onClick={() => {
|
|
101
|
+
copy();
|
|
102
|
+
setVisible(true);
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<Icon className="ri-rss-fill" />
|
|
106
|
+
RSS Feed
|
|
107
|
+
</Button>
|
|
108
|
+
{visible && (
|
|
109
|
+
<p>
|
|
110
|
+
Copied! The RSS link is{' '}
|
|
111
|
+
<a
|
|
112
|
+
href={`${addAppURL(path)}/rss.xml`}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener"
|
|
115
|
+
>
|
|
116
|
+
{' '}
|
|
117
|
+
{`${addAppURL(path)}/rss.xml`}
|
|
118
|
+
</a>
|
|
119
|
+
</p>
|
|
120
|
+
)}
|
|
121
|
+
<div style={{ marginTop: '2em' }}>
|
|
122
|
+
<RenderBlocks {...props} path={path} />
|
|
123
|
+
</div>
|
|
124
|
+
</Segment>
|
|
125
|
+
</Container>
|
|
126
|
+
) : (
|
|
127
|
+
<Container id="page-document">
|
|
128
|
+
{fieldsets?.map((fs) => {
|
|
129
|
+
return (
|
|
130
|
+
<div className="fieldset" key={fs.id}>
|
|
131
|
+
{fs.id !== 'default' && <h2>{fs.title}</h2>}
|
|
132
|
+
{fs.fields?.map((f, key) => {
|
|
133
|
+
let field = {
|
|
134
|
+
...contentSchema?.properties[f],
|
|
135
|
+
id: f,
|
|
136
|
+
widget: getWidget(f, contentSchema?.properties[f]),
|
|
137
|
+
};
|
|
138
|
+
let Widget = views?.getWidget(field);
|
|
139
|
+
return f !== 'title' ? (
|
|
140
|
+
<Grid celled="internally" key={key}>
|
|
141
|
+
<Grid.Row>
|
|
142
|
+
<Label title={field.id}>{field.title}:</Label>
|
|
143
|
+
</Grid.Row>
|
|
144
|
+
<Grid.Row>
|
|
145
|
+
<Segment basic>
|
|
146
|
+
<Widget value={content[f]} />
|
|
147
|
+
</Segment>
|
|
148
|
+
</Grid.Row>
|
|
149
|
+
</Grid>
|
|
150
|
+
) : (
|
|
151
|
+
<Widget key={key} value={content[f]} />
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</Container>
|
|
158
|
+
)
|
|
159
|
+
) : null;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Property types.
|
|
164
|
+
* @property {Object} propTypes Property types.
|
|
165
|
+
* @static
|
|
166
|
+
*/
|
|
167
|
+
DefaultView.propTypes = {
|
|
168
|
+
/**
|
|
169
|
+
* Content of the object
|
|
170
|
+
*/
|
|
171
|
+
content: PropTypes.shape({
|
|
172
|
+
/**
|
|
173
|
+
* Title of the object
|
|
174
|
+
*/
|
|
175
|
+
title: PropTypes.string,
|
|
176
|
+
/**
|
|
177
|
+
* Description of the object
|
|
178
|
+
*/
|
|
179
|
+
description: PropTypes.string,
|
|
180
|
+
/**
|
|
181
|
+
* Text of the object
|
|
182
|
+
*/
|
|
183
|
+
text: PropTypes.shape({
|
|
184
|
+
/**
|
|
185
|
+
* Data of the text of the object
|
|
186
|
+
*/
|
|
187
|
+
data: PropTypes.string,
|
|
188
|
+
}),
|
|
189
|
+
}).isRequired,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export default compose(injectIntl)(DefaultView);
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import superagent from 'superagent';
|
|
3
|
+
import RSS from 'rss';
|
|
4
|
+
import { findBlocks } from '@plone/volto/helpers';
|
|
5
|
+
import config from '@plone/volto/registry';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Retrieves the query data (search criteria) used by the listing block of the rss_feed content type
|
|
9
|
+
* as well as the language, description, title, subjects (tags), date, max_description_length, and
|
|
10
|
+
* max_title_length of the rss_feed.
|
|
11
|
+
*
|
|
12
|
+
* The returned query object will have the following format:
|
|
13
|
+
* {
|
|
14
|
+
* query: [
|
|
15
|
+
* {
|
|
16
|
+
* i: <index>,
|
|
17
|
+
* o: <operator>,
|
|
18
|
+
* v: <value of the search criteria>
|
|
19
|
+
* }
|
|
20
|
+
* ],
|
|
21
|
+
* sort_order: 'ascending',
|
|
22
|
+
* b_size: 25,
|
|
23
|
+
* metadata_fields: '_all',
|
|
24
|
+
* b_start: 0
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* @function getRssFeedData
|
|
28
|
+
* @param {string} apiPath - The base path for the API requests.
|
|
29
|
+
* @param {string} APISUFFIX - The suffix added to the API path depending on the environment.
|
|
30
|
+
* @param {Object} req - The incoming Express request object.
|
|
31
|
+
* @param {Object} settings - Configuration settings for the application.
|
|
32
|
+
* @return {Object} An object containing the query data, language, description, and title of the rss_feed.
|
|
33
|
+
* @throws Will throw an error if no query data is found in the listing block or if the request fails.
|
|
34
|
+
*/
|
|
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');
|
|
44
|
+
|
|
45
|
+
const authToken = req.universalCookies.get('auth_token');
|
|
46
|
+
if (authToken) {
|
|
47
|
+
request.set('Authorization', `Bearer ${authToken}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
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;
|
|
56
|
+
|
|
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
|
+
|
|
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';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (normalizedQueryData.sort_order === 'descending') {
|
|
81
|
+
normalizedQueryData.sort_order = 'reverse';
|
|
82
|
+
}
|
|
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
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Fetches the listing block items based on the provided query data.
|
|
109
|
+
*
|
|
110
|
+
* The function sends a POST request to the @querystring-search endpoint with the query data
|
|
111
|
+
* and returns the items that match the search criteria.
|
|
112
|
+
* ref: https://6.docs.plone.org/plone.restapi/docs/source/endpoints/querystringsearch.html
|
|
113
|
+
*
|
|
114
|
+
* @function fetchListingItems
|
|
115
|
+
* @param {Object} query - The query data used for fetching items.
|
|
116
|
+
* @param {string} apiPath - The base path for the API requests.
|
|
117
|
+
* @param {string} APISUFFIX - The suffix added to the API path depending on the environment.
|
|
118
|
+
* @param {string} authToken - The authentication token for authorized requests.
|
|
119
|
+
* @return {Array} An array of items that match the query criteria.
|
|
120
|
+
* @throws Will throw an error if the request fails.
|
|
121
|
+
*/
|
|
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
|
+
|
|
139
|
+
try {
|
|
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;
|
|
145
|
+
|
|
146
|
+
if (parsedUrl.origin === apiOrigin) {
|
|
147
|
+
parsedUrl.protocol = new URL(publicOrigin).protocol;
|
|
148
|
+
parsedUrl.host = new URL(publicOrigin).host;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return parsedUrl.toString();
|
|
153
|
+
} catch {
|
|
154
|
+
return url;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Truncates the text to the specified length.
|
|
159
|
+
* If the text is longer than the specified length, it will be truncated and '...' will be appended.
|
|
160
|
+
* @function truncateText
|
|
161
|
+
* @param {string} text - The text to be truncated.
|
|
162
|
+
* @param {number} maxLength - The maximum length of the text.
|
|
163
|
+
* @return {string} The truncated text.
|
|
164
|
+
*/
|
|
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;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates an Express middleware for generating an RSS feed using the listing block of the
|
|
179
|
+
* rss_feed content type.
|
|
180
|
+
*
|
|
181
|
+
* The middleware fetches the query data of the listing block, retrieves the matching items,
|
|
182
|
+
* and generates an RSS feed in XML format, which is sent as the response.
|
|
183
|
+
*
|
|
184
|
+
* @function make_rssMiddleware
|
|
185
|
+
* @return {Function} An Express middleware function for generating the RSS feed.
|
|
186
|
+
*/
|
|
187
|
+
function make_rssMiddleware() {
|
|
188
|
+
const { settings } = config;
|
|
189
|
+
const APISUFFIX = settings.legacyTraverse ? '' : '/++api++';
|
|
190
|
+
let apiPath = '';
|
|
191
|
+
const host = process.env.HOST || 'localhost';
|
|
192
|
+
const port = process.env.PORT || '3000';
|
|
193
|
+
const ProdapiPath = `http://${host}:${port}`;
|
|
194
|
+
if (settings.internalApiPath && __SERVER__) {
|
|
195
|
+
apiPath = settings.internalApiPath;
|
|
196
|
+
} else if (__DEVELOPMENT__ && settings.devProxyToApiPath) {
|
|
197
|
+
apiPath = settings.devProxyToApiPath;
|
|
198
|
+
} else {
|
|
199
|
+
apiPath = settings.apiPath;
|
|
200
|
+
}
|
|
201
|
+
if (apiPath === '') {
|
|
202
|
+
apiPath = ProdapiPath;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function rssMiddleware(req, res, next) {
|
|
206
|
+
try {
|
|
207
|
+
const {
|
|
208
|
+
query,
|
|
209
|
+
language,
|
|
210
|
+
description,
|
|
211
|
+
title,
|
|
212
|
+
subjects,
|
|
213
|
+
date,
|
|
214
|
+
max_description_length,
|
|
215
|
+
max_title_length,
|
|
216
|
+
} = await getRssFeedData(apiPath, APISUFFIX, req, settings);
|
|
217
|
+
const items = await fetchListingItems(
|
|
218
|
+
query,
|
|
219
|
+
apiPath,
|
|
220
|
+
APISUFFIX,
|
|
221
|
+
req.universalCookies.get('auth_token'),
|
|
222
|
+
);
|
|
223
|
+
const feedOptions = {
|
|
224
|
+
title: truncateText(title, max_title_length),
|
|
225
|
+
description: truncateText(description, max_description_length),
|
|
226
|
+
feed_url: normalizeFeedURL(req.path, settings.publicURL, apiPath),
|
|
227
|
+
site_url: settings.publicURL,
|
|
228
|
+
generator: 'RSS Feed Generator',
|
|
229
|
+
language: language,
|
|
230
|
+
pubDate: safeDate(date),
|
|
231
|
+
categories: subjects || [],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const feed = new RSS(feedOptions);
|
|
235
|
+
|
|
236
|
+
items.forEach((item) => {
|
|
237
|
+
const link = normalizeFeedURL(item.getURL, settings.publicURL, apiPath);
|
|
238
|
+
let enclosure = undefined;
|
|
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']) {
|
|
244
|
+
enclosure = {
|
|
245
|
+
url: normalizeFeedURL(
|
|
246
|
+
new URL(previewDownload, link).toString(),
|
|
247
|
+
settings.publicURL,
|
|
248
|
+
apiPath,
|
|
249
|
+
),
|
|
250
|
+
type: imageData['content-type'],
|
|
251
|
+
size: imageData.size,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
feed.item({
|
|
256
|
+
title: truncateText(item.title, max_title_length),
|
|
257
|
+
description: truncateText(
|
|
258
|
+
item.description || item.Description || '',
|
|
259
|
+
max_description_length,
|
|
260
|
+
),
|
|
261
|
+
url: link,
|
|
262
|
+
guid: item.UID,
|
|
263
|
+
date: safeDate(item.effective || item.created || item.modified),
|
|
264
|
+
author: item.listCreators?.join(', '),
|
|
265
|
+
categories: item.Subject ? item.Subject : [],
|
|
266
|
+
enclosure: enclosure || undefined,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const xml = feed.xml({ indent: true });
|
|
271
|
+
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
|
|
272
|
+
res.send(xml);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err.response && err.response.status === 401) {
|
|
275
|
+
// Handle unauthorized errors
|
|
276
|
+
res.status(401).json({
|
|
277
|
+
error: 'Unauthorized',
|
|
278
|
+
message:
|
|
279
|
+
'You are not authorized to access the requested RSS feed. Please log in and try again.',
|
|
280
|
+
});
|
|
281
|
+
} else if (err.response && err.response.status === 404) {
|
|
282
|
+
// Handle not found errors
|
|
283
|
+
res.status(404).json({
|
|
284
|
+
error: 'Not Found',
|
|
285
|
+
message:
|
|
286
|
+
'The requested RSS feed could not be found. Please verify the URL and try again.',
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
// Handle other errors
|
|
290
|
+
res.status(500).json({
|
|
291
|
+
error: 'Internal Server Error',
|
|
292
|
+
message:
|
|
293
|
+
'An unexpected error occurred while generating the RSS feed. Please try again later.',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return rssMiddleware;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default function makeMiddlewares() {
|
|
304
|
+
const middleware = express.Router();
|
|
305
|
+
middleware.use(express.urlencoded({ extended: true }));
|
|
306
|
+
middleware.get('**/rss.xml', make_rssMiddleware());
|
|
307
|
+
|
|
308
|
+
middleware.id = 'rss-middleware';
|
|
309
|
+
|
|
310
|
+
return middleware;
|
|
311
|
+
}
|