Skip to content

fix: build REST URLs for sites using plain permalinks - #573

Open
dcalhoun wants to merge 4 commits into
trunkfrom
fix/support-plain-permalinks
Open

fix: build REST URLs for sites using plain permalinks#573
dcalhoun wants to merge 4 commits into
trunkfrom
fix/support-plain-permalinks

Conversation

@dcalhoun

@dcalhoun dcalhoun commented Jul 31, 2026

Copy link
Copy Markdown
Member

What?

Teaches the native REST URL builders to address sites that use plain permalinks, where the only REST root is the query form https://site/?rest_route=/.

Stacks on #572 (the wordpress-rs 0.6.0 bump), so it targets build/bump-wordpress-rs-0.6.0 rather than trunk.

Fixes #563. Fixes CMM-2177.

Why?

A site with plain permalinks has no path-based REST root — /wp-json/ depends on the rewrite rules that plain permalinks disable, so WordPress advertises ?rest_route=/ instead. Both hosts hand that root straight to GutenbergKit (EditorConfiguration+Blog.swift on iOS, GutenbergKitSettingsBuilder.kt on Android) after discovering it from the site's https://api.w.org/ Link header, so it reaches every native URL builder verbatim.

Those builders appended the endpoint to the path, which produced malformed URLs for every native endpoint — editor settings, active theme, site settings, post types, the post itself, and editor assets.

Worth noting the iOS failure was silent, not a 404. Verified against a live plain-permalink site:

GET https://site/wp/v2/settings?rest_route=/   -> 200 {"name":…,"namespaces":[…]}   # the REST API index
GET https://site/?rest_route=/wp/v2/settings   -> 200 {"title":…,"date_format":…}   # the actual settings

The trailing ?rest_route=/ routes to the API root and the path is ignored, so the editor received the wrong resource and failed later at decode time with no indication the URL was wrong. On Android the two query-carrying endpoints did 404 outright.

WP.com sites are unaffected — they use the path-based public-api.wordpress.com/ proxy.

How?

Both platforms mirror the algorithm in @wordpress/api-fetch's root URL middleware, which the web layer already uses (and which is why JS requests work today on these sites): when the root carries a query, append the endpoint to it and merge the path's own query string with &.

  • iOSURL.appending(rawPath:) (Foundation+Extensions.swift), the primitive every iOS REST URL bottoms out in. EditorAssetLibrary used stock URL.appending(path:), which had the same defect, so it now routes through the same primitive.
  • Android — new String.appendingRestPath (StringExtensions.kt), replacing the trimEnd('/')-plus-concat in RESTAPIRepository and both asset libraries. Namespace insertion is split into namespacedPath() so the path transform and the root join are separate concerns.

The fix lives in the join primitive rather than in namespace insertion deliberately: two of the three Android call sites (and the iOS asset library) build their URLs without going through namespacing, so they would otherwise stay broken.

Also aligns a pre-existing platform divergence in namespace insertion — iOS namespaced two-segment paths (components.count >= 2) where Android bailed (parts.size < 3); Android now matches iOS.

Note for #357 / #561

#357 introduces WordPressRESTURL.namespaced (iOS) and RestUrlBuilder.namespaced (Android). This branch and that one merge without textual conflict, but two things need coordinating — details in #357 (comment):

  • iOS composes cleanly (WordPressRESTURL.namespaced already calls appending(rawPath:) on every path), but Android's RestUrlBuilder.namespaced needs to delegate its join to appendingRestPath, or a clean merge silently reverts the Android fix.
  • mediaEndpointURL needs & merging on both platforms before it works on a query root.

Testing Instructions

Requires a self-hosted site set to Settings ▸ Permalinks ▸ Plain. Confirm it advertises the query-form root first:

curl -sI https://your-site.com/ | grep -i '^link'

The rel="https://api.w.org/" value should contain ?rest_route=/ rather than /wp-json/.

  1. Build the demo app on either platform and add the site using an application password.
  2. Open the Editor Configuration screen and confirm the API Root row shows the ?rest_route= form.
  3. Open a post in the editor.

Check that:

  • The post's existing content loads (/wp/v2/posts/{id}).
  • The editor picks up theme styles — colors and fonts match the site's theme rather than defaults (/wp-block-editor/v1/settings and /wp/v2/themes). This is the highest-value check: the themes endpoint is the only one carrying its own query string, so it's what exercises the ?& merge.
  • The block inserter is populated (/wp/v2/types).
  • Saving and publishing work.

Enable Network Logging in the site-preparation screen to inspect the request URLs directly — they should read ?rest_route=/wp/v2/…, not /wp/v2/…?rest_route=/.

Also confirm no regression on a pretty-permalink site and a WP.com site.

Accessibility Testing Instructions

N/A — no user interface changes.

@wpmobilebot

wpmobilebot commented Aug 1, 2026

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/573")

Built from 2c06e89

@dcalhoun
dcalhoun force-pushed the fix/support-plain-permalinks branch from bc4ec12 to af476dc Compare August 1, 2026 13:28
@dcalhoun
dcalhoun marked this pull request as ready for review August 3, 2026 17:30
@dcalhoun
dcalhoun requested a review from jkmassel August 3, 2026 17:41
Base automatically changed from build/bump-wordpress-rs-0.6.0 to trunk August 3, 2026 19:35
dcalhoun and others added 4 commits August 3, 2026 15:35
Sites with plain permalinks have no path-based REST root, so WordPress
advertises the query form `https://site/?rest_route=/` instead. Appending
an endpoint to that root landed the path before the query, malforming
every native REST URL.

Teach `URL.appending(rawPath:)` to append the endpoint to the query value
when the root carries one, merging the path's own query string with `&`.
This mirrors `@wordpress/api-fetch`'s root URL middleware, which the web
layer already uses, so native and web requests resolve identically.

Also route `editorAssetsUrl` through the same primitive instead of
`URL.appending(path:)`, which had the same defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sites with plain permalinks have no path-based REST root, so WordPress
advertises the query form `https://site/?rest_route=/` instead. The root
was normalized with `trimEnd('/')` and concatenated with the endpoint,
which mangled the route value and emitted a second `?` for endpoints that
carry their own query string.

Add `String.appendingRestPath`, which appends the endpoint to the query
value when the root carries one and merges the path's query string with
`&`. This mirrors `@wordpress/api-fetch`'s root URL middleware, which the
web layer already uses, so native and web requests resolve identically.

Route the repository and both asset library URL builders through it, and
align namespace insertion with iOS, which also namespaces two-segment
paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `appending(rawPath:)` doc comment claimed native and web requests
resolve to the same endpoints, but that only holds for the canonical
`?rest_route=/` root. For a root without a trailing slash the two
deliberately diverge: api-fetch strips the leading slash, while this
keeps it because WordPress's `rest_route` matching expects it. Scope the
claim and name the deviation, and rename the test that pins it so it
states the contract rather than implying generic normalization.

Also guard the query-based branch with `!isFileURL`. It fired on any
URL carrying a `?`, and `EditorAssetBundle` calls this on local file
URLs, so the REST-root behavior and the path-joining behavior shared an
invariant nothing enforced. No live bug — `siteId` is a host — but the
two callers are now independent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The doc comment claimed native and web requests resolve to the same
endpoints, but that only holds for the canonical `?rest_route=/` root.
For a root without a trailing slash the two deliberately diverge:
api-fetch strips the leading slash, while this keeps it because
WordPress's `rest_route` matching expects it.

Scope the claim and name the deviation, and rename the test that pins it
so it states the contract rather than implying generic normalization.

Mirrors the iOS change in the preceding commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GBK Plain Permalink Support

2 participants