Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WordPress (wordpress)

WordPress is a “bring your own site” provider for self-hosted WordPress.org sites. Rustiz authenticates with HTTP Basic using a WordPress username and an Application Password, then creates posts through the site’s REST API (/wp-json/wp/v2/...). There is no OAuth app, no scope model and no app review.

  • Rust source: crates/rustiz-providers/src/wordpress.rs
  • Upstream port: wordpress.provider.ts
  • Editor: HTML
  • Connection mode: customFields

This provider is not a WordPress.com/Jetpack connector. WordPress.com uses OAuth2 against public-api.wordpress.com; Rustiz talks to https://<your-site>/wp-json/wp/v2 with Basic auth. Use a self-hosted WordPress.org site.

Overview

PropertyValue
Identifierwordpress
Display nameWordPress
Connection modecustomFields (site domain, username, Application Password)
Editorhtml
Channel kindsocial (article content)
Comments / threadsNo. supports_comments = false; only the first PostDetails is published
Pending / async publishingNo. supports_pending = false; returns PostStatus::Completed
AnalyticsNo. supports_analytics = false
Post analyticsNo. supports_post_analytics = false
MentionsNo. supports_mentions = false
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job8 (sites are the customer’s own servers)
Character limit (max_length)100000 characters for the post content
Inline imagesYes (inline_images = true)
APIWordPress REST API v2

Server Environment Variables

required_env() returns no variables:

#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
    &[]
}
}

required_env_alternatives() is not overridden. There is no base-URL override for this provider: the site origin comes from each channel’s credential payload (the base64 JSON integration token).

VariableRequiredDefaultPurpose
FRONTEND_URLNo (but recommended)http://localhost:5000Used only by media_content_type() to recognize Rustiz’s own uploads ({FRONTEND_URL}/uploads/...) and avoid an extra HEAD request. It does not control the WordPress URL.
ALLOW_PRIVATE_NETWORKNofalseGlobal SSRF switch. A staging/local WordPress on localhost or a private IP is blocked unless this is true (or 1). Only enable it for trusted test deployments.
UPLOAD_DIRECTORYNounsetGlobal media store path. Local main_image.path values are read from here (or fetched from a URL).

Unlike Listmonk and Lemmy, WordPress does not read the encrypted custom_instance_details copy. Its credentials are carried in the channel’s integration token as base64 JSON, so JWT_SECRET is not required for this provider.

Step-by-Step Developer App Setup

There is no app to register. The setup is on the WordPress site.

  1. Confirm the site is WordPress 5.6 or newer and is served over HTTPS. Application Passwords are available in core since 5.6 and are intended for HTTPS; many hosts hide them on plain HTTP.

  2. Confirm the REST API is reachable:

    curl -i https://blog.example.com/wp-json/
    

    A JSON index means the REST API is enabled. A 404, HTML login page, or a security-plugin block means the provider cannot work until that is fixed.

  3. Create or choose a WordPress user with enough capability:

    • publish_posts to publish posts immediately;
    • edit_posts for drafts;
    • upload_files if a featured image will be uploaded;
    • edit_pages / the custom post type’s capabilities for pages or custom types. A least-privilege Author can publish their own posts and upload media; a Contributor can only submit pending posts.
  4. As that user, open Users → Profile (or Users → All Users → Edit) and find Application Passwords.

  5. Enter a name such as Rustiz, click Add New Application Password, and copy the generated password. It is shown only once. WordPress displays it in groups with spaces; spaces are allowed and stripped during validation.

  6. Verify the credentials before connecting:

    curl -s -u 'rustiz:xxxx xxxx xxxx xxxx xxxx xxxx' \
      https://blog.example.com/wp-json/wp/v2/users/me
    

    Rustiz calls exactly this endpoint during authenticate. A 200 with id, name, avatar_urls is success; 401/403 means the password, username, Authorization header, or a security plugin is the problem.

Products and permissions

  • No WordPress products or APIs to enable; the REST API is core.
  • No OAuth scopes. The Application Password inherits the user’s capabilities.
  • rest_cannot_create from WordPress is mapped to The connect user has insufficient permissions to create posts.
  • Security plugins/WAFs commonly strip the Authorization header or block /wp-json/. Rustiz’s authentication errors call this out explicitly.

Sandbox / testing mode

WordPress has no platform sandbox. Use a staging clone of the site, or use the draft, pending or private statuses so a test does not appear publicly.

  • A draft is visible only in wp-admin.
  • pending is visible to editors for review.
  • private is visible to logged-in users with permission.
  • Rustiz does not support WordPress’s future status; scheduling is handled by Rustiz before it calls WordPress.
  • For a local/staging site on a private address, set ALLOW_PRIVATE_NETWORK=true on the Rustiz server (SSRF protection is on by default).

Production app review

There is no app review. The same Application Password works immediately. Site-level plugins, hosting WAF rules and user capabilities are the effective access controls.

Connecting a Channel (CLI & MCP)

The credential payload has three keys:

KeyTypeValidation regexNotes
domaintext/^https?:\/\/(?:www\.)?[\w\-]+(\.[\w\-]+)+([\/?#][^\s]*)?$/Site URL including scheme. A subdirectory install is allowed (for example https://example.com/blog). Do not add a trailing slash — see the bug below.
usernametext/.+/WordPress login name, not the display name or email.
passwordpassword/.+/Application Password. The UI hint says Application password, create in User->Profile.

CLI

{
  "domain": "https://blog.example.com",
  "username": "rustiz",
  "password": "xxxx xxxx xxxx xxxx xxxx xxxx"
}
rustiz channels connect wordpress --fields wordpress.json

printf '%s' '{"domain":"https://blog.example.com","username":"rustiz","password":"xxxx xxxx xxxx xxxx xxxx xxxx"}' \
  | rustiz channels connect wordpress --fields -

Immediate result:

{ "connected": true, "pending": false, "name": "Rustiz" }

The displayed name is the WordPress user’s name, not the site title.

MCP

{
  "provider": "wordpress",
  "customFields": {
    "domain": "https://blog.example.com",
    "username": "rustiz",
    "password": "xxxx xxxx xxxx xxxx xxxx xxxx"
  }
}

No follow-up step. Call integrationList for the channel id. Use --fields / customFields; --external-url / externalUrl is not the credential path for this provider.

Provider functions (triggerTool)

FunctionParametersReturnsNotes
postTypesnone[{"id": "<rest_base>", "name": "<label>"}]Calls /wp-json/wp/v2/types; filters out keys containing wp_ or nav_, and attachment. id is the REST base (posts, pages, etc.), which is what settings.type expects.
categoriesListnone[{"id": <number>, "name": "..."}]Calls /wp-json/wp/v2/categories?per_page=100. Only the first 100 categories are returned.
tagsListnone[{"id": <number>, "name": "..."}]Calls /wp-json/wp/v2/tags?per_page=100. Only the first 100 tags are returned.

MCP examples:

{ "integrationId": "int_...", "methodName": "postTypes" }
{ "integrationId": "int_...", "methodName": "categoriesList" }

All three have an empty input_schema / dataSchema. wp_get() does not go through the publishing error classifier, so an error response that is valid JSON can be returned as if it were data; check the shape before using it.

Post Settings & Media Validation

settings_schema()

CLI note: the simple rustiz posts create --channel <id> --content "..." form cannot pass provider settings. Use rustiz posts create --json post.json with a CreatePostDto (type, date, posts[].settings, …) when any of these fields are required.

FieldTypeRequiredValidation / behavior
titlestringYesminLength: 2; checked. Also used to build the slug.
main_imageobjectNoFeatured image. Properties: id (media library id, string), path (public URL or upload path, string), alt (string), thumbnail (URL string). id and path are required by the schema.
typestringYesREST base of the post type: posts, pages, or a custom type’s rest_base from postTypes. Not the type slug post/page.
categoriesarray of integersNoCategory ids. Must be JSON numbers; strings/floats fail validation.
tagsarray of integersNoTag ids. Must be JSON numbers.
statusstring enumNopublish, draft, pending, private. Missing/empty defaults to publish. WordPress’s future is not accepted.

Example object form:

{
  "title": "Salt & Pepper",
  "main_image": {
    "id": "55",
    "path": "https://blog.example.com/wp-content/uploads/cover.png",
    "alt": "A salt and pepper shaker"
  },
  "type": "posts",
  "categories": [3, 4],
  "tags": [9],
  "status": "draft"
}

MCP settings array:

{
  "settings": [
    { "key": "title", "value": "Salt & Pepper" },
    { "key": "type", "value": "posts" },
    { "key": "categories", "value": [3, 4] },
    { "key": "status", "value": "draft" }
  ]
}

The engine adds "__type": "wordpress" automatically if missing.

validate_settings() rules

  1. title must exist, be a string and have at least 2 characters.
  2. type must exist and be a string.
  3. main_image, if present and not null:
    • must be an object;
    • id must be a string;
    • path must be a string;
    • alt, if present, must be a string;
    • thumbnail, if present and non-empty, must be an http(s) URL with a host.
  4. categories and tags, if present and not null, must be arrays whose items are all integers (i64 or u64). Floats and numeric strings fail.
  5. status, if present and not null, must be one of publish, draft, pending, private.
  6. There is no extension check, size check or RESTRICT_UPLOAD_DOMAINS gate for main_image.path.

Media requirements and upload behavior

  • main_image is uploaded to /wp-json/wp/v2/media as raw bytes with:
    • Content-Disposition: attachment; filename="<last path segment>";
    • a Content-Type derived from a HEAD request when the path is remote and not a Rustiz own upload, otherwise from the file extension;
    • User-Agent: Rustiz/1.0.
  • The returned media id becomes featured_media. If the id is null, an empty string or numeric 0, it is omitted.
  • The post’s media / attachments array is ignored. Only main_image is uploaded as the featured image.
  • inline_images = true: Rustiz’s HTML sanitizer keeps <img> tags in the post content and allows the img tag. Images embedded in the content are the way to put pictures in the body; they are not uploaded by Rustiz.
  • alt and thumbnail in main_image are accepted but not applied to the WordPress media item.
  • Because slugify() is strict ASCII, a title written entirely in a non-Latin script can produce an empty slug. WordPress then falls back to its own slug behavior.

check_validity() is the trait default (Ok(None)). WordPress has no provider-specific media-count, dimension or file-type rule; the only media handling is the main_image upload and the inline <img> preservation described above.

Plugs

No @Plug or @PostPlug methods. run_plug() and run_post_plug() use the trait defaults and return ProviderError::Unsupported.

Implementation Notes & Gotchas

  • Trailing slash bug: auth normalizes, publishing does not. authenticate() trims whitespace and trailing slashes into a local domain variable for the /users/me request: let domain = creds.domain.trim().trim_end_matches('/'). But it stores params.code unchanged as the access token, and post() calls decode_credentials() again and uses creds.domain raw. A user who pastes https://blog.example.com/ authenticates successfully and then publishes to https://blog.example.com//wp-json/wp/v2/... (double slash). Always strip the trailing slash in the connect payload.
  • The channel id embeds the raw domain. authenticate() returns id = format!("{}_{}", creds.domain, user_id), so the same site with and without a trailing slash produces different internal ids. Combined with the bug above, reconnect cleanly if you change the domain format.
  • type is the REST base, not the WordPress type slug. postTypes returns rest_base values such as posts and pages. Sending "type": "post" (as some Postiz examples do) calls /wp-json/wp/v2/post, which is a 404. Use posts.
  • Categories and tags are ids, not names. Both are numeric arrays. The provider’s number_list() also accepts numeric strings at publish time, but validate_settings() rejects strings before that, so always send numbers.
  • Attachments are silently dropped. A post with attachments but no main_image creates a text-only WordPress post. Put body images in the HTML content instead.
  • WordPress status future is unsupported. WordPress can schedule natively, but the Rustiz enum only allows publish, draft, pending, private. Let Rustiz schedule the job and send publish when it runs.
  • No base URL override. Unlike other providers, there is no WORDPRESS_BASE_URL. The domain is per channel, which is correct for a multi-tenant deployment.
  • Application Passwords are per user and revocable. The connect payload is stored as the channel’s integration token (base64 JSON); the server also writes an encrypted copy to custom_instance_details, but the WordPress provider does not read that copy. Base64 is encoding, not encryption: treat the Integration.token column as a secret. Rotating the Application Password requires reconnecting every WordPress channel.
  • JWT_SECRET is not needed for WordPress. Unlike Listmonk and Lemmy, this provider decodes credentials from access_token, not from custom_instance_details. A changed JWT_SECRET does not break existing WordPress channels.
  • Errors from the tools are not classified. wp_get() uses execute() + serde_json::from_str; a WordPress error object is valid JSON and is returned as a successful Value. postTypes then iterates it as a map. If a tool returns strange entries, check the HTTP status and the WordPress error body directly.
  • Media content type can be wrong for signed URLs. For remote URLs, Rustiz sends a HEAD request and trusts Content-Type; if the HEAD fails, it falls back to the file extension. A signed CDN URL with no extension and a failing HEAD becomes application/octet-stream, which WordPress may reject.
  • Security plugins are the usual failure mode. Wordfence and similar plugins can block /wp-json/, strip Authorization, or disable Application Passwords via wp_is_application_passwords_available. The authentication error messages in Rustiz are written for exactly these cases.
  • No refresh. Credentials never expire; refresh_token() returns an empty token with expires_in: 0. A 401/403 requires reconnecting with a valid Application Password.

References