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 tohttps://<your-site>/wp-json/wp/v2with Basic auth. Use a self-hosted WordPress.org site.
Overview
| Property | Value |
|---|---|
| Identifier | wordpress |
| Display name | WordPress |
| Connection mode | customFields (site domain, username, Application Password) |
| Editor | html |
| Channel kind | social (article content) |
| Comments / threads | No. supports_comments = false; only the first PostDetails is published |
| Pending / async publishing | No. supports_pending = false; returns PostStatus::Completed |
| Analytics | No. supports_analytics = false |
| Post analytics | No. supports_post_analytics = false |
| Mentions | No. supports_mentions = false |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 8 (sites are the customer’s own servers) |
Character limit (max_length) | 100000 characters for the post content |
| Inline images | Yes (inline_images = true) |
| API | WordPress 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).
| Variable | Required | Default | Purpose |
|---|---|---|---|
FRONTEND_URL | No (but recommended) | http://localhost:5000 | Used 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_NETWORK | No | false | Global 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_DIRECTORY | No | unset | Global 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.
-
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.
-
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.
-
Create or choose a WordPress user with enough capability:
publish_poststo publish posts immediately;edit_postsfor drafts;upload_filesif a featured image will be uploaded;edit_pages/ the custom post type’s capabilities forpagesor custom types. A least-privilege Author can publish their own posts and upload media; a Contributor can only submitpendingposts.
-
As that user, open Users → Profile (or Users → All Users → Edit) and find Application Passwords.
-
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. -
Verify the credentials before connecting:
curl -s -u 'rustiz:xxxx xxxx xxxx xxxx xxxx xxxx' \ https://blog.example.com/wp-json/wp/v2/users/meRustiz calls exactly this endpoint during
authenticate. A 200 withid,name,avatar_urlsis 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_createfrom WordPress is mapped toThe connect user has insufficient permissions to create posts.- Security plugins/WAFs commonly strip the
Authorizationheader 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
draftis visible only in wp-admin. pendingis visible to editors for review.privateis visible to logged-in users with permission.- Rustiz does not support WordPress’s
futurestatus; scheduling is handled by Rustiz before it calls WordPress. - For a local/staging site on a private address, set
ALLOW_PRIVATE_NETWORK=trueon 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:
| Key | Type | Validation regex | Notes |
|---|---|---|---|
domain | text | /^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. |
username | text | /.+/ | WordPress login name, not the display name or email. |
password | password | /.+/ | 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)
| Function | Parameters | Returns | Notes |
|---|---|---|---|
postTypes | none | [{"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. |
categoriesList | none | [{"id": <number>, "name": "..."}] | Calls /wp-json/wp/v2/categories?per_page=100. Only the first 100 categories are returned. |
tagsList | none | [{"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. Userustiz posts create --json post.jsonwith aCreatePostDto(type,date,posts[].settings, …) when any of these fields are required.
| Field | Type | Required | Validation / behavior |
|---|---|---|---|
title | string | Yes | minLength: 2; checked. Also used to build the slug. |
main_image | object | No | Featured 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. |
type | string | Yes | REST base of the post type: posts, pages, or a custom type’s rest_base from postTypes. Not the type slug post/page. |
categories | array of integers | No | Category ids. Must be JSON numbers; strings/floats fail validation. |
tags | array of integers | No | Tag ids. Must be JSON numbers. |
status | string enum | No | publish, 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
titlemust exist, be a string and have at least 2 characters.typemust exist and be a string.main_image, if present and notnull:- must be an object;
idmust be a string;pathmust be a string;alt, if present, must be a string;thumbnail, if present and non-empty, must be anhttp(s)URL with a host.
categoriesandtags, if present and notnull, must be arrays whose items are all integers (i64oru64). Floats and numeric strings fail.status, if present and notnull, must be one ofpublish,draft,pending,private.- There is no extension check, size check or
RESTRICT_UPLOAD_DOMAINSgate formain_image.path.
Media requirements and upload behavior
main_imageis uploaded to/wp-json/wp/v2/mediaas raw bytes with:Content-Disposition: attachment; filename="<last path segment>";- a
Content-Typederived 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 isnull, an empty string or numeric0, it is omitted. - The post’s
media/attachmentsarray is ignored. Onlymain_imageis uploaded as the featured image. inline_images = true: Rustiz’s HTML sanitizer keeps<img>tags in the post content and allows theimgtag. Images embedded in the content are the way to put pictures in the body; they are not uploaded by Rustiz.altandthumbnailinmain_imageare 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 emptyslug. 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 localdomainvariable for the/users/merequest:let domain = creds.domain.trim().trim_end_matches('/'). But it storesparams.codeunchanged as the access token, andpost()callsdecode_credentials()again and usescreds.domainraw. A user who pasteshttps://blog.example.com/authenticates successfully and then publishes tohttps://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()returnsid = 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. typeis the REST base, not the WordPress type slug.postTypesreturnsrest_basevalues such aspostsandpages. Sending"type": "post"(as some Postiz examples do) calls/wp-json/wp/v2/post, which is a 404. Useposts.- Categories and tags are ids, not names. Both are numeric arrays. The
provider’s
number_list()also accepts numeric strings at publish time, butvalidate_settings()rejects strings before that, so always send numbers. - Attachments are silently dropped. A post with
attachmentsbut nomain_imagecreates a text-only WordPress post. Put body images in the HTML content instead. - WordPress status
futureis unsupported. WordPress can schedule natively, but the Rustiz enum only allowspublish,draft,pending,private. Let Rustiz schedule the job and sendpublishwhen 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 theIntegration.tokencolumn as a secret. Rotating the Application Password requires reconnecting every WordPress channel. JWT_SECRETis not needed for WordPress. Unlike Listmonk and Lemmy, this provider decodes credentials fromaccess_token, not fromcustom_instance_details. A changedJWT_SECRETdoes not break existing WordPress channels.- Errors from the tools are not classified.
wp_get()usesexecute()+serde_json::from_str; a WordPress error object is valid JSON and is returned as a successfulValue.postTypesthen 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 becomesapplication/octet-stream, which WordPress may reject. - Security plugins are the usual failure mode. Wordfence and similar
plugins can block
/wp-json/, stripAuthorization, or disable Application Passwords viawp_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 withexpires_in: 0. A 401/403 requires reconnecting with a valid Application Password.
References
- Application Passwords (creation, HTTPS, Basic auth, revocation): https://developer.wordpress.org/advanced-administration/security/application-passwords/
- WordPress REST API posts reference: https://developer.wordpress.org/rest-api/reference/posts/
- WordPress REST API authentication overview: https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/
- Postiz provider settings: https://docs.postiz.com/public-api/providers/wordpress
- Postiz posting rules: https://docs.postiz.com/general/platforms/overview