@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 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.2.0",
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/components": "4.2.1",
180
- "@plone/scripts": "4.0.1",
178
+ "@plone/registry": "3.0.1",
181
179
  "@plone/volto-slate": "19.0.4",
182
- "@plone/registry": "3.0.1"
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/babel-preset-razzle": "^1.0.1"
293
+ "@plone/types": "2.0.1"
295
294
  },
296
295
  "scripts": {
297
296
  "analyze": "BUNDLE_ANALYZE=true razzle build",
@@ -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
- const apiPathURL = parseUrl(config.settings.apiPath);
35
- const proxyURL = parseUrl(config.settings.devProxyToApiPath);
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 = settings.legacyTraverse ? '' : '/++api++';
20
+ const apiSuffix = getApiSuffix();
21
21
  let apiPath = '';
22
22
 
23
23
  if (settings.internalApiPath && __SERVER__) {
@@ -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 = settings.legacyTraverse ? '' : '/++api++';
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 = settings.legacyTraverse ? '' : '/++api++';
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 = settings.legacyTraverse ? '' : '/++api++';
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 = settings.legacyTraverse ? '' : '/++api++';
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`,
@@ -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 = settings.legacyTraverse ? '' : '/++api++';
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
- const location = parseUrl(url);
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(() => {
@@ -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