Skip to content

Commit 4f12bee

Browse files
authored
refactor: add relative flag to formatAdminURL util (#14925)
Adds a `relative` flag to the `formatAdminURL` util. There are cases where this function can produce a fully qualified URL, like for client-side routing, and other times when it must remain relative, like when matching routes. This flag differentiates these two behaviors declaratively so we don't have to rely on the omission of `serverURL`. ```ts const result = formatAdminURL({ adminRoute: '/admin', basePath: '/v1', path: '/collections/posts', serverURL: 'http://payloadcms.com', relative: true, }) // returns '/v1/admin/collections/posts' ``` Related: #14919 and #14907
1 parent 38c2f89 commit 4f12bee

File tree

4 files changed

+260
-20
lines changed

4 files changed

+260
-20
lines changed

packages/next/src/views/Root/getRouteData.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,12 @@ export const getRouteData = ({
160160
return isPathMatchingRoute({
161161
currentRoute,
162162
exact: true,
163-
path: formatAdminURL({ adminRoute, path: route }),
163+
path: formatAdminURL({
164+
adminRoute,
165+
path: route,
166+
relative: true,
167+
serverURL: config.serverURL,
168+
}),
164169
})
165170
})
166171

packages/next/src/views/Root/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export const RootPage = async ({
6767
const currentRoute = formatAdminURL({
6868
adminRoute,
6969
path: Array.isArray(params.segments) ? `/${params.segments.join('/')}` : null,
70+
relative: true,
71+
serverURL: config.serverURL,
7072
})
7173

7274
const segments = Array.isArray(params.segments) ? params.segments : []
@@ -224,7 +226,12 @@ export const RootPage = async ({
224226
const usersCollection = config.collections.find(({ slug }) => slug === userSlug)
225227
const disableLocalStrategy = usersCollection?.auth?.disableLocalStrategy
226228

227-
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
229+
const createFirstUserRoute = formatAdminURL({
230+
adminRoute,
231+
path: _createFirstUserRoute,
232+
relative: true,
233+
serverURL: config.serverURL,
234+
})
228235

229236
if (disableLocalStrategy && currentRoute === createFirstUserRoute) {
230237
redirect(adminRoute)
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { formatAdminURL } from './formatAdminURL.js'
2+
3+
describe('formatAdminURL', () => {
4+
const serverURL = 'https://example.com'
5+
6+
const defaultAdminRoute = '/admin'
7+
const rootAdminRoute = '/'
8+
9+
const dummyPath = '/collections/posts'
10+
11+
describe('relative URLs', () => {
12+
it('should ignore `serverURL` when relative=true', () => {
13+
const result = formatAdminURL({
14+
adminRoute: defaultAdminRoute,
15+
path: dummyPath,
16+
serverURL,
17+
relative: true,
18+
})
19+
20+
expect(result).toBe(`${defaultAdminRoute}${dummyPath}`)
21+
})
22+
23+
it('should force relative URL when `serverURL` is omitted', () => {
24+
const result = formatAdminURL({
25+
adminRoute: defaultAdminRoute,
26+
path: dummyPath,
27+
relative: false,
28+
})
29+
30+
expect(result).toBe(`${defaultAdminRoute}${dummyPath}`)
31+
})
32+
})
33+
34+
describe('absolute URLs', () => {
35+
it('should return absolute URL with serverURL', () => {
36+
const result = formatAdminURL({
37+
adminRoute: defaultAdminRoute,
38+
path: dummyPath,
39+
serverURL,
40+
})
41+
42+
expect(result).toBe(`${serverURL}${defaultAdminRoute}${dummyPath}`)
43+
})
44+
45+
it('should handle serverURL with trailing slash', () => {
46+
const result = formatAdminURL({
47+
adminRoute: defaultAdminRoute,
48+
path: '/collections/posts',
49+
serverURL: 'https://example.com/',
50+
})
51+
52+
expect(result).toBe('https://example.com/admin/collections/posts')
53+
})
54+
55+
it('should handle serverURL with subdirectory', () => {
56+
const result = formatAdminURL({
57+
adminRoute: defaultAdminRoute,
58+
path: '/collections/posts',
59+
serverURL: 'https://example.com/api/v1',
60+
})
61+
62+
expect(result).toBe('https://example.com/admin/collections/posts')
63+
})
64+
})
65+
66+
describe('admin route handling', () => {
67+
it('should return relative URL for adminRoute="/", no path, no `serverURL`', () => {
68+
const result = formatAdminURL({
69+
adminRoute: rootAdminRoute,
70+
relative: true,
71+
})
72+
73+
expect(result).toBe('/')
74+
})
75+
76+
it('should handle relative URL for adminRoute="/", with path, no `serverURL`', () => {
77+
const result = formatAdminURL({
78+
adminRoute: rootAdminRoute,
79+
path: dummyPath,
80+
relative: true,
81+
})
82+
83+
expect(result).toBe(dummyPath)
84+
})
85+
86+
it('should return absolute URL for adminRoute="/", no path, with `serverURL`', () => {
87+
const result = formatAdminURL({
88+
adminRoute: rootAdminRoute,
89+
serverURL,
90+
})
91+
92+
expect(result).toBe('https://example.com/')
93+
})
94+
95+
it('should handle absolute URL for adminRoute="/", with path and `serverURL`', () => {
96+
const result = formatAdminURL({
97+
adminRoute: rootAdminRoute,
98+
serverURL,
99+
path: dummyPath,
100+
})
101+
102+
expect(result).toBe(`${serverURL}${dummyPath}`)
103+
})
104+
})
105+
106+
describe('base path handling', () => {
107+
it('should include basePath in URL', () => {
108+
const result = formatAdminURL({
109+
adminRoute: defaultAdminRoute,
110+
basePath: '/v1',
111+
path: dummyPath,
112+
serverURL,
113+
})
114+
115+
expect(result).toBe(`${serverURL}/v1${defaultAdminRoute}${dummyPath}`)
116+
})
117+
118+
it('should handle basePath with adminRoute="/"', () => {
119+
const result = formatAdminURL({
120+
adminRoute: rootAdminRoute,
121+
basePath: '/v1',
122+
serverURL,
123+
})
124+
125+
expect(result).toBe(`${serverURL}/v1`)
126+
})
127+
128+
it('should handle basePath with no adminRoute', () => {
129+
const result = formatAdminURL({
130+
adminRoute: undefined,
131+
basePath: '/v1',
132+
path: dummyPath,
133+
serverURL,
134+
})
135+
136+
expect(result).toBe(`${serverURL}/v1${dummyPath}`)
137+
})
138+
139+
it('should handle empty basePath', () => {
140+
const result = formatAdminURL({
141+
adminRoute: defaultAdminRoute,
142+
basePath: '',
143+
path: dummyPath,
144+
serverURL,
145+
})
146+
147+
expect(result).toBe(`${serverURL}${defaultAdminRoute}${dummyPath}`)
148+
})
149+
})
150+
151+
describe('path handling', () => {
152+
it('should handle empty string path', () => {
153+
const result = formatAdminURL({
154+
adminRoute: defaultAdminRoute,
155+
path: '',
156+
serverURL,
157+
})
158+
159+
expect(result).toBe(`${serverURL}${defaultAdminRoute}`)
160+
})
161+
162+
it('should handle null path', () => {
163+
const result = formatAdminURL({
164+
adminRoute: defaultAdminRoute,
165+
path: null,
166+
serverURL,
167+
})
168+
expect(result).toBe(`${serverURL}${defaultAdminRoute}`)
169+
})
170+
171+
it('should handle undefined path', () => {
172+
const result = formatAdminURL({
173+
adminRoute: defaultAdminRoute,
174+
path: undefined,
175+
serverURL,
176+
})
177+
178+
expect(result).toBe(`${serverURL}${defaultAdminRoute}`)
179+
})
180+
181+
it('should handle path with query parameters', () => {
182+
const path = `${dummyPath}?page=2`
183+
184+
const result = formatAdminURL({
185+
adminRoute: defaultAdminRoute,
186+
path,
187+
serverURL,
188+
})
189+
190+
expect(result).toBe(`${serverURL}${defaultAdminRoute}${path}`)
191+
})
192+
})
193+
194+
describe('edge cases', () => {
195+
it('should return "/" when given minimal args', () => {
196+
const result = formatAdminURL({
197+
adminRoute: undefined,
198+
basePath: '',
199+
path: '',
200+
relative: true,
201+
})
202+
203+
expect(result).toBe('/')
204+
})
205+
})
206+
})
Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
11
import type { Config } from '../config/types.js'
22

3-
/** Will read the `routes.admin` config and appropriately handle `"/"` admin paths */
4-
export const formatAdminURL = (args: {
5-
adminRoute: NonNullable<Config['routes']>['admin']
6-
basePath?: string
7-
path: '' | `/${string}` | null | undefined
8-
serverURL?: Config['serverURL']
9-
}): string => {
10-
const { adminRoute, basePath = '', path: pathFromArgs, serverURL } = args
11-
const path = pathFromArgs || ''
3+
/**
4+
* This function builds correct URLs for admin panel routing.
5+
* Its primary responsibilities are:
6+
* 1. Read from your `routes.admin` config and appropriately handle `"/"` admin paths
7+
* 2. Prepend the `basePath` from your Next.js config, if specified
8+
* 3. Return relative or absolute URLs, as needed
9+
*/
10+
export const formatAdminURL = (
11+
args: {
12+
adminRoute: NonNullable<Config['routes']>['admin']
13+
/**
14+
* The subpath of your application, if specified.
15+
* @see https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
16+
* @example '/docs'
17+
*/
18+
basePath?: string
19+
path?: '' | `/${string}` | null
20+
/**
21+
* Return a relative URL, e.g. ignore `serverURL`.
22+
* Useful for route-matching, etc.
23+
*/
24+
relative?: boolean
25+
} & Pick<Config, 'serverURL'>,
26+
): string => {
27+
const { adminRoute, basePath = '', path = '', relative = false, serverURL } = args
1228

13-
if (adminRoute) {
14-
if (adminRoute === '/') {
15-
if (!path) {
16-
return `${serverURL || ''}${basePath}${adminRoute}`
17-
}
18-
} else {
19-
return `${serverURL || ''}${basePath}${adminRoute}${path}`
20-
}
29+
const pathSegments = [basePath]
30+
31+
if (adminRoute && adminRoute !== '/') {
32+
pathSegments.push(adminRoute)
33+
}
34+
35+
if (path && !(adminRoute === '/' && !path)) {
36+
pathSegments.push(path)
37+
}
38+
39+
const pathname = pathSegments.join('') || '/'
40+
41+
if (relative || !serverURL) {
42+
return pathname
2143
}
2244

23-
return `${serverURL || ''}${basePath}${path}`
45+
return new URL(pathname, serverURL).toString()
2446
}

0 commit comments

Comments
 (0)