From 79bc4159f5ca1d59ea67d0b6547e101582b3636e Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 22 Jul 2026 20:32:32 +0800 Subject: [PATCH] fix(admin): join REST URLs correctly under plain permalinks When permalink_structure is empty, rest_url() is index.php?rest_route=/aksrt/v1. Naive base+endpoint produced rest_route=/aksrt/v1/banners?kind=banner (query inside route value), so WP returned rest_no_route and Banner/Hero managers 404'd. Fold path into rest_route and append extra params as siblings. --- assets/admin/src/api/index.js | 78 +++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/assets/admin/src/api/index.js b/assets/admin/src/api/index.js index baf5051d..3f2e33ae 100644 --- a/assets/admin/src/api/index.js +++ b/assets/admin/src/api/index.js @@ -1,7 +1,8 @@ /** * AKSRT Admin API Client * - * Communicates with the WordPress REST API at /wp-json/aksrt/v1. + * Communicates with the WordPress REST API at /wp-json/aksrt/v1 + * (or index.php?rest_route=/aksrt/v1 when pretty permalinks are off). * Methods are organized into domain modules while the client keeps the shared * request pipeline and schema-domain normalization contract. */ @@ -15,6 +16,73 @@ function normalizeDomainKey(domain) { return String(domain || 'global').trim().replace(/-/g, '_') } +/** + * Join REST base + endpoint correctly for both pretty and plain permalinks. + * + * Pretty: https://site/wp-json/aksrt/v1 + /banners?kind=x + * → https://site/wp-json/aksrt/v1/banners?kind=x + * + * Plain: https://site/index.php?rest_route=/aksrt/v1 + /banners?kind=x + * → https://site/index.php?rest_route=/aksrt/v1/banners&kind=x + * (must NOT produce rest_route=/aksrt/v1/banners?kind=x) + * + * @param {string} baseUrl + * @param {string} endpoint path starting with /, may include ?query + * @returns {string} + */ +function joinRestUrl(baseUrl, endpoint) { + let path = endpoint == null || endpoint === '' ? '/' : String(endpoint) + let extraQuery = '' + const qIdx = path.indexOf('?') + if (qIdx !== -1) { + extraQuery = path.slice(qIdx + 1) + path = path.slice(0, qIdx) + } + if (!path.startsWith('/')) { + path = '/' + path + } + + const base = String(baseUrl || '').trim() || '/wp-json/aksrt/v1' + + // Plain permalinks: rest_route query mode + if (base.includes('rest_route=')) { + let u + try { + u = new URL(base, typeof window !== 'undefined' ? window.location.origin : 'https://localhost') + } catch { + // Fallback: naive append (should not hit with valid rest_url) + return base.replace(/\/?$/, '') + path + (extraQuery ? (base.includes('?') ? '&' : '?') + extraQuery : '') + } + + const baseRoute = (u.searchParams.get('rest_route') || '/aksrt/v1').replace(/\/$/, '') || '/aksrt/v1' + let fullRoute + if (path === '/') { + fullRoute = baseRoute + } else if (path.startsWith('/aksrt/')) { + fullRoute = path + } else { + fullRoute = baseRoute + path + } + u.searchParams.set('rest_route', fullRoute) + + if (extraQuery) { + const extra = new URLSearchParams(extraQuery) + extra.forEach((v, k) => { + u.searchParams.set(k, v) + }) + } + return u.toString() + } + + // Pretty /wp-json mode + const root = base.replace(/\/$/, '') + let url = root + (path === '/' ? '' : path) + if (extraQuery) { + url += (url.includes('?') ? '&' : '?') + extraQuery + } + return url +} + class ApiClient { constructor() { this.baseUrl = API_ROOT @@ -58,11 +126,13 @@ class ApiClient { endpoint = endpoint.replace('/aksrt/v1', '') || '/' } - let url = `${this.baseUrl}${endpoint}` + let url = joinRestUrl(this.baseUrl, endpoint) if (params && typeof params === 'object') { const qs = new URLSearchParams(params).toString() - if (qs) url += (url.includes('?') ? '&' : '?') + qs + if (qs) { + url += (url.includes('?') ? '&' : '?') + qs + } } let finalBody = body @@ -116,5 +186,5 @@ class ApiClient { Object.assign(ApiClient.prototype, apiMethods) -export { normalizeDomainKey } +export { normalizeDomainKey, joinRestUrl } export default new ApiClient()