diff --git a/docs/source/configuration/environmentvariables.md b/docs/source/configuration/environmentvariables.md index 1c9979a6e84..5801005e865 100644 --- a/docs/source/configuration/environmentvariables.md +++ b/docs/source/configuration/environmentvariables.md @@ -66,6 +66,12 @@ You can also generate builds on your continuous integration, then deploy them an ```{glossary} :sorted: +`RAZZLE_API_SUFFIX` + Overrides the API traversal suffix that Volto appends to API URLs. + The default suffix is `/++api++`. + Set this variable to use a different suffix, or set `config.settings.apiSuffix` directly in your Volto configuration. + This setting takes precedence over `RAZZLE_LEGACY_TRAVERSE`. + `RAZZLE_LEGACY_TRAVERSE` If `true`, Volto will construct API URLs without the `/++api++` prefix. diff --git a/packages/types/src/config/Settings.d.ts b/packages/types/src/config/Settings.d.ts index 38098f1c310..74826253519 100644 --- a/packages/types/src/config/Settings.d.ts +++ b/packages/types/src/config/Settings.d.ts @@ -56,6 +56,7 @@ export interface SettingsConfig { actions_raising_api_errors: string[]; internalApiPath: string | undefined; websockets: string | false; + apiSuffix: string | undefined; legacyTraverse: string | false; cookieExpires: number; nonContentRoutes: Array; diff --git a/packages/volto/news/+configurable-api-suffix.feature b/packages/volto/news/+configurable-api-suffix.feature new file mode 100644 index 00000000000..81bc9fc2dca --- /dev/null +++ b/packages/volto/news/+configurable-api-suffix.feature @@ -0,0 +1 @@ +Add the `apiSuffix` setting and `RAZZLE_API_SUFFIX` environment variable to configure the API traversal suffix. diff --git a/packages/volto/src/config/index.js b/packages/volto/src/config/index.js index 3fce081f201..b6ec67a6c40 100644 --- a/packages/volto/src/config/index.js +++ b/packages/volto/src/config/index.js @@ -102,6 +102,8 @@ let config = { internalApiPath: process.env.RAZZLE_INTERNAL_API_PATH || undefined, subpathPrefix: process.env.RAZZLE_SUBPATH_PREFIX || '', websockets: process.env.RAZZLE_WEBSOCKETS || false, + // Overrides the API traversal suffix. An empty string disables it. + apiSuffix: process.env.RAZZLE_API_SUFFIX, // TODO: legacyTraverse to be removed when the use of the legacy traverse is deprecated. legacyTraverse: process.env.RAZZLE_LEGACY_TRAVERSE || false, cookieExpires: 15552000, //in seconds. Default is 6 month (15552000) diff --git a/packages/volto/src/helpers/Api/APIResourceWithAuth.js b/packages/volto/src/helpers/Api/APIResourceWithAuth.js index 666e1c2804d..49ff61b0780 100644 --- a/packages/volto/src/helpers/Api/APIResourceWithAuth.js +++ b/packages/volto/src/helpers/Api/APIResourceWithAuth.js @@ -6,7 +6,7 @@ import superagent from 'superagent'; import config from '@plone/volto/registry'; import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy'; -import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url'; +import { getApiSuffix, stripSubpathPrefix } from '@plone/volto/helpers/Url/Url'; /** * Get a resource image/file with authenticated (if token exist) API headers @@ -17,7 +17,7 @@ import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url'; export const getAPIResourceWithAuth = (req) => new Promise((resolve, reject) => { const { settings } = config; - const apiSuffix = settings.legacyTraverse ? '' : '/++api++'; + const apiSuffix = getApiSuffix(); let apiPath = ''; if (settings.internalApiPath && __SERVER__) { diff --git a/packages/volto/src/helpers/Api/Api.js b/packages/volto/src/helpers/Api/Api.js index 1741cb1fe81..9bd53f8b9a7 100644 --- a/packages/volto/src/helpers/Api/Api.js +++ b/packages/volto/src/helpers/Api/Api.js @@ -8,6 +8,7 @@ import Cookies from 'universal-cookie'; import config from '@plone/volto/registry'; import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy'; import { + getApiSuffix, stripQuerystring, stripSubpathPrefix, } from '@plone/volto/helpers/Url/Url'; @@ -22,7 +23,7 @@ const methods = ['get', 'post', 'put', 'patch', 'del']; */ export function formatUrl(path) { const { settings } = config; - const apiSuffix = settings.legacyTraverse ? '' : '/++api++'; + const apiSuffix = getApiSuffix(); if (path.startsWith('http://') || path.startsWith('https://')) return path; diff --git a/packages/volto/src/helpers/Api/Api.plone.rest.test.js b/packages/volto/src/helpers/Api/Api.plone.rest.test.js index 13ea2dfd454..d0330ed3bf8 100644 --- a/packages/volto/src/helpers/Api/Api.plone.rest.test.js +++ b/packages/volto/src/helpers/Api/Api.plone.rest.test.js @@ -15,6 +15,7 @@ vi.mock('superagent', () => ({ })); beforeAll(() => { + config.settings.apiSuffix = undefined; config.settings.legacyTraverse = false; }); @@ -41,4 +42,10 @@ describe('Api', () => { const promise = api.get('https://example.com'); expect(promise.request.url).toBe('https://example.com'); }); + it('uses the configured API suffix', () => { + config.settings.apiSuffix = '/custom-api'; + const promise = api.get('/test'); + expect(promise.request.url).toBe(`${settings.apiPath}/custom-api/test`); + config.settings.apiSuffix = undefined; + }); }); diff --git a/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.js b/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.js index f16b4265534..b6e11d42200 100644 --- a/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.js +++ b/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.js @@ -1,7 +1,7 @@ import superagent from 'superagent'; import config from '@plone/volto/registry'; import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy'; -import { stripSubpathPrefix } from '@plone/volto/helpers/Url/Url'; +import { getApiSuffix, stripSubpathPrefix } from '@plone/volto/helpers/Url/Url'; /** * 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'; export const getPloneBackendAPIResourceWithAuth = (req) => new Promise((resolve, reject) => { const { settings } = config; - const apiSuffix = settings.legacyTraverse ? '' : '/++api++'; + const apiSuffix = getApiSuffix(); let apiPath = ''; if (settings.internalApiPath && __SERVER__) { diff --git a/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.test.js b/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.test.js index 25de85ffbe8..49d6eb650f2 100644 --- a/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.test.js +++ b/packages/volto/src/helpers/Api/PloneBackendAPIResourceWithAuth.test.js @@ -30,6 +30,7 @@ describe('getPloneBackendAPIResourceWithAuth', () => { apiPath: 'http://localhost:3000', devProxyToApiPath: 'http://localhost:8080/Plone', internalApiPath: undefined, + apiSuffix: undefined, }); }); @@ -57,4 +58,15 @@ describe('getPloneBackendAPIResourceWithAuth', () => { 'http://localhost:8080/Plone/@portrait/admin', ); }); + + it('uses apiSuffix in preference to legacyTraverse in development', async () => { + config.settings.legacyTraverse = true; + config.settings.apiSuffix = '/custom-api'; + + await getPloneBackendAPIResourceWithAuth(request); + + expect(superagent.get).toHaveBeenCalledWith( + 'http://localhost:8080/Plone/custom-api/@portrait/admin', + ); + }); }); diff --git a/packages/volto/src/helpers/Sitemap/Sitemap.js b/packages/volto/src/helpers/Sitemap/Sitemap.js index b3946e3f111..834a33d03ae 100644 --- a/packages/volto/src/helpers/Sitemap/Sitemap.js +++ b/packages/volto/src/helpers/Sitemap/Sitemap.js @@ -6,7 +6,7 @@ import superagent from 'superagent'; import map from 'lodash/map'; import zlib from 'zlib'; -import { toPublicURL } from '@plone/volto/helpers/Url/Url'; +import { getApiSuffix, toPublicURL } from '@plone/volto/helpers/Url/Url'; import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy'; import config from '@plone/volto/registry'; @@ -22,7 +22,7 @@ export const SITEMAP_BATCH_SIZE = 5000; export const generateSitemap = (_req, start = 0, size = undefined) => new Promise((resolve) => { const { settings } = config; - const apiSuffix = settings.legacyTraverse ? '' : '/++api++'; + const apiSuffix = getApiSuffix(); const apiPath = settings.internalApiPath ?? settings.apiPath; const request = superagent.get( `${apiPath}${apiSuffix}/@search?metadata_fields=modified&b_start=${start}&b_size=${ @@ -64,7 +64,7 @@ export const generateSitemap = (_req, start = 0, size = undefined) => export const generateSitemapIndex = (_req, gzip = false) => new Promise((resolve) => { const { settings } = config; - const apiSuffix = settings.legacyTraverse ? '' : '/++api++'; + const apiSuffix = getApiSuffix(); const apiPath = settings.internalApiPath ?? settings.apiPath; const request = superagent.get( `${apiPath}${apiSuffix}/@search?metadata_fields=modified&b_size=0&use_site_search_settings=1`, diff --git a/packages/volto/src/helpers/Url/Url.js b/packages/volto/src/helpers/Url/Url.js index a4358771edd..a9b7bde63b2 100644 --- a/packages/volto/src/helpers/Url/Url.js +++ b/packages/volto/src/helpers/Url/Url.js @@ -182,6 +182,17 @@ export function addAppURL(url) { : `${settings.apiPath}${url}`; } +/** + * Get the API traversal suffix. + * + * An explicitly configured apiSuffix takes precedence over legacyTraverse. + * @returns {string} API traversal suffix. + */ +export function getApiSuffix() { + const { settings } = config; + return settings.apiSuffix ?? (settings.legacyTraverse ? '' : '/++api++'); +} + /** * Given a URL expands it to the backend URL * Useful when you have to actually call the backend from the @@ -193,7 +204,7 @@ export function addAppURL(url) { */ export function expandToBackendURL(path) { const { settings } = config; - const apiSuffix = settings.legacyTraverse ? '' : '/++api++'; + const apiSuffix = getApiSuffix(); let adjustedPath; if (path.startsWith('http://') || path.startsWith('https://')) { // flattenToAppURL first if we get a full URL diff --git a/packages/volto/src/helpers/Url/Url.test.js b/packages/volto/src/helpers/Url/Url.test.js index 290c96276a5..c93eab967ac 100644 --- a/packages/volto/src/helpers/Url/Url.test.js +++ b/packages/volto/src/helpers/Url/Url.test.js @@ -24,6 +24,7 @@ import { } from './Url'; beforeEach(() => { + config.settings.apiSuffix = undefined; config.settings.legacyTraverse = false; }); @@ -372,6 +373,20 @@ describe('Url', () => { const href = `https://plone.org/api/ca/my-page`; expect(expandToBackendURL(href)).toBe('https://plone.org/api/ca/my-page'); }); + it('uses apiSuffix in preference to legacyTraverse', () => { + settings.apiPath = 'https://plone.org/api'; + settings.legacyTraverse = true; + settings.apiSuffix = '/custom-api'; + const href = `https://plone.org/api/ca/my-page`; + expect(expandToBackendURL(href)).toBe( + 'https://plone.org/api/custom-api/ca/my-page', + ); + }); + it('allows apiSuffix to disable the suffix', () => { + settings.apiSuffix = ''; + const href = `/ca/my-page`; + expect(expandToBackendURL(href)).toBe('https://plone.org/api/ca/my-page'); + }); it('expandToBackendURL test full URL - deployed seamless', () => { settings.apiPath = 'https://plone.org'; const href = `https://plone.org/ca/my-page`; diff --git a/packages/volto/src/start-client.jsx b/packages/volto/src/start-client.jsx index a7b05a19bcc..8e40efffbeb 100644 --- a/packages/volto/src/start-client.jsx +++ b/packages/volto/src/start-client.jsx @@ -77,6 +77,9 @@ export default function client() { if (window.env.RAZZLE_INTERNAL_API_PATH) { config.settings.internalApiPath = window.env.RAZZLE_INTERNAL_API_PATH; } + if (typeof window.env.RAZZLE_API_SUFFIX !== 'undefined') { + config.settings.apiSuffix = window.env.RAZZLE_API_SUFFIX; + } // TODO: To be removed when the use of the legacy traverse is deprecated. if (window.env.RAZZLE_LEGACY_TRAVERSE) { config.settings.legacyTraverse = true;