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
52 changes: 35 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ One WordPress plugin, two ways to publish a static site to Spacefast:
## Install

1. Download [`spacefast-wordpress.zip`](https://github.com/spacefast/wordpress/releases/latest/download/spacefast-wordpress.zip), then install and activate it.
2. Open **Settings → Spacefast** and choose a publishing mode.
3. Continue to Spacefast, sign in, and authorize exactly one Team.
4. Back in WordPress, choose the Space. The plugin verifies it before showing **Connected**.
2. Open **Settings → Spacefast** and select **Publish with Spacefast**. The plugin can install and activate Simply Static for you.
3. Sign in to Spacefast and authorize exactly one Team.
4. Back in WordPress, create a Space with the suggested site name or choose an existing one. The plugin publishes the first version before showing the connection as complete.

OAuth access is bounded by the user's live Team role, the one Team approved on
the consent screen, and the chosen mode: static mode can publish to Spaces;
headless mode can trigger repository builds. WordPress then selects one Space
the consent screen, and the chosen mode. Static mode can create and publish a
Space. Headless mode can keep its WordPress source settings in sync and trigger
repository builds. WordPress then selects one Space
inside that Team. Tokens are stored in non-autoloaded options and are never
displayed. `SPACEFAST_WORDPRESS_API_URL` may override the API origin for local
development.

## Static WordPress

Install and activate Simply Static, then select **Static WordPress** in
**Settings → Spacefast**. **Export and publish** starts a full Simply Static
export. Starting an export from Simply Static's own screen works too.
Select **Publish with Spacefast** in **Settings → Spacefast**. If necessary,
the same action installs and activates Simply Static. **Publish now** starts a
full Simply Static export. Starting an export from Simply Static's own screen
works too.

Spacefast adds a delivery step after Simply Static has generated and delivered
its files but before cleanup. The step hashes the generated bytes, declares a
Expand All @@ -45,7 +47,7 @@ WordPress features still need a static-compatible replacement.

## Headless CMS

Select **Headless CMS** when an Astro or other repository project reads the
Open **Use WordPress as a CMS for a repository site** when an Astro or other repository project reads the
WordPress REST API during its build. Publishing, updating, unpublishing, or
deleting public REST-visible post types, taxonomy changes, navigation changes,
media changes, and public user changes schedule a production build. Autosaves,
Expand All @@ -56,20 +58,36 @@ Merely opening or navigating wp-admin does not schedule anything.

Delivery uses WP-Cron with a stable idempotency key. Network failures, timeouts,
rate limits, and server errors retry with bounded exponential backoff. A change
arriving while a delivery runs remains pending and schedules one successor after
the quiet window. **Build now** skips the window, runs immediately, and clears
arriving while a build runs remains pending and schedules exactly one successor after
the quiet window. **Build now** skips the window when no build is active and clears
the superseded scheduled event. The generic **Build now** action
in the Spacefast dashboard triggers the same connected production repository.

The plugin sends no post content, title, author, or other WordPress data to
Spacefast. It sends only the configured public WordPress origin and a request
to run the server-owned repository settings.
The plugin never sends drafts, credentials, or database contents to Spacefast.
It sends the public WordPress origin and can sync the public site title,
description, and search visibility. Repository source code, commands, and
publish targets remain server-owned.

## WordPress settings sync

WordPress owns the integration controls. By default, the plugin keeps the
Space title and sharing metadata, search-engine visibility, and—when using a
repository—the public WordPress data source in sync. Every update starts from
the current Space settings and uses the Space settings digest, preserving
unrelated dashboard and repository configuration. A concurrent dashboard edit
is reloaded and merged once; nothing is silently overwritten.

Automatic publishing is on by default. Site and content changes share the same
60-second quiet window, and settings are synced immediately before the next
publish or build. These defaults can be changed under **WordPress sync**.

## Operate and remove

The settings page shows the active mode, pending work, delivery status, and the
last build or version ID. It also provides a connection test and appears in
Site Health.
The settings page leads with the authoritative live/publishing state, live URL,
and one Publish or Build action. Connection tests, receipts, Space changes, and
disconnect live under secondary disclosures. The integration also appears in
Site Health, and Spacefast labels versions published by this plugin as
WordPress.

**Disconnect** revokes refresh access in Spacefast, removes local tokens, and
clears pending work. A short-lived access token may remain valid until it
Expand Down
52 changes: 50 additions & 2 deletions includes/class-spacefast-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private function request( string $method, string $path, ?array $body = null, arr
'redirection' => 0,
'reject_unsafe_urls' => true,
'sslverify' => true,
'limit_response_size' => 65536,
'limit_response_size' => 1024 * 1024,
'headers' => array_merge(
array(
'Authorization' => 'Bearer ' . $access_token,
Expand All @@ -61,7 +61,21 @@ private function request( string $method, string $path, ?array $body = null, arr
}

$status = (int) wp_remote_retrieve_response_code( $response );
$decoded = json_decode( (string) wp_remote_retrieve_body( $response ), true );
$raw_body = (string) wp_remote_retrieve_body( $response );
$decoded = json_decode( $raw_body, true );
if (
$status >= 200
&& $status < 300
&& ( '' === $raw_body || JSON_ERROR_NONE !== json_last_error() || ! is_array( $decoded ) )
) {
return array(
'ok' => false,
'retryable' => true,
'code' => 'invalid_response_body',
'message' => 'Spacefast returned an unreadable response.',
'data' => array(),
);
}
$decoded = is_array( $decoded ) ? $decoded : array();
if ( $status >= 200 && $status < 300 ) {
$data = isset( $decoded['data'] ) && is_array( $decoded['data'] )
Expand Down Expand Up @@ -119,6 +133,25 @@ public function create_space( string $team_id, string $title, string $idempotenc
);
}

/** @return array{ok:bool,retryable:bool,code:string,message:string,data:array<string,mixed>} */
public function get_space(): array {
$settings = Spacefast_Settings::get();
return $this->request( 'GET', '/v1/spaces/' . rawurlencode( (string) $settings['space_id'] ) );
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* @param array<string,mixed> $body Space settings patch.
* @return array{ok:bool,retryable:bool,code:string,message:string,data:array<string,mixed>}
*/
public function update_space( array $body ): array {
$settings = Spacefast_Settings::get();
return $this->request(
'PATCH',
'/v1/spaces/' . rawurlencode( (string) $settings['space_id'] ),
$body
);
}

/** @return array{ok:bool,retryable:bool,code:string,message:string,data:array<string,mixed>} */
private function list_all( string $path ): array {
$items = array();
Expand Down Expand Up @@ -219,6 +252,20 @@ public function trigger_build( string $event_id ): array {
return $result;
}

/** @return array{ok:bool,retryable:bool,code:string,message:string,data:array<string,mixed>} */
public function get_build( string $build_id ): array {
if ( ! preg_match( '/^bld_[A-Za-z0-9_-]+$/', $build_id ) ) {
return array(
'ok' => false,
'retryable' => false,
'code' => 'invalid_build_id',
'message' => 'Spacefast returned an invalid build receipt.',
'data' => array(),
);
}
return $this->request( 'GET', '/v1/builds/' . rawurlencode( $build_id ) );
}

/**
* @param array<int,array{path:string,size:int,sha256:string}> $files Files.
* @return array{ok:bool,retryable:bool,code:string,message:string,data:array<string,mixed>}
Expand All @@ -239,6 +286,7 @@ public function create_static_version( array $files, string $event_id, string $p
'metadata' => array(
'integration' => 'wordpress',
'exporter' => 'simply-static',
'siteUrl' => untrailingslashit( home_url() ),
),
),
),
Expand Down
39 changes: 36 additions & 3 deletions includes/class-spacefast-oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function __construct( ?callable $transport = null ) {
public static function scopes( string $mode ): array {
return Spacefast_Settings::MODE_STATIC === $mode
? array( 'teams:read', 'spaces:read', 'spaces:write', 'spaces:publish', 'offline_access' )
: array( 'teams:read', 'spaces:read', 'builds:trigger', 'offline_access' );
: array( 'teams:read', 'spaces:read', 'spaces:write', 'builds:trigger', 'offline_access' );
}

public static function has_scope( string $scope ): bool {
Expand Down Expand Up @@ -73,6 +73,7 @@ public function begin( string $mode ): array {
$callback = self::callback_url();
$scopes = implode( ' ', self::scopes( $mode ) );
$resource = $api_url . self::RESOURCE_PATH;
$current = Spacefast_Settings::get();
$registration = $this->json_request(
'POST',
$api_url . '/v1/auth/oauth2/register',
Expand Down Expand Up @@ -101,6 +102,18 @@ public function begin( string $mode ): array {
'client_id' => $client_id,
'mode' => $mode,
'callback' => $callback,
'resume_connection' => Spacefast_Settings::configured() && $mode === $current['mode']
? array(
'team_id' => $current['team_id'],
'team_name' => $current['team_name'],
'team_slug' => $current['team_slug'],
'space_id' => $current['space_id'],
'space_name' => $current['space_name'],
'space_slug' => $current['space_slug'],
'live_url' => $current['live_url'],
'verified_at' => $current['verified_at'],
)
: null,
'expires_at' => time() + 10 * MINUTE_IN_SECONDS,
),
false
Expand All @@ -122,7 +135,7 @@ public function begin( string $mode ): array {
return array( 'ok' => true, 'message' => '', 'url' => $url );
}

/** @return array{ok:bool,message:string} */
/** @return array{ok:bool,message:string,resumed?:bool} */
public function finish( string $code, string $state ): array {
$pending = get_option( self::PENDING_OPTION, array() );
delete_option( self::PENDING_OPTION );
Expand Down Expand Up @@ -161,7 +174,27 @@ public function finish( string $code, string $state ): array {
'team_id' => '', 'space_id' => '', 'verified_at' => 0,
)
);
return $this->load_choices();
$choices_result = $this->load_choices();
if ( ! $choices_result['ok'] ) return $choices_result;
$resume = isset( $pending['resume_connection'] ) && is_array( $pending['resume_connection'] )
? $pending['resume_connection']
: null;
$choices = get_option( self::CHOICES_OPTION, array() );
$spaces = is_array( $choices['spaces'] ?? null ) ? $choices['spaces'] : array();
$resume_space_id = is_array( $resume ) ? (string) ( $resume['space_id'] ?? '' ) : '';
$available = false;
foreach ( $spaces as $space ) {
if ( is_array( $space ) && '' !== $resume_space_id && hash_equals( (string) ( $space['id'] ?? '' ), $resume_space_id ) ) {
$available = true;
break;
}
}
if ( $available && is_array( $resume ) ) {
Spacefast_Settings::merge( $resume );
delete_option( self::CHOICES_OPTION );
return array( 'ok' => true, 'message' => '', 'resumed' => true );
}
return array( 'ok' => true, 'message' => '', 'resumed' => false );
}

/** @return array{ok:bool,message:string} */
Expand Down
Loading
Loading