From bacf3a0faca9475d1963e937a41d2534db22040e Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 28 Jul 2026 12:26:16 -0400 Subject: [PATCH 1/8] build(deps): point GutenbergKit at the PR build for #569 GutenbergKit#569 adds `EditorLocalizableString.patternsCount(Int)`, which is source-breaking for hosts that switch over the enum exhaustively. Track the PR build so the companion changes can land and be tested together. Co-Authored-By: Claude Opus 5 (1M context) --- Modules/Package.resolved | 6 +++--- Modules/Package.swift | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 5e4613747529..af6d80fb1c9a 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "394a1dae231289152fc0a7f94cfb495587468c99f146311d7a94ac6a25772e0b", + "originHash" : "e77a9128635d7de8e21c7a3d786fc26258c74c0bfbfdfb0523447e7a66bc163b", "pins" : [ { "identity" : "alamofire", @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "7180587f49d3c3bfdb34cc3e80b2a9d22a3cd93e", - "version" : "0.18.1" + "branch" : "pr-build/569", + "revision" : "b40026523e455890c070c4844e5fdc6de3e4ae8b" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 9d07d6b80bb7..c226195219ec 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -62,7 +62,8 @@ let package = Package( revision: "b34794c9a3f32312e1593d4a3d120572afa0d010" ), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), - .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.18.1"), + // TODO: Restore a version pin once GutenbergKit#569 ships a release. + .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/569"), .package( url: "https://github.com/automattic/wordpress-rs", exact: "0.6.0" From 7450e622271de6ef7f2d0a592cc8d39b9b5782c7 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 28 Jul 2026 12:26:42 -0400 Subject: [PATCH 2/8] fix(editor): translate the patterns count and fall back for new keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GutenbergKit#569 adds `patternsCount(Int)`, which previously rendered as a hardcoded English string. Translate it, splitting singular and plural so each reads naturally. Also delegate unhandled keys to `EditorLocalization.defaultLocalize(_:)`. The switch was exhaustive, so every string the editor added broke this build until someone wrote a translation — and the break surfaced on the dependency bump, not when the string was added. New keys now render in English until translated. Co-Authored-By: Claude Opus 5 (1M context) --- .../NewGutenberg/GBKExtensions.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift index cb89424b2db9..25c691fcacec 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -91,6 +91,24 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) value: "All", comment: "Category name for section showing all patterns" ) + case .patternsCount(let count): + if count == 1 { + NSLocalizedString( + "editor.patterns.count.singular", + value: "1 pattern", + comment: "Singular label displaying the number of patterns in a category" + ) + } else { + String( + format: NSLocalizedString( + "editor.patterns.count.plural", + value: "%1$d patterns", + comment: + "Plural label displaying the number of patterns in a category. %1$d is a placeholder for the number of patterns." + ), + count + ) + } case .loadingEditor: NSLocalizedString( "editor.loading.title", @@ -134,6 +152,8 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) value: "Dismiss", comment: "Button title to dismiss the Lockdown Mode warning" ) + default: + EditorLocalization.defaultLocalize(value) } } From 1ba69a86e0897ab743405886d418cd5a3a2032a1 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 28 Jul 2026 13:07:40 -0400 Subject: [PATCH 3/8] fix(editor): use @unknown default for GutenbergKit string fallback A plain `default` warns "default will never be executed" because this switch covers every case the pinned GutenbergKit defines. `@unknown default` compiles clean and behaves the same once the editor adds a string. Co-Authored-By: Claude Opus 5 (1M context) --- .../Classes/ViewRelated/NewGutenberg/GBKExtensions.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift index 25c691fcacec..1161f51c16a9 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -152,7 +152,10 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) value: "Dismiss", comment: "Button title to dismiss the Lockdown Mode warning" ) - default: + // Renders strings added by a newer GutenbergKit in English rather than + // breaking this build. `@unknown` because a plain `default` warns that it + // will never execute while this switch happens to cover every case. + @unknown default: EditorLocalization.defaultLocalize(value) } } From fbd0948681021d48e23be1b7422d78f6fb801f4b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 28 Jul 2026 14:37:33 -0400 Subject: [PATCH 4/8] build: Update GutenbergKit version --- Modules/Package.resolved | 5 ++--- Modules/Package.swift | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index af6d80fb1c9a..50333fbd4a4d 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e77a9128635d7de8e21c7a3d786fc26258c74c0bfbfdfb0523447e7a66bc163b", + "originHash" : "e2f68a933b297d036201a43c6498ec30ca554bb39673797d95def0c6fde0c7f3", "pins" : [ { "identity" : "alamofire", @@ -131,8 +131,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "branch" : "pr-build/569", - "revision" : "b40026523e455890c070c4844e5fdc6de3e4ae8b" + "revision" : "40f35ecb2c8a88ba8759f911f28cf79571e969a5" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index c226195219ec..3f440e79c750 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -63,7 +63,8 @@ let package = Package( ), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), // TODO: Restore a version pin once GutenbergKit#569 ships a release. - .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/569"), + .package(url: "https://github.com/wordpress-mobile/GutenbergKit", + revision: "40f35ecb2c8a88ba8759f911f28cf79571e969a5"), .package( url: "https://github.com/automattic/wordpress-rs", exact: "0.6.0" From a31aac0d616372985aa66e582358c08b30727ef3 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 29 Jul 2026 14:34:57 -0400 Subject: [PATCH 5/8] fix(editor): decline GutenbergKit strings with nil GutenbergKit#569 changed `EditorLocalization.localize` to return `String?`. Hosts now decline a key by returning `nil` instead of calling a public `defaultLocalize`, so the editor supplies the string and reports the gap. `getLocalizedString(for:)` and `EditorLocalizableString.localized` return `String?`, and the fallback case yields `nil`. The install site in PostGBKEditorViewController needs no change: `{ $0.localized }` already matches the new closure type. Keeps the `@unknown default` from 88bbcf4a11. It stays reachable only for keys a future GutenbergKit adds; every case defined today is still handled explicitly, so the pattern count keeps its own translation rather than falling through. Co-Authored-By: Claude Opus 5 (1M context) --- .../ViewRelated/NewGutenberg/GBKExtensions.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift index 1161f51c16a9..0ac42da4fb9f 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -33,7 +33,7 @@ extension GutenbergKit.EditorViewControllerDelegate { } } -private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String { +private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String? { switch value { case .showMore: NSLocalizedString( @@ -152,16 +152,17 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) value: "Dismiss", comment: "Button title to dismiss the Lockdown Mode warning" ) - // Renders strings added by a newer GutenbergKit in English rather than - // breaking this build. `@unknown` because a plain `default` warns that it - // will never execute while this switch happens to cover every case. + // Declining a key lets the editor render its own string, so strings added + // by a newer GutenbergKit appear in English rather than breaking this build. + // `@unknown` because a plain `default` warns that it will never execute + // while this switch happens to cover every case. @unknown default: - EditorLocalization.defaultLocalize(value) + nil } } extension EditorLocalizableString { - var localized: String { + var localized: String? { getLocalizedString(for: self) } } From 131127826e98474cf1265a7380040e7811f478c6 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 29 Jul 2026 15:17:58 -0400 Subject: [PATCH 6/8] build: Update GutenbergKit version --- Modules/Package.resolved | 4 ++-- Modules/Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 50333fbd4a4d..9f21a0618814 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e2f68a933b297d036201a43c6498ec30ca554bb39673797d95def0c6fde0c7f3", + "originHash" : "4de2f629319d11e1daef4a746ee727d287951510c0fdc4469215767e9df0361b", "pins" : [ { "identity" : "alamofire", @@ -131,7 +131,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "40f35ecb2c8a88ba8759f911f28cf79571e969a5" + "revision" : "b681e106123c9b11b9bdd7923ea18f227dc5a85f" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 3f440e79c750..fc84224a7333 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -64,7 +64,7 @@ let package = Package( .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), // TODO: Restore a version pin once GutenbergKit#569 ships a release. .package(url: "https://github.com/wordpress-mobile/GutenbergKit", - revision: "40f35ecb2c8a88ba8759f911f28cf79571e969a5"), + revision: "b681e106123c9b11b9bdd7923ea18f227dc5a85f"), .package( url: "https://github.com/automattic/wordpress-rs", exact: "0.6.0" From 9cde491d60d5c6ce3e6dbc12265d555d30a0165b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 31 Jul 2026 19:01:07 -0400 Subject: [PATCH 7/8] fix: build the editor assets endpoint for sites using plain permalinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A site using plain permalinks has no path-based REST root — WordPress advertises the query form `https://site/?rest_route=/` instead, and that value is stored verbatim in `Blog.restApiRootURL` by wordpress-rs Link-header discovery. `appendPathComponent` appended the endpoint to the URL path, stranding the query and producing `https://site/wpcom/v2/editor-assets?rest_route=/`, so third-party block assets never loaded for those sites. Concatenate onto the API root instead, which grows the `rest_route` value while leaving path-based and WP.com roots unchanged. This mirrors `@wordpress/api-fetch` and GutenbergKit's native URL builders, so every layer resolves the same endpoints for a given site. Co-Authored-By: Claude Opus 5 (1M context) --- RELEASE-NOTES.txt | 1 + .../Gutenberg/EditorConfigurationTests.swift | 68 +++++++++++++++++++ .../Editor/EditorConfiguration+Blog.swift | 40 +++++++++-- 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 718fbe84fd7c..407e1664a60c 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,6 @@ 27.2 ----- +* [*] Experimental Gutenberg editor: fix block assets not loading on self-hosted sites that use plain permalinks [#25859] 27.1 diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift index 9619becc4cab..92f3ad25133a 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/EditorConfigurationTests.swift @@ -134,4 +134,72 @@ struct EditorConfigurationTests { #expect(config.authHeader == "Basic \(base64Credentials)", "Should use Basic authentication") #expect(config.siteApiNamespace.isEmpty, "Should not have WP.com API namespace") } + + // MARK: - Editor Assets Endpoint + + @Test("Editor assets endpoint is appended to a path-based API root") + func editorAssetsEndpointWithPathBasedApiRoot() async throws { + let context = self.context + + let blog = BlogBuilder(context) + .with(atomic: false) + .isNotHostedAtWPcom() + .with(username: "selfhosteduser") + .with(url: "https://self-hosted.org") + .withApplicationPassword("test-app-password-1234", using: keychain) + .with(restApiRootURL: "https://self-hosted.org/wp-json/") + .build() + + let config = EditorConfiguration(blog: blog, postType: .post, keychain: keychain) + + #expect( + config.editorAssetsEndpoint + == URL(string: "https://self-hosted.org/wp-json/wpcom/v2/editor-assets"), + "Should append the endpoint to the path of a path-based API root" + ) + } + + /// Sites using plain permalinks have no path-based REST root, so WordPress advertises the + /// query form and the endpoint belongs in the `rest_route` value rather than the URL path. + @Test("Editor assets endpoint is appended to the route of a query-based API root") + func editorAssetsEndpointWithQueryBasedApiRoot() async throws { + let context = self.context + + let blog = BlogBuilder(context) + .with(atomic: false) + .isNotHostedAtWPcom() + .with(username: "selfhosteduser") + .with(url: "https://self-hosted.org") + .withApplicationPassword("test-app-password-1234", using: keychain) + .with(restApiRootURL: "https://self-hosted.org/?rest_route=/") + .build() + + let config = EditorConfiguration(blog: blog, postType: .post, keychain: keychain) + + #expect( + config.editorAssetsEndpoint + == URL(string: "https://self-hosted.org/?rest_route=/wpcom/v2/editor-assets"), + "Should append the endpoint to the rest_route value, not the URL path" + ) + } + + @Test("Editor assets endpoint includes the site namespace for WP.com sites") + func editorAssetsEndpointIncludesNamespace() async throws { + let context = self.context + + let blog = BlogBuilder(context) + .with(atomic: false) + .isHostedAtWPcom() + .withAnAccount(username: "simpleuser", authToken: "simple-bearer-token") + .with(dotComID: 12345) + .build() + + let config = EditorConfiguration(blog: blog, postType: .post, keychain: keychain) + + #expect( + config.editorAssetsEndpoint + == URL(string: "https://public-api.wordpress.com/wpcom/v2/sites/12345/editor-assets"), + "Should insert the site namespace before the endpoint" + ) + } } diff --git a/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift b/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift index dd53bcaa557d..2036dce078b1 100644 --- a/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift +++ b/WordPress/Classes/Utility/Editor/EditorConfiguration+Blog.swift @@ -60,17 +60,43 @@ extension EditorConfiguration { .setNetworkFallbackMode(.automatic) // Build editor assets endpoint - var editorAssetsEndpoint = siteApiRoot - editorAssetsEndpoint.appendPathComponent("wpcom/v2/") - if let namespace = siteApiNamespace.first { - editorAssetsEndpoint.appendPathComponent(namespace) - } - editorAssetsEndpoint.appendPathComponent("editor-assets") - builder = builder.setEditorAssetsEndpoint(editorAssetsEndpoint) + let namespacePath = siteApiNamespace.first.map { $0.hasSuffix("/") ? $0 : $0 + "/" } ?? "" + builder = builder.setEditorAssetsEndpoint( + Self.appendingRESTPath("wpcom/v2/\(namespacePath)editor-assets", to: siteApiRoot) + ) self = builder.build() } + /// Appends a REST API path to a site's API root. + /// + /// Sites using plain permalinks have no path-based REST root — WordPress advertises the + /// query form `https://example.com/?rest_route=/` instead — so the path belongs in the + /// `rest_route` value rather than the URL path: + /// + /// ``` + /// https://example.com/?rest_route=/ + wpcom/v2/editor-assets + /// -> https://example.com/?rest_route=/wpcom/v2/editor-assets + /// ``` + /// + /// Path-based roots keep the usual behavior. This mirrors `@wordpress/api-fetch`'s root URL + /// middleware and GutenbergKit's native URL builders, so every layer resolves the same + /// endpoints for a given site. + /// + /// - Parameters: + /// - path: The REST path to append, without a leading slash. + /// - apiRoot: The site's REST API root. + /// - Returns: The endpoint URL, or `apiRoot` unchanged if the result isn't a valid URL. + static func appendingRESTPath(_ path: String, to apiRoot: URL) -> URL { + // Concatenate onto the root's full string rather than appending path components, so a + // query-based root grows its `rest_route` value instead of stranding it behind the path. + // One slash is kept between the two whether the root ends in `wp-json/`, `wp-json`, + // `?rest_route=/`, or `?rest_route=`. + let root = apiRoot.absoluteString + let separator = root.hasSuffix("/") ? "" : "/" + return URL(string: root + separator + path) ?? apiRoot + } + /// Returns true if the plugins should be enabled for the given blog. /// This is used to determine if the editor should load third-party /// plugins providing blocks. From 7c7387453b3c8a2e3de19f99a3b543e6e262067e Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 3 Aug 2026 13:21:48 -0400 Subject: [PATCH 8/8] build: Update GutenbergKit version --- Modules/Package.resolved | 4 ++-- Modules/Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 9f21a0618814..ac466d6faccf 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "4de2f629319d11e1daef4a746ee727d287951510c0fdc4469215767e9df0361b", + "originHash" : "f31432f3579f8c471514d5d994fcbc2a2e5c8dc5d2ca211288c0f9c622bb1da2", "pins" : [ { "identity" : "alamofire", @@ -131,7 +131,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "b681e106123c9b11b9bdd7923ea18f227dc5a85f" + "revision" : "9f3a5c203d0d81b4bdfe6851cd1bf3b7454e22c9" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index fc84224a7333..b9bebe6a0fcd 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -64,7 +64,7 @@ let package = Package( .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), // TODO: Restore a version pin once GutenbergKit#569 ships a release. .package(url: "https://github.com/wordpress-mobile/GutenbergKit", - revision: "b681e106123c9b11b9bdd7923ea18f227dc5a85f"), + revision: "9f3a5c203d0d81b4bdfe6851cd1bf3b7454e22c9"), .package( url: "https://github.com/automattic/wordpress-rs", exact: "0.6.0"