Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,10 @@ local.properties
## Production Build Products
/android/Gutenberg/src/main/assets/assets
/android/Gutenberg/src/main/assets/index.html
/android/Gutenberg/src/main/assets/remote.html

# Disabled removing these files until this is published like Android in CI.
# /ios/Sources/GutenbergKit/Gutenberg/assets
# /ios/Sources/GutenbergKit/Gutenberg/index.html
# /ios/Sources/GutenbergKit/Gutenberg/remote.html

# Translation files
src/translations/
Expand Down
14 changes: 2 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,14 @@ make local-android-library
make test-android
```

### Remote Editor Development

```bash
# Start development server for remote editor
make dev-server-remote
# or
npm run dev:remote
```

## Architecture

### Web Editor Structure

The web editor is built with React and WordPress packages:

- **Entry Points**:
- `src/index.js` - Bundled editor entry
- `src/remote.js` - Remote editor entry (supports plugins)
- `src/index.js` - Main editor entry point (supports plugins via bundled code)
- **Core Components**:
- `src/components/editor/` - Main editor component with host bridge integration
- `src/components/visual-editor/` - Visual editing interface
Expand Down Expand Up @@ -123,7 +113,7 @@ The editor uses a bidirectional bridge pattern:

### Build System

- **Vite**: Handles web bundling with separate configs for local and remote editors
- **Vite**: Handles web bundling and development server
- **Translations**: Automated translation preparation from WordPress packages
- **Asset Distribution**: Built assets are copied to platform-specific directories

Expand Down
3 changes: 0 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ build: npm-dependencies prep-translations
dev-server: npm-dependencies
npm run dev

dev-server-remote: npm-dependencies
npm run dev:remote

fmt-js: npm-dependencies
npm run format

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Install the GutenbergKit dependencies and start the development server by runnin
make dev-server
```

Once finished, the web app can now be accessed in your browser by visiting the URL logged in your terminal. However, it is recommended to use a native host app for testing the changes made to the editor for a more realistic experience. A demo app is included in the GutenbergKit package, along with instructions on how to use it below.
Once finished, the web app can now be accessed in your browser by visiting the URL logged in your terminal. However, it is **recommended to use a native host app for testing** changes made to the editor for a more realistic experience. A demo app is included in the GutenbergKit project, along with instructions on how to use it below.

### Demo App

Expand Down
6 changes: 0 additions & 6 deletions android/Gutenberg/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ android {
"\"${rootProject.ext["gutenbergEditorUrl"] ?: ""}\""
)

buildConfigField(
"String",
"GUTENBERG_EDITOR_REMOTE_URL",
"\"${rootProject.ext["gutenbergEditorRemoteUrl"] ?: ""}\""
)

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import org.json.JSONObject
import java.util.Locale

const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
const val ASSET_URL_REMOTE = "https://appassets.androidplatform.net/assets/remote.html"

class GutenbergView : WebView {
private var isEditorLoaded = false
Expand Down Expand Up @@ -170,7 +169,7 @@ class GutenbergView : WebView {
}

// Allow asset URLs
if (url.host == Uri.parse(ASSET_URL).host || url.host == Uri.parse(ASSET_URL_REMOTE).host) {
if (url.host == Uri.parse(ASSET_URL).host) {
return false
}

Expand All @@ -194,14 +193,6 @@ class GutenbergView : WebView {
}
}

// Allow remote editor server if configured
if (BuildConfig.GUTENBERG_EDITOR_REMOTE_URL.isNotEmpty()) {
val remoteEditorUrl = Uri.parse(BuildConfig.GUTENBERG_EDITOR_REMOTE_URL)
if (url.host == remoteEditorUrl.host) {
return false
}
}

// For all other URLs, open in external browser
val intent = Intent(Intent.ACTION_VIEW, url)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Expand Down
81 changes: 71 additions & 10 deletions android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Inventory2
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -37,6 +44,7 @@ import com.example.gutenbergkit.ui.dialogs.AddConfigurationDialog
import com.example.gutenbergkit.ui.dialogs.DeleteConfigurationDialog
import com.example.gutenbergkit.ui.dialogs.DiscoveringSiteDialog
import com.example.gutenbergkit.ui.theme.AppTheme
import org.wordpress.gutenberg.BuildConfig
import org.wordpress.gutenberg.EditorConfiguration

class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCallback {
Expand Down Expand Up @@ -106,7 +114,7 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa

private fun createRemoteConfiguration(config: ConfigurationItem.RemoteEditor): EditorConfiguration =
createCommonConfigurationBuilder()
.setPlugins(true) // Enable plugins for remote editor
.setPlugins(true)
.setSiteURL(config.siteUrl)
.setSiteApiRoot(config.siteApiRoot)
.setNamespaceExcludedPaths(arrayOf())
Expand Down Expand Up @@ -152,6 +160,10 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa
}
}

private fun isDevServerRunning(): Boolean {
return BuildConfig.GUTENBERG_EDITOR_URL.isNotEmpty()
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
Expand Down Expand Up @@ -192,16 +204,65 @@ fun MainScreen(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(configurations) { config ->
// Bundled editor
item {
ConfigurationCard(
configuration = config,
onClick = { onConfigurationClick(config) },
onLongClick = {
if (config is ConfigurationItem.RemoteEditor) {
configuration = ConfigurationItem.BundledEditor,
onClick = { onConfigurationClick(ConfigurationItem.BundledEditor) },
onLongClick = { }
)
}

// Remote editors section
val remoteEditors = configurations.filterIsInstance<ConfigurationItem.RemoteEditor>()
if (remoteEditors.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.remote_editors_section).uppercase(),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}

items(remoteEditors) { config ->
ConfigurationCard(
configuration = config,
onClick = { onConfigurationClick(config) },
onLongClick = {
showDeleteDialog.value = config
}
}
)
)
}
}

// Editor source note at bottom with info icon
item {
Spacer(modifier = Modifier.height(16.dp))
val editorSourceNote = if (isDevServerRunning()) {
stringResource(R.string.editor_source_dev_server)
} else {
stringResource(R.string.editor_source_built)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = editorSourceNote,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
Expand Down Expand Up @@ -266,7 +327,7 @@ fun ConfigurationCard(
Text(
when (configuration) {
is ConfigurationItem.BundledEditor -> stringResource(R.string.bundled_editor_subtitle)
is ConfigurationItem.RemoteEditor -> configuration.siteUrl
is ConfigurationItem.RemoteEditor -> stringResource(R.string.remote_editor_subtitle)
}
)
},
Expand All @@ -282,4 +343,4 @@ fun ConfigurationCard(
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
Expand All @@ -15,6 +16,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.example.gutenbergkit.R

Expand All @@ -34,6 +36,9 @@ fun AddConfigurationDialog(
onValueChange = onSiteUrlChange,
label = { Text(stringResource(R.string.site_url)) },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri
),
modifier = Modifier.fillMaxWidth()
)
},
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@

<!-- Configuration Activity -->
<string name="demo_title">GutenbergKit Demo</string>
<string name="editor_source_dev_server">Note: Editors are using the dev server started with `make dev-server`.</string>
<string name="editor_source_built">Note: Editors are using the compiled web app built with `make build`.</string>
<string name="bundled_editor">Bundled editor</string>
<string name="bundled_editor_subtitle">Local editor without plugin support</string>
<string name="remote_editors_section">Remote editors</string>
<string name="remote_editor_subtitle">Site-specific editor with plugins</string>
<string name="add_remote_editor_description">Add remote editor</string>

<!-- Dialog strings -->
Expand Down
1 change: 0 additions & 1 deletion android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ if (localPropertiesFile.exists()) {

ext {
set("gutenbergEditorUrl", localProperties.getProperty("GUTENBERG_EDITOR_URL") ?: "")
set("gutenbergEditorRemoteUrl", localProperties.getProperty("GUTENBERG_EDITOR_REMOTE_URL") ?: "")
}

plugins {
Expand Down
52 changes: 24 additions & 28 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ GutenbergKit/
│ │ └── text-editor/ # HTML text editing interface
│ ├── utils/ # Utility functions
│ │ └── bridge.js # Native-to-web communication
│ ├── index.js # Bundled editor entry point
│ └── remote.js # Remote editor entry point
│ └── index.js # Main editor entry point
├── ios/ # iOS Swift package
│ └── Sources/
│ └── GutenbergKit/
Expand Down Expand Up @@ -64,11 +63,10 @@ Development mode (`?dev_mode` query parameter) enables debugging features and by
- **Mock GBKit global is provided** - If the native bridge (`window.GBKit`) is not available, a mock object is automatically provided to allow the editor to load without native integration
- **Development notice is displayed** - A warning notice appears to inform developers that they're running without a native bridge

Add the `?dev_mode` query parameter to any editor URL:
Add the `?dev_mode` query parameter to the editor URL:

```
http://localhost:3000/?dev_mode
http://localhost:3000/remote.html?dev_mode
```

## Logging Configuration
Expand All @@ -80,38 +78,28 @@ The logger utility (`src/utils/logger.js`) supports different log levels that ca
- `?log_level=warn` - Show only warnings and errors
- `?log_level=error` - Show only errors

## Editor Variants
## Plugin Support

By default, GutenbergKit utilizes local `@wordpress` modules. This approach is similar to most modern web applications, where the `@wordpress` modules are bundled with the application.
To enable support for non-core blocks, GutenbergKit can be configured to use remote `@wordpress` modules, where the `@wordpress` modules and plugin-provided editor assets are fetched from a site's remote server. At this time, this functionality is partially implemented and may not work as expected.
GutenbergKit bundles `@wordpress` modules locally for offline capability and fast load times. The editor can optionally load plugin-provided blocks and custom editor assets from a remote server when configured.

The `make build` command builds both the local and remote editors by default. To load the remote editor, you must enable the `plugins` configuration option within the Demo app.
### How Plugin Loading Works

Additionally, a `make dev-server-remote` command is available for serving the latest remote editor changes through a development server. To load the development server in the Demo app, add an environment variable named `GUTENBERG_EDITOR_REMOTE_URL` with the URL of the development server plus `/remote.html`—i.e., `http://<YOUR_LOCAL_IP>:5173/remote.html`.
When the `plugins` configuration option is enabled:

> [!TIP]
> The remote editor redirects to the bundled editor when loading fails. If you need to debug the failure, disable redirects via the `?dev_mode` query parameter..
1. Core `@wordpress` packages are loaded from bundled code
2. Plugin scripts and styles are fetched from the site's editor assets endpoint
3. Custom blocks and editor extensions are registered dynamically
4. The editor integrates both core and custom functionality

### Bundled Editor (`index.html`)
This approach provides:

The bundled editor relies upon local `@wordpress` packages. This variant:
- **Offline-first**: Core editor works without network connectivity
- **Extensibility**: Supports custom blocks and plugins when connected
- **Performance**: Core packages load instantly from local bundle

- Provides offline capability
- Has faster initial load times
- Limited to core blocks only
- No plugin support
### Configuration

**Entry point:** `src/index.js`

### Remote Editor (`remote.html`)

The remote editor loads `@wordpress` packages and plugins from a remote server. This variant:

- Supports custom blocks and plugins
- Requires network connectivity
- Used in production environments with custom implementations

**Entry point:** `src/remote.js`
Enable plugins by setting the `plugins` configuration option. The editor will fetch assets from the configured `editorAssetsEndpoint` or fall back to the default Jetpack endpoint. The demo app UI allows adding site-specific editor configurations, which enables the `plugins` configuration option.

## Testing

Expand All @@ -132,3 +120,11 @@ The remote editor loads `@wordpress` packages and plugins from a remote server.

- JUnit framework
- Run: `make test-android`

## Frequently Asked Questions

### Why do I encounter a `GBKit global not available after timeout` error when opening the GutenbergKit editor in a browser?

This error occurs when the editor is unable to communicate with the native bridge. This is expected when opening the editor in a browser, as the native bridge is not available. GutenbergKit is designed to be used in a native host app that provides the native bridge. The GutenbergKit project includes a demo app that can be used to test the editor.

It is possible to circumvent this this error by adding the `?dev_mode` query parameter to the editor URL in your browser. This will bypass the native bridge requirement and allow the editor to load without the native bridge. However, some features may not work as expected when using this mode.
Binary file modified docs/gutenberg-kit-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@
value = "http://localhost:5173/"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "GUTENBERG_EDITOR_REMOTE_URL"
value = "http://localhost:5174/remote.html"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
Expand Down
Loading