@plone/volto 19.0.0-alpha.8 → 19.0.0-alpha.9
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/cypress/docker/prefixed-rules.yml +26 -0
- package/cypress/docker/prefixed.yml +24 -0
- package/locales/ca/LC_MESSAGES/volto.po +1 -1
- package/locales/de/LC_MESSAGES/volto.po +1 -1
- package/locales/en/LC_MESSAGES/volto.po +1 -1
- package/locales/es/LC_MESSAGES/volto.po +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +1 -1
- package/locales/it/LC_MESSAGES/volto.po +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +1 -1
- package/locales/ru/LC_MESSAGES/volto.po +1 -1
- package/locales/volto.pot +1 -1
- package/locales/zh_CN/LC_MESSAGES/volto.po +1 -1
- package/package.json +7 -7
- package/razzle.config.js +16 -0
- package/src/components/manage/Blocks/Listing/ImageGallery.jsx +6 -4
- package/src/components/manage/Contents/ContentsBreadcrumbs.jsx +3 -3
- package/src/components/theme/Image/Image.jsx +11 -8
- package/src/components/theme/RequestTimeout/RequestTimeout.jsx +1 -1
- package/src/config/index.js +12 -10
- package/src/config/server.js +0 -2
- package/src/express-middleware/devproxy.js +14 -4
- package/src/helpers/Api/APIResourceWithAuth.js +8 -3
- package/src/helpers/Api/Api.js +7 -4
- package/src/helpers/AsyncConnect/ssr.js +4 -1
- package/src/helpers/Html/Html.jsx +13 -4
- package/src/helpers/Sitemap/Sitemap.js +4 -4
- package/src/helpers/Url/Url.js +32 -2
- package/src/helpers/Url/Url.test.js +62 -0
- package/src/server.jsx +40 -8
- package/src/start-client.jsx +9 -2
- package/src/start-server.js +9 -3
- package/types/helpers/Url/Url.d.ts +14 -0
- package/types/start-client.d.ts +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,20 @@ myst:
|
|
|
17
17
|
|
|
18
18
|
<!-- towncrier release notes start -->
|
|
19
19
|
|
|
20
|
+
## 19.0.0-alpha.9 (2025-10-22)
|
|
21
|
+
|
|
22
|
+
### Feature
|
|
23
|
+
|
|
24
|
+
- Add Cypress for a subpath. @wesleybl [#6976](https://github.com/plone/volto/issues/6976)
|
|
25
|
+
- Serve API requests from a subpath. @davisagli [#7326](https://github.com/plone/volto/issues/7326)
|
|
26
|
+
- Add option to serve a Volto site on a subpath using the `RAZZLE_SUBPATH_PREFIX` environment variable. @nileshgulia1, @wesleybl, @davisagli
|
|
27
|
+
|
|
28
|
+
### Internal
|
|
29
|
+
|
|
30
|
+
- Expires the cache if the root `package.json` is changed. @wesleybl [#7536](https://github.com/plone/volto/issues/7536)
|
|
31
|
+
- Move to `dependencies` some bad categorized dependencies in `devDependencies`. @sneridagh
|
|
32
|
+
- Run Cookieplone acceptance test with Node 24. @davisagli
|
|
33
|
+
|
|
20
34
|
## 19.0.0-alpha.8 (2025-10-22)
|
|
21
35
|
|
|
22
36
|
### Internal
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
http:
|
|
2
|
+
routers:
|
|
3
|
+
frontend:
|
|
4
|
+
rule: "Host(`localhost`) && PathPrefix(`/foo`)"
|
|
5
|
+
service: frontend
|
|
6
|
+
backend:
|
|
7
|
+
rule: "Host(`localhost`) && PathPrefix(`/foo/++api++`)"
|
|
8
|
+
service: backend
|
|
9
|
+
middlewares:
|
|
10
|
+
- backend
|
|
11
|
+
|
|
12
|
+
middlewares:
|
|
13
|
+
backend:
|
|
14
|
+
replacePathRegex:
|
|
15
|
+
regex: "^/foo/\\+\\+api\\+\\+($|/.*)"
|
|
16
|
+
replacement: "/VirtualHostBase/http/localhost/plone/++api++/VirtualHostRoot/_vh_foo$1"
|
|
17
|
+
|
|
18
|
+
services:
|
|
19
|
+
frontend:
|
|
20
|
+
loadBalancer:
|
|
21
|
+
servers:
|
|
22
|
+
- url: "http://host.docker.internal:3000"
|
|
23
|
+
backend:
|
|
24
|
+
loadBalancer:
|
|
25
|
+
servers:
|
|
26
|
+
- url: "http://host.docker.internal:55001"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: "3.7"
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
|
|
5
|
+
proxy:
|
|
6
|
+
image: traefik:v2.8
|
|
7
|
+
command:
|
|
8
|
+
- "--api.insecure=true"
|
|
9
|
+
- "--providers.docker=true"
|
|
10
|
+
# - "--providers.docker.exposedbydefault=false"
|
|
11
|
+
- "--providers.file=true"
|
|
12
|
+
- "--providers.file.filename=/etc/traefik/rules.yml"
|
|
13
|
+
- "--entrypoints.web.address=:80"
|
|
14
|
+
- "--api.insecure=true"
|
|
15
|
+
# - "--accesslog=true"
|
|
16
|
+
# - "--log.level=DEBUG"
|
|
17
|
+
ports:
|
|
18
|
+
- 80:80
|
|
19
|
+
- "8888:8080"
|
|
20
|
+
volumes:
|
|
21
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
22
|
+
- ./prefixed-rules.yml:/etc/traefik/rules.yml
|
|
23
|
+
extra_hosts:
|
|
24
|
+
- host.docker.internal:host-gateway
|
|
@@ -2474,7 +2474,7 @@ msgstr ""
|
|
|
2474
2474
|
msgid "No broken relations found."
|
|
2475
2475
|
msgstr ""
|
|
2476
2476
|
|
|
2477
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2477
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2478
2478
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2479
2479
|
msgid "No connection to the server"
|
|
2480
2480
|
msgstr ""
|
|
@@ -2473,7 +2473,7 @@ msgstr "Keine Add-ons installiert."
|
|
|
2473
2473
|
msgid "No broken relations found."
|
|
2474
2474
|
msgstr "Alle Relationen sind OK."
|
|
2475
2475
|
|
|
2476
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2476
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2477
2477
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2478
2478
|
msgid "No connection to the server"
|
|
2479
2479
|
msgstr "Keine Verbindung zum Server"
|
|
@@ -2468,7 +2468,7 @@ msgstr ""
|
|
|
2468
2468
|
msgid "No broken relations found."
|
|
2469
2469
|
msgstr ""
|
|
2470
2470
|
|
|
2471
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2471
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2472
2472
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2473
2473
|
msgid "No connection to the server"
|
|
2474
2474
|
msgstr ""
|
|
@@ -2475,7 +2475,7 @@ msgstr "No se han encontrado complementos"
|
|
|
2475
2475
|
msgid "No broken relations found."
|
|
2476
2476
|
msgstr "No se encontraron relaciones rotas."
|
|
2477
2477
|
|
|
2478
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2478
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2479
2479
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2480
2480
|
msgid "No connection to the server"
|
|
2481
2481
|
msgstr "No hay conexión con el servidor"
|
|
@@ -2475,7 +2475,7 @@ msgstr "Ez da gehigarririk aurkitu"
|
|
|
2475
2475
|
msgid "No broken relations found."
|
|
2476
2476
|
msgstr ""
|
|
2477
2477
|
|
|
2478
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2478
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2479
2479
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2480
2480
|
msgid "No connection to the server"
|
|
2481
2481
|
msgstr "Ez dago konexiorik zerbitzariarekin"
|
|
@@ -2473,7 +2473,7 @@ msgstr "Lisäosia ei löytynyt"
|
|
|
2473
2473
|
msgid "No broken relations found."
|
|
2474
2474
|
msgstr ""
|
|
2475
2475
|
|
|
2476
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2476
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2477
2477
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2478
2478
|
msgid "No connection to the server"
|
|
2479
2479
|
msgstr "Ei yhteyttä palvelimeen"
|
|
@@ -2475,7 +2475,7 @@ msgstr "Aucun module trouvé"
|
|
|
2475
2475
|
msgid "No broken relations found."
|
|
2476
2476
|
msgstr ""
|
|
2477
2477
|
|
|
2478
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2478
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2479
2479
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2480
2480
|
msgid "No connection to the server"
|
|
2481
2481
|
msgstr "Aucune connexion avec le serveur"
|
|
@@ -2468,7 +2468,7 @@ msgstr "कोई ऐड-ऑन नहीं मिला"
|
|
|
2468
2468
|
msgid "No broken relations found."
|
|
2469
2469
|
msgstr "कोई टूटी हुई संबंध नहीं मिला।"
|
|
2470
2470
|
|
|
2471
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2471
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2472
2472
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2473
2473
|
msgid "No connection to the server"
|
|
2474
2474
|
msgstr "सर्वर से कोई कनेक्शन नहीं है"
|
|
@@ -2468,7 +2468,7 @@ msgstr "Nessun addon trovato"
|
|
|
2468
2468
|
msgid "No broken relations found."
|
|
2469
2469
|
msgstr "Nessuna relazione corrotta trovata."
|
|
2470
2470
|
|
|
2471
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2471
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2472
2472
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2473
2473
|
msgid "No connection to the server"
|
|
2474
2474
|
msgstr "Non c'è connessione con il server, a causa di un timeout o di problemi di connessione di rete del tuo dispositivo."
|
|
@@ -2473,7 +2473,7 @@ msgstr ""
|
|
|
2473
2473
|
msgid "No broken relations found."
|
|
2474
2474
|
msgstr ""
|
|
2475
2475
|
|
|
2476
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2476
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2477
2477
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2478
2478
|
msgid "No connection to the server"
|
|
2479
2479
|
msgstr ""
|
|
@@ -2472,7 +2472,7 @@ msgstr "Geen modules gevonden"
|
|
|
2472
2472
|
msgid "No broken relations found."
|
|
2473
2473
|
msgstr "Geen gebroken relaties gevonden."
|
|
2474
2474
|
|
|
2475
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2475
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2476
2476
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2477
2477
|
msgid "No connection to the server"
|
|
2478
2478
|
msgstr "Er is geen verbinding naar de server, wegens een timeout of geen netwerkverbinding."
|
|
@@ -2473,7 +2473,7 @@ msgstr ""
|
|
|
2473
2473
|
msgid "No broken relations found."
|
|
2474
2474
|
msgstr ""
|
|
2475
2475
|
|
|
2476
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2476
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2477
2477
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2478
2478
|
msgid "No connection to the server"
|
|
2479
2479
|
msgstr ""
|
|
@@ -2474,7 +2474,7 @@ msgstr "Nenhum complemento encontrado"
|
|
|
2474
2474
|
msgid "No broken relations found."
|
|
2475
2475
|
msgstr "Nenhum relacionamento rompido foi encontrado"
|
|
2476
2476
|
|
|
2477
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2477
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2478
2478
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2479
2479
|
msgid "No connection to the server"
|
|
2480
2480
|
msgstr "Sem conexão ao servidor"
|
|
@@ -2474,7 +2474,7 @@ msgstr "Nu s-au găsit addons"
|
|
|
2474
2474
|
msgid "No broken relations found."
|
|
2475
2475
|
msgstr "Nu s-au găsit relații rupte."
|
|
2476
2476
|
|
|
2477
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2477
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2478
2478
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2479
2479
|
msgid "No connection to the server"
|
|
2480
2480
|
msgstr "Nu există conexiunea la server, din cauza unui timeout sau a unei conexiuni întrerupte."
|
|
@@ -2473,7 +2473,7 @@ msgstr "Дополнения не найдены"
|
|
|
2473
2473
|
msgid "No broken relations found."
|
|
2474
2474
|
msgstr "Нарушенные связи не выявлены."
|
|
2475
2475
|
|
|
2476
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2476
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2477
2477
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2478
2478
|
msgid "No connection to the server"
|
|
2479
2479
|
msgstr "Нет соединения с сервером"
|
package/locales/volto.pot
CHANGED
|
@@ -2470,7 +2470,7 @@ msgstr ""
|
|
|
2470
2470
|
msgid "No broken relations found."
|
|
2471
2471
|
msgstr ""
|
|
2472
2472
|
|
|
2473
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2473
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2474
2474
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2475
2475
|
msgid "No connection to the server"
|
|
2476
2476
|
msgstr ""
|
|
@@ -2474,7 +2474,7 @@ msgstr "没有找到附件"
|
|
|
2474
2474
|
msgid "No broken relations found."
|
|
2475
2475
|
msgstr ""
|
|
2476
2476
|
|
|
2477
|
-
#. Default: "There is no connection to the server, due to a timeout
|
|
2477
|
+
#. Default: "There is no connection to the server, due to a timeout or no network connection."
|
|
2478
2478
|
#: components/theme/RequestTimeout/RequestTimeout
|
|
2479
2479
|
msgid "No connection to the server"
|
|
2480
2480
|
msgstr "与服务器无连接"
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "19.0.0-alpha.
|
|
12
|
+
"version": "19.0.0-alpha.9",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git@github.com:plone/volto.git"
|
|
@@ -147,6 +147,9 @@
|
|
|
147
147
|
"node": "^22 || ^24"
|
|
148
148
|
},
|
|
149
149
|
"dependencies": {
|
|
150
|
+
"@dnd-kit/core": "6.0.8",
|
|
151
|
+
"@dnd-kit/sortable": "7.0.2",
|
|
152
|
+
"@dnd-kit/utilities": "3.2.2",
|
|
150
153
|
"@loadable/component": "5.14.1",
|
|
151
154
|
"@loadable/server": "5.14.0",
|
|
152
155
|
"@redux-devtools/extension": "^3.3.0",
|
|
@@ -159,6 +162,7 @@
|
|
|
159
162
|
"diff": "3.5.0",
|
|
160
163
|
"express": "4.19.2",
|
|
161
164
|
"filesize": "6",
|
|
165
|
+
"full-icu": "1.4.0",
|
|
162
166
|
"github-slugger": "1.4.0",
|
|
163
167
|
"history": "4.10.1",
|
|
164
168
|
"hoist-non-react-statics": "3.3.2",
|
|
@@ -236,9 +240,9 @@
|
|
|
236
240
|
"url": "^0.11.3",
|
|
237
241
|
"use-deep-compare-effect": "1.8.1",
|
|
238
242
|
"uuid": "^8.3.2",
|
|
239
|
-
"@plone/
|
|
243
|
+
"@plone/scripts": "4.0.0-alpha.3",
|
|
240
244
|
"@plone/volto-slate": "19.0.0-alpha.6",
|
|
241
|
-
"@plone/
|
|
245
|
+
"@plone/registry": "3.0.0-alpha.6"
|
|
242
246
|
},
|
|
243
247
|
"devDependencies": {
|
|
244
248
|
"@babel/core": "^7.0.0",
|
|
@@ -251,9 +255,6 @@
|
|
|
251
255
|
"@babel/plugin-syntax-export-namespace-from": "7.8.3",
|
|
252
256
|
"@babel/runtime": "7.20.6",
|
|
253
257
|
"@babel/types": "7.20.5",
|
|
254
|
-
"@dnd-kit/core": "6.0.8",
|
|
255
|
-
"@dnd-kit/sortable": "7.0.2",
|
|
256
|
-
"@dnd-kit/utilities": "3.2.2",
|
|
257
258
|
"@fiverr/afterbuild-webpack-plugin": "^1.0.0",
|
|
258
259
|
"@jest/globals": "^29.7.0",
|
|
259
260
|
"@loadable/babel-plugin": "5.13.2",
|
|
@@ -314,7 +315,6 @@
|
|
|
314
315
|
"eslint-plugin-prettier": "^5.1.3",
|
|
315
316
|
"eslint-plugin-react": "^7.34.1",
|
|
316
317
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
317
|
-
"full-icu": "1.4.0",
|
|
318
318
|
"html-webpack-plugin": "5.5.0",
|
|
319
319
|
"identity-obj-proxy": "3.0.0",
|
|
320
320
|
"jest": "26.6.3",
|
package/razzle.config.js
CHANGED
|
@@ -414,6 +414,22 @@ const defaultModify = ({
|
|
|
414
414
|
]
|
|
415
415
|
: [];
|
|
416
416
|
|
|
417
|
+
// If Volto is served under a subpath,
|
|
418
|
+
// we have to adjust where Webpack assets are served too.
|
|
419
|
+
const subpathPrefix = process.env.RAZZLE_SUBPATH_PREFIX || '';
|
|
420
|
+
if (subpathPrefix) {
|
|
421
|
+
if (target === 'web' && dev) {
|
|
422
|
+
if (config.devServer.devMiddleware) {
|
|
423
|
+
config.devServer.devMiddleware.publicPath = subpathPrefix;
|
|
424
|
+
} else {
|
|
425
|
+
config.devServer.publicPath += `${subpathPrefix.slice(1)}/`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const publicPath = config.output.publicPath;
|
|
429
|
+
if (publicPath.indexOf(subpathPrefix) === -1) {
|
|
430
|
+
config.output.publicPath = `${publicPath}${subpathPrefix.slice(1)}/`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
417
433
|
return config;
|
|
418
434
|
};
|
|
419
435
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import PropTypes from 'prop-types';
|
|
3
2
|
import loadable from '@loadable/component';
|
|
4
3
|
import 'react-image-gallery/styles/css/image-gallery.css';
|
|
5
4
|
import { Button } from 'semantic-ui-react';
|
|
6
5
|
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
addSubpathPrefix,
|
|
8
|
+
flattenToAppURL,
|
|
9
|
+
} from '@plone/volto/helpers/Url/Url';
|
|
8
10
|
import config from '@plone/volto/registry';
|
|
9
11
|
|
|
10
12
|
import galleryLeftSVG from '@plone/volto/icons/left-key.svg';
|
|
@@ -81,10 +83,10 @@ const ImageGalleryTemplate = ({ items }) => {
|
|
|
81
83
|
);
|
|
82
84
|
const imagesInfo = renderItems.map((item) => {
|
|
83
85
|
return {
|
|
84
|
-
original: `${flattenToAppURL(item['@id'])}/@@images/${
|
|
86
|
+
original: `${addSubpathPrefix(flattenToAppURL(item['@id']))}/@@images/${
|
|
85
87
|
item.image_field
|
|
86
88
|
}/large`,
|
|
87
|
-
thumbnail: `${flattenToAppURL(item['@id'])}/@@images/${
|
|
89
|
+
thumbnail: `${addSubpathPrefix(flattenToAppURL(item['@id']))}/@@images/${
|
|
88
90
|
item.image_field
|
|
89
91
|
}/thumb`,
|
|
90
92
|
};
|
|
@@ -44,11 +44,11 @@ const ContentsBreadcrumbs = (props) => {
|
|
|
44
44
|
</Link>
|
|
45
45
|
<Breadcrumb.Divider />
|
|
46
46
|
<Link
|
|
47
|
-
to={`${navroot['@id']}/contents`}
|
|
47
|
+
to={`${navroot?.['@id']}/contents`}
|
|
48
48
|
className="section"
|
|
49
|
-
title={navroot
|
|
49
|
+
title={navroot?.title}
|
|
50
50
|
>
|
|
51
|
-
{navroot
|
|
51
|
+
{navroot?.title}
|
|
52
52
|
</Link>
|
|
53
53
|
</>
|
|
54
54
|
)}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
2
|
import cx from 'classnames';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
flattenToAppURL,
|
|
5
|
+
flattenScales,
|
|
6
|
+
addSubpathPrefix,
|
|
7
|
+
} from '@plone/volto/helpers/Url/Url';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Image component
|
|
@@ -30,7 +34,7 @@ export default function Image({
|
|
|
30
34
|
attrs.className = cx(className, { responsive }) || undefined;
|
|
31
35
|
|
|
32
36
|
if (!item && src) {
|
|
33
|
-
attrs.src = src;
|
|
37
|
+
attrs.src = addSubpathPrefix(src);
|
|
34
38
|
} else {
|
|
35
39
|
const isFromRealObject = !item.image_scales;
|
|
36
40
|
const imageFieldWithDefault = imageField || item.image_field || 'image';
|
|
@@ -46,9 +50,11 @@ export default function Image({
|
|
|
46
50
|
|
|
47
51
|
const isSvg = image['content-type'] === 'image/svg+xml';
|
|
48
52
|
// In case `base_path` is present (`preview_image_link`) use it as base path
|
|
49
|
-
const basePath =
|
|
53
|
+
const basePath = addSubpathPrefix(
|
|
54
|
+
flattenToAppURL(image.base_path || item['@id']),
|
|
55
|
+
);
|
|
56
|
+
attrs.src = `${basePath}/${image.download}`;
|
|
50
57
|
|
|
51
|
-
attrs.src = `${flattenToAppURL(basePath)}/${image.download}`;
|
|
52
58
|
attrs.width = image.width;
|
|
53
59
|
attrs.height = image.height;
|
|
54
60
|
|
|
@@ -67,10 +73,7 @@ export default function Image({
|
|
|
67
73
|
});
|
|
68
74
|
|
|
69
75
|
attrs.srcSet = sortedScales
|
|
70
|
-
.map(
|
|
71
|
-
(scale) =>
|
|
72
|
-
`${flattenToAppURL(basePath)}/${scale.download} ${scale.width}w`,
|
|
73
|
-
)
|
|
76
|
+
.map((scale) => `${basePath}/${scale.download} ${scale.width}w`)
|
|
74
77
|
.join(', ');
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -29,7 +29,7 @@ const RequestTimeout = () => (
|
|
|
29
29
|
<h1 style={{ textAlign: 'center', lineHeight: '40px' }}>
|
|
30
30
|
<FormattedMessage
|
|
31
31
|
id="No connection to the server"
|
|
32
|
-
defaultMessage="There is no connection to the server, due to a timeout
|
|
32
|
+
defaultMessage="There is no connection to the server, due to a timeout or no network connection."
|
|
33
33
|
/>
|
|
34
34
|
<br />
|
|
35
35
|
<a href={config.settings.apiPath}>{config.settings.apiPath}</a>
|
package/src/config/index.js
CHANGED
|
@@ -30,10 +30,6 @@ import languages from '@plone/volto/constants/Languages.cjs';
|
|
|
30
30
|
const host = process.env.HOST || 'localhost';
|
|
31
31
|
const port = process.env.PORT || '3000';
|
|
32
32
|
|
|
33
|
-
const apiPath =
|
|
34
|
-
process.env.RAZZLE_API_PATH ||
|
|
35
|
-
(__DEVELOPMENT__ ? `http://${host}:${port}` : '');
|
|
36
|
-
|
|
37
33
|
const getServerURL = (url) => {
|
|
38
34
|
if (!url) return;
|
|
39
35
|
const apiPathURL = parseUrl(url);
|
|
@@ -46,12 +42,14 @@ const getServerURL = (url) => {
|
|
|
46
42
|
// if RAZZLE_PUBLIC_URL is present, use it
|
|
47
43
|
// if in DEV, use the host/port combination by default
|
|
48
44
|
// if in PROD, assume it's RAZZLE_API_PATH server name (no /api or alikes) or fallback
|
|
49
|
-
// to DEV settings if RAZZLE_API_PATH is not present
|
|
45
|
+
// to DEV settings if RAZZLE_API_PATH is not present.
|
|
46
|
+
// Finally, add the subpath, if there is one.
|
|
50
47
|
const publicURL =
|
|
51
|
-
process.env.RAZZLE_PUBLIC_URL ||
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
(process.env.RAZZLE_PUBLIC_URL ||
|
|
49
|
+
(__DEVELOPMENT__
|
|
50
|
+
? `http://${host}:${port}`
|
|
51
|
+
: getServerURL(process.env.RAZZLE_API_PATH) ||
|
|
52
|
+
`http://${host}:${port}`)) + (process.env.RAZZLE_SUBPATH_PREFIX || '');
|
|
55
53
|
|
|
56
54
|
const serverConfig =
|
|
57
55
|
typeof __SERVER__ !== 'undefined' && __SERVER__
|
|
@@ -65,7 +63,10 @@ let config = {
|
|
|
65
63
|
// The URL Volto is going to be served (see sensible defaults above)
|
|
66
64
|
publicURL,
|
|
67
65
|
okRoute: '/ok',
|
|
68
|
-
|
|
66
|
+
// Base URL for API requests from the browser.
|
|
67
|
+
// If not explicitly set, use the publicURL (seamless mode) --
|
|
68
|
+
// but in that case it will be updated in server.jsx for each request.
|
|
69
|
+
apiPath: process.env.RAZZLE_API_PATH || publicURL,
|
|
69
70
|
apiExpanders: [
|
|
70
71
|
// Added here for documentation purposes, added at the end because it
|
|
71
72
|
// depends on a value of this object.
|
|
@@ -99,6 +100,7 @@ let config = {
|
|
|
99
100
|
// apiPath: process.env.RAZZLE_API_PATH || 'http://localhost:8081/db/web', // for guillotina
|
|
100
101
|
actions_raising_api_errors: ['GET_CONTENT', 'UPDATE_CONTENT'],
|
|
101
102
|
internalApiPath: process.env.RAZZLE_INTERNAL_API_PATH || undefined,
|
|
103
|
+
subpathPrefix: process.env.RAZZLE_SUBPATH_PREFIX || '',
|
|
102
104
|
websockets: process.env.RAZZLE_WEBSOCKETS || false,
|
|
103
105
|
// TODO: legacyTraverse to be removed when the use of the legacy traverse is deprecated.
|
|
104
106
|
legacyTraverse: process.env.RAZZLE_LEGACY_TRAVERSE || false,
|
package/src/config/server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import imagesMiddleware from '@plone/volto/express-middleware/images';
|
|
2
2
|
import filesMiddleware from '@plone/volto/express-middleware/files';
|
|
3
3
|
import robotstxtMiddleware from '@plone/volto/express-middleware/robotstxt';
|
|
4
|
-
import okMiddleware from '@plone/volto/express-middleware/ok';
|
|
5
4
|
import sitemapMiddleware from '@plone/volto/express-middleware/sitemap';
|
|
6
5
|
import staticsMiddleware from '@plone/volto/express-middleware/static';
|
|
7
6
|
import devProxyMiddleware from '@plone/volto/express-middleware/devproxy';
|
|
@@ -12,7 +11,6 @@ const settings = {
|
|
|
12
11
|
filesMiddleware(),
|
|
13
12
|
imagesMiddleware(),
|
|
14
13
|
robotstxtMiddleware(),
|
|
15
|
-
okMiddleware(),
|
|
16
14
|
sitemapMiddleware(),
|
|
17
15
|
staticsMiddleware(),
|
|
18
16
|
],
|
|
@@ -13,7 +13,10 @@ const filter = function (pathname, req) {
|
|
|
13
13
|
// Check if pathname is defined, there are some corner cases that pathname is null
|
|
14
14
|
if (pathname) {
|
|
15
15
|
// This is the proxy to the API in case the accept header is 'application/json'
|
|
16
|
-
return
|
|
16
|
+
return (
|
|
17
|
+
config.settings.devProxyToApiPath &&
|
|
18
|
+
pathname.startsWith(`${config.settings.subpathPrefix}/++api++`)
|
|
19
|
+
);
|
|
17
20
|
} else {
|
|
18
21
|
return false;
|
|
19
22
|
}
|
|
@@ -78,17 +81,24 @@ export default function devProxyMiddleware() {
|
|
|
78
81
|
},
|
|
79
82
|
pathRewrite: (path, req) => {
|
|
80
83
|
const { apiPathURL, instancePath } = getEnv();
|
|
84
|
+
const vhSubpath = config.settings.subpathPrefix
|
|
85
|
+
? config.settings.subpathPrefix
|
|
86
|
+
.split('/')
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.map((part) => '/_vh_' + part)
|
|
89
|
+
.join('')
|
|
90
|
+
: '';
|
|
81
91
|
const target =
|
|
82
92
|
config.settings.proxyRewriteTarget ||
|
|
83
93
|
`/VirtualHostBase/${apiPathURL.protocol.slice(0, -1)}/${
|
|
84
94
|
apiPathURL.hostname
|
|
85
|
-
}:${apiPathURL.port}${instancePath}/++api++/VirtualHostRoot`;
|
|
95
|
+
}:${apiPathURL.port}${instancePath}/++api++/VirtualHostRoot${vhSubpath}`;
|
|
86
96
|
|
|
87
|
-
return `${target}${path.replace(
|
|
97
|
+
return `${target}${path.replace(`${config.settings.subpathPrefix}/++api++`, '')}`;
|
|
88
98
|
},
|
|
99
|
+
changeOrigin: true,
|
|
89
100
|
logLevel: process.env.DEBUG_HPM ? 'debug' : 'silent',
|
|
90
101
|
...(process.env.RAZZLE_DEV_PROXY_INSECURE && {
|
|
91
|
-
changeOrigin: true,
|
|
92
102
|
secure: false,
|
|
93
103
|
}),
|
|
94
104
|
});
|
|
@@ -6,6 +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
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Get a resource image/file with authenticated (if token exist) API headers
|
|
@@ -16,9 +17,9 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
|
16
17
|
export const getAPIResourceWithAuth = (req) =>
|
|
17
18
|
new Promise((resolve, reject) => {
|
|
18
19
|
const { settings } = config;
|
|
19
|
-
const
|
|
20
|
-
|
|
20
|
+
const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
|
|
21
21
|
let apiPath = '';
|
|
22
|
+
|
|
22
23
|
if (settings.internalApiPath && __SERVER__) {
|
|
23
24
|
apiPath = settings.internalApiPath;
|
|
24
25
|
} else if (__DEVELOPMENT__ && settings.devProxyToApiPath) {
|
|
@@ -26,8 +27,12 @@ export const getAPIResourceWithAuth = (req) =>
|
|
|
26
27
|
} else {
|
|
27
28
|
apiPath = settings.apiPath;
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
//strip subpath if any
|
|
32
|
+
const contentPath = stripSubpathPrefix(req.path);
|
|
33
|
+
|
|
29
34
|
const request = superagent
|
|
30
|
-
.get(`${apiPath}${__DEVELOPMENT__ ? '' :
|
|
35
|
+
.get(`${apiPath}${__DEVELOPMENT__ ? '' : apiSuffix}${contentPath}`)
|
|
31
36
|
.maxResponseSize(settings.maxResponseSize)
|
|
32
37
|
.responseType('blob');
|
|
33
38
|
const authToken = req.universalCookies.get('auth_token');
|
package/src/helpers/Api/Api.js
CHANGED
|
@@ -7,7 +7,10 @@ import superagent from 'superagent';
|
|
|
7
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
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
stripQuerystring,
|
|
12
|
+
stripSubpathPrefix,
|
|
13
|
+
} from '@plone/volto/helpers/Url/Url';
|
|
11
14
|
|
|
12
15
|
const methods = ['get', 'post', 'put', 'patch', 'del'];
|
|
13
16
|
|
|
@@ -19,11 +22,10 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
|
|
|
19
22
|
*/
|
|
20
23
|
export function formatUrl(path) {
|
|
21
24
|
const { settings } = config;
|
|
22
|
-
const
|
|
25
|
+
const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
|
|
23
26
|
|
|
24
27
|
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
|
25
28
|
|
|
26
|
-
const adjustedPath = path[0] !== '/' ? `/${path}` : path;
|
|
27
29
|
let apiPath = '';
|
|
28
30
|
if (settings.internalApiPath && __SERVER__) {
|
|
29
31
|
apiPath = settings.internalApiPath;
|
|
@@ -31,7 +33,8 @@ export function formatUrl(path) {
|
|
|
31
33
|
apiPath = settings.apiPath;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
const contentPath = stripSubpathPrefix(path[0] !== '/' ? `/${path}` : path);
|
|
37
|
+
return `${apiPath}${apiSuffix}${contentPath}`;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { matchRoutes } from 'react-router-config';
|
|
2
2
|
import { mapSeries, isPromise } from './utils';
|
|
3
3
|
import { endGlobalLoad } from '@plone/volto/actions/asyncConnect/asyncConnect';
|
|
4
|
+
import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
4
5
|
|
|
5
6
|
export function filterComponents(branch) {
|
|
6
7
|
return branch.reduce((result, { route, match }) => {
|
|
@@ -18,7 +19,9 @@ export function loadAsyncConnect({
|
|
|
18
19
|
filter = () => true,
|
|
19
20
|
...rest
|
|
20
21
|
}) {
|
|
21
|
-
const layered = filterComponents(
|
|
22
|
+
const layered = filterComponents(
|
|
23
|
+
matchRoutes(routes, stripSubpathPrefix(location.pathname)),
|
|
24
|
+
);
|
|
22
25
|
|
|
23
26
|
if (layered.length === 0) {
|
|
24
27
|
return Promise.resolve();
|
|
@@ -9,6 +9,7 @@ import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
|
9
9
|
import serialize from 'serialize-javascript';
|
|
10
10
|
import join from 'lodash/join';
|
|
11
11
|
import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
|
|
12
|
+
import { addSubpathPrefix } from '@plone/volto/helpers/Url/Url';
|
|
12
13
|
import { runtimeConfig } from '@plone/volto/runtime_config';
|
|
13
14
|
import config from '@plone/volto/registry';
|
|
14
15
|
|
|
@@ -126,14 +127,22 @@ class Html extends Component {
|
|
|
126
127
|
}}
|
|
127
128
|
/>
|
|
128
129
|
|
|
129
|
-
<link
|
|
130
|
-
|
|
130
|
+
<link
|
|
131
|
+
rel="icon"
|
|
132
|
+
href={addSubpathPrefix('/favicon.ico')}
|
|
133
|
+
sizes="any"
|
|
134
|
+
/>
|
|
135
|
+
<link
|
|
136
|
+
rel="icon"
|
|
137
|
+
href={addSubpathPrefix('/icon.svg')}
|
|
138
|
+
type="image/svg+xml"
|
|
139
|
+
/>
|
|
131
140
|
<link
|
|
132
141
|
rel="apple-touch-icon"
|
|
133
142
|
sizes="180x180"
|
|
134
|
-
href=
|
|
143
|
+
href={addSubpathPrefix('/apple-touch-icon.png')}
|
|
135
144
|
/>
|
|
136
|
-
<link rel="manifest" href=
|
|
145
|
+
<link rel="manifest" href={addSubpathPrefix('/site.webmanifest')} />
|
|
137
146
|
<meta name="generator" content="Plone 6 - https://plone.org" />
|
|
138
147
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
139
148
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
@@ -22,10 +22,10 @@ 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
|
|
25
|
+
const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
|
|
26
26
|
const apiPath = settings.internalApiPath ?? settings.apiPath;
|
|
27
27
|
const request = superagent.get(
|
|
28
|
-
`${apiPath}${
|
|
28
|
+
`${apiPath}${apiSuffix}/@search?metadata_fields=modified&b_start=${start}&b_size=${
|
|
29
29
|
size !== undefined ? size : 100000000
|
|
30
30
|
}&use_site_search_settings=1`,
|
|
31
31
|
);
|
|
@@ -64,10 +64,10 @@ 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
|
|
67
|
+
const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
|
|
68
68
|
const apiPath = settings.internalApiPath ?? settings.apiPath;
|
|
69
69
|
const request = superagent.get(
|
|
70
|
-
`${apiPath}${
|
|
70
|
+
`${apiPath}${apiSuffix}/@search?metadata_fields=modified&b_size=0&use_site_search_settings=1`,
|
|
71
71
|
);
|
|
72
72
|
request.set('Accept', 'application/json');
|
|
73
73
|
const authToken = _req.universalCookies.get('auth_token');
|
package/src/helpers/Url/Url.js
CHANGED
|
@@ -193,7 +193,7 @@ export function addAppURL(url) {
|
|
|
193
193
|
*/
|
|
194
194
|
export function expandToBackendURL(path) {
|
|
195
195
|
const { settings } = config;
|
|
196
|
-
const
|
|
196
|
+
const apiSuffix = settings.legacyTraverse ? '' : '/++api++';
|
|
197
197
|
let adjustedPath;
|
|
198
198
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
199
199
|
// flattenToAppURL first if we get a full URL
|
|
@@ -210,7 +210,7 @@ export function expandToBackendURL(path) {
|
|
|
210
210
|
apiPath = settings.apiPath;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
return `${apiPath}${
|
|
213
|
+
return `${apiPath}${apiSuffix}${adjustedPath}`;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/**
|
|
@@ -258,6 +258,36 @@ export function isUrl(url) {
|
|
|
258
258
|
return urlRegex().test(url);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Add subpath path if set in settings
|
|
263
|
+
* @method addSubpathPrefix
|
|
264
|
+
* @param {string} src pathname
|
|
265
|
+
* @returns {string} prefixed subpath pathname
|
|
266
|
+
*/
|
|
267
|
+
export function addSubpathPrefix(src) {
|
|
268
|
+
let url = src;
|
|
269
|
+
const { subpathPrefix } = config.settings;
|
|
270
|
+
if (isInternalURL(src) && subpathPrefix && !src.startsWith(subpathPrefix)) {
|
|
271
|
+
url = subpathPrefix + src; //add subpathPrefix to src if it's an internal url and not a static resource.
|
|
272
|
+
}
|
|
273
|
+
return url;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* strip subpath path particulary from api calls
|
|
278
|
+
* @method stripSubpathPrefix
|
|
279
|
+
* @param {string} src pathname
|
|
280
|
+
* @returns {string} pathname
|
|
281
|
+
*/
|
|
282
|
+
export function stripSubpathPrefix(src) {
|
|
283
|
+
let url = src;
|
|
284
|
+
const { subpathPrefix } = config.settings;
|
|
285
|
+
if (subpathPrefix && src.match(new RegExp(`^${subpathPrefix}(/|$)`))) {
|
|
286
|
+
url = src.slice(subpathPrefix.length);
|
|
287
|
+
}
|
|
288
|
+
return url;
|
|
289
|
+
}
|
|
290
|
+
|
|
261
291
|
/**
|
|
262
292
|
* Get field url
|
|
263
293
|
* @method getFieldURL
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
normaliseMail,
|
|
20
20
|
normalizeTelephone,
|
|
21
21
|
flattenScales,
|
|
22
|
+
addSubpathPrefix,
|
|
23
|
+
stripSubpathPrefix,
|
|
22
24
|
} from './Url';
|
|
23
25
|
|
|
24
26
|
beforeEach(() => {
|
|
@@ -533,4 +535,64 @@ describe('Url', () => {
|
|
|
533
535
|
});
|
|
534
536
|
});
|
|
535
537
|
});
|
|
538
|
+
|
|
539
|
+
describe('Subpath tests', () => {
|
|
540
|
+
beforeEach(() => {
|
|
541
|
+
settings.subpathPrefix = '/site';
|
|
542
|
+
});
|
|
543
|
+
describe('addSubpathPrefix', () => {
|
|
544
|
+
it('adds subpath prefix to internal URLs', () => {
|
|
545
|
+
expect(addSubpathPrefix('/some-page')).toBe('/site/some-page');
|
|
546
|
+
expect(addSubpathPrefix('/news/article')).toBe('/site/news/article');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('does not add subpath prefix to URLs that already have it', () => {
|
|
550
|
+
expect(addSubpathPrefix('/site/some-page')).toBe('/site/some-page');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('does not add subpath prefix to external URLs', () => {
|
|
554
|
+
expect(addSubpathPrefix('https://example.com/page')).toBe(
|
|
555
|
+
'https://example.com/page',
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('handles empty subpath prefix', () => {
|
|
560
|
+
settings.subpathPrefix = '';
|
|
561
|
+
expect(addSubpathPrefix('/some-page')).toBe('/some-page');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('handles undefined subpath prefix', () => {
|
|
565
|
+
settings.subpathPrefix = undefined;
|
|
566
|
+
expect(addSubpathPrefix('/some-page')).toBe('/some-page');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe('stripSubpathPrefix', () => {
|
|
571
|
+
beforeEach(() => {
|
|
572
|
+
settings.subpathPrefix = '/site';
|
|
573
|
+
});
|
|
574
|
+
it('removes subpath prefix from URLs that have it', () => {
|
|
575
|
+
expect(stripSubpathPrefix('/site/some-page')).toBe('/some-page');
|
|
576
|
+
expect(stripSubpathPrefix('/site/news/article')).toBe('/news/article');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('leaves URLs unchanged if they do not have the prefix', () => {
|
|
580
|
+
expect(stripSubpathPrefix('/other/some-page')).toBe('/other/some-page');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('handles the case where URL is exactly the subpath prefix', () => {
|
|
584
|
+
expect(stripSubpathPrefix('/site')).toBe('');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('handles empty subpath prefix', () => {
|
|
588
|
+
settings.subpathPrefix = '';
|
|
589
|
+
expect(stripSubpathPrefix('/some-page')).toBe('/some-page');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('handles undefined subpath prefix', () => {
|
|
593
|
+
settings.subpathPrefix = undefined;
|
|
594
|
+
expect(stripSubpathPrefix('/some-page')).toBe('/some-page');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
});
|
|
536
598
|
});
|
package/src/server.jsx
CHANGED
|
@@ -18,8 +18,9 @@ import { CookiesProvider } from 'react-cookie';
|
|
|
18
18
|
import cookiesMiddleware from 'universal-cookie-express';
|
|
19
19
|
import debug from 'debug';
|
|
20
20
|
|
|
21
|
-
import routes from '@
|
|
21
|
+
import routes from '@plone/volto/routes';
|
|
22
22
|
import config from '@plone/volto/registry';
|
|
23
|
+
import okMiddleware from '@plone/volto/express-middleware/ok';
|
|
23
24
|
|
|
24
25
|
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
25
26
|
import Html from '@plone/volto/helpers/Html/Html';
|
|
@@ -39,7 +40,10 @@ import ErrorPage from '@plone/volto/error';
|
|
|
39
40
|
import languages from '@plone/volto/constants/Languages.cjs';
|
|
40
41
|
|
|
41
42
|
import configureStore from '@plone/volto/store';
|
|
42
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
ReduxAsyncConnect,
|
|
45
|
+
loadOnServer,
|
|
46
|
+
} from '@plone/volto/helpers/AsyncConnect';
|
|
43
47
|
|
|
44
48
|
let locales = {};
|
|
45
49
|
|
|
@@ -69,10 +73,30 @@ const server = express()
|
|
|
69
73
|
})
|
|
70
74
|
.use(cookiesMiddleware());
|
|
71
75
|
|
|
76
|
+
// If Volto is being served under a subpath,
|
|
77
|
+
// make sure the static files are available there too.
|
|
78
|
+
// (The static middleware loads too early to access the config.)
|
|
79
|
+
if (config.settings.subpathPrefix) {
|
|
80
|
+
server.use(
|
|
81
|
+
config.settings.subpathPrefix,
|
|
82
|
+
express.static(
|
|
83
|
+
process.env.BUILD_DIR
|
|
84
|
+
? path.join(process.env.BUILD_DIR, 'public')
|
|
85
|
+
: process.env.RAZZLE_PUBLIC_DIR,
|
|
86
|
+
{
|
|
87
|
+
redirect: false, // Avoid /my-prefix from being redirected to /my-prefix/
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
const middleware = (config.settings.expressMiddleware || []).filter((m) => m);
|
|
73
94
|
|
|
74
95
|
server.all('*', setupServer);
|
|
75
|
-
if (middleware.length)
|
|
96
|
+
if (middleware.length) {
|
|
97
|
+
server.use(config.settings.subpathPrefix || '/', middleware);
|
|
98
|
+
}
|
|
99
|
+
server.use('/', okMiddleware());
|
|
76
100
|
|
|
77
101
|
server.use(function (err, req, res, next) {
|
|
78
102
|
if (err) {
|
|
@@ -160,8 +184,10 @@ function setupServer(req, res, next) {
|
|
|
160
184
|
res.locals.detectedHost = `${
|
|
161
185
|
req.headers['x-forwarded-proto'] || req.protocol
|
|
162
186
|
}://${req.headers.host}`;
|
|
163
|
-
config.settings.apiPath =
|
|
164
|
-
|
|
187
|
+
config.settings.apiPath =
|
|
188
|
+
res.locals.detectedHost + config.settings.subpathPrefix;
|
|
189
|
+
config.settings.publicURL =
|
|
190
|
+
res.locals.detectedHost + config.settings.subpathPrefix;
|
|
165
191
|
}
|
|
166
192
|
|
|
167
193
|
res.locals = {
|
|
@@ -253,7 +279,11 @@ server.get('/*', (req, res) => {
|
|
|
253
279
|
<ChunkExtractorManager extractor={extractor}>
|
|
254
280
|
<CookiesProvider cookies={req.universalCookies}>
|
|
255
281
|
<Provider store={store} onError={reactIntlErrorHandler}>
|
|
256
|
-
<StaticRouter
|
|
282
|
+
<StaticRouter
|
|
283
|
+
context={context}
|
|
284
|
+
location={req.url}
|
|
285
|
+
basename={config.settings.subpathPrefix}
|
|
286
|
+
>
|
|
257
287
|
<ReduxAsyncConnect routes={routes} helpers={api} />
|
|
258
288
|
</StaticRouter>
|
|
259
289
|
</Provider>
|
|
@@ -290,8 +320,8 @@ server.get('/*', (req, res) => {
|
|
|
290
320
|
markup={markup}
|
|
291
321
|
store={store}
|
|
292
322
|
criticalCss={readCriticalCss(req)}
|
|
293
|
-
apiPath={
|
|
294
|
-
publicURL={
|
|
323
|
+
apiPath={config.settings.apiPath}
|
|
324
|
+
publicURL={config.settings.publicURL}
|
|
295
325
|
/>,
|
|
296
326
|
)}
|
|
297
327
|
`,
|
|
@@ -334,6 +364,8 @@ export const defaultReadCriticalCss = () => {
|
|
|
334
364
|
|
|
335
365
|
// Exposed for the console bootstrap info messages
|
|
336
366
|
server.apiPath = config.settings.apiPath;
|
|
367
|
+
server.internalApiPath = config.settings.internalApiPath;
|
|
368
|
+
server.subpathPrefix = config.settings.subpathPrefix;
|
|
337
369
|
server.devProxyToApiPath = config.settings.devProxyToApiPath;
|
|
338
370
|
server.proxyRewriteTarget = config.settings.proxyRewriteTarget;
|
|
339
371
|
server.publicURL = config.settings.publicURL;
|
package/src/start-client.jsx
CHANGED
|
@@ -18,8 +18,6 @@ import Api from '@plone/volto/helpers/Api/Api';
|
|
|
18
18
|
import { persistAuthToken } from '@plone/volto/helpers/AuthToken/AuthToken';
|
|
19
19
|
import ScrollToTop from '@plone/volto/helpers/ScrollToTop/ScrollToTop';
|
|
20
20
|
|
|
21
|
-
export const history = createBrowserHistory();
|
|
22
|
-
|
|
23
21
|
function reactIntlErrorHandler(error) {
|
|
24
22
|
debug('i18n')(error);
|
|
25
23
|
}
|
|
@@ -27,6 +25,15 @@ function reactIntlErrorHandler(error) {
|
|
|
27
25
|
export default function client() {
|
|
28
26
|
const api = new Api();
|
|
29
27
|
|
|
28
|
+
if (window.env.RAZZLE_SUBPATH_PREFIX) {
|
|
29
|
+
config.settings.subpathPrefix = window.env.RAZZLE_SUBPATH_PREFIX;
|
|
30
|
+
}
|
|
31
|
+
const history = createBrowserHistory({
|
|
32
|
+
basename: config.settings.subpathPrefix
|
|
33
|
+
? config.settings.subpathPrefix
|
|
34
|
+
: '/',
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
const store = configureStore(window.__data, history, api);
|
|
31
38
|
persistAuthToken(store);
|
|
32
39
|
|
package/src/start-server.js
CHANGED
|
@@ -18,14 +18,20 @@ export default function server() {
|
|
|
18
18
|
|
|
19
19
|
server
|
|
20
20
|
.listen(port, bind_address, () => {
|
|
21
|
-
console.log(
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
console.log(
|
|
22
|
+
`The Volto server will make API requests to: ${app.internalApiPath || app.apiPath}/++api++`,
|
|
23
|
+
);
|
|
24
|
+
console.log(
|
|
25
|
+
`The Volto client will make API requests to: ${app.apiPath}/++api++`,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (app.devProxyToApiPath) {
|
|
24
29
|
console.log(
|
|
25
30
|
`Proxying API requests from ${app.publicURL}/++api++ to ${
|
|
26
31
|
app.devProxyToApiPath
|
|
27
32
|
}${app.proxyRewriteTarget || ''}`,
|
|
28
33
|
);
|
|
34
|
+
}
|
|
29
35
|
console.log(`🎭 Volto started at ${bind_address}:${port} 🚀`);
|
|
30
36
|
|
|
31
37
|
if (!process.env.RAZZLE_PUBLIC_URL)
|
|
@@ -78,6 +78,20 @@ export function isInternalURL(url: string): boolean;
|
|
|
78
78
|
* @returns {boolean} True if is a valid url
|
|
79
79
|
*/
|
|
80
80
|
export function isUrl(url: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Add subpath path if set in settings
|
|
83
|
+
* @method addSubpathPrefix
|
|
84
|
+
* @param {string} src pathname
|
|
85
|
+
* @returns {string} prefixed subpath pathname
|
|
86
|
+
*/
|
|
87
|
+
export function addSubpathPrefix(src: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* strip subpath path particulary from api calls
|
|
90
|
+
* @method stripSubpathPrefix
|
|
91
|
+
* @param {string} src pathname
|
|
92
|
+
* @returns {string} pathname
|
|
93
|
+
*/
|
|
94
|
+
export function stripSubpathPrefix(src: string): string;
|
|
81
95
|
/**
|
|
82
96
|
* Normalize URL, adds protocol (if required eg. user has not entered the protocol)
|
|
83
97
|
* @method normalizeUrl
|
package/types/start-client.d.ts
CHANGED