@plone/volto 19.2.0 → 19.3.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/CHANGELOG.md +14 -0
- package/package.json +6 -7
- package/src/config/index.js +2 -0
- package/src/express-middleware/devproxy.js +3 -3
- package/src/helpers/Api/APIResourceWithAuth.js +2 -2
- package/src/helpers/Api/Api.js +2 -1
- package/src/helpers/Api/Api.plone.rest.test.js +7 -0
- package/src/helpers/Api/PloneBackendAPIResourceWithAuth.js +2 -2
- package/src/helpers/Api/PloneBackendAPIResourceWithAuth.test.js +12 -0
- package/src/helpers/Sitemap/Sitemap.js +3 -3
- package/src/helpers/Url/Url.js +12 -1
- package/src/helpers/Url/Url.test.js +15 -0
- package/src/server.jsx +19 -2
- package/src/start-client.jsx +3 -0
- package/types/helpers/Url/Url.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,20 @@ myst:
|
|
|
17
17
|
|
|
18
18
|
<!-- towncrier release notes start -->
|
|
19
19
|
|
|
20
|
+
## 19.3.0 (2026-07-22)
|
|
21
|
+
|
|
22
|
+
### Feature
|
|
23
|
+
|
|
24
|
+
- Add the `apiSuffix` setting and `RAZZLE_API_SUFFIX` environment variable to configure the API traversal suffix.
|
|
25
|
+
|
|
26
|
+
### Bugfix
|
|
27
|
+
|
|
28
|
+
- Silence the Node 24 `DEP0169` (`url.parse()`) and `DEP0060` (`util._extend`) deprecation warnings shown when running/building a Volto project. Volto's own server code (`server.jsx`, `devproxy.js`) now uses the WHATWG `URL` API, and pnpm patches update the archived `react-dev-utils` (build/dev-server) and `http-proxy` (dev proxy `util._extend`) dependencies. @sneridagh
|
|
29
|
+
|
|
30
|
+
### Internal
|
|
31
|
+
|
|
32
|
+
- Remove unused react-medium-image-zoom @Tishasoumya-02
|
|
33
|
+
|
|
20
34
|
## 19.2.0 (2026-07-20)
|
|
21
35
|
|
|
22
36
|
### Feature
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "19.
|
|
12
|
+
"version": "19.3.0",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git@github.com:plone/volto.git"
|
|
@@ -142,7 +142,6 @@
|
|
|
142
142
|
"react-intersection-observer": "9.1.0",
|
|
143
143
|
"react-intl": "3.12.1",
|
|
144
144
|
"react-intl-redux": "2.3.0",
|
|
145
|
-
"react-medium-image-zoom": "3.0.15",
|
|
146
145
|
"react-popper": "^2.3.0",
|
|
147
146
|
"react-redux": "8.1.2",
|
|
148
147
|
"react-router": "5.2.0",
|
|
@@ -176,10 +175,10 @@
|
|
|
176
175
|
"url": "^0.11.3",
|
|
177
176
|
"use-deep-compare-effect": "1.8.1",
|
|
178
177
|
"uuid": "^14.0.0",
|
|
179
|
-
"@plone/
|
|
180
|
-
"@plone/scripts": "4.0.1",
|
|
178
|
+
"@plone/registry": "3.0.1",
|
|
181
179
|
"@plone/volto-slate": "19.0.4",
|
|
182
|
-
"@plone/
|
|
180
|
+
"@plone/scripts": "4.0.1",
|
|
181
|
+
"@plone/components": "4.2.1"
|
|
183
182
|
},
|
|
184
183
|
"devDependencies": {
|
|
185
184
|
"@babel/core": "^7.28.5",
|
|
@@ -287,11 +286,11 @@
|
|
|
287
286
|
"webpack-bundle-analyzer": "4.10.1",
|
|
288
287
|
"webpack-dev-server": "^5.2.4",
|
|
289
288
|
"webpack-node-externals": "3.0.0",
|
|
289
|
+
"@plone/babel-preset-razzle": "^1.0.1",
|
|
290
290
|
"@plone/razzle-dev-utils": "1.0.0",
|
|
291
291
|
"@plone/razzle": "1.0.0",
|
|
292
|
-
"@plone/types": "2.0.0",
|
|
293
292
|
"@plone/volto-coresandbox": "1.0.0",
|
|
294
|
-
"@plone/
|
|
293
|
+
"@plone/types": "2.0.1"
|
|
295
294
|
},
|
|
296
295
|
"scripts": {
|
|
297
296
|
"analyze": "BUNDLE_ANALYZE=true razzle build",
|
package/src/config/index.js
CHANGED
|
@@ -102,6 +102,8 @@ let config = {
|
|
|
102
102
|
internalApiPath: process.env.RAZZLE_INTERNAL_API_PATH || undefined,
|
|
103
103
|
subpathPrefix: process.env.RAZZLE_SUBPATH_PREFIX || '',
|
|
104
104
|
websockets: process.env.RAZZLE_WEBSOCKETS || false,
|
|
105
|
+
// Overrides the API traversal suffix. An empty string disables it.
|
|
106
|
+
apiSuffix: process.env.RAZZLE_API_SUFFIX,
|
|
105
107
|
// TODO: legacyTraverse to be removed when the use of the legacy traverse is deprecated.
|
|
106
108
|
legacyTraverse: process.env.RAZZLE_LEGACY_TRAVERSE || false,
|
|
107
109
|
cookieExpires: 15552000, //in seconds. Default is 6 month (15552000)
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
responseInterceptor,
|
|
8
8
|
} from 'http-proxy-middleware';
|
|
9
9
|
import querystring from 'querystring';
|
|
10
|
-
import { parse as parseUrl } from 'url';
|
|
11
10
|
|
|
12
11
|
const filter = function (pathname, req) {
|
|
13
12
|
// Check if pathname is defined, there are some corner cases that pathname is null
|
|
@@ -31,8 +30,9 @@ function getEnv() {
|
|
|
31
30
|
return _env;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
const
|
|
33
|
+
// Use the WHATWG URL API instead of the deprecated `url.parse()` (DEP0169).
|
|
34
|
+
const apiPathURL = new URL(config.settings.apiPath);
|
|
35
|
+
const proxyURL = new URL(config.settings.devProxyToApiPath);
|
|
36
36
|
const serverURL = `${proxyURL.protocol}//${proxyURL.host}`;
|
|
37
37
|
const instancePath = proxyURL.pathname;
|
|
38
38
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import superagent from 'superagent';
|
|
7
7
|
import config from '@plone/volto/registry';
|
|
8
8
|
import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
9
|
-
import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
9
|
+
import { getApiSuffix, stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Get a resource image/file with authenticated (if token exist) API headers
|
|
@@ -17,7 +17,7 @@ import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
|
17
17
|
export const getAPIResourceWithAuth = (req) =>
|
|
18
18
|
new Promise((resolve, reject) => {
|
|
19
19
|
const { settings } = config;
|
|
20
|
-
const apiSuffix =
|
|
20
|
+
const apiSuffix = getApiSuffix();
|
|
21
21
|
let apiPath = '';
|
|
22
22
|
|
|
23
23
|
if (settings.internalApiPath && __SERVER__) {
|
package/src/helpers/Api/Api.js
CHANGED
|
@@ -8,6 +8,7 @@ import Cookies from 'universal-cookie';
|
|
|
8
8
|
import config from '@plone/volto/registry';
|
|
9
9
|
import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
10
10
|
import {
|
|
11
|
+
getApiSuffix,
|
|
11
12
|
stripQuerystring,
|
|
12
13
|
stripSubpathPrefix,
|
|
13
14
|
} from '@plone/volto/helpers/Url/Url';
|
|
@@ -22,7 +23,7 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
|
|
|
22
23
|
*/
|
|
23
24
|
export function formatUrl(path) {
|
|
24
25
|
const { settings } = config;
|
|
25
|
-
const apiSuffix =
|
|
26
|
+
const apiSuffix = getApiSuffix();
|
|
26
27
|
|
|
27
28
|
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
|
28
29
|
|
|
@@ -15,6 +15,7 @@ vi.mock('superagent', () => ({
|
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
17
|
beforeAll(() => {
|
|
18
|
+
config.settings.apiSuffix = undefined;
|
|
18
19
|
config.settings.legacyTraverse = false;
|
|
19
20
|
});
|
|
20
21
|
|
|
@@ -41,4 +42,10 @@ describe('Api', () => {
|
|
|
41
42
|
const promise = api.get('https://example.com');
|
|
42
43
|
expect(promise.request.url).toBe('https://example.com');
|
|
43
44
|
});
|
|
45
|
+
it('uses the configured API suffix', () => {
|
|
46
|
+
config.settings.apiSuffix = '/custom-api';
|
|
47
|
+
const promise = api.get('/test');
|
|
48
|
+
expect(promise.request.url).toBe(`${settings.apiPath}/custom-api/test`);
|
|
49
|
+
config.settings.apiSuffix = undefined;
|
|
50
|
+
});
|
|
44
51
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import superagent from 'superagent';
|
|
2
2
|
import config from '@plone/volto/registry';
|
|
3
3
|
import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
4
|
-
import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
4
|
+
import { getApiSuffix, stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Get a resource from Plone Backend (/++api++) with authenticated (if token exist) API headers
|
|
@@ -12,7 +12,7 @@ import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
|
12
12
|
export const getPloneBackendAPIResourceWithAuth = (req) =>
|
|
13
13
|
new Promise((resolve, reject) => {
|
|
14
14
|
const { settings } = config;
|
|
15
|
-
const apiSuffix =
|
|
15
|
+
const apiSuffix = getApiSuffix();
|
|
16
16
|
let apiPath = '';
|
|
17
17
|
|
|
18
18
|
if (settings.internalApiPath && __SERVER__) {
|
|
@@ -30,6 +30,7 @@ describe('getPloneBackendAPIResourceWithAuth', () => {
|
|
|
30
30
|
apiPath: 'http://localhost:3000',
|
|
31
31
|
devProxyToApiPath: 'http://localhost:8080/Plone',
|
|
32
32
|
internalApiPath: undefined,
|
|
33
|
+
apiSuffix: undefined,
|
|
33
34
|
});
|
|
34
35
|
});
|
|
35
36
|
|
|
@@ -57,4 +58,15 @@ describe('getPloneBackendAPIResourceWithAuth', () => {
|
|
|
57
58
|
'http://localhost:8080/Plone/@portrait/admin',
|
|
58
59
|
);
|
|
59
60
|
});
|
|
61
|
+
|
|
62
|
+
it('uses apiSuffix in preference to legacyTraverse in development', async () => {
|
|
63
|
+
config.settings.legacyTraverse = true;
|
|
64
|
+
config.settings.apiSuffix = '/custom-api';
|
|
65
|
+
|
|
66
|
+
await getPloneBackendAPIResourceWithAuth(request);
|
|
67
|
+
|
|
68
|
+
expect(superagent.get).toHaveBeenCalledWith(
|
|
69
|
+
'http://localhost:8080/Plone/custom-api/@portrait/admin',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
60
72
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import superagent from 'superagent';
|
|
7
7
|
import map from 'lodash/map';
|
|
8
8
|
import zlib from 'zlib';
|
|
9
|
-
import { toPublicURL } from '@plone/volto/helpers/Url/Url';
|
|
9
|
+
import { getApiSuffix, toPublicURL } from '@plone/volto/helpers/Url/Url';
|
|
10
10
|
import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
11
11
|
|
|
12
12
|
import config from '@plone/volto/registry';
|
|
@@ -22,7 +22,7 @@ export const SITEMAP_BATCH_SIZE = 5000;
|
|
|
22
22
|
export const generateSitemap = (_req, start = 0, size = undefined) =>
|
|
23
23
|
new Promise((resolve) => {
|
|
24
24
|
const { settings } = config;
|
|
25
|
-
const apiSuffix =
|
|
25
|
+
const apiSuffix = getApiSuffix();
|
|
26
26
|
const apiPath = settings.internalApiPath ?? settings.apiPath;
|
|
27
27
|
const request = superagent.get(
|
|
28
28
|
`${apiPath}${apiSuffix}/@search?metadata_fields=modified&b_start=${start}&b_size=${
|
|
@@ -64,7 +64,7 @@ export const generateSitemap = (_req, start = 0, size = undefined) =>
|
|
|
64
64
|
export const generateSitemapIndex = (_req, gzip = false) =>
|
|
65
65
|
new Promise((resolve) => {
|
|
66
66
|
const { settings } = config;
|
|
67
|
-
const apiSuffix =
|
|
67
|
+
const apiSuffix = getApiSuffix();
|
|
68
68
|
const apiPath = settings.internalApiPath ?? settings.apiPath;
|
|
69
69
|
const request = superagent.get(
|
|
70
70
|
`${apiPath}${apiSuffix}/@search?metadata_fields=modified&b_size=0&use_site_search_settings=1`,
|
package/src/helpers/Url/Url.js
CHANGED
|
@@ -182,6 +182,17 @@ export function addAppURL(url) {
|
|
|
182
182
|
: `${settings.apiPath}${url}`;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Get the API traversal suffix.
|
|
187
|
+
*
|
|
188
|
+
* An explicitly configured apiSuffix takes precedence over legacyTraverse.
|
|
189
|
+
* @returns {string} API traversal suffix.
|
|
190
|
+
*/
|
|
191
|
+
export function getApiSuffix() {
|
|
192
|
+
const { settings } = config;
|
|
193
|
+
return settings.apiSuffix ?? (settings.legacyTraverse ? '' : '/++api++');
|
|
194
|
+
}
|
|
195
|
+
|
|
185
196
|
/**
|
|
186
197
|
* Given a URL expands it to the backend URL
|
|
187
198
|
* Useful when you have to actually call the backend from the
|
|
@@ -193,7 +204,7 @@ export function addAppURL(url) {
|
|
|
193
204
|
*/
|
|
194
205
|
export function expandToBackendURL(path) {
|
|
195
206
|
const { settings } = config;
|
|
196
|
-
const apiSuffix =
|
|
207
|
+
const apiSuffix = getApiSuffix();
|
|
197
208
|
let adjustedPath;
|
|
198
209
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
199
210
|
// flattenToAppURL first if we get a full URL
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './Url';
|
|
25
25
|
|
|
26
26
|
beforeEach(() => {
|
|
27
|
+
config.settings.apiSuffix = undefined;
|
|
27
28
|
config.settings.legacyTraverse = false;
|
|
28
29
|
});
|
|
29
30
|
|
|
@@ -372,6 +373,20 @@ describe('Url', () => {
|
|
|
372
373
|
const href = `https://plone.org/api/ca/my-page`;
|
|
373
374
|
expect(expandToBackendURL(href)).toBe('https://plone.org/api/ca/my-page');
|
|
374
375
|
});
|
|
376
|
+
it('uses apiSuffix in preference to legacyTraverse', () => {
|
|
377
|
+
settings.apiPath = 'https://plone.org/api';
|
|
378
|
+
settings.legacyTraverse = true;
|
|
379
|
+
settings.apiSuffix = '/custom-api';
|
|
380
|
+
const href = `https://plone.org/api/ca/my-page`;
|
|
381
|
+
expect(expandToBackendURL(href)).toBe(
|
|
382
|
+
'https://plone.org/api/custom-api/ca/my-page',
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
it('allows apiSuffix to disable the suffix', () => {
|
|
386
|
+
settings.apiSuffix = '';
|
|
387
|
+
const href = `/ca/my-page`;
|
|
388
|
+
expect(expandToBackendURL(href)).toBe('https://plone.org/api/ca/my-page');
|
|
389
|
+
});
|
|
375
390
|
it('expandToBackendURL test full URL - deployed seamless', () => {
|
|
376
391
|
settings.apiPath = 'https://plone.org';
|
|
377
392
|
const href = `https://plone.org/ca/my-page`;
|
package/src/server.jsx
CHANGED
|
@@ -7,7 +7,6 @@ import { Provider } from 'react-intl-redux';
|
|
|
7
7
|
import express from 'express';
|
|
8
8
|
import { renderToString } from 'react-dom/server';
|
|
9
9
|
import { createMemoryHistory } from 'history';
|
|
10
|
-
import { parse as parseUrl } from 'url';
|
|
11
10
|
import keys from 'lodash/keys';
|
|
12
11
|
import locale from 'locale';
|
|
13
12
|
import { detect } from 'detect-browser';
|
|
@@ -247,7 +246,25 @@ server.get('/*', (req, res) => {
|
|
|
247
246
|
});
|
|
248
247
|
|
|
249
248
|
const url = req.originalUrl || req.url;
|
|
250
|
-
|
|
249
|
+
// Parse the request URL without the deprecated `url.parse()` (DEP0169).
|
|
250
|
+
// `req.url` is always an origin-form path here, so a plain string split
|
|
251
|
+
// reproduces the exact `url.parse()` shape (including raw, non-encoded
|
|
252
|
+
// query strings) that downstream async-connected components rely on.
|
|
253
|
+
const hashIndex = url.indexOf('#');
|
|
254
|
+
const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
255
|
+
const hash = hashIndex === -1 ? null : url.slice(hashIndex);
|
|
256
|
+
const queryIndex = withoutHash.indexOf('?');
|
|
257
|
+
const pathname =
|
|
258
|
+
queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
|
|
259
|
+
const search = queryIndex === -1 ? null : withoutHash.slice(queryIndex);
|
|
260
|
+
const location = {
|
|
261
|
+
pathname,
|
|
262
|
+
search,
|
|
263
|
+
query: search ? search.slice(1) : null,
|
|
264
|
+
hash,
|
|
265
|
+
path: withoutHash,
|
|
266
|
+
href: url,
|
|
267
|
+
};
|
|
251
268
|
|
|
252
269
|
loadOnServer({ store, location, routes, api })
|
|
253
270
|
.then(() => {
|
package/src/start-client.jsx
CHANGED
|
@@ -77,6 +77,9 @@ export default function client() {
|
|
|
77
77
|
if (window.env.RAZZLE_INTERNAL_API_PATH) {
|
|
78
78
|
config.settings.internalApiPath = window.env.RAZZLE_INTERNAL_API_PATH;
|
|
79
79
|
}
|
|
80
|
+
if (typeof window.env.RAZZLE_API_SUFFIX !== 'undefined') {
|
|
81
|
+
config.settings.apiSuffix = window.env.RAZZLE_API_SUFFIX;
|
|
82
|
+
}
|
|
80
83
|
// TODO: To be removed when the use of the legacy traverse is deprecated.
|
|
81
84
|
if (window.env.RAZZLE_LEGACY_TRAVERSE) {
|
|
82
85
|
config.settings.legacyTraverse = true;
|
|
@@ -54,6 +54,13 @@ export function flattenHTMLToAppURL(html: string): string;
|
|
|
54
54
|
* @returns {string} New URL with app
|
|
55
55
|
*/
|
|
56
56
|
export function addAppURL(url: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Get the API traversal suffix.
|
|
59
|
+
*
|
|
60
|
+
* An explicitly configured apiSuffix takes precedence over legacyTraverse.
|
|
61
|
+
* @returns {string} API traversal suffix.
|
|
62
|
+
*/
|
|
63
|
+
export function getApiSuffix(): string;
|
|
57
64
|
/**
|
|
58
65
|
* Given a URL expands it to the backend URL
|
|
59
66
|
* Useful when you have to actually call the backend from the
|